diff --git a/.claude/agents/code-standards-enforcer.md b/.claude/agents/code-standards-enforcer.md new file mode 100644 index 0000000000..e1191ee8c0 --- /dev/null +++ b/.claude/agents/code-standards-enforcer.md @@ -0,0 +1,150 @@ +--- +name: code-standards-enforcer +description: "Audits recently written or modified code against CLAUDE.md rules, patterns-in-transition, and checklist files. Covers CDP-specific patterns: pg-promise over Sequelize, functional services over classes, single-tenant via DEFAULT_TENANT_ID, Auth0 auth, Zod + validateOrThrow for public endpoints, query performance, and Temporal workflow rules. Invoked in background by review-pr." +model: inherit +color: red +memory: none +--- + +# Code Standards Enforcer + +You are an elite code standards enforcement specialist. Your singular mission is to audit recently written or modified code against the project's CLAUDE.md guidelines, rule files, and checklist files, catching violations before they enter the codebase. + +## Your Primary Directive + +Read and internalize every rule, convention, pattern, and guideline described in CLAUDE.md. These are law. When CLAUDE.md references other documents (rule files in `.claude/rules/`, checklist files under `.claude/skills/review-pr/references/`), read and enforce those too. + +## Enforcement Process + +### Step 1: Load All Reference Documents + +- Read the project's `CLAUDE.md` thoroughly +- Read the user's global CLAUDE.md at `~/.claude/CLAUDE.md` if it exists +- Glob `.claude/rules/*.md` and read every rule file +- Read all checklists under `.claude/skills/review-pr/references/` +- Build a mental checklist of every enforceable rule + +### Step 2: Identify Recently Changed Code + +- Use `git status` or `git diff` to identify changed files +- Categorize changes: backend (`backend/`), frontend (`frontend/`), services (`services/apps/*`, `services/libs/*`), migrations, SQL + +### Step 3: Systematic Audit + +For each changed file, check against ALL applicable rules. The patterns-in-transition from CLAUDE.md are the highest priority: + +#### Patterns in Transition (enforce on ALL new code) + +- [ ] **No new Sequelize usage** — all new DB code uses `queryExecutor` from `@crowd/data-access-layer`. Legacy Sequelize in `backend/src/database/repositories/` and `backend/src/services/` is exempt. +- [ ] **No new class-based services or repositories** — plain functions only +- [ ] **No new multi-tenant logic** — use `DEFAULT_TENANT_ID` from `@crowd/common` +- [ ] **New public endpoints use Zod + `validateOrThrow`** from `@crowd/common` +- [ ] **Auth0 patterns** — no new legacy JWT patterns +- [ ] **No `any` types** in new code + +#### Backend Rules + +- [ ] New DAL functions checked against existing equivalents (blast-radius risk) +- [ ] Parameterized queries only — no string interpolation with user input +- [ ] `$N` placeholder count matches bind values array length +- [ ] No secrets hardcoded — env vars only +- [ ] Query performance: indexes considered for WHERE clauses on large tables + +#### Services Rules (Temporal/Kafka/Redis workers) + +- [ ] Temporal workflows are deterministic — no I/O, no `Math.random()`, no `Date.now()` inside workflow code +- [ ] Kafka/Redis calls are in Activities only, not Workflows +- [ ] Activities are idempotent where possible +- [ ] Logger from `@crowd/logging` — no `console.log` + +#### Frontend Rules + +- [ ] ` +``` + +**Fix:** +```vue + +``` + +--- + +## 2. TanStack Vue Query for server state (SHOULD FIX) + +Use `useQuery` / `useMutation` from TanStack Vue Query for data fetching. Do not use raw `axios` directly in `onMounted` for data that should be cached. + +**Violation:** +```ts +const data = ref(null) +onMounted(async () => { + data.value = await axios.get('/api/members') +}) +``` + +**Fix:** +```ts +import { useQuery } from '@tanstack/vue-query' +const { data } = useQuery({ + queryKey: ['members'], + queryFn: () => axios.get('/api/members').then(r => r.data), +}) +``` + +--- + +## 3. Pinia for shared client state (SHOULD FIX) + +Use Pinia stores for shared client-side state. Do not pass deeply nested props for state that should be in a store. + +--- + +## 4. TypeScript — no `any` (SHOULD FIX) + +Avoid `any`. Use proper types, `unknown` with narrowing, or generics. + +--- + +## 5. Tailwind CSS conventions (SHOULD FIX) + +- **Prefer `gap-*` over `space-y-*`** for vertical stacking +- No hard-coded hex color values in templates — use Tailwind color tokens + +--- + +## 6. Reactive refs over non-reactive values (SHOULD FIX) + +State that should trigger re-renders must use `ref()` or `computed()`. + +**Violation:** +```ts +let isLoading = false // won't trigger re-render +``` + +**Fix:** +```ts +const isLoading = ref(false) +``` + +--- + +## 7. Element Plus usage patterns (NIT) + +The project uses Element Plus (`el-*`). Follow existing component usage patterns — check how similar components are used elsewhere before introducing a new pattern. diff --git a/.claude/skills/review-pr/references/services-checklist.md b/.claude/skills/review-pr/references/services-checklist.md new file mode 100644 index 0000000000..570719b064 --- /dev/null +++ b/.claude/skills/review-pr/references/services-checklist.md @@ -0,0 +1,51 @@ +# Services Review Checklist + +Review standards for microservices under `services/apps/` and shared libraries under `services/libs/`. + +--- + +## Temporal Worker patterns (`services/apps/*/`) + +### 1. Workflows must be deterministic (CRITICAL) + +Temporal workflows must be fully deterministic. No direct I/O, no `Math.random()`, no `Date.now()`, no non-deterministic APIs inside workflow code. Move all I/O into Activities. + +### 2. Activities should be idempotent (SHOULD FIX) + +Temporal may retry activities on failure. Activities should be safe to run multiple times without unintended side effects. + +### 3. No direct Kafka/Redis calls inside Temporal workflows (CRITICAL) + +Kafka producers and Redis clients must only be called from Activities, not from Workflow code. + +--- + +## Shared Libraries (`services/libs/*/`) + +### 4. New DAL functions — check for existing equivalents first (SHOULD FIX) + +Before adding a new function to `services/libs/data-access-layer/src/`, verify no equivalent exists. + +### 5. `queryExecutor` from `@crowd/data-access-layer` (CRITICAL) + +All new database queries must use `queryExecutor`. Do not add Sequelize or raw `pg` queries to service libraries. + +### 6. Query performance awareness (SHOULD FIX) + +Flag queries that clearly scan large tables without an appropriate WHERE clause using an indexed column. + +### 7. Bunyan logger usage (SHOULD FIX) + +Use the logger from `@crowd/logging`, not `console.log/error/warn`. + +### 8. New class-based code (SHOULD FIX) + +New service/worker code should use plain functions, not class-based patterns. Classes are legacy. + +--- + +## Known false positives — do NOT flag + +- Existing class-based workers that have not been refactored — only flag **new** classes +- `queryExecutor` usage in `services/libs/data-access-layer/` — correct pattern +- `DEFAULT_TENANT_ID` from `@crowd/common` — correct pattern diff --git a/.claude/skills/review-pr/references/sql-checklist.md b/.claude/skills/review-pr/references/sql-checklist.md new file mode 100644 index 0000000000..892a524743 --- /dev/null +++ b/.claude/skills/review-pr/references/sql-checklist.md @@ -0,0 +1,75 @@ +# SQL & Migration Review Checklist + +Review standards for database queries in `services/libs/data-access-layer/` and migrations in `backend/src/database/migrations/`. + +--- + +## Flyway Migrations + +### 1. Migrations are append-only (CRITICAL) + +Never modify an existing migration file that has been applied. Create a new migration to alter or fix a previous one. + +**Violation:** Editing `V1234__create_members_table.sql` after it has been applied. + +**Fix:** Create `V1235__alter_members_add_column.sql` with the corrective change. + +--- + +### 2. Migration filename format (SHOULD FIX) + +Migration files must follow: `V{epoch}__{description}.sql` and `U{epoch}__{description}.sql` (undo). The version must be unique and greater than all existing versions. + +--- + +### 3. Migrations must be safe for production (CRITICAL) + +Avoid: +- `DROP TABLE` without verifying data is no longer needed +- `ALTER TABLE ... NOT NULL` without a default or two-step migration (add nullable first, backfill, then add constraint) +- Renaming columns without updating all code references first +- Large table operations without considering lock impact + +--- + +## Data Access Layer Queries + +### 4. Parameterized queries only — no string interpolation (CRITICAL) + +All queries must use parameterized placeholders (`$1`, `$2`, etc.). Never interpolate user input directly into SQL. + +**Violation:** +```ts +await queryExecutor.query(`SELECT * FROM members WHERE email = '${email}'`) +``` + +**Fix:** +```ts +await queryExecutor.query('SELECT * FROM members WHERE email = $1', [email]) +``` + +--- + +### 5. Placeholder count matches bind values (CRITICAL) + +Every `$N` placeholder must have a corresponding value in the binds array, in correct order. Mismatch causes runtime errors. + +--- + +### 6. Index awareness (SHOULD FIX) + +Before writing a query, verify the table has an index on the WHERE clause columns. Flag queries that will cause full table scans on large tables (`members`, `activities`, `organizations`). + +Common indexed columns: `id`, `tenantId`, `platform`, `sourceId`, `memberId`, `organizationId`, `timestamp`, `deletedAt`. + +--- + +### 7. Blast radius check for modified DAL functions (SHOULD FIX) + +If a shared DAL function in `services/libs/data-access-layer/src/` is modified, check all callers. Use `grep -r "functionName" services/ backend/` to find them. + +--- + +### 8. Soft-delete awareness (SHOULD FIX) + +Many tables use `deletedAt` for soft deletes. Queries that don't filter on `deletedAt IS NULL` may return deleted records. Verify the intended behavior. diff --git a/.claude/skills/scaffold-snowflake-connector/SKILL.md b/.claude/skills/scaffold-snowflake-connector/SKILL.md new file mode 100644 index 0000000000..74ff0b8cad --- /dev/null +++ b/.claude/skills/scaffold-snowflake-connector/SKILL.md @@ -0,0 +1,699 @@ +--- +name: scaffold-snowflake-connector +description: Use when adding a new snowflake-connector data source to crowd.dev — a new platform or a new source within an existing platform that needs buildSourceQuery, transformer, activity types, migration, and all associated type registrations scaffolded. +--- + +# Scaffold Snowflake Connector + +## Overview + +This skill scaffolds all code required to add a new snowflake-connector data source to crowd.dev. It covers up to 11 touch points, enforces zero-assumption practices, and requires explicit user validation for every piece of business logic before writing any file to disk. + +**One platform, one source per run.** Multiple sources within a platform are done one run at a time. + +## Process Flow + +```dot +digraph scaffold { + "Phase 1: Initial Questions" [shape=box]; + "Phase 2: Schema Collection" [shape=box]; + "Pre-Analysis: Auto-Derive Mappings" [shape=box]; + "Phase 3: Confirm / Correct Mappings" [shape=box]; + "All mappings confirmed?" [shape=diamond]; + "Phase 4: Generate & Review Files" [shape=box]; + "User approves each file?" [shape=diamond]; + "Phase 5: Testing" [shape=box]; + "Done" [shape=doublecircle]; + + "Phase 1: Initial Questions" -> "Phase 2: Schema Collection"; + "Phase 2: Schema Collection" -> "Pre-Analysis: Auto-Derive Mappings"; + "Pre-Analysis: Auto-Derive Mappings" -> "Phase 3: Confirm / Correct Mappings"; + "Phase 3: Confirm / Correct Mappings" -> "All mappings confirmed?"; + "All mappings confirmed?" -> "Phase 3: Confirm / Correct Mappings" [label="no, revise"]; + "All mappings confirmed?" -> "Phase 4: Generate & Review Files" [label="yes"]; + "Phase 4: Generate & Review Files" -> "User approves each file?"; + "User approves each file?" -> "Phase 4: Generate & Review Files" [label="no, revise"]; + "User approves each file?" -> "Phase 5: Testing" [label="yes"]; + "Phase 5: Testing" -> "Done"; +} +``` + +## File Inventory (All Touch Points) + +Read the current state of each file before modifying. Never modify without reading first. + +### Conditional — only if platform is NEW +| # | File | Change | +|---|------|--------| +| 0 | `services/libs/types/src/enums/platforms.ts` | Add `PlatformType.{PLATFORM}` enum value | + +### Always — structural (template-filled) +| # | File | Change | +|---|------|--------| +| 1 | `services/libs/types/src/enums/organizations.ts` | Add to `OrganizationSource` + `OrganizationAttributeSource` enums | +| 2 | `services/libs/integrations/src/integrations/{platform}/types.ts` | NEW: activity type enum + GRID | +| 3 | `services/libs/integrations/src/integrations/index.ts` | Add `export * from './{platform}/types'` | +| 4 | `services/libs/data-access-layer/src/organizations/attributesConfig.ts` | Add to `ORG_DB_ATTRIBUTE_SOURCE_PRIORITY` | +| 5 | `backend/src/database/migrations/V{epoch}__add{Platform}ActivityTypes.sql` | NEW: INSERT into `activityTypes` | +| 6 | `services/apps/snowflake_connectors/src/integrations/types.ts` | Add `DataSourceName.{PLATFORM}_{SOURCE}` | +| 7 | `services/apps/snowflake_connectors/src/integrations/index.ts` | Import + register in `supported` | + +### Per source — business logic (generated from confirmed inputs) +| # | File | Change | +|---|------|--------| +| 8 | `services/apps/snowflake_connectors/src/integrations/{platform}/{source}/buildSourceQuery.ts` | NEW | +| 9 | `services/apps/snowflake_connectors/src/integrations/{platform}/{source}/transformer.ts` | NEW | +| 10 | `services/apps/snowflake_connectors/src/integrations/{platform}/{platform}TransformerBase.ts` | NEW (optional) | + +--- + +## Context Detection + +Run this **once at skill start**, before any other step. + +Check whether the skill is running inside the crowd.dev repository by testing for the presence of the snowflake connectors integration index: + +``` +services/apps/snowflake_connectors/src/integrations/index.ts +``` + +Use `Glob` or `Read` to check for this file relative to the current working directory. + +### Case A — File found (running inside crowd.dev) + +Set `CROWD_DEV_ROOT = "."` (current directory). All file paths in this skill are relative to CWD. Proceed to Phase 1 immediately. + +### Case B — File not found (running from cross-team skills repo or elsewhere) + +Ask the user: +> "This skill needs access to the crowd.dev repository to read and modify files. Do you have a local clone? +> - If yes: provide the absolute path (e.g., `/Users/you/work/crowd.dev`) +> - If no: I'll give you the clone command first." + +If the user provides a path: verify it by checking `{path}/services/apps/snowflake_connectors/src/integrations/index.ts` exists. If confirmed, set `CROWD_DEV_ROOT = {path}`. Proceed to Phase 1. + +If the path doesn't exist or they need to clone: +> "Run: `git clone https://github.com/linuxfoundation/crowd.dev.git` +> Then provide the path to the cloned directory." + +Wait for a valid path before continuing. + +**For all subsequent file operations**, prefix every path in this skill with `CROWD_DEV_ROOT`. Example: `services/libs/types/src/enums/platforms.ts` becomes `{CROWD_DEV_ROOT}/services/libs/types/src/enums/platforms.ts`. + +--- + +## Phase 1: Initial Questions + +Ask one question at a time. Do not bundle questions. + +### Question 1 — Platform name + +Read `services/libs/types/src/enums/platforms.ts` and list all current `PlatformType` values. + +Ask: +> "What is the platform name for this data source? It must match a `PlatformType` enum value. Current values are: [list them]. If your platform isn't listed, provide the name and I'll add it." + +- If the value **is** in the enum: continue. +- If the value **is not** in the enum: warn the user explicitly: + > "⚠️ `{name}` is not in the `PlatformType` enum. I'll need to add it to `services/libs/types/src/enums/platforms.ts`. Please confirm this is a new platform and confirm the exact string value (e.g., `my-platform`)." + Wait for confirmation before proceeding. + +### Question 2 — New or existing platform? + +Read `services/apps/snowflake_connectors/src/integrations/index.ts`. + +- If the platform already has sources registered: list them and confirm which one we're adding. +- If the platform is new to snowflake_connectors: note that touch points 0–4 and 7 will all be new additions. + +### Question 3 — Source name + +Ask: +> "What is the name for this data source? This becomes the directory name and `DataSourceName` enum suffix (e.g., `enrollments`, `event-registrations`)." + +### Question 4 — Snowflake tables + +Ask for the main table first, then any additional tables: +> "What is the main Snowflake table for this source?" + +Then: +> "Are there any additional tables needed (e.g., for user data, org data, segment matching)? For each one, provide the table name and its purpose — what data it holds and how it relates to the main table." + +Do not assume a direct JOIN between tables. The relation type (JOIN, subquery, CTE, lookup) must come from the user's description of each table's purpose. + +--- + +## Phase 2: Schema Collection + +Once all table names are known, collect column schemas using the LFX BI Layer MCP tools. Only fall back to the manual query when BI Layer lookup fails for a specific table. + +### Step 0 — Check BI Layer connectivity + +Use `ToolSearch` with query `"LFX BI Layer get_all_sources"` to check whether the BI Layer tools are available in the current session. + +- **Tools found**: proceed directly to Step 1. +- **Tools not found**: prompt the user to authenticate: + > "The LFX BI Layer MCP is not connected. To enable automatic schema lookup, run `/mcp` and select **'claude.ai LFX BI Layer'** to authenticate. Once done, say 'continue' and I'll proceed. Or say 'skip' to use the manual query approach instead." + + Wait for the user's response. If they authenticate: re-check with `ToolSearch` and proceed to Step 1. If they skip or authentication still fails: proceed directly to Step 2 for all tables. + +### Step 1 — BI Layer lookup (always attempt first) + +1. Call `mcp__claude_ai_LFX_BI_Layer__get_all_sources`. This returns all registered dbt sources. +2. For each user-provided table, search the results (case-insensitive match on the `name` field). +3. For each table **found**: call `mcp__claude_ai_LFX_BI_Layer__get_source_details` using the `uniqueId` from Step 1. Extract the column registry from `catalog.columns`: each entry has `name`, `type`, and `index` (ordinal position). +4. For each table **not found** in dbt sources: inform the user and fall back to Step 2 for that table only. + +### Step 2 — Manual fallback (only for tables not in dbt sources) + +Generate the ACCOUNT_USAGE query for the missing tables and ask the user to run it: + +```sql +SELECT DISTINCT + table_catalog, + table_schema, + table_name, + column_name, + data_type, + is_nullable, + ordinal_position +FROM SNOWFLAKE.ACCOUNT_USAGE.COLUMNS +WHERE (table_catalog, table_schema, table_name) IN ( + ('DB1', 'SCHEMA1', 'TABLE1') + -- one row per missing table +) +AND DELETED IS NULL +ORDER BY table_name, ordinal_position; +``` + +Ask the user to paste the output or provide a path to an exported file (CSV, JSON, or TSV). If a file path is given, read the file. + +### Column registry + +After Step 1 and/or Step 2, build a column registry per table: +- Column name (exact casing from schema — this is the reference for all code) +- Data type +- Ordinal position + +**Store this as the canonical column reference. Every column name used in generated code must appear in this registry. Never assume or invent a column name.** + +**Flag non-VARCHAR column types** (e.g., `DATE`, `TIME`, `TIMESTAMP_TZ`, `BOOLEAN`, `NUMBER`) — these arrive as native JS types from the Parquet reader, not strings (see touch point 9 rules). + +For each JOIN table, check whether any existing transformer in `services/apps/snowflake_connectors/src/integrations/` queries from the same table. If yes, inherit its column mappings; if no, treat every column as unknown and derive it from sample data in the Pre-Analysis step below. + +### Step 3 — Sample data + +Attempt to retrieve sample data via the LFX BI Layer before asking the user to run a manual query. + +#### Option A — query_metrics (attempt first) + +1. Call `mcp__claude_ai_LFX_BI_Layer__list_metrics`. Search the results for metrics whose name or description relates to the platform/source (e.g., `total_committees`, `total_committee_members`, `total_enrollments`). +2. For each relevant metric found, call `mcp__claude_ai_LFX_BI_Layer__get_dimensions` to list available dimensions. +3. Select all categorical dimensions that are likely to correspond to identity, org, or activity fields (email, username, name, status, type, date, project_slug, etc.). +4. Call `mcp__claude_ai_LFX_BI_Layer__query_metrics` with those dimensions and `limit: 20`. +5. Use the returned rows as sample data for Pre-Analysis. + +**Important caveat:** `query_metrics` queries semantic/transformed dbt models, not raw source tables. Column names in the result come from the semantic model and may differ from raw schema column names. Use the data to infer field roles and value patterns; map back to raw column names using the schema registry from Step 1–2. + +If no relevant metrics are found, or the returned dimensions don't cover the fields needed for mapping, fall through to Option B. + +#### Option B — manual paste (fallback) + +Generate a sample data query using **only explicit column names from the registry** (never `*`) and ask the user to run it in Snowflake: + +```sql +SELECT + main.col1, main.col2, main.col3, -- all columns from main table registry + j1.col1, j1.col2, -- all columns from each JOIN table + ... +FROM DB.SCHEMA.MAIN_TABLE main +LEFT JOIN DB.SCHEMA.JOIN_TABLE1 j1 ON main.join_key = j1.pk +-- ... all joins +LIMIT 20; +``` + +Ask the user: +> "Please run this query in Snowflake and paste the result or provide a path to the exported file (CSV, JSON, or TSV). I'll use the actual data values to auto-derive column mappings before asking for your confirmation." + +--- + +## Pre-Analysis: Auto-Derive Mappings + +Before asking the user any Phase 3 questions, perform this analysis using the schema registry, the pasted sample data, and existing implementations that query the same tables. + +### Step 1 — Inherit from existing implementations + +For each JOIN table, check if any existing transformer imports or queries from it. If found, inherit those column mappings directly — they are already validated. + +Read the relevant transformer files from `services/apps/snowflake_connectors/src/integrations/` and extract: which column maps to email, username, LFID, org name, org website, domain aliases, timestamp, sourceId. + +### Step 2 — Infer remaining roles from names + data values + +For columns not covered by an existing implementation, apply these heuristics: + +| Role | High-confidence signals | +|------|------------------------| +| Email | Name contains `EMAIL`; values match `x@y.z` pattern | +| Platform username | Name is `USER_NAME`, `USERNAME`, `LOGIN`, `HANDLE`; values are non-email strings | +| LFID | Name contains `LF_USERNAME`, `LFID`, `LF_ID` | +| Timestamp (incremental) | Type is TIMESTAMP; name contains `UPDATED`, `MODIFIED`; nullable=NO | +| sourceId | Name ends in `_ID`; values appear unique in sample | +| Org name | Name contains `ACCOUNT_NAME`, `ORGANIZATION_NAME`, `COMPANY` | +| Org website | Name contains `WEBSITE`, `DOMAIN`, `URL`; values start with `http` | +| Domain aliases | Name contains `DOMAIN_ALIASES`, `ALIASES`; values are comma-separated or array | + +Assign each role a confidence level: +- **HIGH** — column name + data values both match unambiguously +- **MEDIUM** — multiple candidates exist, or name matches but data is ambiguous +- **LOW / UNKNOWN** — no clear match + +### Step 3 — Present consolidated proposal + +Present all HIGH-confidence mappings at once (exception to the one-question-per-message rule — batching is intentional here for efficiency). For MEDIUM, offer choices. For LOW/UNKNOWN, ask open-ended: + +> "Based on the schema and sample data, here's what I identified: +> - **Email**: `USER_EMAIL` (contains email values like `user@example.com`) ✅ +> - **Username**: `USER_NAME` ✅ +> - **LFID**: `LF_USERNAME` ✅ +> - **Timestamp**: `UPDATED_TS` (TIMESTAMP_NTZ, not nullable) ✅ +> - **sourceId**: `REGISTRATION_ID` (appears unique in sample) ✅ +> - **Org website**: `ORG_WEBSITE` ✅ +> - **Org name**: `ACCOUNT_NAME` or `COMPANY_NAME`? (both present) +> - **Domain aliases format**: couldn't determine — comma-separated string or array? +> +> Please confirm the ✅ items and resolve the open questions." + +After the user responds, record all confirmed mappings. **Skip any Phase 3 sub-step whose mappings are fully resolved here.** Only enter a sub-step for fields that remain MEDIUM, LOW, or flagged by the user. + +--- + +## Phase 3: Confirm / Correct Mappings + +**Rule:** Skip sub-sections where Pre-Analysis fully resolved the mapping. For remaining sub-sections, use the propose-then-confirm pattern — never ask open-ended questions when a proposal can be made. Present choices when multiple candidates exist. Ask open-ended only when no inference is possible. + +**When raising any ambiguity, always include:** +1. **How existing data sources handle it** — read the relevant transformer(s) from `services/apps/snowflake_connectors/src/integrations/` and show the pattern. If an existing source skips or doesn't implement the feature, say so explicitly (e.g., "TNC doesn't use a logo field — it's left undefined"). +2. **A Snowflake query to resolve it** — if the ambiguity can be answered by inspecting actual data (e.g., checking uniqueness, null rate, value distribution), provide the query so the user can run it instead of guessing. + +When raising any ambiguity that can be resolved by inspecting data, first try `mcp__claude_ai_LFX_BI_Layer__query_metrics` with the relevant dimensions and a `where` filter to narrow results. Only ask the user to run a manual Snowflake query if the semantic layer doesn't have dimensions covering the ambiguous columns. + +Example format for an ambiguity: +> "I see two timestamp columns: `CREATED_AT` (nullable) and `UPDATED_AT` (not nullable). +> - Existing sources (TNC, CVENT) all use a non-nullable `updated_ts`-style column for incremental exports. +> - To check null rates, run: +> ```sql +> SELECT COUNT(*) total, COUNT(CREATED_AT) created_not_null, COUNT(UPDATED_AT) updated_not_null FROM DB.SCHEMA.TABLE LIMIT 10000; +> ``` +> Which column should be used as the incremental timestamp?" + +--- + +### 3a. Identity Mapping + +**Rule:** Every member record must produce at least one identity with `type: MemberIdentityType.USERNAME`. The standard approach is to use the unified `buildMemberIdentities()` method on `TransformerBase` (added in the identity deduplication refactor). Only fall back to inline identity construction if the user explicitly requests it and can justify why the unified method cannot be used (e.g., fundamentally different identity shape not covered by the method's logic). + +The unified method covers the standard fallback chain automatically. Full behavior by case: + +| `platformUsername` | `lfUsername` | Identities produced | +|---|---|---| +| null | null | EMAIL(platform) + USERNAME(platform, email) | +| set | null | EMAIL(platform) + USERNAME(platform, platformUsername) | +| null | set | EMAIL(platform) + USERNAME(LFID, lfUsername) + USERNAME(platform, lfUsername) | +| set | set | EMAIL(platform) + USERNAME(platform, platformUsername) + USERNAME(LFID, lfUsername) | + +**Critical:** Never pass `lfUsername` as `platformUsername`. When a source only has an LFID column (no platform-native username), pass `platformUsername: null` — the lfUsername-only path (row 3 above) already produces the correct USERNAME identity for the platform using the lfUsername value. + +If Pre-Analysis resolved email, platformUsername, and LFID columns with HIGH confidence and the user confirmed them, skip to the summary step below. + +For any unresolved identity field, use this pattern: +- **Multiple candidates found**: "I see columns `A` and `B` that could be the email — which one?" (present choices, not open-ended) +- **One candidate found**: "I believe `USER_EMAIL` is the email column based on its values. Confirm?" (one-tap confirmation) +- **No candidate found**: "I couldn't identify an email column — please specify." + +For each confirmed identity column also confirm: +- `verified: true`? (default yes — ask only if the data suggests otherwise) +- `verifiedBy` value (default: platform type — propose it, don't ask open-ended) + +**Critical:** If a JOIN table for users is NOT the same table used by an existing implementation, validate every column explicitly regardless of Pre-Analysis confidence. Column name heuristics alone are not sufficient for unknown tables. + +After all identity fields are confirmed, summarize how `buildMemberIdentities()` will be called and ask: +> "Here is how identities will be built: +> `this.buildMemberIdentities({ email, sourceId: [col or null], platformUsername: [col or null], lfUsername: [col or null] })` +> Does this look correct?" + +--- + +### 3b. Organization Mapping + +If Pre-Analysis determined there is no org data (no org-related columns found in any table): before asking the user, first read existing transformers in `services/apps/snowflake_connectors/src/integrations/` to check whether any of them join an org table using a key that also exists in the user's tables. If a match is found, prompt the user: +> "I don't see org columns in the tables you provided, but [EXISTING_PLATFORM] sources org data from `{ORG_TABLE}` via `{join_key}` — which also appears in your table. Did you mean to include this? (Recommended)" + +If no existing pattern is joinable, ask: "I don't see any org columns. Does this source have org/company data?" — if yes, ask for the table; if no, skip to 3c. + +If Pre-Analysis identified org columns: + +- If sourced from a JOIN table already used by an existing implementation: read that transformer's `buildOrganizations` method and show the user exactly which columns will be used. Ask: "The org mapping follows the existing [PLATFORM] implementation — does this look correct?" +- If new org table: present the Pre-Analysis proposals for each field, one at a time: + - "Org name: I see `ACCOUNT_NAME` (values like `Acme Corp`). Confirm, or specify another column." + - "Website: I see `ORG_WEBSITE` (values start with `http`). Confirm?" + - "Domain aliases: I see `DOMAIN_ALIASES` — are these comma-separated strings or an array?" + - "Logo URL: I see `LOGO_URL`. Confirm, or is there no logo column?" + - "Industry: I see `ORGANIZATION_INDUSTRY`. Confirm?" + - "Size: I see `ORGANIZATION_SIZE`. Confirm?" + +**Critical — `isIndividualNoAccount`:** +Read `services/apps/snowflake_connectors/src/core/transformerBase.ts` to find `isIndividualNoAccount`. The generated code MUST call this method rather than reimplementing it. Show the user the method signature and confirm it will be used identically to existing sources. + +After all org columns are confirmed, summarize and ask for confirmation before proceeding. + +--- + +### 3c. Activity Types + +**Rule:** Activity type names and scores come entirely from the user. Do not suggest them. + +Ask: +> "Please list all activity types this source can produce. For each, provide: +> - A short name (e.g., `enrolled-certification`) +> - A score from 1–10 +> +> I'll suggest the enum key, label, description, and flags for each one." + +For each activity type the user provides, suggest the following **one at a time**, waiting for approval before moving to the next: + +| Field | Suggestion rule | +|-------|----------------| +| Enum key | SCREAMING_SNAKE_CASE version of the name (e.g., `ENROLLED_CERTIFICATION`) | +| String value | The name the user provided (kebab-case) | +| Label | Human-readable (e.g., `Enrolled in certification`) | +| Description | One sentence describing the event (follow the style in `backend/src/database/migrations/V1771497876__addCventActivityTypes.sql` and `V1772556158__addTncActivityTypes.sql`) | +| `isCodeContribution` | `false` unless it involves code (check existing platforms — almost always false for non-GitHub sources) | +| `isCollaboration` | `false` unless it is a collaborative activity | + +Ask: "Does this look correct for `{type_name}`? Any changes?" before moving to the next type. + +--- + +### 3d. Timestamp & sourceId + +If Pre-Analysis already proposed these with HIGH confidence, present them with this explanation before asking for confirmation (never skip the explanation — it ensures the user understands the criticality): + +> "The **timestamp column** drives all incremental exports — records updated after the last export run are re-fetched using this column. It must never be null. The **sourceId** is the deduplication key — two records with the same sourceId produce only one activity. It must uniquely identify each logical event." + +Then confirm the Pre-Analysis proposals: + +1. Timestamp: "I identified `UPDATED_TS` (TIMESTAMP_NTZ, not nullable) as the incremental timestamp. Confirm?" + - If the proposed column is nullable=Y: "⚠️ `UPDATED_TS` is marked nullable in the schema. Can it actually be null in practice? If yes, we need an alternative or a `WHERE col IS NOT NULL` guard." Wait for resolution. + - If no candidate was found: "I couldn't identify an update timestamp column — please specify. Valid examples: `updated_ts`, `enrolled_at`, `created_at`." + +2. sourceId: "I identified `REGISTRATION_ID` as the sourceId (appears unique in sample). Confirm, and is this guaranteed unique per logical event (not just per user)?" + - If no candidate was found: "I couldn't identify a unique record ID — please specify which column to use as `sourceId`." + +--- + +### 3e. Base Class Check + +- If this is the **first and only source** for this platform: no base class — `transformer.ts` extends `TransformerBase` directly and contains all logic inline (CVENT pattern). Do not propose a base class. + +- If the platform **will have multiple sources** (user confirmed more sources are planned or already exist): + - Check if `services/apps/snowflake_connectors/src/integrations/{platform}/{platform}TransformerBase.ts` exists. + - If yes: new transformer extends it. Confirm with user. + - If no and shared org logic exists: propose creating the base class. Show proposed structure (modeled on `tncTransformerBase.ts`). Ask for confirmation before creating. + +--- + +### 3f. Project Slug for Testing + +Ask: +> "Please provide a `project_slug` from CDP_MATCHED_SEGMENTS that has data in this Snowflake table. It should have a moderate number of records (ideally under a few thousand) and ideally cover all the activity types we're implementing. This slug will be used to restrict the test query and for the staging non-prod guard in `buildSourceQuery`." + +--- + +## Phase 4: File Generation & Review + +Before generating any files, ask the user: +> "How would you like me to proceed with the implementation? +> - **A — Review mode:** I show each file before writing it, you approve or request changes. +> - **B — Auto mode:** I implement all files directly, then show a summary of everything written for a final review." + +Proceed based on the user's choice. Read each target file before modifying it in both modes. + +--- + +### Structural Files (touch points 0–7) + +These are template-filled from confirmed inputs. No business logic reasoning required. + +**Touch point 0 — `platforms.ts`** (only if new platform) + +Add `{PLATFORM} = '{platform-string}',` to the `PlatformType` enum in alphabetical order among the non-standard entries. + +--- + +**Touch point 1 — `organizations.ts`** + +File: `services/libs/types/src/enums/organizations.ts` + +Add to both enums: +- `OrganizationSource.{PLATFORM} = '{platform-string}'` +- `OrganizationAttributeSource.{PLATFORM} = '{platform-string}'` + +--- + +**Touch point 2 — `{platform}/types.ts`** (new file) + +File: `services/libs/integrations/src/integrations/{platform}/types.ts` + +Template (fill with confirmed activity types and scores): + +```typescript +import { IActivityScoringGrid } from '@crowd/types' + +export enum {Platform}ActivityType { + {ENUM_KEY} = '{string-value}', + // ... one entry per confirmed activity type +} + +export const {PLATFORM}_GRID: Record<{Platform}ActivityType, IActivityScoringGrid> = { + [{Platform}ActivityType.{ENUM_KEY}]: { score: {score} }, + // ... one entry per confirmed activity type +} +``` + +--- + +**Touch point 3 — `integrations/index.ts`** + +File: `services/libs/integrations/src/integrations/index.ts` + +Add before `export * from './activityDisplayService'`: +```typescript +export * from './{platform}/types' +``` + +--- + +**Touch point 4 — `attributesConfig.ts`** + +File: `services/libs/data-access-layer/src/organizations/attributesConfig.ts` + +Add `OrganizationAttributeSource.{PLATFORM},` to the `ORG_DB_ATTRIBUTE_SOURCE_PRIORITY` array, positioned after the most recently added snowflake platform (currently after `OrganizationAttributeSource.TNC`). + +--- + +**Touch point 5 — Flyway migration** (two new files) + +Use a 10-digit second-epoch for the timestamp — run `date +%s` in terminal. + +**File A:** `backend/src/database/migrations/V{epoch_seconds}__add{Platform}ActivityTypes.sql` + +```sql +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration", description, "label") VALUES +('{string-value}', '{platform-string}', {false|true}, {false|true}, '{description}', '{label}'), +-- ... one row per confirmed activity type, last row without trailing comma +('{string-value}', '{platform-string}', {false|true}, {false|true}, '{description}', '{label}'); +``` + +**File B:** `backend/src/database/migrations/U{epoch_seconds}__add{Platform}ActivityTypes.sql` + +Empty file — same timestamp and name as File A, prefix `U` instead of `V`. Matches the pattern used by existing migrations (e.g., `U1771497876__addCventActivityTypes.sql`). + +--- + +**Touch point 6 — `snowflake_connectors/types.ts`** + +File: `services/apps/snowflake_connectors/src/integrations/types.ts` + +Add to `DataSourceName` enum: +```typescript +{PLATFORM}_{SOURCE} = '{source-name}', +``` + +--- + +**Touch point 7 — `snowflake_connectors/index.ts`** + +File: `services/apps/snowflake_connectors/src/integrations/index.ts` + +Add import at top: +```typescript +import { buildSourceQuery as {platform}{Source}BuildQuery } from './{platform}/{source}/buildSourceQuery' +import { {Platform}{Source}Transformer } from './{platform}/{source}/transformer' +``` + +Add to `supported` object under the platform key (create the key if new platform): +```typescript +[PlatformType.{PLATFORM}]: { + sources: [ + { + name: DataSourceName.{PLATFORM}_{SOURCE}, + buildSourceQuery: {platform}{Source}BuildQuery, + transformer: new {Platform}{Source}Transformer(), + }, + ], +}, +``` + +If the platform already exists, append to its `sources` array instead of creating a new key. + +--- + +### Business Logic Files (touch points 8–10) + +These are AI-generated from the confirmed column mappings. Apply all rules strictly. + +--- + +**Touch point 8 — `buildSourceQuery.ts`** (new file) + +File: `services/apps/snowflake_connectors/src/integrations/{platform}/{source}/buildSourceQuery.ts` + +**Rules (enforced — do not deviate):** +- Use explicit column names only. Do not use `table.*` or `table.* EXCLUDE (...)` in new implementations — existing sources (TNC, CVENT) use these patterns but new sources should list columns explicitly to avoid parquet encoding/decoding issues +- If any TIMESTAMP_TZ columns exist in the schema, exclude and re-cast them as TIMESTAMP_NTZ (see CVENT pattern) +- Do not concatenate or transform date/time columns in SQL — keep them as separate columns and let the transformer handle type coercion (see touch point 9 rules) +- Follow the CTE structure: + 1. `org_accounts` CTE (if org data present) + 2. `CDP_MATCHED_SEGMENTS` CTE (always) + 3. `new_cdp_segments` CTE (inside the `sinceTimestamp` branch only) +- Incremental pattern: when `sinceTimestamp` is provided, UNION two queries: + - Records where the confirmed timestamp column `> sinceTimestamp` + - Records in newly created segments (using `new_cdp_segments`) +- Deduplication: `QUALIFY ROW_NUMBER() OVER (PARTITION BY {sourceId_column} ORDER BY {dedup_key} DESC) = 1` +- Non-prod guard: `if (!IS_PROD_ENV) { select += \` AND {table}.{project_slug_column} = '{confirmed_project_slug}'\` }` +- All column names must match the confirmed schema registry exactly + +Show the full generated file and ask for confirmation before writing. + +--- + +**Touch point 9 — `transformer.ts`** (new file) + +File: `services/apps/snowflake_connectors/src/integrations/{platform}/{source}/transformer.ts` + +**Rules (enforced — do not deviate):** + +- **Parquet type coercion — never blindly cast `row.COLUMN as string`.** Snowflake types may arrive as native JS types after Parquet decoding (e.g., `DATE` → `Date` object, `TIME` → `number` in ms, `BOOLEAN` → `boolean`). Always check the Snowflake column type from the schema registry and handle the actual JS type the Parquet reader delivers — do not assume every column is a string. +- All string comparisons must be case-insensitive: use `.toLowerCase()` on both sides of comparison only; preserve the original value in the output +- No broad `else` statements — every branch must have an explicit condition +- All column names referenced in code must exactly match the schema registry — never assumed +- **Identity building — always use `this.buildMemberIdentities()` first (preferred):** + - Call `this.buildMemberIdentities({ email, sourceId, platformUsername, lfUsername })` from `TransformerBase` + - Always pass all 4 arguments explicitly, even when the value is `null` or `undefined` — never omit an argument + - `sourceId` = the user ID column from the source table (`undefined` if the table has no user ID column) + - `platformUsername` = the platform-native username column (`null` if absent — do NOT substitute `lfUsername` here) + - `lfUsername` = the LFID column value (`null` if absent) + - **Never pass `lfUsername` as `platformUsername`** — when a source only has an LFID column, pass `platformUsername: null`; the method's lfUsername-only path already produces the correct platform USERNAME from the lfUsername value + - The method handles the full fallback chain automatically (see the 4-case table in §3a) + - Do NOT import `IMemberData` or `MemberIdentityType` in the transformer — those are only needed if falling back to inline construction + - **Only use inline identity construction if the user explicitly requests it and justifies why `buildMemberIdentities()` cannot be used** (e.g., non-standard identity shape not covered by the method). Document the justification in a comment. +- `isIndividualNoAccount` must call `this.isIndividualNoAccount(displayName)` from `TransformerBase` — never reimplement +- **Do not set member attributes** (e.g., `MemberAttributeName.JOB_TITLE`, `AVATAR_URL`, `COUNTRY`) unless: (a) the user explicitly requested them, or (b) the same table and column are already used for that attribute in an existing implementation — in which case follow the existing pattern exactly +- Extends the platform base class if one was confirmed in Phase 3e; otherwise extends `TransformerBase` directly +- If in doubt about any mapping: ask the user before writing + +Show the full generated file and ask for confirmation before writing. + +--- + +**Touch point 10 — `{platform}TransformerBase.ts`** (new file, only if platform has multiple sources AND shared org logic — confirmed in Phase 3e) + +File: `services/apps/snowflake_connectors/src/integrations/{platform}/{platform}TransformerBase.ts` + +Model on `tncTransformerBase.ts`. Contains only the shared `buildOrganizations` logic. Abstract class that extends `TransformerBase` and sets `readonly platform = PlatformType.{PLATFORM}`. + +If the platform has a single source, skip this file entirely — keep all logic in `transformer.ts` (CVENT pattern). + +Show the full generated file and ask for confirmation before writing. + +--- + +## Phase 5: Testing + +After all files are written, provide the user with a test query: + +Take the `buildSourceQuery` output (without `sinceTimestamp`, so the full-load variant), substitute the non-prod project_slug guard with the confirmed `project_slug`, and add `LIMIT 100`: + +```sql +-- Example shape (exact SQL comes from the generated buildSourceQuery): +WITH org_accounts AS (...), +cdp_matched_segments AS (...) +SELECT + -- all explicit columns +FROM {table} t +INNER JOIN cdp_matched_segments cms ON ... +WHERE t.{project_slug_column} = '{confirmed_project_slug}' +QUALIFY ROW_NUMBER() OVER (...) = 1 +LIMIT 100; +``` + +Instruct the user: +> "Please run this query directly in Snowflake and paste the result (JSON or CSV). I'll walk through each row and verify the transformer logic produces the expected `IActivityData` before we consider this done." + +Note: the LFX BI Layer MCP does not support arbitrary SQL execution — the test query must be run manually in the Snowflake UI. + +### Dry-Run Validation + +When the user pastes results, for each row: +- Apply transformer logic in-chat (show inputs → outputs) +- Show the resulting `IActivityData` + segment slug +- Flag immediately: null email, missing USERNAME identity, unexpected activity type, null sourceId, null timestamp +- If any issue found: loop back to the relevant Phase 3 sub-section, fix the mapping, regenerate the affected file + +### Format & Lint + +After all files are written, run format then lint from `services/apps/snowflake_connectors`: + +```bash +cd services/apps/snowflake_connectors +pnpm run format +pnpm run lint +``` + +Fix any errors or warnings and re-run `pnpm run lint` until it reports no complaints. Do not proceed to the completion checklist until lint is clean. + +### Completion Checklist + +Before declaring the implementation complete, verify every item: + +- [ ] All applicable files from the File Inventory written to disk +- [ ] No `table.*` or `table.* EXCLUDE (...)` in any generated query +- [ ] No broad `else` in any transformer +- [ ] All string comparisons are case-insensitive (`.toLowerCase()` on both sides of comparison) +- [ ] `sourceId` confirmed unique in test data +- [ ] Timestamp column confirmed never null in test data +- [ ] All activity types present in test data (or user explicitly acknowledged any absent ones) +- [ ] `isIndividualNoAccount` behavior verified matches existing sources + +--- + +## Anti-Hallucination Rules + +These apply throughout the entire skill execution. They are non-negotiable. + +1. **Never assume a column exists.** Every column reference must come from the schema registry or confirmed JOIN table schema. Pre-Analysis proposals are evidence-based (column names + sample values) — not assumed — but they still require user confirmation before being used in generated code. +2. **Never assume a JOIN table is the same as an existing one.** If a table has not been verified against an existing implementation, use heuristics from sample data but treat every column mapping as MEDIUM confidence at best — require explicit user confirmation. +3. **Never suggest activity type names or scores.** These come from the user. +4. **Never write a file without showing it first and receiving explicit user approval.** +5. **When in doubt, ask.** A question costs seconds. A wrong implementation costs hours. +6. **One question per message.** Never bundle multiple validations. +7. **Read before modifying.** Read every file before making changes to it. diff --git a/.cursor/rules/git-commit.mdc b/.cursor/rules/git-commit.mdc new file mode 100644 index 0000000000..598d20489f --- /dev/null +++ b/.cursor/rules/git-commit.mdc @@ -0,0 +1,18 @@ +--- +description: git commit format spec +globs: +alwaysApply: false +--- +# Git Commit Format + +Always use git commit -s to sign off on the commit +Format: `(scope): ` + +Types: +- `feat` - New feature +- `fix` - Bug fix +- `docs` - Documentation +- `style` - Formatting +- `refactor` - Code changes +- `test` - Tests +- `chore` - Maintenance \ No newline at end of file diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 1ef147363a..0000000000 --- a/.flake8 +++ /dev/null @@ -1,6 +0,0 @@ -[flake8] -exclude = venv,venv-crowd,venv-temp,.git,__pycache__,build,dist,node_modules,admin -max-complexity = 18 -max-line-length = 120 -ignore = E203, E266, E501, W503, F403, F401 -select = B,C,E,F,W,T4,B9 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 99be9436fb..1fd7c91e2e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,19 +1,18 @@ -# Changes proposed ✍️ +## Summary + + -### What -copilot:summary -​ -copilot:poem +## Changes + +- -### Why +## Type of change +- [ ] Bug fix +- [ ] New feature +- [ ] Refactor / cleanup +- [ ] Performance improvement +- [ ] Chore / dependency update +- [ ] Documentation - -### How -copilot:walkthrough - -## Checklist ✅ -- [ ] Label appropriately with `Feature`, `Improvement`, or `Bug`. -- [ ] Add screenshots to the PR description for relevant FE changes -- [ ] New backend functionality has been unit-tested. -- [ ] API documentation has been updated (if necessary) (see [docs on API documentation](https://docs.crowd.dev/docs/updating-api-documentation)). -- [ ] [Quality standards](https://github.com/CrowdDotDev/crowd-github-test-public/blob/main/CONTRIBUTING.md#quality-standards) are met. +## JIRA ticket + diff --git a/.github/actions/build-docker-image/action.yaml b/.github/actions/build-docker-image/action.yaml index 2e297bed55..f411c29873 100644 --- a/.github/actions/build-docker-image/action.yaml +++ b/.github/actions/build-docker-image/action.yaml @@ -9,7 +9,7 @@ inputs: outputs: image: description: Image that was built - value: crowddotdev/${{ inputs.image }}:${{ steps.version-generator.outputs.IMAGE_VERSION }} + value: sjc.ocir.io/axbydjxa5zuh/${{ inputs.image }}:${{ steps.version-generator.outputs.IMAGE_VERSION }} runs: using: composite @@ -26,7 +26,7 @@ runs: - name: Login to docker repository shell: bash - run: echo ${{ env.DOCKERHUB_PASSWORD }} | docker login -u ${{ env.DOCKERHUB_USERNAME }} --password-stdin + run: echo '${{ env.ORACLE_DOCKER_PASSWORD }}' | docker login sjc.ocir.io -u '${{ env.ORACLE_DOCKER_USERNAME }}' --password-stdin - name: Build and push docker image shell: bash diff --git a/.github/actions/deploy-service/action.yaml b/.github/actions/deploy-service/action.yaml index 922d93eefd..f21c4e915c 100644 --- a/.github/actions/deploy-service/action.yaml +++ b/.github/actions/deploy-service/action.yaml @@ -14,6 +14,16 @@ inputs: description: To which cloud cluster to deploy required: true + prioritized: + description: Is the service listening on prioritized queues? + required: false + default: 'false' + + cloud_env: + description: Which cloud environment are we deploying to? + required: false + default: 'default' + runs: using: composite steps: @@ -25,10 +35,38 @@ runs: AWS_SECRET_ACCESS_KEY: ${{ env.AWS_SECRET_ACCESS_KEY }} AWS_REGION: ${{ env.AWS_REGION }} - - name: Deploy image + - name: Deploy image (non prioritized) + if: inputs.prioritized == 'false' shell: bash run: kubectl set image deployments/${{ inputs.service }}-dpl ${{ inputs.service }}=${{ inputs.image }} + - name: Deploy image (prioritized - production) + if: inputs.prioritized == 'true' && inputs.cloud_env == 'prod' + shell: bash + run: | + kubectl set image deployments/${{ inputs.service }}-system-dpl ${{ inputs.service }}-system=${{ inputs.image }} + kubectl set image deployments/${{ inputs.service }}-normal-dpl ${{ inputs.service }}-normal=${{ inputs.image }} + kubectl set image deployments/${{ inputs.service }}-high-dpl ${{ inputs.service }}-high=${{ inputs.image }} + kubectl set image deployments/${{ inputs.service }}-urgent-dpl ${{ inputs.service }}-urgent=${{ inputs.image }} + + - name: Deploy image (prioritized - lfx production) + if: inputs.prioritized == 'true' && inputs.cloud_env == 'lfx_prod' + shell: bash + run: | + kubectl set image deployments/${{ inputs.service }}-system-dpl ${{ inputs.service }}-system=${{ inputs.image }} + kubectl set image deployments/${{ inputs.service }}-normal-dpl ${{ inputs.service }}-normal=${{ inputs.image }} + kubectl set image deployments/${{ inputs.service }}-high-dpl ${{ inputs.service }}-high=${{ inputs.image }} + + - name: Deploy image (prioritized - staging) + if: inputs.prioritized == 'true' && inputs.cloud_env == 'staging' + shell: bash + run: kubectl set image deployments/${{ inputs.service }}-normal-dpl ${{ inputs.service }}-normal=${{ inputs.image }} + + - name: Deploy image (prioritized - lfx staging) + if: inputs.prioritized == 'true' && inputs.cloud_env == 'lfx_staging' + shell: bash + run: kubectl set image deployments/${{ inputs.service }}-normal-dpl ${{ inputs.service }}-normal=${{ inputs.image }} + - uses: ./.github/actions/slack-notify with: message: 'Service *${{ inputs.service }}* was just deployed using docker image `${{ inputs.image }}`' diff --git a/services/apps/template_consumer/.eslintrc.cjs b/.github/actions/node/.eslintrc.cjs similarity index 100% rename from services/apps/template_consumer/.eslintrc.cjs rename to .github/actions/node/.eslintrc.cjs diff --git a/.github/actions/node/.gitignore b/.github/actions/node/.gitignore new file mode 100644 index 0000000000..dc8cdbdf62 --- /dev/null +++ b/.github/actions/node/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +out/ \ No newline at end of file diff --git a/services/apps/template_consumer/.prettierignore b/.github/actions/node/.prettierignore similarity index 100% rename from services/apps/template_consumer/.prettierignore rename to .github/actions/node/.prettierignore diff --git a/services/apps/template_consumer/.prettierrc.cjs b/.github/actions/node/.prettierrc.cjs similarity index 100% rename from services/apps/template_consumer/.prettierrc.cjs rename to .github/actions/node/.prettierrc.cjs diff --git a/.github/actions/node/builder/action.yaml b/.github/actions/node/builder/action.yaml new file mode 100644 index 0000000000..a2178325a4 --- /dev/null +++ b/.github/actions/node/builder/action.yaml @@ -0,0 +1,20 @@ +name: Deployer +description: Custom Deployer Action + +inputs: + services: + description: Services to deploy + required: true + tag: + description: Tag for images + required: false + default: ${{ github.sha }} + steps: + description: Steps to run + required: false + default: build push deploy + +runs: + using: node16 + main: index.js + post: index.js diff --git a/.github/actions/node/builder/index.js b/.github/actions/node/builder/index.js new file mode 100644 index 0000000000..851a9ae7f4 --- /dev/null +++ b/.github/actions/node/builder/index.js @@ -0,0 +1,28429 @@ +/******/ (() => { // webpackBootstrap +/******/ var __webpack_modules__ = ({ + +/***/ 4965: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.issue = exports.issueCommand = void 0; +const os = __importStar(__nccwpck_require__(857)); +const utils_1 = __nccwpck_require__(6425); +/** + * Commands + * + * Command Format: + * ::name key=value,key=value::message + * + * Examples: + * ::warning::This is the message + * ::set-env name=MY_VAR::some value + */ +function issueCommand(command, properties, message) { + const cmd = new Command(command, properties, message); + process.stdout.write(cmd.toString() + os.EOL); +} +exports.issueCommand = issueCommand; +function issue(name, message = '') { + issueCommand(name, {}, message); +} +exports.issue = issue; +const CMD_STRING = '::'; +class Command { + constructor(command, properties, message) { + if (!command) { + command = 'missing.command'; + } + this.command = command; + this.properties = properties; + this.message = message; + } + toString() { + let cmdStr = CMD_STRING + this.command; + if (this.properties && Object.keys(this.properties).length > 0) { + cmdStr += ' '; + let first = true; + for (const key in this.properties) { + if (this.properties.hasOwnProperty(key)) { + const val = this.properties[key]; + if (val) { + if (first) { + first = false; + } + else { + cmdStr += ','; + } + cmdStr += `${key}=${escapeProperty(val)}`; + } + } + } + } + cmdStr += `${CMD_STRING}${escapeData(this.message)}`; + return cmdStr; + } +} +function escapeData(s) { + return (0, utils_1.toCommandValue)(s) + .replace(/%/g, '%25') + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A'); +} +function escapeProperty(s) { + return (0, utils_1.toCommandValue)(s) + .replace(/%/g, '%25') + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A') + .replace(/:/g, '%3A') + .replace(/,/g, '%2C'); +} +//# sourceMappingURL=command.js.map + +/***/ }), + +/***/ 8185: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.platform = exports.toPlatformPath = exports.toWin32Path = exports.toPosixPath = exports.markdownSummary = exports.summary = exports.getIDToken = exports.getState = exports.saveState = exports.group = exports.endGroup = exports.startGroup = exports.info = exports.notice = exports.warning = exports.error = exports.debug = exports.isDebug = exports.setFailed = exports.setCommandEcho = exports.setOutput = exports.getBooleanInput = exports.getMultilineInput = exports.getInput = exports.addPath = exports.setSecret = exports.exportVariable = exports.ExitCode = void 0; +const command_1 = __nccwpck_require__(4965); +const file_command_1 = __nccwpck_require__(1032); +const utils_1 = __nccwpck_require__(6425); +const os = __importStar(__nccwpck_require__(857)); +const path = __importStar(__nccwpck_require__(6928)); +const oidc_utils_1 = __nccwpck_require__(1791); +/** + * The code to exit an action + */ +var ExitCode; +(function (ExitCode) { + /** + * A code indicating that the action was successful + */ + ExitCode[ExitCode["Success"] = 0] = "Success"; + /** + * A code indicating that the action was a failure + */ + ExitCode[ExitCode["Failure"] = 1] = "Failure"; +})(ExitCode || (exports.ExitCode = ExitCode = {})); +//----------------------------------------------------------------------- +// Variables +//----------------------------------------------------------------------- +/** + * Sets env variable for this action and future actions in the job + * @param name the name of the variable to set + * @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function exportVariable(name, val) { + const convertedVal = (0, utils_1.toCommandValue)(val); + process.env[name] = convertedVal; + const filePath = process.env['GITHUB_ENV'] || ''; + if (filePath) { + return (0, file_command_1.issueFileCommand)('ENV', (0, file_command_1.prepareKeyValueMessage)(name, val)); + } + (0, command_1.issueCommand)('set-env', { name }, convertedVal); +} +exports.exportVariable = exportVariable; +/** + * Registers a secret which will get masked from logs + * @param secret value of the secret + */ +function setSecret(secret) { + (0, command_1.issueCommand)('add-mask', {}, secret); +} +exports.setSecret = setSecret; +/** + * Prepends inputPath to the PATH (for this action and future actions) + * @param inputPath + */ +function addPath(inputPath) { + const filePath = process.env['GITHUB_PATH'] || ''; + if (filePath) { + (0, file_command_1.issueFileCommand)('PATH', inputPath); + } + else { + (0, command_1.issueCommand)('add-path', {}, inputPath); + } + process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`; +} +exports.addPath = addPath; +/** + * Gets the value of an input. + * Unless trimWhitespace is set to false in InputOptions, the value is also trimmed. + * Returns an empty string if the value is not defined. + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns string + */ +function getInput(name, options) { + const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || ''; + if (options && options.required && !val) { + throw new Error(`Input required and not supplied: ${name}`); + } + if (options && options.trimWhitespace === false) { + return val; + } + return val.trim(); +} +exports.getInput = getInput; +/** + * Gets the values of an multiline input. Each value is also trimmed. + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns string[] + * + */ +function getMultilineInput(name, options) { + const inputs = getInput(name, options) + .split('\n') + .filter(x => x !== ''); + if (options && options.trimWhitespace === false) { + return inputs; + } + return inputs.map(input => input.trim()); +} +exports.getMultilineInput = getMultilineInput; +/** + * Gets the input value of the boolean type in the YAML 1.2 "core schema" specification. + * Support boolean input list: `true | True | TRUE | false | False | FALSE` . + * The return value is also in boolean type. + * ref: https://yaml.org/spec/1.2/spec.html#id2804923 + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns boolean + */ +function getBooleanInput(name, options) { + const trueValue = ['true', 'True', 'TRUE']; + const falseValue = ['false', 'False', 'FALSE']; + const val = getInput(name, options); + if (trueValue.includes(val)) + return true; + if (falseValue.includes(val)) + return false; + throw new TypeError(`Input does not meet YAML 1.2 "Core Schema" specification: ${name}\n` + + `Support boolean input list: \`true | True | TRUE | false | False | FALSE\``); +} +exports.getBooleanInput = getBooleanInput; +/** + * Sets the value of an output. + * + * @param name name of the output to set + * @param value value to store. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function setOutput(name, value) { + const filePath = process.env['GITHUB_OUTPUT'] || ''; + if (filePath) { + return (0, file_command_1.issueFileCommand)('OUTPUT', (0, file_command_1.prepareKeyValueMessage)(name, value)); + } + process.stdout.write(os.EOL); + (0, command_1.issueCommand)('set-output', { name }, (0, utils_1.toCommandValue)(value)); +} +exports.setOutput = setOutput; +/** + * Enables or disables the echoing of commands into stdout for the rest of the step. + * Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set. + * + */ +function setCommandEcho(enabled) { + (0, command_1.issue)('echo', enabled ? 'on' : 'off'); +} +exports.setCommandEcho = setCommandEcho; +//----------------------------------------------------------------------- +// Results +//----------------------------------------------------------------------- +/** + * Sets the action status to failed. + * When the action exits it will be with an exit code of 1 + * @param message add error issue message + */ +function setFailed(message) { + process.exitCode = ExitCode.Failure; + error(message); +} +exports.setFailed = setFailed; +//----------------------------------------------------------------------- +// Logging Commands +//----------------------------------------------------------------------- +/** + * Gets whether Actions Step Debug is on or not + */ +function isDebug() { + return process.env['RUNNER_DEBUG'] === '1'; +} +exports.isDebug = isDebug; +/** + * Writes debug message to user log + * @param message debug message + */ +function debug(message) { + (0, command_1.issueCommand)('debug', {}, message); +} +exports.debug = debug; +/** + * Adds an error issue + * @param message error issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. + */ +function error(message, properties = {}) { + (0, command_1.issueCommand)('error', (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message); +} +exports.error = error; +/** + * Adds a warning issue + * @param message warning issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. + */ +function warning(message, properties = {}) { + (0, command_1.issueCommand)('warning', (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message); +} +exports.warning = warning; +/** + * Adds a notice issue + * @param message notice issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. + */ +function notice(message, properties = {}) { + (0, command_1.issueCommand)('notice', (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message); +} +exports.notice = notice; +/** + * Writes info to log with console.log. + * @param message info message + */ +function info(message) { + process.stdout.write(message + os.EOL); +} +exports.info = info; +/** + * Begin an output group. + * + * Output until the next `groupEnd` will be foldable in this group + * + * @param name The name of the output group + */ +function startGroup(name) { + (0, command_1.issue)('group', name); +} +exports.startGroup = startGroup; +/** + * End an output group. + */ +function endGroup() { + (0, command_1.issue)('endgroup'); +} +exports.endGroup = endGroup; +/** + * Wrap an asynchronous function call in a group. + * + * Returns the same type as the function itself. + * + * @param name The name of the group + * @param fn The function to wrap in the group + */ +function group(name, fn) { + return __awaiter(this, void 0, void 0, function* () { + startGroup(name); + let result; + try { + result = yield fn(); + } + finally { + endGroup(); + } + return result; + }); +} +exports.group = group; +//----------------------------------------------------------------------- +// Wrapper action state +//----------------------------------------------------------------------- +/** + * Saves state for current action, the state can only be retrieved by this action's post job execution. + * + * @param name name of the state to store + * @param value value to store. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function saveState(name, value) { + const filePath = process.env['GITHUB_STATE'] || ''; + if (filePath) { + return (0, file_command_1.issueFileCommand)('STATE', (0, file_command_1.prepareKeyValueMessage)(name, value)); + } + (0, command_1.issueCommand)('save-state', { name }, (0, utils_1.toCommandValue)(value)); +} +exports.saveState = saveState; +/** + * Gets the value of an state set by this action's main execution. + * + * @param name name of the state to get + * @returns string + */ +function getState(name) { + return process.env[`STATE_${name}`] || ''; +} +exports.getState = getState; +function getIDToken(aud) { + return __awaiter(this, void 0, void 0, function* () { + return yield oidc_utils_1.OidcClient.getIDToken(aud); + }); +} +exports.getIDToken = getIDToken; +/** + * Summary exports + */ +var summary_1 = __nccwpck_require__(824); +Object.defineProperty(exports, "summary", ({ enumerable: true, get: function () { return summary_1.summary; } })); +/** + * @deprecated use core.summary + */ +var summary_2 = __nccwpck_require__(824); +Object.defineProperty(exports, "markdownSummary", ({ enumerable: true, get: function () { return summary_2.markdownSummary; } })); +/** + * Path exports + */ +var path_utils_1 = __nccwpck_require__(3397); +Object.defineProperty(exports, "toPosixPath", ({ enumerable: true, get: function () { return path_utils_1.toPosixPath; } })); +Object.defineProperty(exports, "toWin32Path", ({ enumerable: true, get: function () { return path_utils_1.toWin32Path; } })); +Object.defineProperty(exports, "toPlatformPath", ({ enumerable: true, get: function () { return path_utils_1.toPlatformPath; } })); +/** + * Platform utilities exports + */ +exports.platform = __importStar(__nccwpck_require__(3901)); +//# sourceMappingURL=core.js.map + +/***/ }), + +/***/ 1032: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +// For internal use, subject to change. +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.prepareKeyValueMessage = exports.issueFileCommand = void 0; +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ +const crypto = __importStar(__nccwpck_require__(6982)); +const fs = __importStar(__nccwpck_require__(9896)); +const os = __importStar(__nccwpck_require__(857)); +const utils_1 = __nccwpck_require__(6425); +function issueFileCommand(command, message) { + const filePath = process.env[`GITHUB_${command}`]; + if (!filePath) { + throw new Error(`Unable to find environment variable for file command ${command}`); + } + if (!fs.existsSync(filePath)) { + throw new Error(`Missing file at path: ${filePath}`); + } + fs.appendFileSync(filePath, `${(0, utils_1.toCommandValue)(message)}${os.EOL}`, { + encoding: 'utf8' + }); +} +exports.issueFileCommand = issueFileCommand; +function prepareKeyValueMessage(key, value) { + const delimiter = `ghadelimiter_${crypto.randomUUID()}`; + const convertedValue = (0, utils_1.toCommandValue)(value); + // These should realistically never happen, but just in case someone finds a + // way to exploit uuid generation let's not allow keys or values that contain + // the delimiter. + if (key.includes(delimiter)) { + throw new Error(`Unexpected input: name should not contain the delimiter "${delimiter}"`); + } + if (convertedValue.includes(delimiter)) { + throw new Error(`Unexpected input: value should not contain the delimiter "${delimiter}"`); + } + return `${key}<<${delimiter}${os.EOL}${convertedValue}${os.EOL}${delimiter}`; +} +exports.prepareKeyValueMessage = prepareKeyValueMessage; +//# sourceMappingURL=file-command.js.map + +/***/ }), + +/***/ 1791: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.OidcClient = void 0; +const http_client_1 = __nccwpck_require__(5313); +const auth_1 = __nccwpck_require__(7047); +const core_1 = __nccwpck_require__(8185); +class OidcClient { + static createHttpClient(allowRetry = true, maxRetry = 10) { + const requestOptions = { + allowRetries: allowRetry, + maxRetries: maxRetry + }; + return new http_client_1.HttpClient('actions/oidc-client', [new auth_1.BearerCredentialHandler(OidcClient.getRequestToken())], requestOptions); + } + static getRequestToken() { + const token = process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN']; + if (!token) { + throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_TOKEN env variable'); + } + return token; + } + static getIDTokenUrl() { + const runtimeUrl = process.env['ACTIONS_ID_TOKEN_REQUEST_URL']; + if (!runtimeUrl) { + throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable'); + } + return runtimeUrl; + } + static getCall(id_token_url) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + const httpclient = OidcClient.createHttpClient(); + const res = yield httpclient + .getJson(id_token_url) + .catch(error => { + throw new Error(`Failed to get ID Token. \n + Error Code : ${error.statusCode}\n + Error Message: ${error.message}`); + }); + const id_token = (_a = res.result) === null || _a === void 0 ? void 0 : _a.value; + if (!id_token) { + throw new Error('Response json body do not have ID Token field'); + } + return id_token; + }); + } + static getIDToken(audience) { + return __awaiter(this, void 0, void 0, function* () { + try { + // New ID Token is requested from action service + let id_token_url = OidcClient.getIDTokenUrl(); + if (audience) { + const encodedAudience = encodeURIComponent(audience); + id_token_url = `${id_token_url}&audience=${encodedAudience}`; + } + (0, core_1.debug)(`ID token url is ${id_token_url}`); + const id_token = yield OidcClient.getCall(id_token_url); + (0, core_1.setSecret)(id_token); + return id_token; + } + catch (error) { + throw new Error(`Error message: ${error.message}`); + } + }); + } +} +exports.OidcClient = OidcClient; +//# sourceMappingURL=oidc-utils.js.map + +/***/ }), + +/***/ 3397: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.toPlatformPath = exports.toWin32Path = exports.toPosixPath = void 0; +const path = __importStar(__nccwpck_require__(6928)); +/** + * toPosixPath converts the given path to the posix form. On Windows, \\ will be + * replaced with /. + * + * @param pth. Path to transform. + * @return string Posix path. + */ +function toPosixPath(pth) { + return pth.replace(/[\\]/g, '/'); +} +exports.toPosixPath = toPosixPath; +/** + * toWin32Path converts the given path to the win32 form. On Linux, / will be + * replaced with \\. + * + * @param pth. Path to transform. + * @return string Win32 path. + */ +function toWin32Path(pth) { + return pth.replace(/[/]/g, '\\'); +} +exports.toWin32Path = toWin32Path; +/** + * toPlatformPath converts the given path to a platform-specific path. It does + * this by replacing instances of / and \ with the platform-specific path + * separator. + * + * @param pth The path to platformize. + * @return string The platform-specific path. + */ +function toPlatformPath(pth) { + return pth.replace(/[/\\]/g, path.sep); +} +exports.toPlatformPath = toPlatformPath; +//# sourceMappingURL=path-utils.js.map + +/***/ }), + +/***/ 3901: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getDetails = exports.isLinux = exports.isMacOS = exports.isWindows = exports.arch = exports.platform = void 0; +const os_1 = __importDefault(__nccwpck_require__(857)); +const exec = __importStar(__nccwpck_require__(9046)); +const getWindowsInfo = () => __awaiter(void 0, void 0, void 0, function* () { + const { stdout: version } = yield exec.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Version"', undefined, { + silent: true + }); + const { stdout: name } = yield exec.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Caption"', undefined, { + silent: true + }); + return { + name: name.trim(), + version: version.trim() + }; +}); +const getMacOsInfo = () => __awaiter(void 0, void 0, void 0, function* () { + var _a, _b, _c, _d; + const { stdout } = yield exec.getExecOutput('sw_vers', undefined, { + silent: true + }); + const version = (_b = (_a = stdout.match(/ProductVersion:\s*(.+)/)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : ''; + const name = (_d = (_c = stdout.match(/ProductName:\s*(.+)/)) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : ''; + return { + name, + version + }; +}); +const getLinuxInfo = () => __awaiter(void 0, void 0, void 0, function* () { + const { stdout } = yield exec.getExecOutput('lsb_release', ['-i', '-r', '-s'], { + silent: true + }); + const [name, version] = stdout.trim().split('\n'); + return { + name, + version + }; +}); +exports.platform = os_1.default.platform(); +exports.arch = os_1.default.arch(); +exports.isWindows = exports.platform === 'win32'; +exports.isMacOS = exports.platform === 'darwin'; +exports.isLinux = exports.platform === 'linux'; +function getDetails() { + return __awaiter(this, void 0, void 0, function* () { + return Object.assign(Object.assign({}, (yield (exports.isWindows + ? getWindowsInfo() + : exports.isMacOS + ? getMacOsInfo() + : getLinuxInfo()))), { platform: exports.platform, + arch: exports.arch, + isWindows: exports.isWindows, + isMacOS: exports.isMacOS, + isLinux: exports.isLinux }); + }); +} +exports.getDetails = getDetails; +//# sourceMappingURL=platform.js.map + +/***/ }), + +/***/ 824: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0; +const os_1 = __nccwpck_require__(857); +const fs_1 = __nccwpck_require__(9896); +const { access, appendFile, writeFile } = fs_1.promises; +exports.SUMMARY_ENV_VAR = 'GITHUB_STEP_SUMMARY'; +exports.SUMMARY_DOCS_URL = 'https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary'; +class Summary { + constructor() { + this._buffer = ''; + } + /** + * Finds the summary file path from the environment, rejects if env var is not found or file does not exist + * Also checks r/w permissions. + * + * @returns step summary file path + */ + filePath() { + return __awaiter(this, void 0, void 0, function* () { + if (this._filePath) { + return this._filePath; + } + const pathFromEnv = process.env[exports.SUMMARY_ENV_VAR]; + if (!pathFromEnv) { + throw new Error(`Unable to find environment variable for $${exports.SUMMARY_ENV_VAR}. Check if your runtime environment supports job summaries.`); + } + try { + yield access(pathFromEnv, fs_1.constants.R_OK | fs_1.constants.W_OK); + } + catch (_a) { + throw new Error(`Unable to access summary file: '${pathFromEnv}'. Check if the file has correct read/write permissions.`); + } + this._filePath = pathFromEnv; + return this._filePath; + }); + } + /** + * Wraps content in an HTML tag, adding any HTML attributes + * + * @param {string} tag HTML tag to wrap + * @param {string | null} content content within the tag + * @param {[attribute: string]: string} attrs key-value list of HTML attributes to add + * + * @returns {string} content wrapped in HTML element + */ + wrap(tag, content, attrs = {}) { + const htmlAttrs = Object.entries(attrs) + .map(([key, value]) => ` ${key}="${value}"`) + .join(''); + if (!content) { + return `<${tag}${htmlAttrs}>`; + } + return `<${tag}${htmlAttrs}>${content}`; + } + /** + * Writes text in the buffer to the summary buffer file and empties buffer. Will append by default. + * + * @param {SummaryWriteOptions} [options] (optional) options for write operation + * + * @returns {Promise} summary instance + */ + write(options) { + return __awaiter(this, void 0, void 0, function* () { + const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite); + const filePath = yield this.filePath(); + const writeFunc = overwrite ? writeFile : appendFile; + yield writeFunc(filePath, this._buffer, { encoding: 'utf8' }); + return this.emptyBuffer(); + }); + } + /** + * Clears the summary buffer and wipes the summary file + * + * @returns {Summary} summary instance + */ + clear() { + return __awaiter(this, void 0, void 0, function* () { + return this.emptyBuffer().write({ overwrite: true }); + }); + } + /** + * Returns the current summary buffer as a string + * + * @returns {string} string of summary buffer + */ + stringify() { + return this._buffer; + } + /** + * If the summary buffer is empty + * + * @returns {boolen} true if the buffer is empty + */ + isEmptyBuffer() { + return this._buffer.length === 0; + } + /** + * Resets the summary buffer without writing to summary file + * + * @returns {Summary} summary instance + */ + emptyBuffer() { + this._buffer = ''; + return this; + } + /** + * Adds raw text to the summary buffer + * + * @param {string} text content to add + * @param {boolean} [addEOL=false] (optional) append an EOL to the raw text (default: false) + * + * @returns {Summary} summary instance + */ + addRaw(text, addEOL = false) { + this._buffer += text; + return addEOL ? this.addEOL() : this; + } + /** + * Adds the operating system-specific end-of-line marker to the buffer + * + * @returns {Summary} summary instance + */ + addEOL() { + return this.addRaw(os_1.EOL); + } + /** + * Adds an HTML codeblock to the summary buffer + * + * @param {string} code content to render within fenced code block + * @param {string} lang (optional) language to syntax highlight code + * + * @returns {Summary} summary instance + */ + addCodeBlock(code, lang) { + const attrs = Object.assign({}, (lang && { lang })); + const element = this.wrap('pre', this.wrap('code', code), attrs); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML list to the summary buffer + * + * @param {string[]} items list of items to render + * @param {boolean} [ordered=false] (optional) if the rendered list should be ordered or not (default: false) + * + * @returns {Summary} summary instance + */ + addList(items, ordered = false) { + const tag = ordered ? 'ol' : 'ul'; + const listItems = items.map(item => this.wrap('li', item)).join(''); + const element = this.wrap(tag, listItems); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML table to the summary buffer + * + * @param {SummaryTableCell[]} rows table rows + * + * @returns {Summary} summary instance + */ + addTable(rows) { + const tableBody = rows + .map(row => { + const cells = row + .map(cell => { + if (typeof cell === 'string') { + return this.wrap('td', cell); + } + const { header, data, colspan, rowspan } = cell; + const tag = header ? 'th' : 'td'; + const attrs = Object.assign(Object.assign({}, (colspan && { colspan })), (rowspan && { rowspan })); + return this.wrap(tag, data, attrs); + }) + .join(''); + return this.wrap('tr', cells); + }) + .join(''); + const element = this.wrap('table', tableBody); + return this.addRaw(element).addEOL(); + } + /** + * Adds a collapsable HTML details element to the summary buffer + * + * @param {string} label text for the closed state + * @param {string} content collapsable content + * + * @returns {Summary} summary instance + */ + addDetails(label, content) { + const element = this.wrap('details', this.wrap('summary', label) + content); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML image tag to the summary buffer + * + * @param {string} src path to the image you to embed + * @param {string} alt text description of the image + * @param {SummaryImageOptions} options (optional) addition image attributes + * + * @returns {Summary} summary instance + */ + addImage(src, alt, options) { + const { width, height } = options || {}; + const attrs = Object.assign(Object.assign({}, (width && { width })), (height && { height })); + const element = this.wrap('img', null, Object.assign({ src, alt }, attrs)); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML section heading element + * + * @param {string} text heading text + * @param {number | string} [level=1] (optional) the heading level, default: 1 + * + * @returns {Summary} summary instance + */ + addHeading(text, level) { + const tag = `h${level}`; + const allowedTag = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag) + ? tag + : 'h1'; + const element = this.wrap(allowedTag, text); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML thematic break (
) to the summary buffer + * + * @returns {Summary} summary instance + */ + addSeparator() { + const element = this.wrap('hr', null); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML line break (
) to the summary buffer + * + * @returns {Summary} summary instance + */ + addBreak() { + const element = this.wrap('br', null); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML blockquote to the summary buffer + * + * @param {string} text quote text + * @param {string} cite (optional) citation url + * + * @returns {Summary} summary instance + */ + addQuote(text, cite) { + const attrs = Object.assign({}, (cite && { cite })); + const element = this.wrap('blockquote', text, attrs); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML anchor tag to the summary buffer + * + * @param {string} text link text/content + * @param {string} href hyperlink + * + * @returns {Summary} summary instance + */ + addLink(text, href) { + const element = this.wrap('a', text, { href }); + return this.addRaw(element).addEOL(); + } +} +const _summary = new Summary(); +/** + * @deprecated use `core.summary` + */ +exports.markdownSummary = _summary; +exports.summary = _summary; +//# sourceMappingURL=summary.js.map + +/***/ }), + +/***/ 6425: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.toCommandProperties = exports.toCommandValue = void 0; +/** + * Sanitizes an input into a string so it can be passed into issueCommand safely + * @param input input to sanitize into a string + */ +function toCommandValue(input) { + if (input === null || input === undefined) { + return ''; + } + else if (typeof input === 'string' || input instanceof String) { + return input; + } + return JSON.stringify(input); +} +exports.toCommandValue = toCommandValue; +/** + * + * @param annotationProperties + * @returns The command properties to send with the actual annotation command + * See IssueCommandProperties: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionCommandManager.cs#L646 + */ +function toCommandProperties(annotationProperties) { + if (!Object.keys(annotationProperties).length) { + return {}; + } + return { + title: annotationProperties.title, + file: annotationProperties.file, + line: annotationProperties.startLine, + endLine: annotationProperties.endLine, + col: annotationProperties.startColumn, + endColumn: annotationProperties.endColumn + }; +} +exports.toCommandProperties = toCommandProperties; +//# sourceMappingURL=utils.js.map + +/***/ }), + +/***/ 9046: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getExecOutput = exports.exec = void 0; +const string_decoder_1 = __nccwpck_require__(3193); +const tr = __importStar(__nccwpck_require__(6527)); +/** + * Exec a command. + * Output will be streamed to the live console. + * Returns promise with return code + * + * @param commandLine command to execute (can include additional args). Must be correctly escaped. + * @param args optional arguments for tool. Escaping is handled by the lib. + * @param options optional exec options. See ExecOptions + * @returns Promise exit code + */ +function exec(commandLine, args, options) { + return __awaiter(this, void 0, void 0, function* () { + const commandArgs = tr.argStringToArray(commandLine); + if (commandArgs.length === 0) { + throw new Error(`Parameter 'commandLine' cannot be null or empty.`); + } + // Path to tool to execute should be first arg + const toolPath = commandArgs[0]; + args = commandArgs.slice(1).concat(args || []); + const runner = new tr.ToolRunner(toolPath, args, options); + return runner.exec(); + }); +} +exports.exec = exec; +/** + * Exec a command and get the output. + * Output will be streamed to the live console. + * Returns promise with the exit code and collected stdout and stderr + * + * @param commandLine command to execute (can include additional args). Must be correctly escaped. + * @param args optional arguments for tool. Escaping is handled by the lib. + * @param options optional exec options. See ExecOptions + * @returns Promise exit code, stdout, and stderr + */ +function getExecOutput(commandLine, args, options) { + var _a, _b; + return __awaiter(this, void 0, void 0, function* () { + let stdout = ''; + let stderr = ''; + //Using string decoder covers the case where a mult-byte character is split + const stdoutDecoder = new string_decoder_1.StringDecoder('utf8'); + const stderrDecoder = new string_decoder_1.StringDecoder('utf8'); + const originalStdoutListener = (_a = options === null || options === void 0 ? void 0 : options.listeners) === null || _a === void 0 ? void 0 : _a.stdout; + const originalStdErrListener = (_b = options === null || options === void 0 ? void 0 : options.listeners) === null || _b === void 0 ? void 0 : _b.stderr; + const stdErrListener = (data) => { + stderr += stderrDecoder.write(data); + if (originalStdErrListener) { + originalStdErrListener(data); + } + }; + const stdOutListener = (data) => { + stdout += stdoutDecoder.write(data); + if (originalStdoutListener) { + originalStdoutListener(data); + } + }; + const listeners = Object.assign(Object.assign({}, options === null || options === void 0 ? void 0 : options.listeners), { stdout: stdOutListener, stderr: stdErrListener }); + const exitCode = yield exec(commandLine, args, Object.assign(Object.assign({}, options), { listeners })); + //flush any remaining characters + stdout += stdoutDecoder.end(); + stderr += stderrDecoder.end(); + return { + exitCode, + stdout, + stderr + }; + }); +} +exports.getExecOutput = getExecOutput; +//# sourceMappingURL=exec.js.map + +/***/ }), + +/***/ 6527: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.argStringToArray = exports.ToolRunner = void 0; +const os = __importStar(__nccwpck_require__(857)); +const events = __importStar(__nccwpck_require__(4434)); +const child = __importStar(__nccwpck_require__(5317)); +const path = __importStar(__nccwpck_require__(6928)); +const io = __importStar(__nccwpck_require__(5607)); +const ioUtil = __importStar(__nccwpck_require__(6280)); +const timers_1 = __nccwpck_require__(3557); +/* eslint-disable @typescript-eslint/unbound-method */ +const IS_WINDOWS = process.platform === 'win32'; +/* + * Class for running command line tools. Handles quoting and arg parsing in a platform agnostic way. + */ +class ToolRunner extends events.EventEmitter { + constructor(toolPath, args, options) { + super(); + if (!toolPath) { + throw new Error("Parameter 'toolPath' cannot be null or empty."); + } + this.toolPath = toolPath; + this.args = args || []; + this.options = options || {}; + } + _debug(message) { + if (this.options.listeners && this.options.listeners.debug) { + this.options.listeners.debug(message); + } + } + _getCommandString(options, noPrefix) { + const toolPath = this._getSpawnFileName(); + const args = this._getSpawnArgs(options); + let cmd = noPrefix ? '' : '[command]'; // omit prefix when piped to a second tool + if (IS_WINDOWS) { + // Windows + cmd file + if (this._isCmdFile()) { + cmd += toolPath; + for (const a of args) { + cmd += ` ${a}`; + } + } + // Windows + verbatim + else if (options.windowsVerbatimArguments) { + cmd += `"${toolPath}"`; + for (const a of args) { + cmd += ` ${a}`; + } + } + // Windows (regular) + else { + cmd += this._windowsQuoteCmdArg(toolPath); + for (const a of args) { + cmd += ` ${this._windowsQuoteCmdArg(a)}`; + } + } + } + else { + // OSX/Linux - this can likely be improved with some form of quoting. + // creating processes on Unix is fundamentally different than Windows. + // on Unix, execvp() takes an arg array. + cmd += toolPath; + for (const a of args) { + cmd += ` ${a}`; + } + } + return cmd; + } + _processLineBuffer(data, strBuffer, onLine) { + try { + let s = strBuffer + data.toString(); + let n = s.indexOf(os.EOL); + while (n > -1) { + const line = s.substring(0, n); + onLine(line); + // the rest of the string ... + s = s.substring(n + os.EOL.length); + n = s.indexOf(os.EOL); + } + return s; + } + catch (err) { + // streaming lines to console is best effort. Don't fail a build. + this._debug(`error processing line. Failed with error ${err}`); + return ''; + } + } + _getSpawnFileName() { + if (IS_WINDOWS) { + if (this._isCmdFile()) { + return process.env['COMSPEC'] || 'cmd.exe'; + } + } + return this.toolPath; + } + _getSpawnArgs(options) { + if (IS_WINDOWS) { + if (this._isCmdFile()) { + let argline = `/D /S /C "${this._windowsQuoteCmdArg(this.toolPath)}`; + for (const a of this.args) { + argline += ' '; + argline += options.windowsVerbatimArguments + ? a + : this._windowsQuoteCmdArg(a); + } + argline += '"'; + return [argline]; + } + } + return this.args; + } + _endsWith(str, end) { + return str.endsWith(end); + } + _isCmdFile() { + const upperToolPath = this.toolPath.toUpperCase(); + return (this._endsWith(upperToolPath, '.CMD') || + this._endsWith(upperToolPath, '.BAT')); + } + _windowsQuoteCmdArg(arg) { + // for .exe, apply the normal quoting rules that libuv applies + if (!this._isCmdFile()) { + return this._uvQuoteCmdArg(arg); + } + // otherwise apply quoting rules specific to the cmd.exe command line parser. + // the libuv rules are generic and are not designed specifically for cmd.exe + // command line parser. + // + // for a detailed description of the cmd.exe command line parser, refer to + // http://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts/7970912#7970912 + // need quotes for empty arg + if (!arg) { + return '""'; + } + // determine whether the arg needs to be quoted + const cmdSpecialChars = [ + ' ', + '\t', + '&', + '(', + ')', + '[', + ']', + '{', + '}', + '^', + '=', + ';', + '!', + "'", + '+', + ',', + '`', + '~', + '|', + '<', + '>', + '"' + ]; + let needsQuotes = false; + for (const char of arg) { + if (cmdSpecialChars.some(x => x === char)) { + needsQuotes = true; + break; + } + } + // short-circuit if quotes not needed + if (!needsQuotes) { + return arg; + } + // the following quoting rules are very similar to the rules that by libuv applies. + // + // 1) wrap the string in quotes + // + // 2) double-up quotes - i.e. " => "" + // + // this is different from the libuv quoting rules. libuv replaces " with \", which unfortunately + // doesn't work well with a cmd.exe command line. + // + // note, replacing " with "" also works well if the arg is passed to a downstream .NET console app. + // for example, the command line: + // foo.exe "myarg:""my val""" + // is parsed by a .NET console app into an arg array: + // [ "myarg:\"my val\"" ] + // which is the same end result when applying libuv quoting rules. although the actual + // command line from libuv quoting rules would look like: + // foo.exe "myarg:\"my val\"" + // + // 3) double-up slashes that precede a quote, + // e.g. hello \world => "hello \world" + // hello\"world => "hello\\""world" + // hello\\"world => "hello\\\\""world" + // hello world\ => "hello world\\" + // + // technically this is not required for a cmd.exe command line, or the batch argument parser. + // the reasons for including this as a .cmd quoting rule are: + // + // a) this is optimized for the scenario where the argument is passed from the .cmd file to an + // external program. many programs (e.g. .NET console apps) rely on the slash-doubling rule. + // + // b) it's what we've been doing previously (by deferring to node default behavior) and we + // haven't heard any complaints about that aspect. + // + // note, a weakness of the quoting rules chosen here, is that % is not escaped. in fact, % cannot be + // escaped when used on the command line directly - even though within a .cmd file % can be escaped + // by using %%. + // + // the saving grace is, on the command line, %var% is left as-is if var is not defined. this contrasts + // the line parsing rules within a .cmd file, where if var is not defined it is replaced with nothing. + // + // one option that was explored was replacing % with ^% - i.e. %var% => ^%var^%. this hack would + // often work, since it is unlikely that var^ would exist, and the ^ character is removed when the + // variable is used. the problem, however, is that ^ is not removed when %* is used to pass the args + // to an external program. + // + // an unexplored potential solution for the % escaping problem, is to create a wrapper .cmd file. + // % can be escaped within a .cmd file. + let reverse = '"'; + let quoteHit = true; + for (let i = arg.length; i > 0; i--) { + // walk the string in reverse + reverse += arg[i - 1]; + if (quoteHit && arg[i - 1] === '\\') { + reverse += '\\'; // double the slash + } + else if (arg[i - 1] === '"') { + quoteHit = true; + reverse += '"'; // double the quote + } + else { + quoteHit = false; + } + } + reverse += '"'; + return reverse + .split('') + .reverse() + .join(''); + } + _uvQuoteCmdArg(arg) { + // Tool runner wraps child_process.spawn() and needs to apply the same quoting as + // Node in certain cases where the undocumented spawn option windowsVerbatimArguments + // is used. + // + // Since this function is a port of quote_cmd_arg from Node 4.x (technically, lib UV, + // see https://github.com/nodejs/node/blob/v4.x/deps/uv/src/win/process.c for details), + // pasting copyright notice from Node within this function: + // + // Copyright Joyent, Inc. and other Node contributors. All rights reserved. + // + // Permission is hereby granted, free of charge, to any person obtaining a copy + // of this software and associated documentation files (the "Software"), to + // deal in the Software without restriction, including without limitation the + // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + // sell copies of the Software, and to permit persons to whom the Software is + // furnished to do so, subject to the following conditions: + // + // The above copyright notice and this permission notice shall be included in + // all copies or substantial portions of the Software. + // + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + // IN THE SOFTWARE. + if (!arg) { + // Need double quotation for empty argument + return '""'; + } + if (!arg.includes(' ') && !arg.includes('\t') && !arg.includes('"')) { + // No quotation needed + return arg; + } + if (!arg.includes('"') && !arg.includes('\\')) { + // No embedded double quotes or backslashes, so I can just wrap + // quote marks around the whole thing. + return `"${arg}"`; + } + // Expected input/output: + // input : hello"world + // output: "hello\"world" + // input : hello""world + // output: "hello\"\"world" + // input : hello\world + // output: hello\world + // input : hello\\world + // output: hello\\world + // input : hello\"world + // output: "hello\\\"world" + // input : hello\\"world + // output: "hello\\\\\"world" + // input : hello world\ + // output: "hello world\\" - note the comment in libuv actually reads "hello world\" + // but it appears the comment is wrong, it should be "hello world\\" + let reverse = '"'; + let quoteHit = true; + for (let i = arg.length; i > 0; i--) { + // walk the string in reverse + reverse += arg[i - 1]; + if (quoteHit && arg[i - 1] === '\\') { + reverse += '\\'; + } + else if (arg[i - 1] === '"') { + quoteHit = true; + reverse += '\\'; + } + else { + quoteHit = false; + } + } + reverse += '"'; + return reverse + .split('') + .reverse() + .join(''); + } + _cloneExecOptions(options) { + options = options || {}; + const result = { + cwd: options.cwd || process.cwd(), + env: options.env || process.env, + silent: options.silent || false, + windowsVerbatimArguments: options.windowsVerbatimArguments || false, + failOnStdErr: options.failOnStdErr || false, + ignoreReturnCode: options.ignoreReturnCode || false, + delay: options.delay || 10000 + }; + result.outStream = options.outStream || process.stdout; + result.errStream = options.errStream || process.stderr; + return result; + } + _getSpawnOptions(options, toolPath) { + options = options || {}; + const result = {}; + result.cwd = options.cwd; + result.env = options.env; + result['windowsVerbatimArguments'] = + options.windowsVerbatimArguments || this._isCmdFile(); + if (options.windowsVerbatimArguments) { + result.argv0 = `"${toolPath}"`; + } + return result; + } + /** + * Exec a tool. + * Output will be streamed to the live console. + * Returns promise with return code + * + * @param tool path to tool to exec + * @param options optional exec options. See ExecOptions + * @returns number + */ + exec() { + return __awaiter(this, void 0, void 0, function* () { + // root the tool path if it is unrooted and contains relative pathing + if (!ioUtil.isRooted(this.toolPath) && + (this.toolPath.includes('/') || + (IS_WINDOWS && this.toolPath.includes('\\')))) { + // prefer options.cwd if it is specified, however options.cwd may also need to be rooted + this.toolPath = path.resolve(process.cwd(), this.options.cwd || process.cwd(), this.toolPath); + } + // if the tool is only a file name, then resolve it from the PATH + // otherwise verify it exists (add extension on Windows if necessary) + this.toolPath = yield io.which(this.toolPath, true); + return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { + this._debug(`exec tool: ${this.toolPath}`); + this._debug('arguments:'); + for (const arg of this.args) { + this._debug(` ${arg}`); + } + const optionsNonNull = this._cloneExecOptions(this.options); + if (!optionsNonNull.silent && optionsNonNull.outStream) { + optionsNonNull.outStream.write(this._getCommandString(optionsNonNull) + os.EOL); + } + const state = new ExecState(optionsNonNull, this.toolPath); + state.on('debug', (message) => { + this._debug(message); + }); + if (this.options.cwd && !(yield ioUtil.exists(this.options.cwd))) { + return reject(new Error(`The cwd: ${this.options.cwd} does not exist!`)); + } + const fileName = this._getSpawnFileName(); + const cp = child.spawn(fileName, this._getSpawnArgs(optionsNonNull), this._getSpawnOptions(this.options, fileName)); + let stdbuffer = ''; + if (cp.stdout) { + cp.stdout.on('data', (data) => { + if (this.options.listeners && this.options.listeners.stdout) { + this.options.listeners.stdout(data); + } + if (!optionsNonNull.silent && optionsNonNull.outStream) { + optionsNonNull.outStream.write(data); + } + stdbuffer = this._processLineBuffer(data, stdbuffer, (line) => { + if (this.options.listeners && this.options.listeners.stdline) { + this.options.listeners.stdline(line); + } + }); + }); + } + let errbuffer = ''; + if (cp.stderr) { + cp.stderr.on('data', (data) => { + state.processStderr = true; + if (this.options.listeners && this.options.listeners.stderr) { + this.options.listeners.stderr(data); + } + if (!optionsNonNull.silent && + optionsNonNull.errStream && + optionsNonNull.outStream) { + const s = optionsNonNull.failOnStdErr + ? optionsNonNull.errStream + : optionsNonNull.outStream; + s.write(data); + } + errbuffer = this._processLineBuffer(data, errbuffer, (line) => { + if (this.options.listeners && this.options.listeners.errline) { + this.options.listeners.errline(line); + } + }); + }); + } + cp.on('error', (err) => { + state.processError = err.message; + state.processExited = true; + state.processClosed = true; + state.CheckComplete(); + }); + cp.on('exit', (code) => { + state.processExitCode = code; + state.processExited = true; + this._debug(`Exit code ${code} received from tool '${this.toolPath}'`); + state.CheckComplete(); + }); + cp.on('close', (code) => { + state.processExitCode = code; + state.processExited = true; + state.processClosed = true; + this._debug(`STDIO streams have closed for tool '${this.toolPath}'`); + state.CheckComplete(); + }); + state.on('done', (error, exitCode) => { + if (stdbuffer.length > 0) { + this.emit('stdline', stdbuffer); + } + if (errbuffer.length > 0) { + this.emit('errline', errbuffer); + } + cp.removeAllListeners(); + if (error) { + reject(error); + } + else { + resolve(exitCode); + } + }); + if (this.options.input) { + if (!cp.stdin) { + throw new Error('child process missing stdin'); + } + cp.stdin.end(this.options.input); + } + })); + }); + } +} +exports.ToolRunner = ToolRunner; +/** + * Convert an arg string to an array of args. Handles escaping + * + * @param argString string of arguments + * @returns string[] array of arguments + */ +function argStringToArray(argString) { + const args = []; + let inQuotes = false; + let escaped = false; + let arg = ''; + function append(c) { + // we only escape double quotes. + if (escaped && c !== '"') { + arg += '\\'; + } + arg += c; + escaped = false; + } + for (let i = 0; i < argString.length; i++) { + const c = argString.charAt(i); + if (c === '"') { + if (!escaped) { + inQuotes = !inQuotes; + } + else { + append(c); + } + continue; + } + if (c === '\\' && escaped) { + append(c); + continue; + } + if (c === '\\' && inQuotes) { + escaped = true; + continue; + } + if (c === ' ' && !inQuotes) { + if (arg.length > 0) { + args.push(arg); + arg = ''; + } + continue; + } + append(c); + } + if (arg.length > 0) { + args.push(arg.trim()); + } + return args; +} +exports.argStringToArray = argStringToArray; +class ExecState extends events.EventEmitter { + constructor(options, toolPath) { + super(); + this.processClosed = false; // tracks whether the process has exited and stdio is closed + this.processError = ''; + this.processExitCode = 0; + this.processExited = false; // tracks whether the process has exited + this.processStderr = false; // tracks whether stderr was written to + this.delay = 10000; // 10 seconds + this.done = false; + this.timeout = null; + if (!toolPath) { + throw new Error('toolPath must not be empty'); + } + this.options = options; + this.toolPath = toolPath; + if (options.delay) { + this.delay = options.delay; + } + } + CheckComplete() { + if (this.done) { + return; + } + if (this.processClosed) { + this._setResult(); + } + else if (this.processExited) { + this.timeout = timers_1.setTimeout(ExecState.HandleTimeout, this.delay, this); + } + } + _debug(message) { + this.emit('debug', message); + } + _setResult() { + // determine whether there is an error + let error; + if (this.processExited) { + if (this.processError) { + error = new Error(`There was an error when attempting to execute the process '${this.toolPath}'. This may indicate the process failed to start. Error: ${this.processError}`); + } + else if (this.processExitCode !== 0 && !this.options.ignoreReturnCode) { + error = new Error(`The process '${this.toolPath}' failed with exit code ${this.processExitCode}`); + } + else if (this.processStderr && this.options.failOnStdErr) { + error = new Error(`The process '${this.toolPath}' failed because one or more lines were written to the STDERR stream`); + } + } + // clear the timeout + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = null; + } + this.done = true; + this.emit('done', error, this.processExitCode); + } + static HandleTimeout(state) { + if (state.done) { + return; + } + if (!state.processClosed && state.processExited) { + const message = `The STDIO streams did not close within ${state.delay / + 1000} seconds of the exit event from process '${state.toolPath}'. This may indicate a child process inherited the STDIO streams and has not yet exited.`; + state._debug(message); + } + state._setResult(); + } +} +//# sourceMappingURL=toolrunner.js.map + +/***/ }), + +/***/ 7047: +/***/ (function(__unused_webpack_module, exports) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.PersonalAccessTokenCredentialHandler = exports.BearerCredentialHandler = exports.BasicCredentialHandler = void 0; +class BasicCredentialHandler { + constructor(username, password) { + this.username = username; + this.password = password; + } + prepareRequest(options) { + if (!options.headers) { + throw Error('The request has no headers'); + } + options.headers['Authorization'] = `Basic ${Buffer.from(`${this.username}:${this.password}`).toString('base64')}`; + } + // This handler cannot handle 401 + canHandleAuthentication() { + return false; + } + handleAuthentication() { + return __awaiter(this, void 0, void 0, function* () { + throw new Error('not implemented'); + }); + } +} +exports.BasicCredentialHandler = BasicCredentialHandler; +class BearerCredentialHandler { + constructor(token) { + this.token = token; + } + // currently implements pre-authorization + // TODO: support preAuth = false where it hooks on 401 + prepareRequest(options) { + if (!options.headers) { + throw Error('The request has no headers'); + } + options.headers['Authorization'] = `Bearer ${this.token}`; + } + // This handler cannot handle 401 + canHandleAuthentication() { + return false; + } + handleAuthentication() { + return __awaiter(this, void 0, void 0, function* () { + throw new Error('not implemented'); + }); + } +} +exports.BearerCredentialHandler = BearerCredentialHandler; +class PersonalAccessTokenCredentialHandler { + constructor(token) { + this.token = token; + } + // currently implements pre-authorization + // TODO: support preAuth = false where it hooks on 401 + prepareRequest(options) { + if (!options.headers) { + throw Error('The request has no headers'); + } + options.headers['Authorization'] = `Basic ${Buffer.from(`PAT:${this.token}`).toString('base64')}`; + } + // This handler cannot handle 401 + canHandleAuthentication() { + return false; + } + handleAuthentication() { + return __awaiter(this, void 0, void 0, function* () { + throw new Error('not implemented'); + }); + } +} +exports.PersonalAccessTokenCredentialHandler = PersonalAccessTokenCredentialHandler; +//# sourceMappingURL=auth.js.map + +/***/ }), + +/***/ 5313: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.HttpClient = exports.isHttps = exports.HttpClientResponse = exports.HttpClientError = exports.getProxyUrl = exports.MediaTypes = exports.Headers = exports.HttpCodes = void 0; +const http = __importStar(__nccwpck_require__(8611)); +const https = __importStar(__nccwpck_require__(5692)); +const pm = __importStar(__nccwpck_require__(4985)); +const tunnel = __importStar(__nccwpck_require__(858)); +const undici_1 = __nccwpck_require__(2740); +var HttpCodes; +(function (HttpCodes) { + HttpCodes[HttpCodes["OK"] = 200] = "OK"; + HttpCodes[HttpCodes["MultipleChoices"] = 300] = "MultipleChoices"; + HttpCodes[HttpCodes["MovedPermanently"] = 301] = "MovedPermanently"; + HttpCodes[HttpCodes["ResourceMoved"] = 302] = "ResourceMoved"; + HttpCodes[HttpCodes["SeeOther"] = 303] = "SeeOther"; + HttpCodes[HttpCodes["NotModified"] = 304] = "NotModified"; + HttpCodes[HttpCodes["UseProxy"] = 305] = "UseProxy"; + HttpCodes[HttpCodes["SwitchProxy"] = 306] = "SwitchProxy"; + HttpCodes[HttpCodes["TemporaryRedirect"] = 307] = "TemporaryRedirect"; + HttpCodes[HttpCodes["PermanentRedirect"] = 308] = "PermanentRedirect"; + HttpCodes[HttpCodes["BadRequest"] = 400] = "BadRequest"; + HttpCodes[HttpCodes["Unauthorized"] = 401] = "Unauthorized"; + HttpCodes[HttpCodes["PaymentRequired"] = 402] = "PaymentRequired"; + HttpCodes[HttpCodes["Forbidden"] = 403] = "Forbidden"; + HttpCodes[HttpCodes["NotFound"] = 404] = "NotFound"; + HttpCodes[HttpCodes["MethodNotAllowed"] = 405] = "MethodNotAllowed"; + HttpCodes[HttpCodes["NotAcceptable"] = 406] = "NotAcceptable"; + HttpCodes[HttpCodes["ProxyAuthenticationRequired"] = 407] = "ProxyAuthenticationRequired"; + HttpCodes[HttpCodes["RequestTimeout"] = 408] = "RequestTimeout"; + HttpCodes[HttpCodes["Conflict"] = 409] = "Conflict"; + HttpCodes[HttpCodes["Gone"] = 410] = "Gone"; + HttpCodes[HttpCodes["TooManyRequests"] = 429] = "TooManyRequests"; + HttpCodes[HttpCodes["InternalServerError"] = 500] = "InternalServerError"; + HttpCodes[HttpCodes["NotImplemented"] = 501] = "NotImplemented"; + HttpCodes[HttpCodes["BadGateway"] = 502] = "BadGateway"; + HttpCodes[HttpCodes["ServiceUnavailable"] = 503] = "ServiceUnavailable"; + HttpCodes[HttpCodes["GatewayTimeout"] = 504] = "GatewayTimeout"; +})(HttpCodes || (exports.HttpCodes = HttpCodes = {})); +var Headers; +(function (Headers) { + Headers["Accept"] = "accept"; + Headers["ContentType"] = "content-type"; +})(Headers || (exports.Headers = Headers = {})); +var MediaTypes; +(function (MediaTypes) { + MediaTypes["ApplicationJson"] = "application/json"; +})(MediaTypes || (exports.MediaTypes = MediaTypes = {})); +/** + * Returns the proxy URL, depending upon the supplied url and proxy environment variables. + * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com + */ +function getProxyUrl(serverUrl) { + const proxyUrl = pm.getProxyUrl(new URL(serverUrl)); + return proxyUrl ? proxyUrl.href : ''; +} +exports.getProxyUrl = getProxyUrl; +const HttpRedirectCodes = [ + HttpCodes.MovedPermanently, + HttpCodes.ResourceMoved, + HttpCodes.SeeOther, + HttpCodes.TemporaryRedirect, + HttpCodes.PermanentRedirect +]; +const HttpResponseRetryCodes = [ + HttpCodes.BadGateway, + HttpCodes.ServiceUnavailable, + HttpCodes.GatewayTimeout +]; +const RetryableHttpVerbs = ['OPTIONS', 'GET', 'DELETE', 'HEAD']; +const ExponentialBackoffCeiling = 10; +const ExponentialBackoffTimeSlice = 5; +class HttpClientError extends Error { + constructor(message, statusCode) { + super(message); + this.name = 'HttpClientError'; + this.statusCode = statusCode; + Object.setPrototypeOf(this, HttpClientError.prototype); + } +} +exports.HttpClientError = HttpClientError; +class HttpClientResponse { + constructor(message) { + this.message = message; + } + readBody() { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { + let output = Buffer.alloc(0); + this.message.on('data', (chunk) => { + output = Buffer.concat([output, chunk]); + }); + this.message.on('end', () => { + resolve(output.toString()); + }); + })); + }); + } + readBodyBuffer() { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { + const chunks = []; + this.message.on('data', (chunk) => { + chunks.push(chunk); + }); + this.message.on('end', () => { + resolve(Buffer.concat(chunks)); + }); + })); + }); + } +} +exports.HttpClientResponse = HttpClientResponse; +function isHttps(requestUrl) { + const parsedUrl = new URL(requestUrl); + return parsedUrl.protocol === 'https:'; +} +exports.isHttps = isHttps; +class HttpClient { + constructor(userAgent, handlers, requestOptions) { + this._ignoreSslError = false; + this._allowRedirects = true; + this._allowRedirectDowngrade = false; + this._maxRedirects = 50; + this._allowRetries = false; + this._maxRetries = 1; + this._keepAlive = false; + this._disposed = false; + this.userAgent = userAgent; + this.handlers = handlers || []; + this.requestOptions = requestOptions; + if (requestOptions) { + if (requestOptions.ignoreSslError != null) { + this._ignoreSslError = requestOptions.ignoreSslError; + } + this._socketTimeout = requestOptions.socketTimeout; + if (requestOptions.allowRedirects != null) { + this._allowRedirects = requestOptions.allowRedirects; + } + if (requestOptions.allowRedirectDowngrade != null) { + this._allowRedirectDowngrade = requestOptions.allowRedirectDowngrade; + } + if (requestOptions.maxRedirects != null) { + this._maxRedirects = Math.max(requestOptions.maxRedirects, 0); + } + if (requestOptions.keepAlive != null) { + this._keepAlive = requestOptions.keepAlive; + } + if (requestOptions.allowRetries != null) { + this._allowRetries = requestOptions.allowRetries; + } + if (requestOptions.maxRetries != null) { + this._maxRetries = requestOptions.maxRetries; + } + } + } + options(requestUrl, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('OPTIONS', requestUrl, null, additionalHeaders || {}); + }); + } + get(requestUrl, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('GET', requestUrl, null, additionalHeaders || {}); + }); + } + del(requestUrl, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('DELETE', requestUrl, null, additionalHeaders || {}); + }); + } + post(requestUrl, data, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('POST', requestUrl, data, additionalHeaders || {}); + }); + } + patch(requestUrl, data, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('PATCH', requestUrl, data, additionalHeaders || {}); + }); + } + put(requestUrl, data, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('PUT', requestUrl, data, additionalHeaders || {}); + }); + } + head(requestUrl, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('HEAD', requestUrl, null, additionalHeaders || {}); + }); + } + sendStream(verb, requestUrl, stream, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request(verb, requestUrl, stream, additionalHeaders); + }); + } + /** + * Gets a typed object from an endpoint + * Be aware that not found returns a null. Other errors (4xx, 5xx) reject the promise + */ + getJson(requestUrl, additionalHeaders = {}) { + return __awaiter(this, void 0, void 0, function* () { + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + const res = yield this.get(requestUrl, additionalHeaders); + return this._processResponse(res, this.requestOptions); + }); + } + postJson(requestUrl, obj, additionalHeaders = {}) { + return __awaiter(this, void 0, void 0, function* () { + const data = JSON.stringify(obj, null, 2); + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); + const res = yield this.post(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + }); + } + putJson(requestUrl, obj, additionalHeaders = {}) { + return __awaiter(this, void 0, void 0, function* () { + const data = JSON.stringify(obj, null, 2); + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); + const res = yield this.put(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + }); + } + patchJson(requestUrl, obj, additionalHeaders = {}) { + return __awaiter(this, void 0, void 0, function* () { + const data = JSON.stringify(obj, null, 2); + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); + const res = yield this.patch(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + }); + } + /** + * Makes a raw http request. + * All other methods such as get, post, patch, and request ultimately call this. + * Prefer get, del, post and patch + */ + request(verb, requestUrl, data, headers) { + return __awaiter(this, void 0, void 0, function* () { + if (this._disposed) { + throw new Error('Client has already been disposed.'); + } + const parsedUrl = new URL(requestUrl); + let info = this._prepareRequest(verb, parsedUrl, headers); + // Only perform retries on reads since writes may not be idempotent. + const maxTries = this._allowRetries && RetryableHttpVerbs.includes(verb) + ? this._maxRetries + 1 + : 1; + let numTries = 0; + let response; + do { + response = yield this.requestRaw(info, data); + // Check if it's an authentication challenge + if (response && + response.message && + response.message.statusCode === HttpCodes.Unauthorized) { + let authenticationHandler; + for (const handler of this.handlers) { + if (handler.canHandleAuthentication(response)) { + authenticationHandler = handler; + break; + } + } + if (authenticationHandler) { + return authenticationHandler.handleAuthentication(this, info, data); + } + else { + // We have received an unauthorized response but have no handlers to handle it. + // Let the response return to the caller. + return response; + } + } + let redirectsRemaining = this._maxRedirects; + while (response.message.statusCode && + HttpRedirectCodes.includes(response.message.statusCode) && + this._allowRedirects && + redirectsRemaining > 0) { + const redirectUrl = response.message.headers['location']; + if (!redirectUrl) { + // if there's no location to redirect to, we won't + break; + } + const parsedRedirectUrl = new URL(redirectUrl); + if (parsedUrl.protocol === 'https:' && + parsedUrl.protocol !== parsedRedirectUrl.protocol && + !this._allowRedirectDowngrade) { + throw new Error('Redirect from HTTPS to HTTP protocol. This downgrade is not allowed for security reasons. If you want to allow this behavior, set the allowRedirectDowngrade option to true.'); + } + // we need to finish reading the response before reassigning response + // which will leak the open socket. + yield response.readBody(); + // strip authorization header if redirected to a different hostname + if (parsedRedirectUrl.hostname !== parsedUrl.hostname) { + for (const header in headers) { + // header names are case insensitive + if (header.toLowerCase() === 'authorization') { + delete headers[header]; + } + } + } + // let's make the request with the new redirectUrl + info = this._prepareRequest(verb, parsedRedirectUrl, headers); + response = yield this.requestRaw(info, data); + redirectsRemaining--; + } + if (!response.message.statusCode || + !HttpResponseRetryCodes.includes(response.message.statusCode)) { + // If not a retry code, return immediately instead of retrying + return response; + } + numTries += 1; + if (numTries < maxTries) { + yield response.readBody(); + yield this._performExponentialBackoff(numTries); + } + } while (numTries < maxTries); + return response; + }); + } + /** + * Needs to be called if keepAlive is set to true in request options. + */ + dispose() { + if (this._agent) { + this._agent.destroy(); + } + this._disposed = true; + } + /** + * Raw request. + * @param info + * @param data + */ + requestRaw(info, data) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve, reject) => { + function callbackForResult(err, res) { + if (err) { + reject(err); + } + else if (!res) { + // If `err` is not passed, then `res` must be passed. + reject(new Error('Unknown error')); + } + else { + resolve(res); + } + } + this.requestRawWithCallback(info, data, callbackForResult); + }); + }); + } + /** + * Raw request with callback. + * @param info + * @param data + * @param onResult + */ + requestRawWithCallback(info, data, onResult) { + if (typeof data === 'string') { + if (!info.options.headers) { + info.options.headers = {}; + } + info.options.headers['Content-Length'] = Buffer.byteLength(data, 'utf8'); + } + let callbackCalled = false; + function handleResult(err, res) { + if (!callbackCalled) { + callbackCalled = true; + onResult(err, res); + } + } + const req = info.httpModule.request(info.options, (msg) => { + const res = new HttpClientResponse(msg); + handleResult(undefined, res); + }); + let socket; + req.on('socket', sock => { + socket = sock; + }); + // If we ever get disconnected, we want the socket to timeout eventually + req.setTimeout(this._socketTimeout || 3 * 60000, () => { + if (socket) { + socket.end(); + } + handleResult(new Error(`Request timeout: ${info.options.path}`)); + }); + req.on('error', function (err) { + // err has statusCode property + // res should have headers + handleResult(err); + }); + if (data && typeof data === 'string') { + req.write(data, 'utf8'); + } + if (data && typeof data !== 'string') { + data.on('close', function () { + req.end(); + }); + data.pipe(req); + } + else { + req.end(); + } + } + /** + * Gets an http agent. This function is useful when you need an http agent that handles + * routing through a proxy server - depending upon the url and proxy environment variables. + * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com + */ + getAgent(serverUrl) { + const parsedUrl = new URL(serverUrl); + return this._getAgent(parsedUrl); + } + getAgentDispatcher(serverUrl) { + const parsedUrl = new URL(serverUrl); + const proxyUrl = pm.getProxyUrl(parsedUrl); + const useProxy = proxyUrl && proxyUrl.hostname; + if (!useProxy) { + return; + } + return this._getProxyAgentDispatcher(parsedUrl, proxyUrl); + } + _prepareRequest(method, requestUrl, headers) { + const info = {}; + info.parsedUrl = requestUrl; + const usingSsl = info.parsedUrl.protocol === 'https:'; + info.httpModule = usingSsl ? https : http; + const defaultPort = usingSsl ? 443 : 80; + info.options = {}; + info.options.host = info.parsedUrl.hostname; + info.options.port = info.parsedUrl.port + ? parseInt(info.parsedUrl.port) + : defaultPort; + info.options.path = + (info.parsedUrl.pathname || '') + (info.parsedUrl.search || ''); + info.options.method = method; + info.options.headers = this._mergeHeaders(headers); + if (this.userAgent != null) { + info.options.headers['user-agent'] = this.userAgent; + } + info.options.agent = this._getAgent(info.parsedUrl); + // gives handlers an opportunity to participate + if (this.handlers) { + for (const handler of this.handlers) { + handler.prepareRequest(info.options); + } + } + return info; + } + _mergeHeaders(headers) { + if (this.requestOptions && this.requestOptions.headers) { + return Object.assign({}, lowercaseKeys(this.requestOptions.headers), lowercaseKeys(headers || {})); + } + return lowercaseKeys(headers || {}); + } + _getExistingOrDefaultHeader(additionalHeaders, header, _default) { + let clientHeader; + if (this.requestOptions && this.requestOptions.headers) { + clientHeader = lowercaseKeys(this.requestOptions.headers)[header]; + } + return additionalHeaders[header] || clientHeader || _default; + } + _getAgent(parsedUrl) { + let agent; + const proxyUrl = pm.getProxyUrl(parsedUrl); + const useProxy = proxyUrl && proxyUrl.hostname; + if (this._keepAlive && useProxy) { + agent = this._proxyAgent; + } + if (!useProxy) { + agent = this._agent; + } + // if agent is already assigned use that agent. + if (agent) { + return agent; + } + const usingSsl = parsedUrl.protocol === 'https:'; + let maxSockets = 100; + if (this.requestOptions) { + maxSockets = this.requestOptions.maxSockets || http.globalAgent.maxSockets; + } + // This is `useProxy` again, but we need to check `proxyURl` directly for TypeScripts's flow analysis. + if (proxyUrl && proxyUrl.hostname) { + const agentOptions = { + maxSockets, + keepAlive: this._keepAlive, + proxy: Object.assign(Object.assign({}, ((proxyUrl.username || proxyUrl.password) && { + proxyAuth: `${proxyUrl.username}:${proxyUrl.password}` + })), { host: proxyUrl.hostname, port: proxyUrl.port }) + }; + let tunnelAgent; + const overHttps = proxyUrl.protocol === 'https:'; + if (usingSsl) { + tunnelAgent = overHttps ? tunnel.httpsOverHttps : tunnel.httpsOverHttp; + } + else { + tunnelAgent = overHttps ? tunnel.httpOverHttps : tunnel.httpOverHttp; + } + agent = tunnelAgent(agentOptions); + this._proxyAgent = agent; + } + // if tunneling agent isn't assigned create a new agent + if (!agent) { + const options = { keepAlive: this._keepAlive, maxSockets }; + agent = usingSsl ? new https.Agent(options) : new http.Agent(options); + this._agent = agent; + } + if (usingSsl && this._ignoreSslError) { + // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process + // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options + // we have to cast it to any and change it directly + agent.options = Object.assign(agent.options || {}, { + rejectUnauthorized: false + }); + } + return agent; + } + _getProxyAgentDispatcher(parsedUrl, proxyUrl) { + let proxyAgent; + if (this._keepAlive) { + proxyAgent = this._proxyAgentDispatcher; + } + // if agent is already assigned use that agent. + if (proxyAgent) { + return proxyAgent; + } + const usingSsl = parsedUrl.protocol === 'https:'; + proxyAgent = new undici_1.ProxyAgent(Object.assign({ uri: proxyUrl.href, pipelining: !this._keepAlive ? 0 : 1 }, ((proxyUrl.username || proxyUrl.password) && { + token: `Basic ${Buffer.from(`${proxyUrl.username}:${proxyUrl.password}`).toString('base64')}` + }))); + this._proxyAgentDispatcher = proxyAgent; + if (usingSsl && this._ignoreSslError) { + // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process + // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options + // we have to cast it to any and change it directly + proxyAgent.options = Object.assign(proxyAgent.options.requestTls || {}, { + rejectUnauthorized: false + }); + } + return proxyAgent; + } + _performExponentialBackoff(retryNumber) { + return __awaiter(this, void 0, void 0, function* () { + retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber); + const ms = ExponentialBackoffTimeSlice * Math.pow(2, retryNumber); + return new Promise(resolve => setTimeout(() => resolve(), ms)); + }); + } + _processResponse(res, options) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { + const statusCode = res.message.statusCode || 0; + const response = { + statusCode, + result: null, + headers: {} + }; + // not found leads to null obj returned + if (statusCode === HttpCodes.NotFound) { + resolve(response); + } + // get the result from the body + function dateTimeDeserializer(key, value) { + if (typeof value === 'string') { + const a = new Date(value); + if (!isNaN(a.valueOf())) { + return a; + } + } + return value; + } + let obj; + let contents; + try { + contents = yield res.readBody(); + if (contents && contents.length > 0) { + if (options && options.deserializeDates) { + obj = JSON.parse(contents, dateTimeDeserializer); + } + else { + obj = JSON.parse(contents); + } + response.result = obj; + } + response.headers = res.message.headers; + } + catch (err) { + // Invalid resource (contents not json); leaving result obj null + } + // note that 3xx redirects are handled by the http layer. + if (statusCode > 299) { + let msg; + // if exception/error in body, attempt to get better error + if (obj && obj.message) { + msg = obj.message; + } + else if (contents && contents.length > 0) { + // it may be the case that the exception is in the body message as string + msg = contents; + } + else { + msg = `Failed request: (${statusCode})`; + } + const err = new HttpClientError(msg, statusCode); + err.result = response.result; + reject(err); + } + else { + resolve(response); + } + })); + }); + } +} +exports.HttpClient = HttpClient; +const lowercaseKeys = (obj) => Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {}); +//# sourceMappingURL=index.js.map + +/***/ }), + +/***/ 4985: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.checkBypass = exports.getProxyUrl = void 0; +function getProxyUrl(reqUrl) { + const usingSsl = reqUrl.protocol === 'https:'; + if (checkBypass(reqUrl)) { + return undefined; + } + const proxyVar = (() => { + if (usingSsl) { + return process.env['https_proxy'] || process.env['HTTPS_PROXY']; + } + else { + return process.env['http_proxy'] || process.env['HTTP_PROXY']; + } + })(); + if (proxyVar) { + try { + return new DecodedURL(proxyVar); + } + catch (_a) { + if (!proxyVar.startsWith('http://') && !proxyVar.startsWith('https://')) + return new DecodedURL(`http://${proxyVar}`); + } + } + else { + return undefined; + } +} +exports.getProxyUrl = getProxyUrl; +function checkBypass(reqUrl) { + if (!reqUrl.hostname) { + return false; + } + const reqHost = reqUrl.hostname; + if (isLoopbackAddress(reqHost)) { + return true; + } + const noProxy = process.env['no_proxy'] || process.env['NO_PROXY'] || ''; + if (!noProxy) { + return false; + } + // Determine the request port + let reqPort; + if (reqUrl.port) { + reqPort = Number(reqUrl.port); + } + else if (reqUrl.protocol === 'http:') { + reqPort = 80; + } + else if (reqUrl.protocol === 'https:') { + reqPort = 443; + } + // Format the request hostname and hostname with port + const upperReqHosts = [reqUrl.hostname.toUpperCase()]; + if (typeof reqPort === 'number') { + upperReqHosts.push(`${upperReqHosts[0]}:${reqPort}`); + } + // Compare request host against noproxy + for (const upperNoProxyItem of noProxy + .split(',') + .map(x => x.trim().toUpperCase()) + .filter(x => x)) { + if (upperNoProxyItem === '*' || + upperReqHosts.some(x => x === upperNoProxyItem || + x.endsWith(`.${upperNoProxyItem}`) || + (upperNoProxyItem.startsWith('.') && + x.endsWith(`${upperNoProxyItem}`)))) { + return true; + } + } + return false; +} +exports.checkBypass = checkBypass; +function isLoopbackAddress(host) { + const hostLower = host.toLowerCase(); + return (hostLower === 'localhost' || + hostLower.startsWith('127.') || + hostLower.startsWith('[::1]') || + hostLower.startsWith('[0:0:0:0:0:0:0:1]')); +} +class DecodedURL extends URL { + constructor(url, base) { + super(url, base); + this._decodedUsername = decodeURIComponent(super.username); + this._decodedPassword = decodeURIComponent(super.password); + } + get username() { + return this._decodedUsername; + } + get password() { + return this._decodedPassword; + } +} +//# sourceMappingURL=proxy.js.map + +/***/ }), + +/***/ 6280: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var _a; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getCmdPath = exports.tryGetExecutablePath = exports.isRooted = exports.isDirectory = exports.exists = exports.READONLY = exports.UV_FS_O_EXLOCK = exports.IS_WINDOWS = exports.unlink = exports.symlink = exports.stat = exports.rmdir = exports.rm = exports.rename = exports.readlink = exports.readdir = exports.open = exports.mkdir = exports.lstat = exports.copyFile = exports.chmod = void 0; +const fs = __importStar(__nccwpck_require__(9896)); +const path = __importStar(__nccwpck_require__(6928)); +_a = fs.promises +// export const {open} = 'fs' +, exports.chmod = _a.chmod, exports.copyFile = _a.copyFile, exports.lstat = _a.lstat, exports.mkdir = _a.mkdir, exports.open = _a.open, exports.readdir = _a.readdir, exports.readlink = _a.readlink, exports.rename = _a.rename, exports.rm = _a.rm, exports.rmdir = _a.rmdir, exports.stat = _a.stat, exports.symlink = _a.symlink, exports.unlink = _a.unlink; +// export const {open} = 'fs' +exports.IS_WINDOWS = process.platform === 'win32'; +// See https://github.com/nodejs/node/blob/d0153aee367422d0858105abec186da4dff0a0c5/deps/uv/include/uv/win.h#L691 +exports.UV_FS_O_EXLOCK = 0x10000000; +exports.READONLY = fs.constants.O_RDONLY; +function exists(fsPath) { + return __awaiter(this, void 0, void 0, function* () { + try { + yield exports.stat(fsPath); + } + catch (err) { + if (err.code === 'ENOENT') { + return false; + } + throw err; + } + return true; + }); +} +exports.exists = exists; +function isDirectory(fsPath, useStat = false) { + return __awaiter(this, void 0, void 0, function* () { + const stats = useStat ? yield exports.stat(fsPath) : yield exports.lstat(fsPath); + return stats.isDirectory(); + }); +} +exports.isDirectory = isDirectory; +/** + * On OSX/Linux, true if path starts with '/'. On Windows, true for paths like: + * \, \hello, \\hello\share, C:, and C:\hello (and corresponding alternate separator cases). + */ +function isRooted(p) { + p = normalizeSeparators(p); + if (!p) { + throw new Error('isRooted() parameter "p" cannot be empty'); + } + if (exports.IS_WINDOWS) { + return (p.startsWith('\\') || /^[A-Z]:/i.test(p) // e.g. \ or \hello or \\hello + ); // e.g. C: or C:\hello + } + return p.startsWith('/'); +} +exports.isRooted = isRooted; +/** + * Best effort attempt to determine whether a file exists and is executable. + * @param filePath file path to check + * @param extensions additional file extensions to try + * @return if file exists and is executable, returns the file path. otherwise empty string. + */ +function tryGetExecutablePath(filePath, extensions) { + return __awaiter(this, void 0, void 0, function* () { + let stats = undefined; + try { + // test file exists + stats = yield exports.stat(filePath); + } + catch (err) { + if (err.code !== 'ENOENT') { + // eslint-disable-next-line no-console + console.log(`Unexpected error attempting to determine if executable file exists '${filePath}': ${err}`); + } + } + if (stats && stats.isFile()) { + if (exports.IS_WINDOWS) { + // on Windows, test for valid extension + const upperExt = path.extname(filePath).toUpperCase(); + if (extensions.some(validExt => validExt.toUpperCase() === upperExt)) { + return filePath; + } + } + else { + if (isUnixExecutable(stats)) { + return filePath; + } + } + } + // try each extension + const originalFilePath = filePath; + for (const extension of extensions) { + filePath = originalFilePath + extension; + stats = undefined; + try { + stats = yield exports.stat(filePath); + } + catch (err) { + if (err.code !== 'ENOENT') { + // eslint-disable-next-line no-console + console.log(`Unexpected error attempting to determine if executable file exists '${filePath}': ${err}`); + } + } + if (stats && stats.isFile()) { + if (exports.IS_WINDOWS) { + // preserve the case of the actual file (since an extension was appended) + try { + const directory = path.dirname(filePath); + const upperName = path.basename(filePath).toUpperCase(); + for (const actualName of yield exports.readdir(directory)) { + if (upperName === actualName.toUpperCase()) { + filePath = path.join(directory, actualName); + break; + } + } + } + catch (err) { + // eslint-disable-next-line no-console + console.log(`Unexpected error attempting to determine the actual case of the file '${filePath}': ${err}`); + } + return filePath; + } + else { + if (isUnixExecutable(stats)) { + return filePath; + } + } + } + } + return ''; + }); +} +exports.tryGetExecutablePath = tryGetExecutablePath; +function normalizeSeparators(p) { + p = p || ''; + if (exports.IS_WINDOWS) { + // convert slashes on Windows + p = p.replace(/\//g, '\\'); + // remove redundant slashes + return p.replace(/\\\\+/g, '\\'); + } + // remove redundant slashes + return p.replace(/\/\/+/g, '/'); +} +// on Mac/Linux, test the execute bit +// R W X R W X R W X +// 256 128 64 32 16 8 4 2 1 +function isUnixExecutable(stats) { + return ((stats.mode & 1) > 0 || + ((stats.mode & 8) > 0 && stats.gid === process.getgid()) || + ((stats.mode & 64) > 0 && stats.uid === process.getuid())); +} +// Get the path of cmd.exe in windows +function getCmdPath() { + var _a; + return (_a = process.env['COMSPEC']) !== null && _a !== void 0 ? _a : `cmd.exe`; +} +exports.getCmdPath = getCmdPath; +//# sourceMappingURL=io-util.js.map + +/***/ }), + +/***/ 5607: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.findInPath = exports.which = exports.mkdirP = exports.rmRF = exports.mv = exports.cp = void 0; +const assert_1 = __nccwpck_require__(2613); +const path = __importStar(__nccwpck_require__(6928)); +const ioUtil = __importStar(__nccwpck_require__(6280)); +/** + * Copies a file or folder. + * Based off of shelljs - https://github.com/shelljs/shelljs/blob/9237f66c52e5daa40458f94f9565e18e8132f5a6/src/cp.js + * + * @param source source path + * @param dest destination path + * @param options optional. See CopyOptions. + */ +function cp(source, dest, options = {}) { + return __awaiter(this, void 0, void 0, function* () { + const { force, recursive, copySourceDirectory } = readCopyOptions(options); + const destStat = (yield ioUtil.exists(dest)) ? yield ioUtil.stat(dest) : null; + // Dest is an existing file, but not forcing + if (destStat && destStat.isFile() && !force) { + return; + } + // If dest is an existing directory, should copy inside. + const newDest = destStat && destStat.isDirectory() && copySourceDirectory + ? path.join(dest, path.basename(source)) + : dest; + if (!(yield ioUtil.exists(source))) { + throw new Error(`no such file or directory: ${source}`); + } + const sourceStat = yield ioUtil.stat(source); + if (sourceStat.isDirectory()) { + if (!recursive) { + throw new Error(`Failed to copy. ${source} is a directory, but tried to copy without recursive flag.`); + } + else { + yield cpDirRecursive(source, newDest, 0, force); + } + } + else { + if (path.relative(source, newDest) === '') { + // a file cannot be copied to itself + throw new Error(`'${newDest}' and '${source}' are the same file`); + } + yield copyFile(source, newDest, force); + } + }); +} +exports.cp = cp; +/** + * Moves a path. + * + * @param source source path + * @param dest destination path + * @param options optional. See MoveOptions. + */ +function mv(source, dest, options = {}) { + return __awaiter(this, void 0, void 0, function* () { + if (yield ioUtil.exists(dest)) { + let destExists = true; + if (yield ioUtil.isDirectory(dest)) { + // If dest is directory copy src into dest + dest = path.join(dest, path.basename(source)); + destExists = yield ioUtil.exists(dest); + } + if (destExists) { + if (options.force == null || options.force) { + yield rmRF(dest); + } + else { + throw new Error('Destination already exists'); + } + } + } + yield mkdirP(path.dirname(dest)); + yield ioUtil.rename(source, dest); + }); +} +exports.mv = mv; +/** + * Remove a path recursively with force + * + * @param inputPath path to remove + */ +function rmRF(inputPath) { + return __awaiter(this, void 0, void 0, function* () { + if (ioUtil.IS_WINDOWS) { + // Check for invalid characters + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + if (/[*"<>|]/.test(inputPath)) { + throw new Error('File path must not contain `*`, `"`, `<`, `>` or `|` on Windows'); + } + } + try { + // note if path does not exist, error is silent + yield ioUtil.rm(inputPath, { + force: true, + maxRetries: 3, + recursive: true, + retryDelay: 300 + }); + } + catch (err) { + throw new Error(`File was unable to be removed ${err}`); + } + }); +} +exports.rmRF = rmRF; +/** + * Make a directory. Creates the full path with folders in between + * Will throw if it fails + * + * @param fsPath path to create + * @returns Promise + */ +function mkdirP(fsPath) { + return __awaiter(this, void 0, void 0, function* () { + assert_1.ok(fsPath, 'a path argument must be provided'); + yield ioUtil.mkdir(fsPath, { recursive: true }); + }); +} +exports.mkdirP = mkdirP; +/** + * Returns path of a tool had the tool actually been invoked. Resolves via paths. + * If you check and the tool does not exist, it will throw. + * + * @param tool name of the tool + * @param check whether to check if tool exists + * @returns Promise path to tool + */ +function which(tool, check) { + return __awaiter(this, void 0, void 0, function* () { + if (!tool) { + throw new Error("parameter 'tool' is required"); + } + // recursive when check=true + if (check) { + const result = yield which(tool, false); + if (!result) { + if (ioUtil.IS_WINDOWS) { + throw new Error(`Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also verify the file has a valid extension for an executable file.`); + } + else { + throw new Error(`Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also check the file mode to verify the file is executable.`); + } + } + return result; + } + const matches = yield findInPath(tool); + if (matches && matches.length > 0) { + return matches[0]; + } + return ''; + }); +} +exports.which = which; +/** + * Returns a list of all occurrences of the given tool on the system path. + * + * @returns Promise the paths of the tool + */ +function findInPath(tool) { + return __awaiter(this, void 0, void 0, function* () { + if (!tool) { + throw new Error("parameter 'tool' is required"); + } + // build the list of extensions to try + const extensions = []; + if (ioUtil.IS_WINDOWS && process.env['PATHEXT']) { + for (const extension of process.env['PATHEXT'].split(path.delimiter)) { + if (extension) { + extensions.push(extension); + } + } + } + // if it's rooted, return it if exists. otherwise return empty. + if (ioUtil.isRooted(tool)) { + const filePath = yield ioUtil.tryGetExecutablePath(tool, extensions); + if (filePath) { + return [filePath]; + } + return []; + } + // if any path separators, return empty + if (tool.includes(path.sep)) { + return []; + } + // build the list of directories + // + // Note, technically "where" checks the current directory on Windows. From a toolkit perspective, + // it feels like we should not do this. Checking the current directory seems like more of a use + // case of a shell, and the which() function exposed by the toolkit should strive for consistency + // across platforms. + const directories = []; + if (process.env.PATH) { + for (const p of process.env.PATH.split(path.delimiter)) { + if (p) { + directories.push(p); + } + } + } + // find all matches + const matches = []; + for (const directory of directories) { + const filePath = yield ioUtil.tryGetExecutablePath(path.join(directory, tool), extensions); + if (filePath) { + matches.push(filePath); + } + } + return matches; + }); +} +exports.findInPath = findInPath; +function readCopyOptions(options) { + const force = options.force == null ? true : options.force; + const recursive = Boolean(options.recursive); + const copySourceDirectory = options.copySourceDirectory == null + ? true + : Boolean(options.copySourceDirectory); + return { force, recursive, copySourceDirectory }; +} +function cpDirRecursive(sourceDir, destDir, currentDepth, force) { + return __awaiter(this, void 0, void 0, function* () { + // Ensure there is not a run away recursive copy + if (currentDepth >= 255) + return; + currentDepth++; + yield mkdirP(destDir); + const files = yield ioUtil.readdir(sourceDir); + for (const fileName of files) { + const srcFile = `${sourceDir}/${fileName}`; + const destFile = `${destDir}/${fileName}`; + const srcFileStat = yield ioUtil.lstat(srcFile); + if (srcFileStat.isDirectory()) { + // Recurse + yield cpDirRecursive(srcFile, destFile, currentDepth, force); + } + else { + yield copyFile(srcFile, destFile, force); + } + } + // Change the mode for the newly created directory + yield ioUtil.chmod(destDir, (yield ioUtil.stat(sourceDir)).mode); + }); +} +// Buffered file copy +function copyFile(srcFile, destFile, force) { + return __awaiter(this, void 0, void 0, function* () { + if ((yield ioUtil.lstat(srcFile)).isSymbolicLink()) { + // unlink/re-link it + try { + yield ioUtil.lstat(destFile); + yield ioUtil.unlink(destFile); + } + catch (e) { + // Try to override file permission + if (e.code === 'EPERM') { + yield ioUtil.chmod(destFile, '0666'); + yield ioUtil.unlink(destFile); + } + // other errors = it doesn't exist, no work to do + } + // Copy over symlink + const symlinkFull = yield ioUtil.readlink(srcFile); + yield ioUtil.symlink(symlinkFull, destFile, ioUtil.IS_WINDOWS ? 'junction' : null); + } + else if (!(yield ioUtil.exists(destFile)) || force) { + yield ioUtil.copyFile(srcFile, destFile); + } + }); +} +//# sourceMappingURL=io.js.map + +/***/ }), + +/***/ 3085: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +/* @flow */ +/*:: + +type DotenvParseOptions = { + debug?: boolean +} + +// keys and values from src +type DotenvParseOutput = { [string]: string } + +type DotenvConfigOptions = { + path?: string, // path to .env file + encoding?: string, // encoding of .env file + debug?: string // turn on logging for debugging purposes +} + +type DotenvConfigOutput = { + parsed?: DotenvParseOutput, + error?: Error +} + +*/ + +const fs = __nccwpck_require__(9896) +const path = __nccwpck_require__(6928) + +function log (message /*: string */) { + console.log(`[dotenv][DEBUG] ${message}`) +} + +const NEWLINE = '\n' +const RE_INI_KEY_VAL = /^\s*([\w.-]+)\s*=\s*(.*)?\s*$/ +const RE_NEWLINES = /\\n/g +const NEWLINES_MATCH = /\n|\r|\r\n/ + +// Parses src into an Object +function parse (src /*: string | Buffer */, options /*: ?DotenvParseOptions */) /*: DotenvParseOutput */ { + const debug = Boolean(options && options.debug) + const obj = {} + + // convert Buffers before splitting into lines and processing + src.toString().split(NEWLINES_MATCH).forEach(function (line, idx) { + // matching "KEY' and 'VAL' in 'KEY=VAL' + const keyValueArr = line.match(RE_INI_KEY_VAL) + // matched? + if (keyValueArr != null) { + const key = keyValueArr[1] + // default undefined or missing values to empty string + let val = (keyValueArr[2] || '') + const end = val.length - 1 + const isDoubleQuoted = val[0] === '"' && val[end] === '"' + const isSingleQuoted = val[0] === "'" && val[end] === "'" + + // if single or double quoted, remove quotes + if (isSingleQuoted || isDoubleQuoted) { + val = val.substring(1, end) + + // if double quoted, expand newlines + if (isDoubleQuoted) { + val = val.replace(RE_NEWLINES, NEWLINE) + } + } else { + // remove surrounding whitespace + val = val.trim() + } + + obj[key] = val + } else if (debug) { + log(`did not match key and value when parsing line ${idx + 1}: ${line}`) + } + }) + + return obj +} + +// Populates process.env from .env file +function config (options /*: ?DotenvConfigOptions */) /*: DotenvConfigOutput */ { + let dotenvPath = path.resolve(process.cwd(), '.env') + let encoding /*: string */ = 'utf8' + let debug = false + + if (options) { + if (options.path != null) { + dotenvPath = options.path + } + if (options.encoding != null) { + encoding = options.encoding + } + if (options.debug != null) { + debug = true + } + } + + try { + // specifying an encoding returns a string instead of a buffer + const parsed = parse(fs.readFileSync(dotenvPath, { encoding }), { debug }) + + Object.keys(parsed).forEach(function (key) { + if (!Object.prototype.hasOwnProperty.call(process.env, key)) { + process.env[key] = parsed[key] + } else if (debug) { + log(`"${key}" is already defined in \`process.env\` and will not be overwritten`) + } + }) + + return { parsed } + } catch (e) { + return { error: e } + } +} + +module.exports.config = config +module.exports.parse = parse + + +/***/ }), + +/***/ 858: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +module.exports = __nccwpck_require__(4450); + + +/***/ }), + +/***/ 4450: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +var net = __nccwpck_require__(9278); +var tls = __nccwpck_require__(4756); +var http = __nccwpck_require__(8611); +var https = __nccwpck_require__(5692); +var events = __nccwpck_require__(4434); +var assert = __nccwpck_require__(2613); +var util = __nccwpck_require__(9023); + + +exports.httpOverHttp = httpOverHttp; +exports.httpsOverHttp = httpsOverHttp; +exports.httpOverHttps = httpOverHttps; +exports.httpsOverHttps = httpsOverHttps; + + +function httpOverHttp(options) { + var agent = new TunnelingAgent(options); + agent.request = http.request; + return agent; +} + +function httpsOverHttp(options) { + var agent = new TunnelingAgent(options); + agent.request = http.request; + agent.createSocket = createSecureSocket; + agent.defaultPort = 443; + return agent; +} + +function httpOverHttps(options) { + var agent = new TunnelingAgent(options); + agent.request = https.request; + return agent; +} + +function httpsOverHttps(options) { + var agent = new TunnelingAgent(options); + agent.request = https.request; + agent.createSocket = createSecureSocket; + agent.defaultPort = 443; + return agent; +} + + +function TunnelingAgent(options) { + var self = this; + self.options = options || {}; + self.proxyOptions = self.options.proxy || {}; + self.maxSockets = self.options.maxSockets || http.Agent.defaultMaxSockets; + self.requests = []; + self.sockets = []; + + self.on('free', function onFree(socket, host, port, localAddress) { + var options = toOptions(host, port, localAddress); + for (var i = 0, len = self.requests.length; i < len; ++i) { + var pending = self.requests[i]; + if (pending.host === options.host && pending.port === options.port) { + // Detect the request to connect same origin server, + // reuse the connection. + self.requests.splice(i, 1); + pending.request.onSocket(socket); + return; + } + } + socket.destroy(); + self.removeSocket(socket); + }); +} +util.inherits(TunnelingAgent, events.EventEmitter); + +TunnelingAgent.prototype.addRequest = function addRequest(req, host, port, localAddress) { + var self = this; + var options = mergeOptions({request: req}, self.options, toOptions(host, port, localAddress)); + + if (self.sockets.length >= this.maxSockets) { + // We are over limit so we'll add it to the queue. + self.requests.push(options); + return; + } + + // If we are under maxSockets create a new one. + self.createSocket(options, function(socket) { + socket.on('free', onFree); + socket.on('close', onCloseOrRemove); + socket.on('agentRemove', onCloseOrRemove); + req.onSocket(socket); + + function onFree() { + self.emit('free', socket, options); + } + + function onCloseOrRemove(err) { + self.removeSocket(socket); + socket.removeListener('free', onFree); + socket.removeListener('close', onCloseOrRemove); + socket.removeListener('agentRemove', onCloseOrRemove); + } + }); +}; + +TunnelingAgent.prototype.createSocket = function createSocket(options, cb) { + var self = this; + var placeholder = {}; + self.sockets.push(placeholder); + + var connectOptions = mergeOptions({}, self.proxyOptions, { + method: 'CONNECT', + path: options.host + ':' + options.port, + agent: false, + headers: { + host: options.host + ':' + options.port + } + }); + if (options.localAddress) { + connectOptions.localAddress = options.localAddress; + } + if (connectOptions.proxyAuth) { + connectOptions.headers = connectOptions.headers || {}; + connectOptions.headers['Proxy-Authorization'] = 'Basic ' + + new Buffer(connectOptions.proxyAuth).toString('base64'); + } + + debug('making CONNECT request'); + var connectReq = self.request(connectOptions); + connectReq.useChunkedEncodingByDefault = false; // for v0.6 + connectReq.once('response', onResponse); // for v0.6 + connectReq.once('upgrade', onUpgrade); // for v0.6 + connectReq.once('connect', onConnect); // for v0.7 or later + connectReq.once('error', onError); + connectReq.end(); + + function onResponse(res) { + // Very hacky. This is necessary to avoid http-parser leaks. + res.upgrade = true; + } + + function onUpgrade(res, socket, head) { + // Hacky. + process.nextTick(function() { + onConnect(res, socket, head); + }); + } + + function onConnect(res, socket, head) { + connectReq.removeAllListeners(); + socket.removeAllListeners(); + + if (res.statusCode !== 200) { + debug('tunneling socket could not be established, statusCode=%d', + res.statusCode); + socket.destroy(); + var error = new Error('tunneling socket could not be established, ' + + 'statusCode=' + res.statusCode); + error.code = 'ECONNRESET'; + options.request.emit('error', error); + self.removeSocket(placeholder); + return; + } + if (head.length > 0) { + debug('got illegal response body from proxy'); + socket.destroy(); + var error = new Error('got illegal response body from proxy'); + error.code = 'ECONNRESET'; + options.request.emit('error', error); + self.removeSocket(placeholder); + return; + } + debug('tunneling connection has established'); + self.sockets[self.sockets.indexOf(placeholder)] = socket; + return cb(socket); + } + + function onError(cause) { + connectReq.removeAllListeners(); + + debug('tunneling socket could not be established, cause=%s\n', + cause.message, cause.stack); + var error = new Error('tunneling socket could not be established, ' + + 'cause=' + cause.message); + error.code = 'ECONNRESET'; + options.request.emit('error', error); + self.removeSocket(placeholder); + } +}; + +TunnelingAgent.prototype.removeSocket = function removeSocket(socket) { + var pos = this.sockets.indexOf(socket) + if (pos === -1) { + return; + } + this.sockets.splice(pos, 1); + + var pending = this.requests.shift(); + if (pending) { + // If we have pending requests and a socket gets closed a new one + // needs to be created to take over in the pool for the one that closed. + this.createSocket(pending, function(socket) { + pending.request.onSocket(socket); + }); + } +}; + +function createSecureSocket(options, cb) { + var self = this; + TunnelingAgent.prototype.createSocket.call(self, options, function(socket) { + var hostHeader = options.request.getHeader('host'); + var tlsOptions = mergeOptions({}, self.options, { + socket: socket, + servername: hostHeader ? hostHeader.replace(/:.*$/, '') : options.host + }); + + // 0 is dummy port for v0.6 + var secureSocket = tls.connect(0, tlsOptions); + self.sockets[self.sockets.indexOf(socket)] = secureSocket; + cb(secureSocket); + }); +} + + +function toOptions(host, port, localAddress) { + if (typeof host === 'string') { // since v0.10 + return { + host: host, + port: port, + localAddress: localAddress + }; + } + return host; // for v0.11 or later +} + +function mergeOptions(target) { + for (var i = 1, len = arguments.length; i < len; ++i) { + var overrides = arguments[i]; + if (typeof overrides === 'object') { + var keys = Object.keys(overrides); + for (var j = 0, keyLen = keys.length; j < keyLen; ++j) { + var k = keys[j]; + if (overrides[k] !== undefined) { + target[k] = overrides[k]; + } + } + } + } + return target; +} + + +var debug; +if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) { + debug = function() { + var args = Array.prototype.slice.call(arguments); + if (typeof args[0] === 'string') { + args[0] = 'TUNNEL: ' + args[0]; + } else { + args.unshift('TUNNEL:'); + } + console.error.apply(console, args); + } +} else { + debug = function() {}; +} +exports.debug = debug; // for test + + +/***/ }), + +/***/ 2740: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const Client = __nccwpck_require__(5833) +const Dispatcher = __nccwpck_require__(5903) +const errors = __nccwpck_require__(1975) +const Pool = __nccwpck_require__(6976) +const BalancedPool = __nccwpck_require__(7137) +const Agent = __nccwpck_require__(2841) +const util = __nccwpck_require__(1436) +const { InvalidArgumentError } = errors +const api = __nccwpck_require__(4355) +const buildConnector = __nccwpck_require__(3940) +const MockClient = __nccwpck_require__(441) +const MockAgent = __nccwpck_require__(4169) +const MockPool = __nccwpck_require__(9216) +const mockErrors = __nccwpck_require__(4257) +const ProxyAgent = __nccwpck_require__(3644) +const RetryHandler = __nccwpck_require__(3441) +const { getGlobalDispatcher, setGlobalDispatcher } = __nccwpck_require__(8377) +const DecoratorHandler = __nccwpck_require__(3276) +const RedirectHandler = __nccwpck_require__(6303) +const createRedirectInterceptor = __nccwpck_require__(1475) + +let hasCrypto +try { + __nccwpck_require__(6982) + hasCrypto = true +} catch { + hasCrypto = false +} + +Object.assign(Dispatcher.prototype, api) + +module.exports.Dispatcher = Dispatcher +module.exports.Client = Client +module.exports.Pool = Pool +module.exports.BalancedPool = BalancedPool +module.exports.Agent = Agent +module.exports.ProxyAgent = ProxyAgent +module.exports.RetryHandler = RetryHandler + +module.exports.DecoratorHandler = DecoratorHandler +module.exports.RedirectHandler = RedirectHandler +module.exports.createRedirectInterceptor = createRedirectInterceptor + +module.exports.buildConnector = buildConnector +module.exports.errors = errors + +function makeDispatcher (fn) { + return (url, opts, handler) => { + if (typeof opts === 'function') { + handler = opts + opts = null + } + + if (!url || (typeof url !== 'string' && typeof url !== 'object' && !(url instanceof URL))) { + throw new InvalidArgumentError('invalid url') + } + + if (opts != null && typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (opts && opts.path != null) { + if (typeof opts.path !== 'string') { + throw new InvalidArgumentError('invalid opts.path') + } + + let path = opts.path + if (!opts.path.startsWith('/')) { + path = `/${path}` + } + + url = new URL(util.parseOrigin(url).origin + path) + } else { + if (!opts) { + opts = typeof url === 'object' ? url : {} + } + + url = util.parseURL(url) + } + + const { agent, dispatcher = getGlobalDispatcher() } = opts + + if (agent) { + throw new InvalidArgumentError('unsupported opts.agent. Did you mean opts.client?') + } + + return fn.call(dispatcher, { + ...opts, + origin: url.origin, + path: url.search ? `${url.pathname}${url.search}` : url.pathname, + method: opts.method || (opts.body ? 'PUT' : 'GET') + }, handler) + } +} + +module.exports.setGlobalDispatcher = setGlobalDispatcher +module.exports.getGlobalDispatcher = getGlobalDispatcher + +if (util.nodeMajor > 16 || (util.nodeMajor === 16 && util.nodeMinor >= 8)) { + let fetchImpl = null + module.exports.fetch = async function fetch (resource) { + if (!fetchImpl) { + fetchImpl = (__nccwpck_require__(1279).fetch) + } + + try { + return await fetchImpl(...arguments) + } catch (err) { + if (typeof err === 'object') { + Error.captureStackTrace(err, this) + } + + throw err + } + } + module.exports.Headers = __nccwpck_require__(161).Headers + module.exports.Response = __nccwpck_require__(2440).Response + module.exports.Request = __nccwpck_require__(1558).Request + module.exports.FormData = __nccwpck_require__(2813).FormData + module.exports.File = __nccwpck_require__(7085).File + module.exports.FileReader = __nccwpck_require__(1428).FileReader + + const { setGlobalOrigin, getGlobalOrigin } = __nccwpck_require__(960) + + module.exports.setGlobalOrigin = setGlobalOrigin + module.exports.getGlobalOrigin = getGlobalOrigin + + const { CacheStorage } = __nccwpck_require__(254) + const { kConstruct } = __nccwpck_require__(7916) + + // Cache & CacheStorage are tightly coupled with fetch. Even if it may run + // in an older version of Node, it doesn't have any use without fetch. + module.exports.caches = new CacheStorage(kConstruct) +} + +if (util.nodeMajor >= 16) { + const { deleteCookie, getCookies, getSetCookies, setCookie } = __nccwpck_require__(4260) + + module.exports.deleteCookie = deleteCookie + module.exports.getCookies = getCookies + module.exports.getSetCookies = getSetCookies + module.exports.setCookie = setCookie + + const { parseMIMEType, serializeAMimeType } = __nccwpck_require__(5294) + + module.exports.parseMIMEType = parseMIMEType + module.exports.serializeAMimeType = serializeAMimeType +} + +if (util.nodeMajor >= 18 && hasCrypto) { + const { WebSocket } = __nccwpck_require__(599) + + module.exports.WebSocket = WebSocket +} + +module.exports.request = makeDispatcher(api.request) +module.exports.stream = makeDispatcher(api.stream) +module.exports.pipeline = makeDispatcher(api.pipeline) +module.exports.connect = makeDispatcher(api.connect) +module.exports.upgrade = makeDispatcher(api.upgrade) + +module.exports.MockClient = MockClient +module.exports.MockPool = MockPool +module.exports.MockAgent = MockAgent +module.exports.mockErrors = mockErrors + + +/***/ }), + +/***/ 2841: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { InvalidArgumentError } = __nccwpck_require__(1975) +const { kClients, kRunning, kClose, kDestroy, kDispatch, kInterceptors } = __nccwpck_require__(9583) +const DispatcherBase = __nccwpck_require__(3301) +const Pool = __nccwpck_require__(6976) +const Client = __nccwpck_require__(5833) +const util = __nccwpck_require__(1436) +const createRedirectInterceptor = __nccwpck_require__(1475) +const { WeakRef, FinalizationRegistry } = __nccwpck_require__(5254)() + +const kOnConnect = Symbol('onConnect') +const kOnDisconnect = Symbol('onDisconnect') +const kOnConnectionError = Symbol('onConnectionError') +const kMaxRedirections = Symbol('maxRedirections') +const kOnDrain = Symbol('onDrain') +const kFactory = Symbol('factory') +const kFinalizer = Symbol('finalizer') +const kOptions = Symbol('options') + +function defaultFactory (origin, opts) { + return opts && opts.connections === 1 + ? new Client(origin, opts) + : new Pool(origin, opts) +} + +class Agent extends DispatcherBase { + constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { + super() + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { + throw new InvalidArgumentError('connect must be a function or an object') + } + + if (!Number.isInteger(maxRedirections) || maxRedirections < 0) { + throw new InvalidArgumentError('maxRedirections must be a positive number') + } + + if (connect && typeof connect !== 'function') { + connect = { ...connect } + } + + this[kInterceptors] = options.interceptors && options.interceptors.Agent && Array.isArray(options.interceptors.Agent) + ? options.interceptors.Agent + : [createRedirectInterceptor({ maxRedirections })] + + this[kOptions] = { ...util.deepClone(options), connect } + this[kOptions].interceptors = options.interceptors + ? { ...options.interceptors } + : undefined + this[kMaxRedirections] = maxRedirections + this[kFactory] = factory + this[kClients] = new Map() + this[kFinalizer] = new FinalizationRegistry(/* istanbul ignore next: gc is undeterministic */ key => { + const ref = this[kClients].get(key) + if (ref !== undefined && ref.deref() === undefined) { + this[kClients].delete(key) + } + }) + + const agent = this + + this[kOnDrain] = (origin, targets) => { + agent.emit('drain', origin, [agent, ...targets]) + } + + this[kOnConnect] = (origin, targets) => { + agent.emit('connect', origin, [agent, ...targets]) + } + + this[kOnDisconnect] = (origin, targets, err) => { + agent.emit('disconnect', origin, [agent, ...targets], err) + } + + this[kOnConnectionError] = (origin, targets, err) => { + agent.emit('connectionError', origin, [agent, ...targets], err) + } + } + + get [kRunning] () { + let ret = 0 + for (const ref of this[kClients].values()) { + const client = ref.deref() + /* istanbul ignore next: gc is undeterministic */ + if (client) { + ret += client[kRunning] + } + } + return ret + } + + [kDispatch] (opts, handler) { + let key + if (opts.origin && (typeof opts.origin === 'string' || opts.origin instanceof URL)) { + key = String(opts.origin) + } else { + throw new InvalidArgumentError('opts.origin must be a non-empty string or URL.') + } + + const ref = this[kClients].get(key) + + let dispatcher = ref ? ref.deref() : null + if (!dispatcher) { + dispatcher = this[kFactory](opts.origin, this[kOptions]) + .on('drain', this[kOnDrain]) + .on('connect', this[kOnConnect]) + .on('disconnect', this[kOnDisconnect]) + .on('connectionError', this[kOnConnectionError]) + + this[kClients].set(key, new WeakRef(dispatcher)) + this[kFinalizer].register(dispatcher, key) + } + + return dispatcher.dispatch(opts, handler) + } + + async [kClose] () { + const closePromises = [] + for (const ref of this[kClients].values()) { + const client = ref.deref() + /* istanbul ignore else: gc is undeterministic */ + if (client) { + closePromises.push(client.close()) + } + } + + await Promise.all(closePromises) + } + + async [kDestroy] (err) { + const destroyPromises = [] + for (const ref of this[kClients].values()) { + const client = ref.deref() + /* istanbul ignore else: gc is undeterministic */ + if (client) { + destroyPromises.push(client.destroy(err)) + } + } + + await Promise.all(destroyPromises) + } +} + +module.exports = Agent + + +/***/ }), + +/***/ 5482: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const { addAbortListener } = __nccwpck_require__(1436) +const { RequestAbortedError } = __nccwpck_require__(1975) + +const kListener = Symbol('kListener') +const kSignal = Symbol('kSignal') + +function abort (self) { + if (self.abort) { + self.abort() + } else { + self.onError(new RequestAbortedError()) + } +} + +function addSignal (self, signal) { + self[kSignal] = null + self[kListener] = null + + if (!signal) { + return + } + + if (signal.aborted) { + abort(self) + return + } + + self[kSignal] = signal + self[kListener] = () => { + abort(self) + } + + addAbortListener(self[kSignal], self[kListener]) +} + +function removeSignal (self) { + if (!self[kSignal]) { + return + } + + if ('removeEventListener' in self[kSignal]) { + self[kSignal].removeEventListener('abort', self[kListener]) + } else { + self[kSignal].removeListener('abort', self[kListener]) + } + + self[kSignal] = null + self[kListener] = null +} + +module.exports = { + addSignal, + removeSignal +} + + +/***/ }), + +/***/ 472: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { AsyncResource } = __nccwpck_require__(290) +const { InvalidArgumentError, RequestAbortedError, SocketError } = __nccwpck_require__(1975) +const util = __nccwpck_require__(1436) +const { addSignal, removeSignal } = __nccwpck_require__(5482) + +class ConnectHandler extends AsyncResource { + constructor (opts, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + const { signal, opaque, responseHeaders } = opts + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + super('UNDICI_CONNECT') + + this.opaque = opaque || null + this.responseHeaders = responseHeaders || null + this.callback = callback + this.abort = null + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (!this.callback) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = context + } + + onHeaders () { + throw new SocketError('bad connect', null) + } + + onUpgrade (statusCode, rawHeaders, socket) { + const { callback, opaque, context } = this + + removeSignal(this) + + this.callback = null + + let headers = rawHeaders + // Indicates is an HTTP2Session + if (headers != null) { + headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + } + + this.runInAsyncScope(callback, null, null, { + statusCode, + headers, + socket, + opaque, + context + }) + } + + onError (err) { + const { callback, opaque } = this + + removeSignal(this) + + if (callback) { + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + } +} + +function connect (opts, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + connect.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + const connectHandler = new ConnectHandler(opts, callback) + this.dispatch({ ...opts, method: 'CONNECT' }, connectHandler) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts && opts.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = connect + + +/***/ }), + +/***/ 7922: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + Readable, + Duplex, + PassThrough +} = __nccwpck_require__(2203) +const { + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError +} = __nccwpck_require__(1975) +const util = __nccwpck_require__(1436) +const { AsyncResource } = __nccwpck_require__(290) +const { addSignal, removeSignal } = __nccwpck_require__(5482) +const assert = __nccwpck_require__(2613) + +const kResume = Symbol('resume') + +class PipelineRequest extends Readable { + constructor () { + super({ autoDestroy: true }) + + this[kResume] = null + } + + _read () { + const { [kResume]: resume } = this + + if (resume) { + this[kResume] = null + resume() + } + } + + _destroy (err, callback) { + this._read() + + callback(err) + } +} + +class PipelineResponse extends Readable { + constructor (resume) { + super({ autoDestroy: true }) + this[kResume] = resume + } + + _read () { + this[kResume]() + } + + _destroy (err, callback) { + if (!err && !this._readableState.endEmitted) { + err = new RequestAbortedError() + } + + callback(err) + } +} + +class PipelineHandler extends AsyncResource { + constructor (opts, handler) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (typeof handler !== 'function') { + throw new InvalidArgumentError('invalid handler') + } + + const { signal, method, opaque, onInfo, responseHeaders } = opts + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + if (method === 'CONNECT') { + throw new InvalidArgumentError('invalid method') + } + + if (onInfo && typeof onInfo !== 'function') { + throw new InvalidArgumentError('invalid onInfo callback') + } + + super('UNDICI_PIPELINE') + + this.opaque = opaque || null + this.responseHeaders = responseHeaders || null + this.handler = handler + this.abort = null + this.context = null + this.onInfo = onInfo || null + + this.req = new PipelineRequest().on('error', util.nop) + + this.ret = new Duplex({ + readableObjectMode: opts.objectMode, + autoDestroy: true, + read: () => { + const { body } = this + + if (body && body.resume) { + body.resume() + } + }, + write: (chunk, encoding, callback) => { + const { req } = this + + if (req.push(chunk, encoding) || req._readableState.destroyed) { + callback() + } else { + req[kResume] = callback + } + }, + destroy: (err, callback) => { + const { body, req, res, ret, abort } = this + + if (!err && !ret._readableState.endEmitted) { + err = new RequestAbortedError() + } + + if (abort && err) { + abort() + } + + util.destroy(body, err) + util.destroy(req, err) + util.destroy(res, err) + + removeSignal(this) + + callback(err) + } + }).on('prefinish', () => { + const { req } = this + + // Node < 15 does not call _final in same tick. + req.push(null) + }) + + this.res = null + + addSignal(this, signal) + } + + onConnect (abort, context) { + const { ret, res } = this + + assert(!res, 'pipeline cannot be retried') + + if (ret.destroyed) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = context + } + + onHeaders (statusCode, rawHeaders, resume) { + const { opaque, handler, context } = this + + if (statusCode < 200) { + if (this.onInfo) { + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + this.onInfo({ statusCode, headers }) + } + return + } + + this.res = new PipelineResponse(resume) + + let body + try { + this.handler = null + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + body = this.runInAsyncScope(handler, null, { + statusCode, + headers, + opaque, + body: this.res, + context + }) + } catch (err) { + this.res.on('error', util.nop) + throw err + } + + if (!body || typeof body.on !== 'function') { + throw new InvalidReturnValueError('expected Readable') + } + + body + .on('data', (chunk) => { + const { ret, body } = this + + if (!ret.push(chunk) && body.pause) { + body.pause() + } + }) + .on('error', (err) => { + const { ret } = this + + util.destroy(ret, err) + }) + .on('end', () => { + const { ret } = this + + ret.push(null) + }) + .on('close', () => { + const { ret } = this + + if (!ret._readableState.ended) { + util.destroy(ret, new RequestAbortedError()) + } + }) + + this.body = body + } + + onData (chunk) { + const { res } = this + return res.push(chunk) + } + + onComplete (trailers) { + const { res } = this + res.push(null) + } + + onError (err) { + const { ret } = this + this.handler = null + util.destroy(ret, err) + } +} + +function pipeline (opts, handler) { + try { + const pipelineHandler = new PipelineHandler(opts, handler) + this.dispatch({ ...opts, body: pipelineHandler.req }, pipelineHandler) + return pipelineHandler.ret + } catch (err) { + return new PassThrough().destroy(err) + } +} + +module.exports = pipeline + + +/***/ }), + +/***/ 2215: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const Readable = __nccwpck_require__(8195) +const { + InvalidArgumentError, + RequestAbortedError +} = __nccwpck_require__(1975) +const util = __nccwpck_require__(1436) +const { getResolveErrorBodyCallback } = __nccwpck_require__(1083) +const { AsyncResource } = __nccwpck_require__(290) +const { addSignal, removeSignal } = __nccwpck_require__(5482) + +class RequestHandler extends AsyncResource { + constructor (opts, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError, highWaterMark } = opts + + try { + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (highWaterMark && (typeof highWaterMark !== 'number' || highWaterMark < 0)) { + throw new InvalidArgumentError('invalid highWaterMark') + } + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + if (method === 'CONNECT') { + throw new InvalidArgumentError('invalid method') + } + + if (onInfo && typeof onInfo !== 'function') { + throw new InvalidArgumentError('invalid onInfo callback') + } + + super('UNDICI_REQUEST') + } catch (err) { + if (util.isStream(body)) { + util.destroy(body.on('error', util.nop), err) + } + throw err + } + + this.responseHeaders = responseHeaders || null + this.opaque = opaque || null + this.callback = callback + this.res = null + this.abort = null + this.body = body + this.trailers = {} + this.context = null + this.onInfo = onInfo || null + this.throwOnError = throwOnError + this.highWaterMark = highWaterMark + + if (util.isStream(body)) { + body.on('error', (err) => { + this.onError(err) + }) + } + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (!this.callback) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = context + } + + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + const { callback, opaque, abort, context, responseHeaders, highWaterMark } = this + + const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + + if (statusCode < 200) { + if (this.onInfo) { + this.onInfo({ statusCode, headers }) + } + return + } + + const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers + const contentType = parsedHeaders['content-type'] + const body = new Readable({ resume, abort, contentType, highWaterMark }) + + this.callback = null + this.res = body + if (callback !== null) { + if (this.throwOnError && statusCode >= 400) { + this.runInAsyncScope(getResolveErrorBodyCallback, null, + { callback, body, contentType, statusCode, statusMessage, headers } + ) + } else { + this.runInAsyncScope(callback, null, null, { + statusCode, + headers, + trailers: this.trailers, + opaque, + body, + context + }) + } + } + } + + onData (chunk) { + const { res } = this + return res.push(chunk) + } + + onComplete (trailers) { + const { res } = this + + removeSignal(this) + + util.parseHeaders(trailers, this.trailers) + + res.push(null) + } + + onError (err) { + const { res, callback, body, opaque } = this + + removeSignal(this) + + if (callback) { + // TODO: Does this need queueMicrotask? + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + + if (res) { + this.res = null + // Ensure all queued handlers are invoked before destroying res. + queueMicrotask(() => { + util.destroy(res, err) + }) + } + + if (body) { + this.body = null + util.destroy(body, err) + } + } +} + +function request (opts, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + request.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + this.dispatch(opts, new RequestHandler(opts, callback)) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts && opts.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = request +module.exports.RequestHandler = RequestHandler + + +/***/ }), + +/***/ 4676: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { finished, PassThrough } = __nccwpck_require__(2203) +const { + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError +} = __nccwpck_require__(1975) +const util = __nccwpck_require__(1436) +const { getResolveErrorBodyCallback } = __nccwpck_require__(1083) +const { AsyncResource } = __nccwpck_require__(290) +const { addSignal, removeSignal } = __nccwpck_require__(5482) + +class StreamHandler extends AsyncResource { + constructor (opts, factory, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError } = opts + + try { + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('invalid factory') + } + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + if (method === 'CONNECT') { + throw new InvalidArgumentError('invalid method') + } + + if (onInfo && typeof onInfo !== 'function') { + throw new InvalidArgumentError('invalid onInfo callback') + } + + super('UNDICI_STREAM') + } catch (err) { + if (util.isStream(body)) { + util.destroy(body.on('error', util.nop), err) + } + throw err + } + + this.responseHeaders = responseHeaders || null + this.opaque = opaque || null + this.factory = factory + this.callback = callback + this.res = null + this.abort = null + this.context = null + this.trailers = null + this.body = body + this.onInfo = onInfo || null + this.throwOnError = throwOnError || false + + if (util.isStream(body)) { + body.on('error', (err) => { + this.onError(err) + }) + } + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (!this.callback) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = context + } + + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + const { factory, opaque, context, callback, responseHeaders } = this + + const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + + if (statusCode < 200) { + if (this.onInfo) { + this.onInfo({ statusCode, headers }) + } + return + } + + this.factory = null + + let res + + if (this.throwOnError && statusCode >= 400) { + const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers + const contentType = parsedHeaders['content-type'] + res = new PassThrough() + + this.callback = null + this.runInAsyncScope(getResolveErrorBodyCallback, null, + { callback, body: res, contentType, statusCode, statusMessage, headers } + ) + } else { + if (factory === null) { + return + } + + res = this.runInAsyncScope(factory, null, { + statusCode, + headers, + opaque, + context + }) + + if ( + !res || + typeof res.write !== 'function' || + typeof res.end !== 'function' || + typeof res.on !== 'function' + ) { + throw new InvalidReturnValueError('expected Writable') + } + + // TODO: Avoid finished. It registers an unnecessary amount of listeners. + finished(res, { readable: false }, (err) => { + const { callback, res, opaque, trailers, abort } = this + + this.res = null + if (err || !res.readable) { + util.destroy(res, err) + } + + this.callback = null + this.runInAsyncScope(callback, null, err || null, { opaque, trailers }) + + if (err) { + abort() + } + }) + } + + res.on('drain', resume) + + this.res = res + + const needDrain = res.writableNeedDrain !== undefined + ? res.writableNeedDrain + : res._writableState && res._writableState.needDrain + + return needDrain !== true + } + + onData (chunk) { + const { res } = this + + return res ? res.write(chunk) : true + } + + onComplete (trailers) { + const { res } = this + + removeSignal(this) + + if (!res) { + return + } + + this.trailers = util.parseHeaders(trailers) + + res.end() + } + + onError (err) { + const { res, callback, opaque, body } = this + + removeSignal(this) + + this.factory = null + + if (res) { + this.res = null + util.destroy(res, err) + } else if (callback) { + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + + if (body) { + this.body = null + util.destroy(body, err) + } + } +} + +function stream (opts, factory, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + stream.call(this, opts, factory, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + this.dispatch(opts, new StreamHandler(opts, factory, callback)) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts && opts.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = stream + + +/***/ }), + +/***/ 6662: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { InvalidArgumentError, RequestAbortedError, SocketError } = __nccwpck_require__(1975) +const { AsyncResource } = __nccwpck_require__(290) +const util = __nccwpck_require__(1436) +const { addSignal, removeSignal } = __nccwpck_require__(5482) +const assert = __nccwpck_require__(2613) + +class UpgradeHandler extends AsyncResource { + constructor (opts, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + const { signal, opaque, responseHeaders } = opts + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + super('UNDICI_UPGRADE') + + this.responseHeaders = responseHeaders || null + this.opaque = opaque || null + this.callback = callback + this.abort = null + this.context = null + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (!this.callback) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = null + } + + onHeaders () { + throw new SocketError('bad upgrade', null) + } + + onUpgrade (statusCode, rawHeaders, socket) { + const { callback, opaque, context } = this + + assert.strictEqual(statusCode, 101) + + removeSignal(this) + + this.callback = null + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + this.runInAsyncScope(callback, null, null, { + headers, + socket, + opaque, + context + }) + } + + onError (err) { + const { callback, opaque } = this + + removeSignal(this) + + if (callback) { + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + } +} + +function upgrade (opts, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + upgrade.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + const upgradeHandler = new UpgradeHandler(opts, callback) + this.dispatch({ + ...opts, + method: opts.method || 'GET', + upgrade: opts.protocol || 'Websocket' + }, upgradeHandler) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts && opts.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = upgrade + + +/***/ }), + +/***/ 4355: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +module.exports.request = __nccwpck_require__(2215) +module.exports.stream = __nccwpck_require__(4676) +module.exports.pipeline = __nccwpck_require__(7922) +module.exports.upgrade = __nccwpck_require__(6662) +module.exports.connect = __nccwpck_require__(472) + + +/***/ }), + +/***/ 8195: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +// Ported from https://github.com/nodejs/undici/pull/907 + + + +const assert = __nccwpck_require__(2613) +const { Readable } = __nccwpck_require__(2203) +const { RequestAbortedError, NotSupportedError, InvalidArgumentError } = __nccwpck_require__(1975) +const util = __nccwpck_require__(1436) +const { ReadableStreamFrom, toUSVString } = __nccwpck_require__(1436) + +let Blob + +const kConsume = Symbol('kConsume') +const kReading = Symbol('kReading') +const kBody = Symbol('kBody') +const kAbort = Symbol('abort') +const kContentType = Symbol('kContentType') + +const noop = () => {} + +module.exports = class BodyReadable extends Readable { + constructor ({ + resume, + abort, + contentType = '', + highWaterMark = 64 * 1024 // Same as nodejs fs streams. + }) { + super({ + autoDestroy: true, + read: resume, + highWaterMark + }) + + this._readableState.dataEmitted = false + + this[kAbort] = abort + this[kConsume] = null + this[kBody] = null + this[kContentType] = contentType + + // Is stream being consumed through Readable API? + // This is an optimization so that we avoid checking + // for 'data' and 'readable' listeners in the hot path + // inside push(). + this[kReading] = false + } + + destroy (err) { + if (this.destroyed) { + // Node < 16 + return this + } + + if (!err && !this._readableState.endEmitted) { + err = new RequestAbortedError() + } + + if (err) { + this[kAbort]() + } + + return super.destroy(err) + } + + emit (ev, ...args) { + if (ev === 'data') { + // Node < 16.7 + this._readableState.dataEmitted = true + } else if (ev === 'error') { + // Node < 16 + this._readableState.errorEmitted = true + } + return super.emit(ev, ...args) + } + + on (ev, ...args) { + if (ev === 'data' || ev === 'readable') { + this[kReading] = true + } + return super.on(ev, ...args) + } + + addListener (ev, ...args) { + return this.on(ev, ...args) + } + + off (ev, ...args) { + const ret = super.off(ev, ...args) + if (ev === 'data' || ev === 'readable') { + this[kReading] = ( + this.listenerCount('data') > 0 || + this.listenerCount('readable') > 0 + ) + } + return ret + } + + removeListener (ev, ...args) { + return this.off(ev, ...args) + } + + push (chunk) { + if (this[kConsume] && chunk !== null && this.readableLength === 0) { + consumePush(this[kConsume], chunk) + return this[kReading] ? super.push(chunk) : true + } + return super.push(chunk) + } + + // https://fetch.spec.whatwg.org/#dom-body-text + async text () { + return consume(this, 'text') + } + + // https://fetch.spec.whatwg.org/#dom-body-json + async json () { + return consume(this, 'json') + } + + // https://fetch.spec.whatwg.org/#dom-body-blob + async blob () { + return consume(this, 'blob') + } + + // https://fetch.spec.whatwg.org/#dom-body-arraybuffer + async arrayBuffer () { + return consume(this, 'arrayBuffer') + } + + // https://fetch.spec.whatwg.org/#dom-body-formdata + async formData () { + // TODO: Implement. + throw new NotSupportedError() + } + + // https://fetch.spec.whatwg.org/#dom-body-bodyused + get bodyUsed () { + return util.isDisturbed(this) + } + + // https://fetch.spec.whatwg.org/#dom-body-body + get body () { + if (!this[kBody]) { + this[kBody] = ReadableStreamFrom(this) + if (this[kConsume]) { + // TODO: Is this the best way to force a lock? + this[kBody].getReader() // Ensure stream is locked. + assert(this[kBody].locked) + } + } + return this[kBody] + } + + dump (opts) { + let limit = opts && Number.isFinite(opts.limit) ? opts.limit : 262144 + const signal = opts && opts.signal + + if (signal) { + try { + if (typeof signal !== 'object' || !('aborted' in signal)) { + throw new InvalidArgumentError('signal must be an AbortSignal') + } + util.throwIfAborted(signal) + } catch (err) { + return Promise.reject(err) + } + } + + if (this.closed) { + return Promise.resolve(null) + } + + return new Promise((resolve, reject) => { + const signalListenerCleanup = signal + ? util.addAbortListener(signal, () => { + this.destroy() + }) + : noop + + this + .on('close', function () { + signalListenerCleanup() + if (signal && signal.aborted) { + reject(signal.reason || Object.assign(new Error('The operation was aborted'), { name: 'AbortError' })) + } else { + resolve(null) + } + }) + .on('error', noop) + .on('data', function (chunk) { + limit -= chunk.length + if (limit <= 0) { + this.destroy() + } + }) + .resume() + }) + } +} + +// https://streams.spec.whatwg.org/#readablestream-locked +function isLocked (self) { + // Consume is an implicit lock. + return (self[kBody] && self[kBody].locked === true) || self[kConsume] +} + +// https://fetch.spec.whatwg.org/#body-unusable +function isUnusable (self) { + return util.isDisturbed(self) || isLocked(self) +} + +async function consume (stream, type) { + if (isUnusable(stream)) { + throw new TypeError('unusable') + } + + assert(!stream[kConsume]) + + return new Promise((resolve, reject) => { + stream[kConsume] = { + type, + stream, + resolve, + reject, + length: 0, + body: [] + } + + stream + .on('error', function (err) { + consumeFinish(this[kConsume], err) + }) + .on('close', function () { + if (this[kConsume].body !== null) { + consumeFinish(this[kConsume], new RequestAbortedError()) + } + }) + + process.nextTick(consumeStart, stream[kConsume]) + }) +} + +function consumeStart (consume) { + if (consume.body === null) { + return + } + + const { _readableState: state } = consume.stream + + for (const chunk of state.buffer) { + consumePush(consume, chunk) + } + + if (state.endEmitted) { + consumeEnd(this[kConsume]) + } else { + consume.stream.on('end', function () { + consumeEnd(this[kConsume]) + }) + } + + consume.stream.resume() + + while (consume.stream.read() != null) { + // Loop + } +} + +function consumeEnd (consume) { + const { type, body, resolve, stream, length } = consume + + try { + if (type === 'text') { + resolve(toUSVString(Buffer.concat(body))) + } else if (type === 'json') { + resolve(JSON.parse(Buffer.concat(body))) + } else if (type === 'arrayBuffer') { + const dst = new Uint8Array(length) + + let pos = 0 + for (const buf of body) { + dst.set(buf, pos) + pos += buf.byteLength + } + + resolve(dst.buffer) + } else if (type === 'blob') { + if (!Blob) { + Blob = (__nccwpck_require__(181).Blob) + } + resolve(new Blob(body, { type: stream[kContentType] })) + } + + consumeFinish(consume) + } catch (err) { + stream.destroy(err) + } +} + +function consumePush (consume, chunk) { + consume.length += chunk.length + consume.body.push(chunk) +} + +function consumeFinish (consume, err) { + if (consume.body === null) { + return + } + + if (err) { + consume.reject(err) + } else { + consume.resolve() + } + + consume.type = null + consume.stream = null + consume.resolve = null + consume.reject = null + consume.length = 0 + consume.body = null +} + + +/***/ }), + +/***/ 1083: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const assert = __nccwpck_require__(2613) +const { + ResponseStatusCodeError +} = __nccwpck_require__(1975) +const { toUSVString } = __nccwpck_require__(1436) + +async function getResolveErrorBodyCallback ({ callback, body, contentType, statusCode, statusMessage, headers }) { + assert(body) + + let chunks = [] + let limit = 0 + + for await (const chunk of body) { + chunks.push(chunk) + limit += chunk.length + if (limit > 128 * 1024) { + chunks = null + break + } + } + + if (statusCode === 204 || !contentType || !chunks) { + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) + return + } + + try { + if (contentType.startsWith('application/json')) { + const payload = JSON.parse(toUSVString(Buffer.concat(chunks))) + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) + return + } + + if (contentType.startsWith('text/')) { + const payload = toUSVString(Buffer.concat(chunks)) + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) + return + } + } catch (err) { + // Process in a fallback if error + } + + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) +} + +module.exports = { getResolveErrorBodyCallback } + + +/***/ }), + +/***/ 7137: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + BalancedPoolMissingUpstreamError, + InvalidArgumentError +} = __nccwpck_require__(1975) +const { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kRemoveClient, + kGetDispatcher +} = __nccwpck_require__(7556) +const Pool = __nccwpck_require__(6976) +const { kUrl, kInterceptors } = __nccwpck_require__(9583) +const { parseOrigin } = __nccwpck_require__(1436) +const kFactory = Symbol('factory') + +const kOptions = Symbol('options') +const kGreatestCommonDivisor = Symbol('kGreatestCommonDivisor') +const kCurrentWeight = Symbol('kCurrentWeight') +const kIndex = Symbol('kIndex') +const kWeight = Symbol('kWeight') +const kMaxWeightPerServer = Symbol('kMaxWeightPerServer') +const kErrorPenalty = Symbol('kErrorPenalty') + +function getGreatestCommonDivisor (a, b) { + if (b === 0) return a + return getGreatestCommonDivisor(b, a % b) +} + +function defaultFactory (origin, opts) { + return new Pool(origin, opts) +} + +class BalancedPool extends PoolBase { + constructor (upstreams = [], { factory = defaultFactory, ...opts } = {}) { + super() + + this[kOptions] = opts + this[kIndex] = -1 + this[kCurrentWeight] = 0 + + this[kMaxWeightPerServer] = this[kOptions].maxWeightPerServer || 100 + this[kErrorPenalty] = this[kOptions].errorPenalty || 15 + + if (!Array.isArray(upstreams)) { + upstreams = [upstreams] + } + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + this[kInterceptors] = opts.interceptors && opts.interceptors.BalancedPool && Array.isArray(opts.interceptors.BalancedPool) + ? opts.interceptors.BalancedPool + : [] + this[kFactory] = factory + + for (const upstream of upstreams) { + this.addUpstream(upstream) + } + this._updateBalancedPoolStats() + } + + addUpstream (upstream) { + const upstreamOrigin = parseOrigin(upstream).origin + + if (this[kClients].find((pool) => ( + pool[kUrl].origin === upstreamOrigin && + pool.closed !== true && + pool.destroyed !== true + ))) { + return this + } + const pool = this[kFactory](upstreamOrigin, Object.assign({}, this[kOptions])) + + this[kAddClient](pool) + pool.on('connect', () => { + pool[kWeight] = Math.min(this[kMaxWeightPerServer], pool[kWeight] + this[kErrorPenalty]) + }) + + pool.on('connectionError', () => { + pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty]) + this._updateBalancedPoolStats() + }) + + pool.on('disconnect', (...args) => { + const err = args[2] + if (err && err.code === 'UND_ERR_SOCKET') { + // decrease the weight of the pool. + pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty]) + this._updateBalancedPoolStats() + } + }) + + for (const client of this[kClients]) { + client[kWeight] = this[kMaxWeightPerServer] + } + + this._updateBalancedPoolStats() + + return this + } + + _updateBalancedPoolStats () { + this[kGreatestCommonDivisor] = this[kClients].map(p => p[kWeight]).reduce(getGreatestCommonDivisor, 0) + } + + removeUpstream (upstream) { + const upstreamOrigin = parseOrigin(upstream).origin + + const pool = this[kClients].find((pool) => ( + pool[kUrl].origin === upstreamOrigin && + pool.closed !== true && + pool.destroyed !== true + )) + + if (pool) { + this[kRemoveClient](pool) + } + + return this + } + + get upstreams () { + return this[kClients] + .filter(dispatcher => dispatcher.closed !== true && dispatcher.destroyed !== true) + .map((p) => p[kUrl].origin) + } + + [kGetDispatcher] () { + // We validate that pools is greater than 0, + // otherwise we would have to wait until an upstream + // is added, which might never happen. + if (this[kClients].length === 0) { + throw new BalancedPoolMissingUpstreamError() + } + + const dispatcher = this[kClients].find(dispatcher => ( + !dispatcher[kNeedDrain] && + dispatcher.closed !== true && + dispatcher.destroyed !== true + )) + + if (!dispatcher) { + return + } + + const allClientsBusy = this[kClients].map(pool => pool[kNeedDrain]).reduce((a, b) => a && b, true) + + if (allClientsBusy) { + return + } + + let counter = 0 + + let maxWeightIndex = this[kClients].findIndex(pool => !pool[kNeedDrain]) + + while (counter++ < this[kClients].length) { + this[kIndex] = (this[kIndex] + 1) % this[kClients].length + const pool = this[kClients][this[kIndex]] + + // find pool index with the largest weight + if (pool[kWeight] > this[kClients][maxWeightIndex][kWeight] && !pool[kNeedDrain]) { + maxWeightIndex = this[kIndex] + } + + // decrease the current weight every `this[kClients].length`. + if (this[kIndex] === 0) { + // Set the current weight to the next lower weight. + this[kCurrentWeight] = this[kCurrentWeight] - this[kGreatestCommonDivisor] + + if (this[kCurrentWeight] <= 0) { + this[kCurrentWeight] = this[kMaxWeightPerServer] + } + } + if (pool[kWeight] >= this[kCurrentWeight] && (!pool[kNeedDrain])) { + return pool + } + } + + this[kCurrentWeight] = this[kClients][maxWeightIndex][kWeight] + this[kIndex] = maxWeightIndex + return this[kClients][maxWeightIndex] + } +} + +module.exports = BalancedPool + + +/***/ }), + +/***/ 2387: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kConstruct } = __nccwpck_require__(7916) +const { urlEquals, fieldValues: getFieldValues } = __nccwpck_require__(277) +const { kEnumerableProperty, isDisturbed } = __nccwpck_require__(1436) +const { kHeadersList } = __nccwpck_require__(9583) +const { webidl } = __nccwpck_require__(274) +const { Response, cloneResponse } = __nccwpck_require__(2440) +const { Request } = __nccwpck_require__(1558) +const { kState, kHeaders, kGuard, kRealm } = __nccwpck_require__(834) +const { fetching } = __nccwpck_require__(1279) +const { urlIsHttpHttpsScheme, createDeferredPromise, readAllBytes } = __nccwpck_require__(3359) +const assert = __nccwpck_require__(2613) +const { getGlobalDispatcher } = __nccwpck_require__(8377) + +/** + * @see https://w3c.github.io/ServiceWorker/#dfn-cache-batch-operation + * @typedef {Object} CacheBatchOperation + * @property {'delete' | 'put'} type + * @property {any} request + * @property {any} response + * @property {import('../../types/cache').CacheQueryOptions} options + */ + +/** + * @see https://w3c.github.io/ServiceWorker/#dfn-request-response-list + * @typedef {[any, any][]} requestResponseList + */ + +class Cache { + /** + * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-request-response-list + * @type {requestResponseList} + */ + #relevantRequestResponseList + + constructor () { + if (arguments[0] !== kConstruct) { + webidl.illegalConstructor() + } + + this.#relevantRequestResponseList = arguments[1] + } + + async match (request, options = {}) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.match' }) + + request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options) + + const p = await this.matchAll(request, options) + + if (p.length === 0) { + return + } + + return p[0] + } + + async matchAll (request = undefined, options = {}) { + webidl.brandCheck(this, Cache) + + if (request !== undefined) request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options) + + // 1. + let r = null + + // 2. + if (request !== undefined) { + if (request instanceof Request) { + // 2.1.1 + r = request[kState] + + // 2.1.2 + if (r.method !== 'GET' && !options.ignoreMethod) { + return [] + } + } else if (typeof request === 'string') { + // 2.2.1 + r = new Request(request)[kState] + } + } + + // 5. + // 5.1 + const responses = [] + + // 5.2 + if (request === undefined) { + // 5.2.1 + for (const requestResponse of this.#relevantRequestResponseList) { + responses.push(requestResponse[1]) + } + } else { // 5.3 + // 5.3.1 + const requestResponses = this.#queryCache(r, options) + + // 5.3.2 + for (const requestResponse of requestResponses) { + responses.push(requestResponse[1]) + } + } + + // 5.4 + // We don't implement CORs so we don't need to loop over the responses, yay! + + // 5.5.1 + const responseList = [] + + // 5.5.2 + for (const response of responses) { + // 5.5.2.1 + const responseObject = new Response(response.body?.source ?? null) + const body = responseObject[kState].body + responseObject[kState] = response + responseObject[kState].body = body + responseObject[kHeaders][kHeadersList] = response.headersList + responseObject[kHeaders][kGuard] = 'immutable' + + responseList.push(responseObject) + } + + // 6. + return Object.freeze(responseList) + } + + async add (request) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.add' }) + + request = webidl.converters.RequestInfo(request) + + // 1. + const requests = [request] + + // 2. + const responseArrayPromise = this.addAll(requests) + + // 3. + return await responseArrayPromise + } + + async addAll (requests) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.addAll' }) + + requests = webidl.converters['sequence'](requests) + + // 1. + const responsePromises = [] + + // 2. + const requestList = [] + + // 3. + for (const request of requests) { + if (typeof request === 'string') { + continue + } + + // 3.1 + const r = request[kState] + + // 3.2 + if (!urlIsHttpHttpsScheme(r.url) || r.method !== 'GET') { + throw webidl.errors.exception({ + header: 'Cache.addAll', + message: 'Expected http/s scheme when method is not GET.' + }) + } + } + + // 4. + /** @type {ReturnType[]} */ + const fetchControllers = [] + + // 5. + for (const request of requests) { + // 5.1 + const r = new Request(request)[kState] + + // 5.2 + if (!urlIsHttpHttpsScheme(r.url)) { + throw webidl.errors.exception({ + header: 'Cache.addAll', + message: 'Expected http/s scheme.' + }) + } + + // 5.4 + r.initiator = 'fetch' + r.destination = 'subresource' + + // 5.5 + requestList.push(r) + + // 5.6 + const responsePromise = createDeferredPromise() + + // 5.7 + fetchControllers.push(fetching({ + request: r, + dispatcher: getGlobalDispatcher(), + processResponse (response) { + // 1. + if (response.type === 'error' || response.status === 206 || response.status < 200 || response.status > 299) { + responsePromise.reject(webidl.errors.exception({ + header: 'Cache.addAll', + message: 'Received an invalid status code or the request failed.' + })) + } else if (response.headersList.contains('vary')) { // 2. + // 2.1 + const fieldValues = getFieldValues(response.headersList.get('vary')) + + // 2.2 + for (const fieldValue of fieldValues) { + // 2.2.1 + if (fieldValue === '*') { + responsePromise.reject(webidl.errors.exception({ + header: 'Cache.addAll', + message: 'invalid vary field value' + })) + + for (const controller of fetchControllers) { + controller.abort() + } + + return + } + } + } + }, + processResponseEndOfBody (response) { + // 1. + if (response.aborted) { + responsePromise.reject(new DOMException('aborted', 'AbortError')) + return + } + + // 2. + responsePromise.resolve(response) + } + })) + + // 5.8 + responsePromises.push(responsePromise.promise) + } + + // 6. + const p = Promise.all(responsePromises) + + // 7. + const responses = await p + + // 7.1 + const operations = [] + + // 7.2 + let index = 0 + + // 7.3 + for (const response of responses) { + // 7.3.1 + /** @type {CacheBatchOperation} */ + const operation = { + type: 'put', // 7.3.2 + request: requestList[index], // 7.3.3 + response // 7.3.4 + } + + operations.push(operation) // 7.3.5 + + index++ // 7.3.6 + } + + // 7.5 + const cacheJobPromise = createDeferredPromise() + + // 7.6.1 + let errorData = null + + // 7.6.2 + try { + this.#batchCacheOperations(operations) + } catch (e) { + errorData = e + } + + // 7.6.3 + queueMicrotask(() => { + // 7.6.3.1 + if (errorData === null) { + cacheJobPromise.resolve(undefined) + } else { + // 7.6.3.2 + cacheJobPromise.reject(errorData) + } + }) + + // 7.7 + return cacheJobPromise.promise + } + + async put (request, response) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 2, { header: 'Cache.put' }) + + request = webidl.converters.RequestInfo(request) + response = webidl.converters.Response(response) + + // 1. + let innerRequest = null + + // 2. + if (request instanceof Request) { + innerRequest = request[kState] + } else { // 3. + innerRequest = new Request(request)[kState] + } + + // 4. + if (!urlIsHttpHttpsScheme(innerRequest.url) || innerRequest.method !== 'GET') { + throw webidl.errors.exception({ + header: 'Cache.put', + message: 'Expected an http/s scheme when method is not GET' + }) + } + + // 5. + const innerResponse = response[kState] + + // 6. + if (innerResponse.status === 206) { + throw webidl.errors.exception({ + header: 'Cache.put', + message: 'Got 206 status' + }) + } + + // 7. + if (innerResponse.headersList.contains('vary')) { + // 7.1. + const fieldValues = getFieldValues(innerResponse.headersList.get('vary')) + + // 7.2. + for (const fieldValue of fieldValues) { + // 7.2.1 + if (fieldValue === '*') { + throw webidl.errors.exception({ + header: 'Cache.put', + message: 'Got * vary field value' + }) + } + } + } + + // 8. + if (innerResponse.body && (isDisturbed(innerResponse.body.stream) || innerResponse.body.stream.locked)) { + throw webidl.errors.exception({ + header: 'Cache.put', + message: 'Response body is locked or disturbed' + }) + } + + // 9. + const clonedResponse = cloneResponse(innerResponse) + + // 10. + const bodyReadPromise = createDeferredPromise() + + // 11. + if (innerResponse.body != null) { + // 11.1 + const stream = innerResponse.body.stream + + // 11.2 + const reader = stream.getReader() + + // 11.3 + readAllBytes(reader).then(bodyReadPromise.resolve, bodyReadPromise.reject) + } else { + bodyReadPromise.resolve(undefined) + } + + // 12. + /** @type {CacheBatchOperation[]} */ + const operations = [] + + // 13. + /** @type {CacheBatchOperation} */ + const operation = { + type: 'put', // 14. + request: innerRequest, // 15. + response: clonedResponse // 16. + } + + // 17. + operations.push(operation) + + // 19. + const bytes = await bodyReadPromise.promise + + if (clonedResponse.body != null) { + clonedResponse.body.source = bytes + } + + // 19.1 + const cacheJobPromise = createDeferredPromise() + + // 19.2.1 + let errorData = null + + // 19.2.2 + try { + this.#batchCacheOperations(operations) + } catch (e) { + errorData = e + } + + // 19.2.3 + queueMicrotask(() => { + // 19.2.3.1 + if (errorData === null) { + cacheJobPromise.resolve() + } else { // 19.2.3.2 + cacheJobPromise.reject(errorData) + } + }) + + return cacheJobPromise.promise + } + + async delete (request, options = {}) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.delete' }) + + request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options) + + /** + * @type {Request} + */ + let r = null + + if (request instanceof Request) { + r = request[kState] + + if (r.method !== 'GET' && !options.ignoreMethod) { + return false + } + } else { + assert(typeof request === 'string') + + r = new Request(request)[kState] + } + + /** @type {CacheBatchOperation[]} */ + const operations = [] + + /** @type {CacheBatchOperation} */ + const operation = { + type: 'delete', + request: r, + options + } + + operations.push(operation) + + const cacheJobPromise = createDeferredPromise() + + let errorData = null + let requestResponses + + try { + requestResponses = this.#batchCacheOperations(operations) + } catch (e) { + errorData = e + } + + queueMicrotask(() => { + if (errorData === null) { + cacheJobPromise.resolve(!!requestResponses?.length) + } else { + cacheJobPromise.reject(errorData) + } + }) + + return cacheJobPromise.promise + } + + /** + * @see https://w3c.github.io/ServiceWorker/#dom-cache-keys + * @param {any} request + * @param {import('../../types/cache').CacheQueryOptions} options + * @returns {readonly Request[]} + */ + async keys (request = undefined, options = {}) { + webidl.brandCheck(this, Cache) + + if (request !== undefined) request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options) + + // 1. + let r = null + + // 2. + if (request !== undefined) { + // 2.1 + if (request instanceof Request) { + // 2.1.1 + r = request[kState] + + // 2.1.2 + if (r.method !== 'GET' && !options.ignoreMethod) { + return [] + } + } else if (typeof request === 'string') { // 2.2 + r = new Request(request)[kState] + } + } + + // 4. + const promise = createDeferredPromise() + + // 5. + // 5.1 + const requests = [] + + // 5.2 + if (request === undefined) { + // 5.2.1 + for (const requestResponse of this.#relevantRequestResponseList) { + // 5.2.1.1 + requests.push(requestResponse[0]) + } + } else { // 5.3 + // 5.3.1 + const requestResponses = this.#queryCache(r, options) + + // 5.3.2 + for (const requestResponse of requestResponses) { + // 5.3.2.1 + requests.push(requestResponse[0]) + } + } + + // 5.4 + queueMicrotask(() => { + // 5.4.1 + const requestList = [] + + // 5.4.2 + for (const request of requests) { + const requestObject = new Request('https://a') + requestObject[kState] = request + requestObject[kHeaders][kHeadersList] = request.headersList + requestObject[kHeaders][kGuard] = 'immutable' + requestObject[kRealm] = request.client + + // 5.4.2.1 + requestList.push(requestObject) + } + + // 5.4.3 + promise.resolve(Object.freeze(requestList)) + }) + + return promise.promise + } + + /** + * @see https://w3c.github.io/ServiceWorker/#batch-cache-operations-algorithm + * @param {CacheBatchOperation[]} operations + * @returns {requestResponseList} + */ + #batchCacheOperations (operations) { + // 1. + const cache = this.#relevantRequestResponseList + + // 2. + const backupCache = [...cache] + + // 3. + const addedItems = [] + + // 4.1 + const resultList = [] + + try { + // 4.2 + for (const operation of operations) { + // 4.2.1 + if (operation.type !== 'delete' && operation.type !== 'put') { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'operation type does not match "delete" or "put"' + }) + } + + // 4.2.2 + if (operation.type === 'delete' && operation.response != null) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'delete operation should not have an associated response' + }) + } + + // 4.2.3 + if (this.#queryCache(operation.request, operation.options, addedItems).length) { + throw new DOMException('???', 'InvalidStateError') + } + + // 4.2.4 + let requestResponses + + // 4.2.5 + if (operation.type === 'delete') { + // 4.2.5.1 + requestResponses = this.#queryCache(operation.request, operation.options) + + // TODO: the spec is wrong, this is needed to pass WPTs + if (requestResponses.length === 0) { + return [] + } + + // 4.2.5.2 + for (const requestResponse of requestResponses) { + const idx = cache.indexOf(requestResponse) + assert(idx !== -1) + + // 4.2.5.2.1 + cache.splice(idx, 1) + } + } else if (operation.type === 'put') { // 4.2.6 + // 4.2.6.1 + if (operation.response == null) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'put operation should have an associated response' + }) + } + + // 4.2.6.2 + const r = operation.request + + // 4.2.6.3 + if (!urlIsHttpHttpsScheme(r.url)) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'expected http or https scheme' + }) + } + + // 4.2.6.4 + if (r.method !== 'GET') { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'not get method' + }) + } + + // 4.2.6.5 + if (operation.options != null) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'options must not be defined' + }) + } + + // 4.2.6.6 + requestResponses = this.#queryCache(operation.request) + + // 4.2.6.7 + for (const requestResponse of requestResponses) { + const idx = cache.indexOf(requestResponse) + assert(idx !== -1) + + // 4.2.6.7.1 + cache.splice(idx, 1) + } + + // 4.2.6.8 + cache.push([operation.request, operation.response]) + + // 4.2.6.10 + addedItems.push([operation.request, operation.response]) + } + + // 4.2.7 + resultList.push([operation.request, operation.response]) + } + + // 4.3 + return resultList + } catch (e) { // 5. + // 5.1 + this.#relevantRequestResponseList.length = 0 + + // 5.2 + this.#relevantRequestResponseList = backupCache + + // 5.3 + throw e + } + } + + /** + * @see https://w3c.github.io/ServiceWorker/#query-cache + * @param {any} requestQuery + * @param {import('../../types/cache').CacheQueryOptions} options + * @param {requestResponseList} targetStorage + * @returns {requestResponseList} + */ + #queryCache (requestQuery, options, targetStorage) { + /** @type {requestResponseList} */ + const resultList = [] + + const storage = targetStorage ?? this.#relevantRequestResponseList + + for (const requestResponse of storage) { + const [cachedRequest, cachedResponse] = requestResponse + if (this.#requestMatchesCachedItem(requestQuery, cachedRequest, cachedResponse, options)) { + resultList.push(requestResponse) + } + } + + return resultList + } + + /** + * @see https://w3c.github.io/ServiceWorker/#request-matches-cached-item-algorithm + * @param {any} requestQuery + * @param {any} request + * @param {any | null} response + * @param {import('../../types/cache').CacheQueryOptions | undefined} options + * @returns {boolean} + */ + #requestMatchesCachedItem (requestQuery, request, response = null, options) { + // if (options?.ignoreMethod === false && request.method === 'GET') { + // return false + // } + + const queryURL = new URL(requestQuery.url) + + const cachedURL = new URL(request.url) + + if (options?.ignoreSearch) { + cachedURL.search = '' + + queryURL.search = '' + } + + if (!urlEquals(queryURL, cachedURL, true)) { + return false + } + + if ( + response == null || + options?.ignoreVary || + !response.headersList.contains('vary') + ) { + return true + } + + const fieldValues = getFieldValues(response.headersList.get('vary')) + + for (const fieldValue of fieldValues) { + if (fieldValue === '*') { + return false + } + + const requestValue = request.headersList.get(fieldValue) + const queryValue = requestQuery.headersList.get(fieldValue) + + // If one has the header and the other doesn't, or one has + // a different value than the other, return false + if (requestValue !== queryValue) { + return false + } + } + + return true + } +} + +Object.defineProperties(Cache.prototype, { + [Symbol.toStringTag]: { + value: 'Cache', + configurable: true + }, + match: kEnumerableProperty, + matchAll: kEnumerableProperty, + add: kEnumerableProperty, + addAll: kEnumerableProperty, + put: kEnumerableProperty, + delete: kEnumerableProperty, + keys: kEnumerableProperty +}) + +const cacheQueryOptionConverters = [ + { + key: 'ignoreSearch', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'ignoreMethod', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'ignoreVary', + converter: webidl.converters.boolean, + defaultValue: false + } +] + +webidl.converters.CacheQueryOptions = webidl.dictionaryConverter(cacheQueryOptionConverters) + +webidl.converters.MultiCacheQueryOptions = webidl.dictionaryConverter([ + ...cacheQueryOptionConverters, + { + key: 'cacheName', + converter: webidl.converters.DOMString + } +]) + +webidl.converters.Response = webidl.interfaceConverter(Response) + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.RequestInfo +) + +module.exports = { + Cache +} + + +/***/ }), + +/***/ 254: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kConstruct } = __nccwpck_require__(7916) +const { Cache } = __nccwpck_require__(2387) +const { webidl } = __nccwpck_require__(274) +const { kEnumerableProperty } = __nccwpck_require__(1436) + +class CacheStorage { + /** + * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-name-to-cache-map + * @type {Map} + */ + async has (cacheName) { + webidl.brandCheck(this, CacheStorage) + webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.has' }) + + cacheName = webidl.converters.DOMString(cacheName) + + // 2.1.1 + // 2.2 + return this.#caches.has(cacheName) + } + + /** + * @see https://w3c.github.io/ServiceWorker/#dom-cachestorage-open + * @param {string} cacheName + * @returns {Promise} + */ + async open (cacheName) { + webidl.brandCheck(this, CacheStorage) + webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.open' }) + + cacheName = webidl.converters.DOMString(cacheName) + + // 2.1 + if (this.#caches.has(cacheName)) { + // await caches.open('v1') !== await caches.open('v1') + + // 2.1.1 + const cache = this.#caches.get(cacheName) + + // 2.1.1.1 + return new Cache(kConstruct, cache) + } + + // 2.2 + const cache = [] + + // 2.3 + this.#caches.set(cacheName, cache) + + // 2.4 + return new Cache(kConstruct, cache) + } + + /** + * @see https://w3c.github.io/ServiceWorker/#cache-storage-delete + * @param {string} cacheName + * @returns {Promise} + */ + async delete (cacheName) { + webidl.brandCheck(this, CacheStorage) + webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.delete' }) + + cacheName = webidl.converters.DOMString(cacheName) + + return this.#caches.delete(cacheName) + } + + /** + * @see https://w3c.github.io/ServiceWorker/#cache-storage-keys + * @returns {string[]} + */ + async keys () { + webidl.brandCheck(this, CacheStorage) + + // 2.1 + const keys = this.#caches.keys() + + // 2.2 + return [...keys] + } +} + +Object.defineProperties(CacheStorage.prototype, { + [Symbol.toStringTag]: { + value: 'CacheStorage', + configurable: true + }, + match: kEnumerableProperty, + has: kEnumerableProperty, + open: kEnumerableProperty, + delete: kEnumerableProperty, + keys: kEnumerableProperty +}) + +module.exports = { + CacheStorage +} + + +/***/ }), + +/***/ 7916: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +module.exports = { + kConstruct: (__nccwpck_require__(9583).kConstruct) +} + + +/***/ }), + +/***/ 277: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const assert = __nccwpck_require__(2613) +const { URLSerializer } = __nccwpck_require__(5294) +const { isValidHeaderName } = __nccwpck_require__(3359) + +/** + * @see https://url.spec.whatwg.org/#concept-url-equals + * @param {URL} A + * @param {URL} B + * @param {boolean | undefined} excludeFragment + * @returns {boolean} + */ +function urlEquals (A, B, excludeFragment = false) { + const serializedA = URLSerializer(A, excludeFragment) + + const serializedB = URLSerializer(B, excludeFragment) + + return serializedA === serializedB +} + +/** + * @see https://github.com/chromium/chromium/blob/694d20d134cb553d8d89e5500b9148012b1ba299/content/browser/cache_storage/cache_storage_cache.cc#L260-L262 + * @param {string} header + */ +function fieldValues (header) { + assert(header !== null) + + const values = [] + + for (let value of header.split(',')) { + value = value.trim() + + if (!value.length) { + continue + } else if (!isValidHeaderName(value)) { + continue + } + + values.push(value) + } + + return values +} + +module.exports = { + urlEquals, + fieldValues +} + + +/***/ }), + +/***/ 5833: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +// @ts-check + + + +/* global WebAssembly */ + +const assert = __nccwpck_require__(2613) +const net = __nccwpck_require__(9278) +const http = __nccwpck_require__(8611) +const { pipeline } = __nccwpck_require__(2203) +const util = __nccwpck_require__(1436) +const timers = __nccwpck_require__(2688) +const Request = __nccwpck_require__(3371) +const DispatcherBase = __nccwpck_require__(3301) +const { + RequestContentLengthMismatchError, + ResponseContentLengthMismatchError, + InvalidArgumentError, + RequestAbortedError, + HeadersTimeoutError, + HeadersOverflowError, + SocketError, + InformationalError, + BodyTimeoutError, + HTTPParserError, + ResponseExceededMaxSizeError, + ClientDestroyedError +} = __nccwpck_require__(1975) +const buildConnector = __nccwpck_require__(3940) +const { + kUrl, + kReset, + kServerName, + kClient, + kBusy, + kParser, + kConnect, + kBlocking, + kResuming, + kRunning, + kPending, + kSize, + kWriting, + kQueue, + kConnected, + kConnecting, + kNeedDrain, + kNoRef, + kKeepAliveDefaultTimeout, + kHostHeader, + kPendingIdx, + kRunningIdx, + kError, + kPipelining, + kSocket, + kKeepAliveTimeoutValue, + kMaxHeadersSize, + kKeepAliveMaxTimeout, + kKeepAliveTimeoutThreshold, + kHeadersTimeout, + kBodyTimeout, + kStrictContentLength, + kConnector, + kMaxRedirections, + kMaxRequests, + kCounter, + kClose, + kDestroy, + kDispatch, + kInterceptors, + kLocalAddress, + kMaxResponseSize, + kHTTPConnVersion, + // HTTP2 + kHost, + kHTTP2Session, + kHTTP2SessionState, + kHTTP2BuildRequest, + kHTTP2CopyHeaders, + kHTTP1BuildRequest +} = __nccwpck_require__(9583) + +/** @type {import('http2')} */ +let http2 +try { + http2 = __nccwpck_require__(5675) +} catch { + // @ts-ignore + http2 = { constants: {} } +} + +const { + constants: { + HTTP2_HEADER_AUTHORITY, + HTTP2_HEADER_METHOD, + HTTP2_HEADER_PATH, + HTTP2_HEADER_SCHEME, + HTTP2_HEADER_CONTENT_LENGTH, + HTTP2_HEADER_EXPECT, + HTTP2_HEADER_STATUS + } +} = http2 + +// Experimental +let h2ExperimentalWarned = false + +const FastBuffer = Buffer[Symbol.species] + +const kClosedResolve = Symbol('kClosedResolve') + +const channels = {} + +try { + const diagnosticsChannel = __nccwpck_require__(1637) + channels.sendHeaders = diagnosticsChannel.channel('undici:client:sendHeaders') + channels.beforeConnect = diagnosticsChannel.channel('undici:client:beforeConnect') + channels.connectError = diagnosticsChannel.channel('undici:client:connectError') + channels.connected = diagnosticsChannel.channel('undici:client:connected') +} catch { + channels.sendHeaders = { hasSubscribers: false } + channels.beforeConnect = { hasSubscribers: false } + channels.connectError = { hasSubscribers: false } + channels.connected = { hasSubscribers: false } +} + +/** + * @type {import('../types/client').default} + */ +class Client extends DispatcherBase { + /** + * + * @param {string|URL} url + * @param {import('../types/client').Client.Options} options + */ + constructor (url, { + interceptors, + maxHeaderSize, + headersTimeout, + socketTimeout, + requestTimeout, + connectTimeout, + bodyTimeout, + idleTimeout, + keepAlive, + keepAliveTimeout, + maxKeepAliveTimeout, + keepAliveMaxTimeout, + keepAliveTimeoutThreshold, + socketPath, + pipelining, + tls, + strictContentLength, + maxCachedSessions, + maxRedirections, + connect, + maxRequestsPerClient, + localAddress, + maxResponseSize, + autoSelectFamily, + autoSelectFamilyAttemptTimeout, + // h2 + allowH2, + maxConcurrentStreams + } = {}) { + super() + + if (keepAlive !== undefined) { + throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead') + } + + if (socketTimeout !== undefined) { + throw new InvalidArgumentError('unsupported socketTimeout, use headersTimeout & bodyTimeout instead') + } + + if (requestTimeout !== undefined) { + throw new InvalidArgumentError('unsupported requestTimeout, use headersTimeout & bodyTimeout instead') + } + + if (idleTimeout !== undefined) { + throw new InvalidArgumentError('unsupported idleTimeout, use keepAliveTimeout instead') + } + + if (maxKeepAliveTimeout !== undefined) { + throw new InvalidArgumentError('unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead') + } + + if (maxHeaderSize != null && !Number.isFinite(maxHeaderSize)) { + throw new InvalidArgumentError('invalid maxHeaderSize') + } + + if (socketPath != null && typeof socketPath !== 'string') { + throw new InvalidArgumentError('invalid socketPath') + } + + if (connectTimeout != null && (!Number.isFinite(connectTimeout) || connectTimeout < 0)) { + throw new InvalidArgumentError('invalid connectTimeout') + } + + if (keepAliveTimeout != null && (!Number.isFinite(keepAliveTimeout) || keepAliveTimeout <= 0)) { + throw new InvalidArgumentError('invalid keepAliveTimeout') + } + + if (keepAliveMaxTimeout != null && (!Number.isFinite(keepAliveMaxTimeout) || keepAliveMaxTimeout <= 0)) { + throw new InvalidArgumentError('invalid keepAliveMaxTimeout') + } + + if (keepAliveTimeoutThreshold != null && !Number.isFinite(keepAliveTimeoutThreshold)) { + throw new InvalidArgumentError('invalid keepAliveTimeoutThreshold') + } + + if (headersTimeout != null && (!Number.isInteger(headersTimeout) || headersTimeout < 0)) { + throw new InvalidArgumentError('headersTimeout must be a positive integer or zero') + } + + if (bodyTimeout != null && (!Number.isInteger(bodyTimeout) || bodyTimeout < 0)) { + throw new InvalidArgumentError('bodyTimeout must be a positive integer or zero') + } + + if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { + throw new InvalidArgumentError('connect must be a function or an object') + } + + if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) { + throw new InvalidArgumentError('maxRedirections must be a positive number') + } + + if (maxRequestsPerClient != null && (!Number.isInteger(maxRequestsPerClient) || maxRequestsPerClient < 0)) { + throw new InvalidArgumentError('maxRequestsPerClient must be a positive number') + } + + if (localAddress != null && (typeof localAddress !== 'string' || net.isIP(localAddress) === 0)) { + throw new InvalidArgumentError('localAddress must be valid string IP address') + } + + if (maxResponseSize != null && (!Number.isInteger(maxResponseSize) || maxResponseSize < -1)) { + throw new InvalidArgumentError('maxResponseSize must be a positive number') + } + + if ( + autoSelectFamilyAttemptTimeout != null && + (!Number.isInteger(autoSelectFamilyAttemptTimeout) || autoSelectFamilyAttemptTimeout < -1) + ) { + throw new InvalidArgumentError('autoSelectFamilyAttemptTimeout must be a positive number') + } + + // h2 + if (allowH2 != null && typeof allowH2 !== 'boolean') { + throw new InvalidArgumentError('allowH2 must be a valid boolean value') + } + + if (maxConcurrentStreams != null && (typeof maxConcurrentStreams !== 'number' || maxConcurrentStreams < 1)) { + throw new InvalidArgumentError('maxConcurrentStreams must be a possitive integer, greater than 0') + } + + if (typeof connect !== 'function') { + connect = buildConnector({ + ...tls, + maxCachedSessions, + allowH2, + socketPath, + timeout: connectTimeout, + ...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), + ...connect + }) + } + + this[kInterceptors] = interceptors && interceptors.Client && Array.isArray(interceptors.Client) + ? interceptors.Client + : [createRedirectInterceptor({ maxRedirections })] + this[kUrl] = util.parseOrigin(url) + this[kConnector] = connect + this[kSocket] = null + this[kPipelining] = pipelining != null ? pipelining : 1 + this[kMaxHeadersSize] = maxHeaderSize || http.maxHeaderSize + this[kKeepAliveDefaultTimeout] = keepAliveTimeout == null ? 4e3 : keepAliveTimeout + this[kKeepAliveMaxTimeout] = keepAliveMaxTimeout == null ? 600e3 : keepAliveMaxTimeout + this[kKeepAliveTimeoutThreshold] = keepAliveTimeoutThreshold == null ? 1e3 : keepAliveTimeoutThreshold + this[kKeepAliveTimeoutValue] = this[kKeepAliveDefaultTimeout] + this[kServerName] = null + this[kLocalAddress] = localAddress != null ? localAddress : null + this[kResuming] = 0 // 0, idle, 1, scheduled, 2 resuming + this[kNeedDrain] = 0 // 0, idle, 1, scheduled, 2 resuming + this[kHostHeader] = `host: ${this[kUrl].hostname}${this[kUrl].port ? `:${this[kUrl].port}` : ''}\r\n` + this[kBodyTimeout] = bodyTimeout != null ? bodyTimeout : 300e3 + this[kHeadersTimeout] = headersTimeout != null ? headersTimeout : 300e3 + this[kStrictContentLength] = strictContentLength == null ? true : strictContentLength + this[kMaxRedirections] = maxRedirections + this[kMaxRequests] = maxRequestsPerClient + this[kClosedResolve] = null + this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1 + this[kHTTPConnVersion] = 'h1' + + // HTTP/2 + this[kHTTP2Session] = null + this[kHTTP2SessionState] = !allowH2 + ? null + : { + // streams: null, // Fixed queue of streams - For future support of `push` + openStreams: 0, // Keep track of them to decide wether or not unref the session + maxConcurrentStreams: maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server + } + this[kHost] = `${this[kUrl].hostname}${this[kUrl].port ? `:${this[kUrl].port}` : ''}` + + // kQueue is built up of 3 sections separated by + // the kRunningIdx and kPendingIdx indices. + // | complete | running | pending | + // ^ kRunningIdx ^ kPendingIdx ^ kQueue.length + // kRunningIdx points to the first running element. + // kPendingIdx points to the first pending element. + // This implements a fast queue with an amortized + // time of O(1). + + this[kQueue] = [] + this[kRunningIdx] = 0 + this[kPendingIdx] = 0 + } + + get pipelining () { + return this[kPipelining] + } + + set pipelining (value) { + this[kPipelining] = value + resume(this, true) + } + + get [kPending] () { + return this[kQueue].length - this[kPendingIdx] + } + + get [kRunning] () { + return this[kPendingIdx] - this[kRunningIdx] + } + + get [kSize] () { + return this[kQueue].length - this[kRunningIdx] + } + + get [kConnected] () { + return !!this[kSocket] && !this[kConnecting] && !this[kSocket].destroyed + } + + get [kBusy] () { + const socket = this[kSocket] + return ( + (socket && (socket[kReset] || socket[kWriting] || socket[kBlocking])) || + (this[kSize] >= (this[kPipelining] || 1)) || + this[kPending] > 0 + ) + } + + /* istanbul ignore: only used for test */ + [kConnect] (cb) { + connect(this) + this.once('connect', cb) + } + + [kDispatch] (opts, handler) { + const origin = opts.origin || this[kUrl].origin + + const request = this[kHTTPConnVersion] === 'h2' + ? Request[kHTTP2BuildRequest](origin, opts, handler) + : Request[kHTTP1BuildRequest](origin, opts, handler) + + this[kQueue].push(request) + if (this[kResuming]) { + // Do nothing. + } else if (util.bodyLength(request.body) == null && util.isIterable(request.body)) { + // Wait a tick in case stream/iterator is ended in the same tick. + this[kResuming] = 1 + process.nextTick(resume, this) + } else { + resume(this, true) + } + + if (this[kResuming] && this[kNeedDrain] !== 2 && this[kBusy]) { + this[kNeedDrain] = 2 + } + + return this[kNeedDrain] < 2 + } + + async [kClose] () { + // TODO: for H2 we need to gracefully flush the remaining enqueued + // request and close each stream. + return new Promise((resolve) => { + if (!this[kSize]) { + resolve(null) + } else { + this[kClosedResolve] = resolve + } + }) + } + + async [kDestroy] (err) { + return new Promise((resolve) => { + const requests = this[kQueue].splice(this[kPendingIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(this, request, err) + } + + const callback = () => { + if (this[kClosedResolve]) { + // TODO (fix): Should we error here with ClientDestroyedError? + this[kClosedResolve]() + this[kClosedResolve] = null + } + resolve() + } + + if (this[kHTTP2Session] != null) { + util.destroy(this[kHTTP2Session], err) + this[kHTTP2Session] = null + this[kHTTP2SessionState] = null + } + + if (!this[kSocket]) { + queueMicrotask(callback) + } else { + util.destroy(this[kSocket].on('close', callback), err) + } + + resume(this) + }) + } +} + +function onHttp2SessionError (err) { + assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') + + this[kSocket][kError] = err + + onError(this[kClient], err) +} + +function onHttp2FrameError (type, code, id) { + const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) + + if (id === 0) { + this[kSocket][kError] = err + onError(this[kClient], err) + } +} + +function onHttp2SessionEnd () { + util.destroy(this, new SocketError('other side closed')) + util.destroy(this[kSocket], new SocketError('other side closed')) +} + +function onHTTP2GoAway (code) { + const client = this[kClient] + const err = new InformationalError(`HTTP/2: "GOAWAY" frame received with code ${code}`) + client[kSocket] = null + client[kHTTP2Session] = null + + if (client.destroyed) { + assert(this[kPending] === 0) + + // Fail entire queue. + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(this, request, err) + } + } else if (client[kRunning] > 0) { + // Fail head of pipeline. + const request = client[kQueue][client[kRunningIdx]] + client[kQueue][client[kRunningIdx]++] = null + + errorRequest(client, request, err) + } + + client[kPendingIdx] = client[kRunningIdx] + + assert(client[kRunning] === 0) + + client.emit('disconnect', + client[kUrl], + [client], + err + ) + + resume(client) +} + +const constants = __nccwpck_require__(3564) +const createRedirectInterceptor = __nccwpck_require__(1475) +const EMPTY_BUF = Buffer.alloc(0) + +async function lazyllhttp () { + const llhttpWasmData = process.env.JEST_WORKER_ID ? __nccwpck_require__(5506) : undefined + + let mod + try { + mod = await WebAssembly.compile(Buffer.from(__nccwpck_require__(3006), 'base64')) + } catch (e) { + /* istanbul ignore next */ + + // We could check if the error was caused by the simd option not + // being enabled, but the occurring of this other error + // * https://github.com/emscripten-core/emscripten/issues/11495 + // got me to remove that check to avoid breaking Node 12. + mod = await WebAssembly.compile(Buffer.from(llhttpWasmData || __nccwpck_require__(5506), 'base64')) + } + + return await WebAssembly.instantiate(mod, { + env: { + /* eslint-disable camelcase */ + + wasm_on_url: (p, at, len) => { + /* istanbul ignore next */ + return 0 + }, + wasm_on_status: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 + }, + wasm_on_message_begin: (p) => { + assert.strictEqual(currentParser.ptr, p) + return currentParser.onMessageBegin() || 0 + }, + wasm_on_header_field: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 + }, + wasm_on_header_value: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 + }, + wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => { + assert.strictEqual(currentParser.ptr, p) + return currentParser.onHeadersComplete(statusCode, Boolean(upgrade), Boolean(shouldKeepAlive)) || 0 + }, + wasm_on_body: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 + }, + wasm_on_message_complete: (p) => { + assert.strictEqual(currentParser.ptr, p) + return currentParser.onMessageComplete() || 0 + } + + /* eslint-enable camelcase */ + } + }) +} + +let llhttpInstance = null +let llhttpPromise = lazyllhttp() +llhttpPromise.catch() + +let currentParser = null +let currentBufferRef = null +let currentBufferSize = 0 +let currentBufferPtr = null + +const TIMEOUT_HEADERS = 1 +const TIMEOUT_BODY = 2 +const TIMEOUT_IDLE = 3 + +class Parser { + constructor (client, socket, { exports }) { + assert(Number.isFinite(client[kMaxHeadersSize]) && client[kMaxHeadersSize] > 0) + + this.llhttp = exports + this.ptr = this.llhttp.llhttp_alloc(constants.TYPE.RESPONSE) + this.client = client + this.socket = socket + this.timeout = null + this.timeoutValue = null + this.timeoutType = null + this.statusCode = null + this.statusText = '' + this.upgrade = false + this.headers = [] + this.headersSize = 0 + this.headersMaxSize = client[kMaxHeadersSize] + this.shouldKeepAlive = false + this.paused = false + this.resume = this.resume.bind(this) + + this.bytesRead = 0 + + this.keepAlive = '' + this.contentLength = '' + this.connection = '' + this.maxResponseSize = client[kMaxResponseSize] + } + + setTimeout (value, type) { + this.timeoutType = type + if (value !== this.timeoutValue) { + timers.clearTimeout(this.timeout) + if (value) { + this.timeout = timers.setTimeout(onParserTimeout, value, this) + // istanbul ignore else: only for jest + if (this.timeout.unref) { + this.timeout.unref() + } + } else { + this.timeout = null + } + this.timeoutValue = value + } else if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + } + + resume () { + if (this.socket.destroyed || !this.paused) { + return + } + + assert(this.ptr != null) + assert(currentParser == null) + + this.llhttp.llhttp_resume(this.ptr) + + assert(this.timeoutType === TIMEOUT_BODY) + if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + this.paused = false + this.execute(this.socket.read() || EMPTY_BUF) // Flush parser. + this.readMore() + } + + readMore () { + while (!this.paused && this.ptr) { + const chunk = this.socket.read() + if (chunk === null) { + break + } + this.execute(chunk) + } + } + + execute (data) { + assert(this.ptr != null) + assert(currentParser == null) + assert(!this.paused) + + const { socket, llhttp } = this + + if (data.length > currentBufferSize) { + if (currentBufferPtr) { + llhttp.free(currentBufferPtr) + } + currentBufferSize = Math.ceil(data.length / 4096) * 4096 + currentBufferPtr = llhttp.malloc(currentBufferSize) + } + + new Uint8Array(llhttp.memory.buffer, currentBufferPtr, currentBufferSize).set(data) + + // Call `execute` on the wasm parser. + // We pass the `llhttp_parser` pointer address, the pointer address of buffer view data, + // and finally the length of bytes to parse. + // The return value is an error code or `constants.ERROR.OK`. + try { + let ret + + try { + currentBufferRef = data + currentParser = this + ret = llhttp.llhttp_execute(this.ptr, currentBufferPtr, data.length) + /* eslint-disable-next-line no-useless-catch */ + } catch (err) { + /* istanbul ignore next: difficult to make a test case for */ + throw err + } finally { + currentParser = null + currentBufferRef = null + } + + const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr + + if (ret === constants.ERROR.PAUSED_UPGRADE) { + this.onUpgrade(data.slice(offset)) + } else if (ret === constants.ERROR.PAUSED) { + this.paused = true + socket.unshift(data.slice(offset)) + } else if (ret !== constants.ERROR.OK) { + const ptr = llhttp.llhttp_get_error_reason(this.ptr) + let message = '' + /* istanbul ignore else: difficult to make a test case for */ + if (ptr) { + const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0) + message = + 'Response does not match the HTTP/1.1 protocol (' + + Buffer.from(llhttp.memory.buffer, ptr, len).toString() + + ')' + } + throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset)) + } + } catch (err) { + util.destroy(socket, err) + } + } + + destroy () { + assert(this.ptr != null) + assert(currentParser == null) + + this.llhttp.llhttp_free(this.ptr) + this.ptr = null + + timers.clearTimeout(this.timeout) + this.timeout = null + this.timeoutValue = null + this.timeoutType = null + + this.paused = false + } + + onStatus (buf) { + this.statusText = buf.toString() + } + + onMessageBegin () { + const { socket, client } = this + + /* istanbul ignore next: difficult to make a test case for */ + if (socket.destroyed) { + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + if (!request) { + return -1 + } + } + + onHeaderField (buf) { + const len = this.headers.length + + if ((len & 1) === 0) { + this.headers.push(buf) + } else { + this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf]) + } + + this.trackHeader(buf.length) + } + + onHeaderValue (buf) { + let len = this.headers.length + + if ((len & 1) === 1) { + this.headers.push(buf) + len += 1 + } else { + this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf]) + } + + const key = this.headers[len - 2] + if (key.length === 10 && key.toString().toLowerCase() === 'keep-alive') { + this.keepAlive += buf.toString() + } else if (key.length === 10 && key.toString().toLowerCase() === 'connection') { + this.connection += buf.toString() + } else if (key.length === 14 && key.toString().toLowerCase() === 'content-length') { + this.contentLength += buf.toString() + } + + this.trackHeader(buf.length) + } + + trackHeader (len) { + this.headersSize += len + if (this.headersSize >= this.headersMaxSize) { + util.destroy(this.socket, new HeadersOverflowError()) + } + } + + onUpgrade (head) { + const { upgrade, client, socket, headers, statusCode } = this + + assert(upgrade) + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + + assert(!socket.destroyed) + assert(socket === client[kSocket]) + assert(!this.paused) + assert(request.upgrade || request.method === 'CONNECT') + + this.statusCode = null + this.statusText = '' + this.shouldKeepAlive = null + + assert(this.headers.length % 2 === 0) + this.headers = [] + this.headersSize = 0 + + socket.unshift(head) + + socket[kParser].destroy() + socket[kParser] = null + + socket[kClient] = null + socket[kError] = null + socket + .removeListener('error', onSocketError) + .removeListener('readable', onSocketReadable) + .removeListener('end', onSocketEnd) + .removeListener('close', onSocketClose) + + client[kSocket] = null + client[kQueue][client[kRunningIdx]++] = null + client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade')) + + try { + request.onUpgrade(statusCode, headers, socket) + } catch (err) { + util.destroy(socket, err) + } + + resume(client) + } + + onHeadersComplete (statusCode, upgrade, shouldKeepAlive) { + const { client, socket, headers, statusText } = this + + /* istanbul ignore next: difficult to make a test case for */ + if (socket.destroyed) { + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + + /* istanbul ignore next: difficult to make a test case for */ + if (!request) { + return -1 + } + + assert(!this.upgrade) + assert(this.statusCode < 200) + + if (statusCode === 100) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))) + return -1 + } + + /* this can only happen if server is misbehaving */ + if (upgrade && !request.upgrade) { + util.destroy(socket, new SocketError('bad upgrade', util.getSocketInfo(socket))) + return -1 + } + + assert.strictEqual(this.timeoutType, TIMEOUT_HEADERS) + + this.statusCode = statusCode + this.shouldKeepAlive = ( + shouldKeepAlive || + // Override llhttp value which does not allow keepAlive for HEAD. + (request.method === 'HEAD' && !socket[kReset] && this.connection.toLowerCase() === 'keep-alive') + ) + + if (this.statusCode >= 200) { + const bodyTimeout = request.bodyTimeout != null + ? request.bodyTimeout + : client[kBodyTimeout] + this.setTimeout(bodyTimeout, TIMEOUT_BODY) + } else if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + if (request.method === 'CONNECT') { + assert(client[kRunning] === 1) + this.upgrade = true + return 2 + } + + if (upgrade) { + assert(client[kRunning] === 1) + this.upgrade = true + return 2 + } + + assert(this.headers.length % 2 === 0) + this.headers = [] + this.headersSize = 0 + + if (this.shouldKeepAlive && client[kPipelining]) { + const keepAliveTimeout = this.keepAlive ? util.parseKeepAliveTimeout(this.keepAlive) : null + + if (keepAliveTimeout != null) { + const timeout = Math.min( + keepAliveTimeout - client[kKeepAliveTimeoutThreshold], + client[kKeepAliveMaxTimeout] + ) + if (timeout <= 0) { + socket[kReset] = true + } else { + client[kKeepAliveTimeoutValue] = timeout + } + } else { + client[kKeepAliveTimeoutValue] = client[kKeepAliveDefaultTimeout] + } + } else { + // Stop more requests from being dispatched. + socket[kReset] = true + } + + const pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false + + if (request.aborted) { + return -1 + } + + if (request.method === 'HEAD') { + return 1 + } + + if (statusCode < 200) { + return 1 + } + + if (socket[kBlocking]) { + socket[kBlocking] = false + resume(client) + } + + return pause ? constants.ERROR.PAUSED : 0 + } + + onBody (buf) { + const { client, socket, statusCode, maxResponseSize } = this + + if (socket.destroyed) { + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + + assert.strictEqual(this.timeoutType, TIMEOUT_BODY) + if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + assert(statusCode >= 200) + + if (maxResponseSize > -1 && this.bytesRead + buf.length > maxResponseSize) { + util.destroy(socket, new ResponseExceededMaxSizeError()) + return -1 + } + + this.bytesRead += buf.length + + if (request.onData(buf) === false) { + return constants.ERROR.PAUSED + } + } + + onMessageComplete () { + const { client, socket, statusCode, upgrade, headers, contentLength, bytesRead, shouldKeepAlive } = this + + if (socket.destroyed && (!statusCode || shouldKeepAlive)) { + return -1 + } + + if (upgrade) { + return + } + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + + assert(statusCode >= 100) + + this.statusCode = null + this.statusText = '' + this.bytesRead = 0 + this.contentLength = '' + this.keepAlive = '' + this.connection = '' + + assert(this.headers.length % 2 === 0) + this.headers = [] + this.headersSize = 0 + + if (statusCode < 200) { + return + } + + /* istanbul ignore next: should be handled by llhttp? */ + if (request.method !== 'HEAD' && contentLength && bytesRead !== parseInt(contentLength, 10)) { + util.destroy(socket, new ResponseContentLengthMismatchError()) + return -1 + } + + request.onComplete(headers) + + client[kQueue][client[kRunningIdx]++] = null + + if (socket[kWriting]) { + assert.strictEqual(client[kRunning], 0) + // Response completed before request. + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (!shouldKeepAlive) { + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (socket[kReset] && client[kRunning] === 0) { + // Destroy socket once all requests have completed. + // The request at the tail of the pipeline is the one + // that requested reset and no further requests should + // have been queued since then. + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (client[kPipelining] === 1) { + // We must wait a full event loop cycle to reuse this socket to make sure + // that non-spec compliant servers are not closing the connection even if they + // said they won't. + setImmediate(resume, client) + } else { + resume(client) + } + } +} + +function onParserTimeout (parser) { + const { socket, timeoutType, client } = parser + + /* istanbul ignore else */ + if (timeoutType === TIMEOUT_HEADERS) { + if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) { + assert(!parser.paused, 'cannot be paused while waiting for headers') + util.destroy(socket, new HeadersTimeoutError()) + } + } else if (timeoutType === TIMEOUT_BODY) { + if (!parser.paused) { + util.destroy(socket, new BodyTimeoutError()) + } + } else if (timeoutType === TIMEOUT_IDLE) { + assert(client[kRunning] === 0 && client[kKeepAliveTimeoutValue]) + util.destroy(socket, new InformationalError('socket idle timeout')) + } +} + +function onSocketReadable () { + const { [kParser]: parser } = this + if (parser) { + parser.readMore() + } +} + +function onSocketError (err) { + const { [kClient]: client, [kParser]: parser } = this + + assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') + + if (client[kHTTPConnVersion] !== 'h2') { + // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded + // to the user. + if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so for as a valid response. + parser.onMessageComplete() + return + } + } + + this[kError] = err + + onError(this[kClient], err) +} + +function onError (client, err) { + if ( + client[kRunning] === 0 && + err.code !== 'UND_ERR_INFO' && + err.code !== 'UND_ERR_SOCKET' + ) { + // Error is not caused by running request and not a recoverable + // socket error. + + assert(client[kPendingIdx] === client[kRunningIdx]) + + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(client, request, err) + } + assert(client[kSize] === 0) + } +} + +function onSocketEnd () { + const { [kParser]: parser, [kClient]: client } = this + + if (client[kHTTPConnVersion] !== 'h2') { + if (parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so far as a valid response. + parser.onMessageComplete() + return + } + } + + util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this))) +} + +function onSocketClose () { + const { [kClient]: client, [kParser]: parser } = this + + if (client[kHTTPConnVersion] === 'h1' && parser) { + if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so far as a valid response. + parser.onMessageComplete() + } + + this[kParser].destroy() + this[kParser] = null + } + + const err = this[kError] || new SocketError('closed', util.getSocketInfo(this)) + + client[kSocket] = null + + if (client.destroyed) { + assert(client[kPending] === 0) + + // Fail entire queue. + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(client, request, err) + } + } else if (client[kRunning] > 0 && err.code !== 'UND_ERR_INFO') { + // Fail head of pipeline. + const request = client[kQueue][client[kRunningIdx]] + client[kQueue][client[kRunningIdx]++] = null + + errorRequest(client, request, err) + } + + client[kPendingIdx] = client[kRunningIdx] + + assert(client[kRunning] === 0) + + client.emit('disconnect', client[kUrl], [client], err) + + resume(client) +} + +async function connect (client) { + assert(!client[kConnecting]) + assert(!client[kSocket]) + + let { host, hostname, protocol, port } = client[kUrl] + + // Resolve ipv6 + if (hostname[0] === '[') { + const idx = hostname.indexOf(']') + + assert(idx !== -1) + const ip = hostname.substring(1, idx) + + assert(net.isIP(ip)) + hostname = ip + } + + client[kConnecting] = true + + if (channels.beforeConnect.hasSubscribers) { + channels.beforeConnect.publish({ + connectParams: { + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector] + }) + } + + try { + const socket = await new Promise((resolve, reject) => { + client[kConnector]({ + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, (err, socket) => { + if (err) { + reject(err) + } else { + resolve(socket) + } + }) + }) + + if (client.destroyed) { + util.destroy(socket.on('error', () => {}), new ClientDestroyedError()) + return + } + + client[kConnecting] = false + + assert(socket) + + const isH2 = socket.alpnProtocol === 'h2' + if (isH2) { + if (!h2ExperimentalWarned) { + h2ExperimentalWarned = true + process.emitWarning('H2 support is experimental, expect them to change at any time.', { + code: 'UNDICI-H2' + }) + } + + const session = http2.connect(client[kUrl], { + createConnection: () => socket, + peerMaxConcurrentStreams: client[kHTTP2SessionState].maxConcurrentStreams + }) + + client[kHTTPConnVersion] = 'h2' + session[kClient] = client + session[kSocket] = socket + session.on('error', onHttp2SessionError) + session.on('frameError', onHttp2FrameError) + session.on('end', onHttp2SessionEnd) + session.on('goaway', onHTTP2GoAway) + session.on('close', onSocketClose) + session.unref() + + client[kHTTP2Session] = session + socket[kHTTP2Session] = session + } else { + if (!llhttpInstance) { + llhttpInstance = await llhttpPromise + llhttpPromise = null + } + + socket[kNoRef] = false + socket[kWriting] = false + socket[kReset] = false + socket[kBlocking] = false + socket[kParser] = new Parser(client, socket, llhttpInstance) + } + + socket[kCounter] = 0 + socket[kMaxRequests] = client[kMaxRequests] + socket[kClient] = client + socket[kError] = null + + socket + .on('error', onSocketError) + .on('readable', onSocketReadable) + .on('end', onSocketEnd) + .on('close', onSocketClose) + + client[kSocket] = socket + + if (channels.connected.hasSubscribers) { + channels.connected.publish({ + connectParams: { + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector], + socket + }) + } + client.emit('connect', client[kUrl], [client]) + } catch (err) { + if (client.destroyed) { + return + } + + client[kConnecting] = false + + if (channels.connectError.hasSubscribers) { + channels.connectError.publish({ + connectParams: { + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector], + error: err + }) + } + + if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') { + assert(client[kRunning] === 0) + while (client[kPending] > 0 && client[kQueue][client[kPendingIdx]].servername === client[kServerName]) { + const request = client[kQueue][client[kPendingIdx]++] + errorRequest(client, request, err) + } + } else { + onError(client, err) + } + + client.emit('connectionError', client[kUrl], [client], err) + } + + resume(client) +} + +function emitDrain (client) { + client[kNeedDrain] = 0 + client.emit('drain', client[kUrl], [client]) +} + +function resume (client, sync) { + if (client[kResuming] === 2) { + return + } + + client[kResuming] = 2 + + _resume(client, sync) + client[kResuming] = 0 + + if (client[kRunningIdx] > 256) { + client[kQueue].splice(0, client[kRunningIdx]) + client[kPendingIdx] -= client[kRunningIdx] + client[kRunningIdx] = 0 + } +} + +function _resume (client, sync) { + while (true) { + if (client.destroyed) { + assert(client[kPending] === 0) + return + } + + if (client[kClosedResolve] && !client[kSize]) { + client[kClosedResolve]() + client[kClosedResolve] = null + return + } + + const socket = client[kSocket] + + if (socket && !socket.destroyed && socket.alpnProtocol !== 'h2') { + if (client[kSize] === 0) { + if (!socket[kNoRef] && socket.unref) { + socket.unref() + socket[kNoRef] = true + } + } else if (socket[kNoRef] && socket.ref) { + socket.ref() + socket[kNoRef] = false + } + + if (client[kSize] === 0) { + if (socket[kParser].timeoutType !== TIMEOUT_IDLE) { + socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_IDLE) + } + } else if (client[kRunning] > 0 && socket[kParser].statusCode < 200) { + if (socket[kParser].timeoutType !== TIMEOUT_HEADERS) { + const request = client[kQueue][client[kRunningIdx]] + const headersTimeout = request.headersTimeout != null + ? request.headersTimeout + : client[kHeadersTimeout] + socket[kParser].setTimeout(headersTimeout, TIMEOUT_HEADERS) + } + } + } + + if (client[kBusy]) { + client[kNeedDrain] = 2 + } else if (client[kNeedDrain] === 2) { + if (sync) { + client[kNeedDrain] = 1 + process.nextTick(emitDrain, client) + } else { + emitDrain(client) + } + continue + } + + if (client[kPending] === 0) { + return + } + + if (client[kRunning] >= (client[kPipelining] || 1)) { + return + } + + const request = client[kQueue][client[kPendingIdx]] + + if (client[kUrl].protocol === 'https:' && client[kServerName] !== request.servername) { + if (client[kRunning] > 0) { + return + } + + client[kServerName] = request.servername + + if (socket && socket.servername !== request.servername) { + util.destroy(socket, new InformationalError('servername changed')) + return + } + } + + if (client[kConnecting]) { + return + } + + if (!socket && !client[kHTTP2Session]) { + connect(client) + return + } + + if (socket.destroyed || socket[kWriting] || socket[kReset] || socket[kBlocking]) { + return + } + + if (client[kRunning] > 0 && !request.idempotent) { + // Non-idempotent request cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + return + } + + if (client[kRunning] > 0 && (request.upgrade || request.method === 'CONNECT')) { + // Don't dispatch an upgrade until all preceding requests have completed. + // A misbehaving server might upgrade the connection before all pipelined + // request has completed. + return + } + + if (client[kRunning] > 0 && util.bodyLength(request.body) !== 0 && + (util.isStream(request.body) || util.isAsyncIterable(request.body))) { + // Request with stream or iterator body can error while other requests + // are inflight and indirectly error those as well. + // Ensure this doesn't happen by waiting for inflight + // to complete before dispatching. + + // Request with stream or iterator body cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + return + } + + if (!request.aborted && write(client, request)) { + client[kPendingIdx]++ + } else { + client[kQueue].splice(client[kPendingIdx], 1) + } + } +} + +// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2 +function shouldSendContentLength (method) { + return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT' +} + +function write (client, request) { + if (client[kHTTPConnVersion] === 'h2') { + writeH2(client, client[kHTTP2Session], request) + return + } + + const { body, method, path, host, upgrade, headers, blocking, reset } = request + + // https://tools.ietf.org/html/rfc7231#section-4.3.1 + // https://tools.ietf.org/html/rfc7231#section-4.3.2 + // https://tools.ietf.org/html/rfc7231#section-4.3.5 + + // Sending a payload body on a request that does not + // expect it can cause undefined behavior on some + // servers and corrupt connection state. Do not + // re-use the connection for further requests. + + const expectsPayload = ( + method === 'PUT' || + method === 'POST' || + method === 'PATCH' + ) + + if (body && typeof body.read === 'function') { + // Try to read EOF in order to get length. + body.read(0) + } + + const bodyLength = util.bodyLength(body) + + let contentLength = bodyLength + + if (contentLength === null) { + contentLength = request.contentLength + } + + if (contentLength === 0 && !expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD NOT send a Content-Length header field when + // the request message does not contain a payload body and the method + // semantics do not anticipate such a body. + + contentLength = null + } + + // https://github.com/nodejs/undici/issues/2046 + // A user agent may send a Content-Length header with 0 value, this should be allowed. + if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength !== null && request.contentLength !== contentLength) { + if (client[kStrictContentLength]) { + errorRequest(client, request, new RequestContentLengthMismatchError()) + return false + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + const socket = client[kSocket] + + try { + request.onConnect((err) => { + if (request.aborted || request.completed) { + return + } + + errorRequest(client, request, err || new RequestAbortedError()) + + util.destroy(socket, new InformationalError('aborted')) + }) + } catch (err) { + errorRequest(client, request, err) + } + + if (request.aborted) { + return false + } + + if (method === 'HEAD') { + // https://github.com/mcollina/undici/issues/258 + // Close after a HEAD request to interop with misbehaving servers + // that may send a body in the response. + + socket[kReset] = true + } + + if (upgrade || method === 'CONNECT') { + // On CONNECT or upgrade, block pipeline from dispatching further + // requests on this connection. + + socket[kReset] = true + } + + if (reset != null) { + socket[kReset] = reset + } + + if (client[kMaxRequests] && socket[kCounter]++ >= client[kMaxRequests]) { + socket[kReset] = true + } + + if (blocking) { + socket[kBlocking] = true + } + + let header = `${method} ${path} HTTP/1.1\r\n` + + if (typeof host === 'string') { + header += `host: ${host}\r\n` + } else { + header += client[kHostHeader] + } + + if (upgrade) { + header += `connection: upgrade\r\nupgrade: ${upgrade}\r\n` + } else if (client[kPipelining] && !socket[kReset]) { + header += 'connection: keep-alive\r\n' + } else { + header += 'connection: close\r\n' + } + + if (headers) { + header += headers + } + + if (channels.sendHeaders.hasSubscribers) { + channels.sendHeaders.publish({ request, headers: header, socket }) + } + + /* istanbul ignore else: assertion */ + if (!body || bodyLength === 0) { + if (contentLength === 0) { + socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1') + } else { + assert(contentLength === null, 'no body must not have content length') + socket.write(`${header}\r\n`, 'latin1') + } + request.onRequestSent() + } else if (util.isBuffer(body)) { + assert(contentLength === body.byteLength, 'buffer body must have content length') + + socket.cork() + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') + socket.write(body) + socket.uncork() + request.onBodySent(body) + request.onRequestSent() + if (!expectsPayload) { + socket[kReset] = true + } + } else if (util.isBlobLike(body)) { + if (typeof body.stream === 'function') { + writeIterable({ body: body.stream(), client, request, socket, contentLength, header, expectsPayload }) + } else { + writeBlob({ body, client, request, socket, contentLength, header, expectsPayload }) + } + } else if (util.isStream(body)) { + writeStream({ body, client, request, socket, contentLength, header, expectsPayload }) + } else if (util.isIterable(body)) { + writeIterable({ body, client, request, socket, contentLength, header, expectsPayload }) + } else { + assert(false) + } + + return true +} + +function writeH2 (client, session, request) { + const { body, method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request + + let headers + if (typeof reqHeaders === 'string') headers = Request[kHTTP2CopyHeaders](reqHeaders.trim()) + else headers = reqHeaders + + if (upgrade) { + errorRequest(client, request, new Error('Upgrade not supported for H2')) + return false + } + + try { + // TODO(HTTP/2): Should we call onConnect immediately or on stream ready event? + request.onConnect((err) => { + if (request.aborted || request.completed) { + return + } + + errorRequest(client, request, err || new RequestAbortedError()) + }) + } catch (err) { + errorRequest(client, request, err) + } + + if (request.aborted) { + return false + } + + /** @type {import('node:http2').ClientHttp2Stream} */ + let stream + const h2State = client[kHTTP2SessionState] + + headers[HTTP2_HEADER_AUTHORITY] = host || client[kHost] + headers[HTTP2_HEADER_METHOD] = method + + if (method === 'CONNECT') { + session.ref() + // we are already connected, streams are pending, first request + // will create a new stream. We trigger a request to create the stream and wait until + // `ready` event is triggered + // We disabled endStream to allow the user to write to the stream + stream = session.request(headers, { endStream: false, signal }) + + if (stream.id && !stream.pending) { + request.onUpgrade(null, null, stream) + ++h2State.openStreams + } else { + stream.once('ready', () => { + request.onUpgrade(null, null, stream) + ++h2State.openStreams + }) + } + + stream.once('close', () => { + h2State.openStreams -= 1 + // TODO(HTTP/2): unref only if current streams count is 0 + if (h2State.openStreams === 0) session.unref() + }) + + return true + } + + // https://tools.ietf.org/html/rfc7540#section-8.3 + // :path and :scheme headers must be omited when sending CONNECT + + headers[HTTP2_HEADER_PATH] = path + headers[HTTP2_HEADER_SCHEME] = 'https' + + // https://tools.ietf.org/html/rfc7231#section-4.3.1 + // https://tools.ietf.org/html/rfc7231#section-4.3.2 + // https://tools.ietf.org/html/rfc7231#section-4.3.5 + + // Sending a payload body on a request that does not + // expect it can cause undefined behavior on some + // servers and corrupt connection state. Do not + // re-use the connection for further requests. + + const expectsPayload = ( + method === 'PUT' || + method === 'POST' || + method === 'PATCH' + ) + + if (body && typeof body.read === 'function') { + // Try to read EOF in order to get length. + body.read(0) + } + + let contentLength = util.bodyLength(body) + + if (contentLength == null) { + contentLength = request.contentLength + } + + if (contentLength === 0 || !expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD NOT send a Content-Length header field when + // the request message does not contain a payload body and the method + // semantics do not anticipate such a body. + + contentLength = null + } + + // https://github.com/nodejs/undici/issues/2046 + // A user agent may send a Content-Length header with 0 value, this should be allowed. + if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength != null && request.contentLength !== contentLength) { + if (client[kStrictContentLength]) { + errorRequest(client, request, new RequestContentLengthMismatchError()) + return false + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + if (contentLength != null) { + assert(body, 'no body must not have content length') + headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}` + } + + session.ref() + + const shouldEndStream = method === 'GET' || method === 'HEAD' + if (expectContinue) { + headers[HTTP2_HEADER_EXPECT] = '100-continue' + stream = session.request(headers, { endStream: shouldEndStream, signal }) + + stream.once('continue', writeBodyH2) + } else { + stream = session.request(headers, { + endStream: shouldEndStream, + signal + }) + writeBodyH2() + } + + // Increment counter as we have new several streams open + ++h2State.openStreams + + stream.once('response', headers => { + const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers + + if (request.onHeaders(Number(statusCode), realHeaders, stream.resume.bind(stream), '') === false) { + stream.pause() + } + }) + + stream.once('end', () => { + request.onComplete([]) + }) + + stream.on('data', (chunk) => { + if (request.onData(chunk) === false) { + stream.pause() + } + }) + + stream.once('close', () => { + h2State.openStreams -= 1 + // TODO(HTTP/2): unref only if current streams count is 0 + if (h2State.openStreams === 0) { + session.unref() + } + }) + + stream.once('error', function (err) { + if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) { + h2State.streams -= 1 + util.destroy(stream, err) + } + }) + + stream.once('frameError', (type, code) => { + const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) + errorRequest(client, request, err) + + if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) { + h2State.streams -= 1 + util.destroy(stream, err) + } + }) + + // stream.on('aborted', () => { + // // TODO(HTTP/2): Support aborted + // }) + + // stream.on('timeout', () => { + // // TODO(HTTP/2): Support timeout + // }) + + // stream.on('push', headers => { + // // TODO(HTTP/2): Suppor push + // }) + + // stream.on('trailers', headers => { + // // TODO(HTTP/2): Support trailers + // }) + + return true + + function writeBodyH2 () { + /* istanbul ignore else: assertion */ + if (!body) { + request.onRequestSent() + } else if (util.isBuffer(body)) { + assert(contentLength === body.byteLength, 'buffer body must have content length') + stream.cork() + stream.write(body) + stream.uncork() + stream.end() + request.onBodySent(body) + request.onRequestSent() + } else if (util.isBlobLike(body)) { + if (typeof body.stream === 'function') { + writeIterable({ + client, + request, + contentLength, + h2stream: stream, + expectsPayload, + body: body.stream(), + socket: client[kSocket], + header: '' + }) + } else { + writeBlob({ + body, + client, + request, + contentLength, + expectsPayload, + h2stream: stream, + header: '', + socket: client[kSocket] + }) + } + } else if (util.isStream(body)) { + writeStream({ + body, + client, + request, + contentLength, + expectsPayload, + socket: client[kSocket], + h2stream: stream, + header: '' + }) + } else if (util.isIterable(body)) { + writeIterable({ + body, + client, + request, + contentLength, + expectsPayload, + header: '', + h2stream: stream, + socket: client[kSocket] + }) + } else { + assert(false) + } + } +} + +function writeStream ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined') + + if (client[kHTTPConnVersion] === 'h2') { + // For HTTP/2, is enough to pipe the stream + const pipe = pipeline( + body, + h2stream, + (err) => { + if (err) { + util.destroy(body, err) + util.destroy(h2stream, err) + } else { + request.onRequestSent() + } + } + ) + + pipe.on('data', onPipeData) + pipe.once('end', () => { + pipe.removeListener('data', onPipeData) + util.destroy(pipe) + }) + + function onPipeData (chunk) { + request.onBodySent(chunk) + } + + return + } + + let finished = false + + const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header }) + + const onData = function (chunk) { + if (finished) { + return + } + + try { + if (!writer.write(chunk) && this.pause) { + this.pause() + } + } catch (err) { + util.destroy(this, err) + } + } + const onDrain = function () { + if (finished) { + return + } + + if (body.resume) { + body.resume() + } + } + const onAbort = function () { + if (finished) { + return + } + const err = new RequestAbortedError() + queueMicrotask(() => onFinished(err)) + } + const onFinished = function (err) { + if (finished) { + return + } + + finished = true + + assert(socket.destroyed || (socket[kWriting] && client[kRunning] <= 1)) + + socket + .off('drain', onDrain) + .off('error', onFinished) + + body + .removeListener('data', onData) + .removeListener('end', onFinished) + .removeListener('error', onFinished) + .removeListener('close', onAbort) + + if (!err) { + try { + writer.end() + } catch (er) { + err = er + } + } + + writer.destroy(err) + + if (err && (err.code !== 'UND_ERR_INFO' || err.message !== 'reset')) { + util.destroy(body, err) + } else { + util.destroy(body) + } + } + + body + .on('data', onData) + .on('end', onFinished) + .on('error', onFinished) + .on('close', onAbort) + + if (body.resume) { + body.resume() + } + + socket + .on('drain', onDrain) + .on('error', onFinished) +} + +async function writeBlob ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength === body.size, 'blob body must have content length') + + const isH2 = client[kHTTPConnVersion] === 'h2' + try { + if (contentLength != null && contentLength !== body.size) { + throw new RequestContentLengthMismatchError() + } + + const buffer = Buffer.from(await body.arrayBuffer()) + + if (isH2) { + h2stream.cork() + h2stream.write(buffer) + h2stream.uncork() + } else { + socket.cork() + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') + socket.write(buffer) + socket.uncork() + } + + request.onBodySent(buffer) + request.onRequestSent() + + if (!expectsPayload) { + socket[kReset] = true + } + + resume(client) + } catch (err) { + util.destroy(isH2 ? h2stream : socket, err) + } +} + +async function writeIterable ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined') + + let callback = null + function onDrain () { + if (callback) { + const cb = callback + callback = null + cb() + } + } + + const waitForDrain = () => new Promise((resolve, reject) => { + assert(callback === null) + + if (socket[kError]) { + reject(socket[kError]) + } else { + callback = resolve + } + }) + + if (client[kHTTPConnVersion] === 'h2') { + h2stream + .on('close', onDrain) + .on('drain', onDrain) + + try { + // It's up to the user to somehow abort the async iterable. + for await (const chunk of body) { + if (socket[kError]) { + throw socket[kError] + } + + const res = h2stream.write(chunk) + request.onBodySent(chunk) + if (!res) { + await waitForDrain() + } + } + } catch (err) { + h2stream.destroy(err) + } finally { + request.onRequestSent() + h2stream.end() + h2stream + .off('close', onDrain) + .off('drain', onDrain) + } + + return + } + + socket + .on('close', onDrain) + .on('drain', onDrain) + + const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header }) + try { + // It's up to the user to somehow abort the async iterable. + for await (const chunk of body) { + if (socket[kError]) { + throw socket[kError] + } + + if (!writer.write(chunk)) { + await waitForDrain() + } + } + + writer.end() + } catch (err) { + writer.destroy(err) + } finally { + socket + .off('close', onDrain) + .off('drain', onDrain) + } +} + +class AsyncWriter { + constructor ({ socket, request, contentLength, client, expectsPayload, header }) { + this.socket = socket + this.request = request + this.contentLength = contentLength + this.client = client + this.bytesWritten = 0 + this.expectsPayload = expectsPayload + this.header = header + + socket[kWriting] = true + } + + write (chunk) { + const { socket, request, contentLength, client, bytesWritten, expectsPayload, header } = this + + if (socket[kError]) { + throw socket[kError] + } + + if (socket.destroyed) { + return false + } + + const len = Buffer.byteLength(chunk) + if (!len) { + return true + } + + // We should defer writing chunks. + if (contentLength !== null && bytesWritten + len > contentLength) { + if (client[kStrictContentLength]) { + throw new RequestContentLengthMismatchError() + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + socket.cork() + + if (bytesWritten === 0) { + if (!expectsPayload) { + socket[kReset] = true + } + + if (contentLength === null) { + socket.write(`${header}transfer-encoding: chunked\r\n`, 'latin1') + } else { + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') + } + } + + if (contentLength === null) { + socket.write(`\r\n${len.toString(16)}\r\n`, 'latin1') + } + + this.bytesWritten += len + + const ret = socket.write(chunk) + + socket.uncork() + + request.onBodySent(chunk) + + if (!ret) { + if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { + // istanbul ignore else: only for jest + if (socket[kParser].timeout.refresh) { + socket[kParser].timeout.refresh() + } + } + } + + return ret + } + + end () { + const { socket, contentLength, client, bytesWritten, expectsPayload, header, request } = this + request.onRequestSent() + + socket[kWriting] = false + + if (socket[kError]) { + throw socket[kError] + } + + if (socket.destroyed) { + return + } + + if (bytesWritten === 0) { + if (expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD send a Content-Length in a request message when + // no Transfer-Encoding is sent and the request method defines a meaning + // for an enclosed payload body. + + socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1') + } else { + socket.write(`${header}\r\n`, 'latin1') + } + } else if (contentLength === null) { + socket.write('\r\n0\r\n\r\n', 'latin1') + } + + if (contentLength !== null && bytesWritten !== contentLength) { + if (client[kStrictContentLength]) { + throw new RequestContentLengthMismatchError() + } else { + process.emitWarning(new RequestContentLengthMismatchError()) + } + } + + if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { + // istanbul ignore else: only for jest + if (socket[kParser].timeout.refresh) { + socket[kParser].timeout.refresh() + } + } + + resume(client) + } + + destroy (err) { + const { socket, client } = this + + socket[kWriting] = false + + if (err) { + assert(client[kRunning] <= 1, 'pipeline should only contain this request') + util.destroy(socket, err) + } + } +} + +function errorRequest (client, request, err) { + try { + request.onError(err) + assert(request.aborted) + } catch (err) { + client.emit('error', err) + } +} + +module.exports = Client + + +/***/ }), + +/***/ 5254: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +/* istanbul ignore file: only for Node 12 */ + +const { kConnected, kSize } = __nccwpck_require__(9583) + +class CompatWeakRef { + constructor (value) { + this.value = value + } + + deref () { + return this.value[kConnected] === 0 && this.value[kSize] === 0 + ? undefined + : this.value + } +} + +class CompatFinalizer { + constructor (finalizer) { + this.finalizer = finalizer + } + + register (dispatcher, key) { + if (dispatcher.on) { + dispatcher.on('disconnect', () => { + if (dispatcher[kConnected] === 0 && dispatcher[kSize] === 0) { + this.finalizer(key) + } + }) + } + } +} + +module.exports = function () { + // FIXME: remove workaround when the Node bug is fixed + // https://github.com/nodejs/node/issues/49344#issuecomment-1741776308 + if (process.env.NODE_V8_COVERAGE) { + return { + WeakRef: CompatWeakRef, + FinalizationRegistry: CompatFinalizer + } + } + return { + WeakRef: global.WeakRef || CompatWeakRef, + FinalizationRegistry: global.FinalizationRegistry || CompatFinalizer + } +} + + +/***/ }), + +/***/ 1161: +/***/ ((module) => { + +"use strict"; + + +// https://wicg.github.io/cookie-store/#cookie-maximum-attribute-value-size +const maxAttributeValueSize = 1024 + +// https://wicg.github.io/cookie-store/#cookie-maximum-name-value-pair-size +const maxNameValuePairSize = 4096 + +module.exports = { + maxAttributeValueSize, + maxNameValuePairSize +} + + +/***/ }), + +/***/ 4260: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { parseSetCookie } = __nccwpck_require__(1687) +const { stringify, getHeadersList } = __nccwpck_require__(9078) +const { webidl } = __nccwpck_require__(274) +const { Headers } = __nccwpck_require__(161) + +/** + * @typedef {Object} Cookie + * @property {string} name + * @property {string} value + * @property {Date|number|undefined} expires + * @property {number|undefined} maxAge + * @property {string|undefined} domain + * @property {string|undefined} path + * @property {boolean|undefined} secure + * @property {boolean|undefined} httpOnly + * @property {'Strict'|'Lax'|'None'} sameSite + * @property {string[]} unparsed + */ + +/** + * @param {Headers} headers + * @returns {Record} + */ +function getCookies (headers) { + webidl.argumentLengthCheck(arguments, 1, { header: 'getCookies' }) + + webidl.brandCheck(headers, Headers, { strict: false }) + + const cookie = headers.get('cookie') + const out = {} + + if (!cookie) { + return out + } + + for (const piece of cookie.split(';')) { + const [name, ...value] = piece.split('=') + + out[name.trim()] = value.join('=') + } + + return out +} + +/** + * @param {Headers} headers + * @param {string} name + * @param {{ path?: string, domain?: string }|undefined} attributes + * @returns {void} + */ +function deleteCookie (headers, name, attributes) { + webidl.argumentLengthCheck(arguments, 2, { header: 'deleteCookie' }) + + webidl.brandCheck(headers, Headers, { strict: false }) + + name = webidl.converters.DOMString(name) + attributes = webidl.converters.DeleteCookieAttributes(attributes) + + // Matches behavior of + // https://github.com/denoland/deno_std/blob/63827b16330b82489a04614027c33b7904e08be5/http/cookie.ts#L278 + setCookie(headers, { + name, + value: '', + expires: new Date(0), + ...attributes + }) +} + +/** + * @param {Headers} headers + * @returns {Cookie[]} + */ +function getSetCookies (headers) { + webidl.argumentLengthCheck(arguments, 1, { header: 'getSetCookies' }) + + webidl.brandCheck(headers, Headers, { strict: false }) + + const cookies = getHeadersList(headers).cookies + + if (!cookies) { + return [] + } + + // In older versions of undici, cookies is a list of name:value. + return cookies.map((pair) => parseSetCookie(Array.isArray(pair) ? pair[1] : pair)) +} + +/** + * @param {Headers} headers + * @param {Cookie} cookie + * @returns {void} + */ +function setCookie (headers, cookie) { + webidl.argumentLengthCheck(arguments, 2, { header: 'setCookie' }) + + webidl.brandCheck(headers, Headers, { strict: false }) + + cookie = webidl.converters.Cookie(cookie) + + const str = stringify(cookie) + + if (str) { + headers.append('Set-Cookie', stringify(cookie)) + } +} + +webidl.converters.DeleteCookieAttributes = webidl.dictionaryConverter([ + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: 'path', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: 'domain', + defaultValue: null + } +]) + +webidl.converters.Cookie = webidl.dictionaryConverter([ + { + converter: webidl.converters.DOMString, + key: 'name' + }, + { + converter: webidl.converters.DOMString, + key: 'value' + }, + { + converter: webidl.nullableConverter((value) => { + if (typeof value === 'number') { + return webidl.converters['unsigned long long'](value) + } + + return new Date(value) + }), + key: 'expires', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters['long long']), + key: 'maxAge', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: 'domain', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: 'path', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters.boolean), + key: 'secure', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters.boolean), + key: 'httpOnly', + defaultValue: null + }, + { + converter: webidl.converters.USVString, + key: 'sameSite', + allowedValues: ['Strict', 'Lax', 'None'] + }, + { + converter: webidl.sequenceConverter(webidl.converters.DOMString), + key: 'unparsed', + defaultValue: [] + } +]) + +module.exports = { + getCookies, + deleteCookie, + getSetCookies, + setCookie +} + + +/***/ }), + +/***/ 1687: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { maxNameValuePairSize, maxAttributeValueSize } = __nccwpck_require__(1161) +const { isCTLExcludingHtab } = __nccwpck_require__(9078) +const { collectASequenceOfCodePointsFast } = __nccwpck_require__(5294) +const assert = __nccwpck_require__(2613) + +/** + * @description Parses the field-value attributes of a set-cookie header string. + * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4 + * @param {string} header + * @returns if the header is invalid, null will be returned + */ +function parseSetCookie (header) { + // 1. If the set-cookie-string contains a %x00-08 / %x0A-1F / %x7F + // character (CTL characters excluding HTAB): Abort these steps and + // ignore the set-cookie-string entirely. + if (isCTLExcludingHtab(header)) { + return null + } + + let nameValuePair = '' + let unparsedAttributes = '' + let name = '' + let value = '' + + // 2. If the set-cookie-string contains a %x3B (";") character: + if (header.includes(';')) { + // 1. The name-value-pair string consists of the characters up to, + // but not including, the first %x3B (";"), and the unparsed- + // attributes consist of the remainder of the set-cookie-string + // (including the %x3B (";") in question). + const position = { position: 0 } + + nameValuePair = collectASequenceOfCodePointsFast(';', header, position) + unparsedAttributes = header.slice(position.position) + } else { + // Otherwise: + + // 1. The name-value-pair string consists of all the characters + // contained in the set-cookie-string, and the unparsed- + // attributes is the empty string. + nameValuePair = header + } + + // 3. If the name-value-pair string lacks a %x3D ("=") character, then + // the name string is empty, and the value string is the value of + // name-value-pair. + if (!nameValuePair.includes('=')) { + value = nameValuePair + } else { + // Otherwise, the name string consists of the characters up to, but + // not including, the first %x3D ("=") character, and the (possibly + // empty) value string consists of the characters after the first + // %x3D ("=") character. + const position = { position: 0 } + name = collectASequenceOfCodePointsFast( + '=', + nameValuePair, + position + ) + value = nameValuePair.slice(position.position + 1) + } + + // 4. Remove any leading or trailing WSP characters from the name + // string and the value string. + name = name.trim() + value = value.trim() + + // 5. If the sum of the lengths of the name string and the value string + // is more than 4096 octets, abort these steps and ignore the set- + // cookie-string entirely. + if (name.length + value.length > maxNameValuePairSize) { + return null + } + + // 6. The cookie-name is the name string, and the cookie-value is the + // value string. + return { + name, value, ...parseUnparsedAttributes(unparsedAttributes) + } +} + +/** + * Parses the remaining attributes of a set-cookie header + * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4 + * @param {string} unparsedAttributes + * @param {[Object.]={}} cookieAttributeList + */ +function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) { + // 1. If the unparsed-attributes string is empty, skip the rest of + // these steps. + if (unparsedAttributes.length === 0) { + return cookieAttributeList + } + + // 2. Discard the first character of the unparsed-attributes (which + // will be a %x3B (";") character). + assert(unparsedAttributes[0] === ';') + unparsedAttributes = unparsedAttributes.slice(1) + + let cookieAv = '' + + // 3. If the remaining unparsed-attributes contains a %x3B (";") + // character: + if (unparsedAttributes.includes(';')) { + // 1. Consume the characters of the unparsed-attributes up to, but + // not including, the first %x3B (";") character. + cookieAv = collectASequenceOfCodePointsFast( + ';', + unparsedAttributes, + { position: 0 } + ) + unparsedAttributes = unparsedAttributes.slice(cookieAv.length) + } else { + // Otherwise: + + // 1. Consume the remainder of the unparsed-attributes. + cookieAv = unparsedAttributes + unparsedAttributes = '' + } + + // Let the cookie-av string be the characters consumed in this step. + + let attributeName = '' + let attributeValue = '' + + // 4. If the cookie-av string contains a %x3D ("=") character: + if (cookieAv.includes('=')) { + // 1. The (possibly empty) attribute-name string consists of the + // characters up to, but not including, the first %x3D ("=") + // character, and the (possibly empty) attribute-value string + // consists of the characters after the first %x3D ("=") + // character. + const position = { position: 0 } + + attributeName = collectASequenceOfCodePointsFast( + '=', + cookieAv, + position + ) + attributeValue = cookieAv.slice(position.position + 1) + } else { + // Otherwise: + + // 1. The attribute-name string consists of the entire cookie-av + // string, and the attribute-value string is empty. + attributeName = cookieAv + } + + // 5. Remove any leading or trailing WSP characters from the attribute- + // name string and the attribute-value string. + attributeName = attributeName.trim() + attributeValue = attributeValue.trim() + + // 6. If the attribute-value is longer than 1024 octets, ignore the + // cookie-av string and return to Step 1 of this algorithm. + if (attributeValue.length > maxAttributeValueSize) { + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) + } + + // 7. Process the attribute-name and attribute-value according to the + // requirements in the following subsections. (Notice that + // attributes with unrecognized attribute-names are ignored.) + const attributeNameLowercase = attributeName.toLowerCase() + + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.1 + // If the attribute-name case-insensitively matches the string + // "Expires", the user agent MUST process the cookie-av as follows. + if (attributeNameLowercase === 'expires') { + // 1. Let the expiry-time be the result of parsing the attribute-value + // as cookie-date (see Section 5.1.1). + const expiryTime = new Date(attributeValue) + + // 2. If the attribute-value failed to parse as a cookie date, ignore + // the cookie-av. + + cookieAttributeList.expires = expiryTime + } else if (attributeNameLowercase === 'max-age') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.2 + // If the attribute-name case-insensitively matches the string "Max- + // Age", the user agent MUST process the cookie-av as follows. + + // 1. If the first character of the attribute-value is not a DIGIT or a + // "-" character, ignore the cookie-av. + const charCode = attributeValue.charCodeAt(0) + + if ((charCode < 48 || charCode > 57) && attributeValue[0] !== '-') { + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) + } + + // 2. If the remainder of attribute-value contains a non-DIGIT + // character, ignore the cookie-av. + if (!/^\d+$/.test(attributeValue)) { + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) + } + + // 3. Let delta-seconds be the attribute-value converted to an integer. + const deltaSeconds = Number(attributeValue) + + // 4. Let cookie-age-limit be the maximum age of the cookie (which + // SHOULD be 400 days or less, see Section 4.1.2.2). + + // 5. Set delta-seconds to the smaller of its present value and cookie- + // age-limit. + // deltaSeconds = Math.min(deltaSeconds * 1000, maxExpiresMs) + + // 6. If delta-seconds is less than or equal to zero (0), let expiry- + // time be the earliest representable date and time. Otherwise, let + // the expiry-time be the current date and time plus delta-seconds + // seconds. + // const expiryTime = deltaSeconds <= 0 ? Date.now() : Date.now() + deltaSeconds + + // 7. Append an attribute to the cookie-attribute-list with an + // attribute-name of Max-Age and an attribute-value of expiry-time. + cookieAttributeList.maxAge = deltaSeconds + } else if (attributeNameLowercase === 'domain') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.3 + // If the attribute-name case-insensitively matches the string "Domain", + // the user agent MUST process the cookie-av as follows. + + // 1. Let cookie-domain be the attribute-value. + let cookieDomain = attributeValue + + // 2. If cookie-domain starts with %x2E ("."), let cookie-domain be + // cookie-domain without its leading %x2E ("."). + if (cookieDomain[0] === '.') { + cookieDomain = cookieDomain.slice(1) + } + + // 3. Convert the cookie-domain to lower case. + cookieDomain = cookieDomain.toLowerCase() + + // 4. Append an attribute to the cookie-attribute-list with an + // attribute-name of Domain and an attribute-value of cookie-domain. + cookieAttributeList.domain = cookieDomain + } else if (attributeNameLowercase === 'path') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.4 + // If the attribute-name case-insensitively matches the string "Path", + // the user agent MUST process the cookie-av as follows. + + // 1. If the attribute-value is empty or if the first character of the + // attribute-value is not %x2F ("/"): + let cookiePath = '' + if (attributeValue.length === 0 || attributeValue[0] !== '/') { + // 1. Let cookie-path be the default-path. + cookiePath = '/' + } else { + // Otherwise: + + // 1. Let cookie-path be the attribute-value. + cookiePath = attributeValue + } + + // 2. Append an attribute to the cookie-attribute-list with an + // attribute-name of Path and an attribute-value of cookie-path. + cookieAttributeList.path = cookiePath + } else if (attributeNameLowercase === 'secure') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.5 + // If the attribute-name case-insensitively matches the string "Secure", + // the user agent MUST append an attribute to the cookie-attribute-list + // with an attribute-name of Secure and an empty attribute-value. + + cookieAttributeList.secure = true + } else if (attributeNameLowercase === 'httponly') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.6 + // If the attribute-name case-insensitively matches the string + // "HttpOnly", the user agent MUST append an attribute to the cookie- + // attribute-list with an attribute-name of HttpOnly and an empty + // attribute-value. + + cookieAttributeList.httpOnly = true + } else if (attributeNameLowercase === 'samesite') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7 + // If the attribute-name case-insensitively matches the string + // "SameSite", the user agent MUST process the cookie-av as follows: + + // 1. Let enforcement be "Default". + let enforcement = 'Default' + + const attributeValueLowercase = attributeValue.toLowerCase() + // 2. If cookie-av's attribute-value is a case-insensitive match for + // "None", set enforcement to "None". + if (attributeValueLowercase.includes('none')) { + enforcement = 'None' + } + + // 3. If cookie-av's attribute-value is a case-insensitive match for + // "Strict", set enforcement to "Strict". + if (attributeValueLowercase.includes('strict')) { + enforcement = 'Strict' + } + + // 4. If cookie-av's attribute-value is a case-insensitive match for + // "Lax", set enforcement to "Lax". + if (attributeValueLowercase.includes('lax')) { + enforcement = 'Lax' + } + + // 5. Append an attribute to the cookie-attribute-list with an + // attribute-name of "SameSite" and an attribute-value of + // enforcement. + cookieAttributeList.sameSite = enforcement + } else { + cookieAttributeList.unparsed ??= [] + + cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`) + } + + // 8. Return to Step 1 of this algorithm. + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) +} + +module.exports = { + parseSetCookie, + parseUnparsedAttributes +} + + +/***/ }), + +/***/ 9078: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const assert = __nccwpck_require__(2613) +const { kHeadersList } = __nccwpck_require__(9583) + +function isCTLExcludingHtab (value) { + if (value.length === 0) { + return false + } + + for (const char of value) { + const code = char.charCodeAt(0) + + if ( + (code >= 0x00 || code <= 0x08) || + (code >= 0x0A || code <= 0x1F) || + code === 0x7F + ) { + return false + } + } +} + +/** + CHAR = + token = 1* + separators = "(" | ")" | "<" | ">" | "@" + | "," | ";" | ":" | "\" | <"> + | "/" | "[" | "]" | "?" | "=" + | "{" | "}" | SP | HT + * @param {string} name + */ +function validateCookieName (name) { + for (const char of name) { + const code = char.charCodeAt(0) + + if ( + (code <= 0x20 || code > 0x7F) || + char === '(' || + char === ')' || + char === '>' || + char === '<' || + char === '@' || + char === ',' || + char === ';' || + char === ':' || + char === '\\' || + char === '"' || + char === '/' || + char === '[' || + char === ']' || + char === '?' || + char === '=' || + char === '{' || + char === '}' + ) { + throw new Error('Invalid cookie name') + } + } +} + +/** + cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) + cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E + ; US-ASCII characters excluding CTLs, + ; whitespace DQUOTE, comma, semicolon, + ; and backslash + * @param {string} value + */ +function validateCookieValue (value) { + for (const char of value) { + const code = char.charCodeAt(0) + + if ( + code < 0x21 || // exclude CTLs (0-31) + code === 0x22 || + code === 0x2C || + code === 0x3B || + code === 0x5C || + code > 0x7E // non-ascii + ) { + throw new Error('Invalid header value') + } + } +} + +/** + * path-value = + * @param {string} path + */ +function validateCookiePath (path) { + for (const char of path) { + const code = char.charCodeAt(0) + + if (code < 0x21 || char === ';') { + throw new Error('Invalid cookie path') + } + } +} + +/** + * I have no idea why these values aren't allowed to be honest, + * but Deno tests these. - Khafra + * @param {string} domain + */ +function validateCookieDomain (domain) { + if ( + domain.startsWith('-') || + domain.endsWith('.') || + domain.endsWith('-') + ) { + throw new Error('Invalid cookie domain') + } +} + +/** + * @see https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1 + * @param {number|Date} date + IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT + ; fixed length/zone/capitalization subset of the format + ; see Section 3.3 of [RFC5322] + + day-name = %x4D.6F.6E ; "Mon", case-sensitive + / %x54.75.65 ; "Tue", case-sensitive + / %x57.65.64 ; "Wed", case-sensitive + / %x54.68.75 ; "Thu", case-sensitive + / %x46.72.69 ; "Fri", case-sensitive + / %x53.61.74 ; "Sat", case-sensitive + / %x53.75.6E ; "Sun", case-sensitive + date1 = day SP month SP year + ; e.g., 02 Jun 1982 + + day = 2DIGIT + month = %x4A.61.6E ; "Jan", case-sensitive + / %x46.65.62 ; "Feb", case-sensitive + / %x4D.61.72 ; "Mar", case-sensitive + / %x41.70.72 ; "Apr", case-sensitive + / %x4D.61.79 ; "May", case-sensitive + / %x4A.75.6E ; "Jun", case-sensitive + / %x4A.75.6C ; "Jul", case-sensitive + / %x41.75.67 ; "Aug", case-sensitive + / %x53.65.70 ; "Sep", case-sensitive + / %x4F.63.74 ; "Oct", case-sensitive + / %x4E.6F.76 ; "Nov", case-sensitive + / %x44.65.63 ; "Dec", case-sensitive + year = 4DIGIT + + GMT = %x47.4D.54 ; "GMT", case-sensitive + + time-of-day = hour ":" minute ":" second + ; 00:00:00 - 23:59:60 (leap second) + + hour = 2DIGIT + minute = 2DIGIT + second = 2DIGIT + */ +function toIMFDate (date) { + if (typeof date === 'number') { + date = new Date(date) + } + + const days = [ + 'Sun', 'Mon', 'Tue', 'Wed', + 'Thu', 'Fri', 'Sat' + ] + + const months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ] + + const dayName = days[date.getUTCDay()] + const day = date.getUTCDate().toString().padStart(2, '0') + const month = months[date.getUTCMonth()] + const year = date.getUTCFullYear() + const hour = date.getUTCHours().toString().padStart(2, '0') + const minute = date.getUTCMinutes().toString().padStart(2, '0') + const second = date.getUTCSeconds().toString().padStart(2, '0') + + return `${dayName}, ${day} ${month} ${year} ${hour}:${minute}:${second} GMT` +} + +/** + max-age-av = "Max-Age=" non-zero-digit *DIGIT + ; In practice, both expires-av and max-age-av + ; are limited to dates representable by the + ; user agent. + * @param {number} maxAge + */ +function validateCookieMaxAge (maxAge) { + if (maxAge < 0) { + throw new Error('Invalid cookie max-age') + } +} + +/** + * @see https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1 + * @param {import('./index').Cookie} cookie + */ +function stringify (cookie) { + if (cookie.name.length === 0) { + return null + } + + validateCookieName(cookie.name) + validateCookieValue(cookie.value) + + const out = [`${cookie.name}=${cookie.value}`] + + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.1 + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.2 + if (cookie.name.startsWith('__Secure-')) { + cookie.secure = true + } + + if (cookie.name.startsWith('__Host-')) { + cookie.secure = true + cookie.domain = null + cookie.path = '/' + } + + if (cookie.secure) { + out.push('Secure') + } + + if (cookie.httpOnly) { + out.push('HttpOnly') + } + + if (typeof cookie.maxAge === 'number') { + validateCookieMaxAge(cookie.maxAge) + out.push(`Max-Age=${cookie.maxAge}`) + } + + if (cookie.domain) { + validateCookieDomain(cookie.domain) + out.push(`Domain=${cookie.domain}`) + } + + if (cookie.path) { + validateCookiePath(cookie.path) + out.push(`Path=${cookie.path}`) + } + + if (cookie.expires && cookie.expires.toString() !== 'Invalid Date') { + out.push(`Expires=${toIMFDate(cookie.expires)}`) + } + + if (cookie.sameSite) { + out.push(`SameSite=${cookie.sameSite}`) + } + + for (const part of cookie.unparsed) { + if (!part.includes('=')) { + throw new Error('Invalid unparsed') + } + + const [key, ...value] = part.split('=') + + out.push(`${key.trim()}=${value.join('=')}`) + } + + return out.join('; ') +} + +let kHeadersListNode + +function getHeadersList (headers) { + if (headers[kHeadersList]) { + return headers[kHeadersList] + } + + if (!kHeadersListNode) { + kHeadersListNode = Object.getOwnPropertySymbols(headers).find( + (symbol) => symbol.description === 'headers list' + ) + + assert(kHeadersListNode, 'Headers cannot be parsed') + } + + const headersList = headers[kHeadersListNode] + assert(headersList) + + return headersList +} + +module.exports = { + isCTLExcludingHtab, + stringify, + getHeadersList +} + + +/***/ }), + +/***/ 3940: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const net = __nccwpck_require__(9278) +const assert = __nccwpck_require__(2613) +const util = __nccwpck_require__(1436) +const { InvalidArgumentError, ConnectTimeoutError } = __nccwpck_require__(1975) + +let tls // include tls conditionally since it is not always available + +// TODO: session re-use does not wait for the first +// connection to resolve the session and might therefore +// resolve the same servername multiple times even when +// re-use is enabled. + +let SessionCache +// FIXME: remove workaround when the Node bug is fixed +// https://github.com/nodejs/node/issues/49344#issuecomment-1741776308 +if (global.FinalizationRegistry && !process.env.NODE_V8_COVERAGE) { + SessionCache = class WeakSessionCache { + constructor (maxCachedSessions) { + this._maxCachedSessions = maxCachedSessions + this._sessionCache = new Map() + this._sessionRegistry = new global.FinalizationRegistry((key) => { + if (this._sessionCache.size < this._maxCachedSessions) { + return + } + + const ref = this._sessionCache.get(key) + if (ref !== undefined && ref.deref() === undefined) { + this._sessionCache.delete(key) + } + }) + } + + get (sessionKey) { + const ref = this._sessionCache.get(sessionKey) + return ref ? ref.deref() : null + } + + set (sessionKey, session) { + if (this._maxCachedSessions === 0) { + return + } + + this._sessionCache.set(sessionKey, new WeakRef(session)) + this._sessionRegistry.register(session, sessionKey) + } + } +} else { + SessionCache = class SimpleSessionCache { + constructor (maxCachedSessions) { + this._maxCachedSessions = maxCachedSessions + this._sessionCache = new Map() + } + + get (sessionKey) { + return this._sessionCache.get(sessionKey) + } + + set (sessionKey, session) { + if (this._maxCachedSessions === 0) { + return + } + + if (this._sessionCache.size >= this._maxCachedSessions) { + // remove the oldest session + const { value: oldestKey } = this._sessionCache.keys().next() + this._sessionCache.delete(oldestKey) + } + + this._sessionCache.set(sessionKey, session) + } + } +} + +function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, ...opts }) { + if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) { + throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero') + } + + const options = { path: socketPath, ...opts } + const sessionCache = new SessionCache(maxCachedSessions == null ? 100 : maxCachedSessions) + timeout = timeout == null ? 10e3 : timeout + allowH2 = allowH2 != null ? allowH2 : false + return function connect ({ hostname, host, protocol, port, servername, localAddress, httpSocket }, callback) { + let socket + if (protocol === 'https:') { + if (!tls) { + tls = __nccwpck_require__(4756) + } + servername = servername || options.servername || util.getServerName(host) || null + + const sessionKey = servername || hostname + const session = sessionCache.get(sessionKey) || null + + assert(sessionKey) + + socket = tls.connect({ + highWaterMark: 16384, // TLS in node can't have bigger HWM anyway... + ...options, + servername, + session, + localAddress, + // TODO(HTTP/2): Add support for h2c + ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'], + socket: httpSocket, // upgrade socket connection + port: port || 443, + host: hostname + }) + + socket + .on('session', function (session) { + // TODO (fix): Can a session become invalid once established? Don't think so? + sessionCache.set(sessionKey, session) + }) + } else { + assert(!httpSocket, 'httpSocket can only be sent on TLS update') + socket = net.connect({ + highWaterMark: 64 * 1024, // Same as nodejs fs streams. + ...options, + localAddress, + port: port || 80, + host: hostname + }) + } + + // Set TCP keep alive options on the socket here instead of in connect() for the case of assigning the socket + if (options.keepAlive == null || options.keepAlive) { + const keepAliveInitialDelay = options.keepAliveInitialDelay === undefined ? 60e3 : options.keepAliveInitialDelay + socket.setKeepAlive(true, keepAliveInitialDelay) + } + + const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout) + + socket + .setNoDelay(true) + .once(protocol === 'https:' ? 'secureConnect' : 'connect', function () { + cancelTimeout() + + if (callback) { + const cb = callback + callback = null + cb(null, this) + } + }) + .on('error', function (err) { + cancelTimeout() + + if (callback) { + const cb = callback + callback = null + cb(err) + } + }) + + return socket + } +} + +function setupTimeout (onConnectTimeout, timeout) { + if (!timeout) { + return () => {} + } + + let s1 = null + let s2 = null + const timeoutId = setTimeout(() => { + // setImmediate is added to make sure that we priotorise socket error events over timeouts + s1 = setImmediate(() => { + if (process.platform === 'win32') { + // Windows needs an extra setImmediate probably due to implementation differences in the socket logic + s2 = setImmediate(() => onConnectTimeout()) + } else { + onConnectTimeout() + } + }) + }, timeout) + return () => { + clearTimeout(timeoutId) + clearImmediate(s1) + clearImmediate(s2) + } +} + +function onConnectTimeout (socket) { + util.destroy(socket, new ConnectTimeoutError()) +} + +module.exports = buildConnector + + +/***/ }), + +/***/ 5411: +/***/ ((module) => { + +"use strict"; + + +/** @type {Record} */ +const headerNameLowerCasedRecord = {} + +// https://developer.mozilla.org/docs/Web/HTTP/Headers +const wellknownHeaderNames = [ + 'Accept', + 'Accept-Encoding', + 'Accept-Language', + 'Accept-Ranges', + 'Access-Control-Allow-Credentials', + 'Access-Control-Allow-Headers', + 'Access-Control-Allow-Methods', + 'Access-Control-Allow-Origin', + 'Access-Control-Expose-Headers', + 'Access-Control-Max-Age', + 'Access-Control-Request-Headers', + 'Access-Control-Request-Method', + 'Age', + 'Allow', + 'Alt-Svc', + 'Alt-Used', + 'Authorization', + 'Cache-Control', + 'Clear-Site-Data', + 'Connection', + 'Content-Disposition', + 'Content-Encoding', + 'Content-Language', + 'Content-Length', + 'Content-Location', + 'Content-Range', + 'Content-Security-Policy', + 'Content-Security-Policy-Report-Only', + 'Content-Type', + 'Cookie', + 'Cross-Origin-Embedder-Policy', + 'Cross-Origin-Opener-Policy', + 'Cross-Origin-Resource-Policy', + 'Date', + 'Device-Memory', + 'Downlink', + 'ECT', + 'ETag', + 'Expect', + 'Expect-CT', + 'Expires', + 'Forwarded', + 'From', + 'Host', + 'If-Match', + 'If-Modified-Since', + 'If-None-Match', + 'If-Range', + 'If-Unmodified-Since', + 'Keep-Alive', + 'Last-Modified', + 'Link', + 'Location', + 'Max-Forwards', + 'Origin', + 'Permissions-Policy', + 'Pragma', + 'Proxy-Authenticate', + 'Proxy-Authorization', + 'RTT', + 'Range', + 'Referer', + 'Referrer-Policy', + 'Refresh', + 'Retry-After', + 'Sec-WebSocket-Accept', + 'Sec-WebSocket-Extensions', + 'Sec-WebSocket-Key', + 'Sec-WebSocket-Protocol', + 'Sec-WebSocket-Version', + 'Server', + 'Server-Timing', + 'Service-Worker-Allowed', + 'Service-Worker-Navigation-Preload', + 'Set-Cookie', + 'SourceMap', + 'Strict-Transport-Security', + 'Supports-Loading-Mode', + 'TE', + 'Timing-Allow-Origin', + 'Trailer', + 'Transfer-Encoding', + 'Upgrade', + 'Upgrade-Insecure-Requests', + 'User-Agent', + 'Vary', + 'Via', + 'WWW-Authenticate', + 'X-Content-Type-Options', + 'X-DNS-Prefetch-Control', + 'X-Frame-Options', + 'X-Permitted-Cross-Domain-Policies', + 'X-Powered-By', + 'X-Requested-With', + 'X-XSS-Protection' +] + +for (let i = 0; i < wellknownHeaderNames.length; ++i) { + const key = wellknownHeaderNames[i] + const lowerCasedKey = key.toLowerCase() + headerNameLowerCasedRecord[key] = headerNameLowerCasedRecord[lowerCasedKey] = + lowerCasedKey +} + +// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`. +Object.setPrototypeOf(headerNameLowerCasedRecord, null) + +module.exports = { + wellknownHeaderNames, + headerNameLowerCasedRecord +} + + +/***/ }), + +/***/ 1975: +/***/ ((module) => { + +"use strict"; + + +class UndiciError extends Error { + constructor (message) { + super(message) + this.name = 'UndiciError' + this.code = 'UND_ERR' + } +} + +class ConnectTimeoutError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ConnectTimeoutError) + this.name = 'ConnectTimeoutError' + this.message = message || 'Connect Timeout Error' + this.code = 'UND_ERR_CONNECT_TIMEOUT' + } +} + +class HeadersTimeoutError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, HeadersTimeoutError) + this.name = 'HeadersTimeoutError' + this.message = message || 'Headers Timeout Error' + this.code = 'UND_ERR_HEADERS_TIMEOUT' + } +} + +class HeadersOverflowError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, HeadersOverflowError) + this.name = 'HeadersOverflowError' + this.message = message || 'Headers Overflow Error' + this.code = 'UND_ERR_HEADERS_OVERFLOW' + } +} + +class BodyTimeoutError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, BodyTimeoutError) + this.name = 'BodyTimeoutError' + this.message = message || 'Body Timeout Error' + this.code = 'UND_ERR_BODY_TIMEOUT' + } +} + +class ResponseStatusCodeError extends UndiciError { + constructor (message, statusCode, headers, body) { + super(message) + Error.captureStackTrace(this, ResponseStatusCodeError) + this.name = 'ResponseStatusCodeError' + this.message = message || 'Response Status Code Error' + this.code = 'UND_ERR_RESPONSE_STATUS_CODE' + this.body = body + this.status = statusCode + this.statusCode = statusCode + this.headers = headers + } +} + +class InvalidArgumentError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, InvalidArgumentError) + this.name = 'InvalidArgumentError' + this.message = message || 'Invalid Argument Error' + this.code = 'UND_ERR_INVALID_ARG' + } +} + +class InvalidReturnValueError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, InvalidReturnValueError) + this.name = 'InvalidReturnValueError' + this.message = message || 'Invalid Return Value Error' + this.code = 'UND_ERR_INVALID_RETURN_VALUE' + } +} + +class RequestAbortedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, RequestAbortedError) + this.name = 'AbortError' + this.message = message || 'Request aborted' + this.code = 'UND_ERR_ABORTED' + } +} + +class InformationalError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, InformationalError) + this.name = 'InformationalError' + this.message = message || 'Request information' + this.code = 'UND_ERR_INFO' + } +} + +class RequestContentLengthMismatchError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, RequestContentLengthMismatchError) + this.name = 'RequestContentLengthMismatchError' + this.message = message || 'Request body length does not match content-length header' + this.code = 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' + } +} + +class ResponseContentLengthMismatchError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ResponseContentLengthMismatchError) + this.name = 'ResponseContentLengthMismatchError' + this.message = message || 'Response body length does not match content-length header' + this.code = 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH' + } +} + +class ClientDestroyedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ClientDestroyedError) + this.name = 'ClientDestroyedError' + this.message = message || 'The client is destroyed' + this.code = 'UND_ERR_DESTROYED' + } +} + +class ClientClosedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ClientClosedError) + this.name = 'ClientClosedError' + this.message = message || 'The client is closed' + this.code = 'UND_ERR_CLOSED' + } +} + +class SocketError extends UndiciError { + constructor (message, socket) { + super(message) + Error.captureStackTrace(this, SocketError) + this.name = 'SocketError' + this.message = message || 'Socket error' + this.code = 'UND_ERR_SOCKET' + this.socket = socket + } +} + +class NotSupportedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, NotSupportedError) + this.name = 'NotSupportedError' + this.message = message || 'Not supported error' + this.code = 'UND_ERR_NOT_SUPPORTED' + } +} + +class BalancedPoolMissingUpstreamError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, NotSupportedError) + this.name = 'MissingUpstreamError' + this.message = message || 'No upstream has been added to the BalancedPool' + this.code = 'UND_ERR_BPL_MISSING_UPSTREAM' + } +} + +class HTTPParserError extends Error { + constructor (message, code, data) { + super(message) + Error.captureStackTrace(this, HTTPParserError) + this.name = 'HTTPParserError' + this.code = code ? `HPE_${code}` : undefined + this.data = data ? data.toString() : undefined + } +} + +class ResponseExceededMaxSizeError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ResponseExceededMaxSizeError) + this.name = 'ResponseExceededMaxSizeError' + this.message = message || 'Response content exceeded max size' + this.code = 'UND_ERR_RES_EXCEEDED_MAX_SIZE' + } +} + +class RequestRetryError extends UndiciError { + constructor (message, code, { headers, data }) { + super(message) + Error.captureStackTrace(this, RequestRetryError) + this.name = 'RequestRetryError' + this.message = message || 'Request retry error' + this.code = 'UND_ERR_REQ_RETRY' + this.statusCode = code + this.data = data + this.headers = headers + } +} + +module.exports = { + HTTPParserError, + UndiciError, + HeadersTimeoutError, + HeadersOverflowError, + BodyTimeoutError, + RequestContentLengthMismatchError, + ConnectTimeoutError, + ResponseStatusCodeError, + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError, + ClientDestroyedError, + ClientClosedError, + InformationalError, + SocketError, + NotSupportedError, + ResponseContentLengthMismatchError, + BalancedPoolMissingUpstreamError, + ResponseExceededMaxSizeError, + RequestRetryError +} + + +/***/ }), + +/***/ 3371: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + InvalidArgumentError, + NotSupportedError +} = __nccwpck_require__(1975) +const assert = __nccwpck_require__(2613) +const { kHTTP2BuildRequest, kHTTP2CopyHeaders, kHTTP1BuildRequest } = __nccwpck_require__(9583) +const util = __nccwpck_require__(1436) + +// tokenRegExp and headerCharRegex have been lifted from +// https://github.com/nodejs/node/blob/main/lib/_http_common.js + +/** + * Verifies that the given val is a valid HTTP token + * per the rules defined in RFC 7230 + * See https://tools.ietf.org/html/rfc7230#section-3.2.6 + */ +const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/ + +/** + * Matches if val contains an invalid field-vchar + * field-value = *( field-content / obs-fold ) + * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + * field-vchar = VCHAR / obs-text + */ +const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/ + +// Verifies that a given path is valid does not contain control chars \x00 to \x20 +const invalidPathRegex = /[^\u0021-\u00ff]/ + +const kHandler = Symbol('handler') + +const channels = {} + +let extractBody + +try { + const diagnosticsChannel = __nccwpck_require__(1637) + channels.create = diagnosticsChannel.channel('undici:request:create') + channels.bodySent = diagnosticsChannel.channel('undici:request:bodySent') + channels.headers = diagnosticsChannel.channel('undici:request:headers') + channels.trailers = diagnosticsChannel.channel('undici:request:trailers') + channels.error = diagnosticsChannel.channel('undici:request:error') +} catch { + channels.create = { hasSubscribers: false } + channels.bodySent = { hasSubscribers: false } + channels.headers = { hasSubscribers: false } + channels.trailers = { hasSubscribers: false } + channels.error = { hasSubscribers: false } +} + +class Request { + constructor (origin, { + path, + method, + body, + headers, + query, + idempotent, + blocking, + upgrade, + headersTimeout, + bodyTimeout, + reset, + throwOnError, + expectContinue + }, handler) { + if (typeof path !== 'string') { + throw new InvalidArgumentError('path must be a string') + } else if ( + path[0] !== '/' && + !(path.startsWith('http://') || path.startsWith('https://')) && + method !== 'CONNECT' + ) { + throw new InvalidArgumentError('path must be an absolute URL or start with a slash') + } else if (invalidPathRegex.exec(path) !== null) { + throw new InvalidArgumentError('invalid request path') + } + + if (typeof method !== 'string') { + throw new InvalidArgumentError('method must be a string') + } else if (tokenRegExp.exec(method) === null) { + throw new InvalidArgumentError('invalid request method') + } + + if (upgrade && typeof upgrade !== 'string') { + throw new InvalidArgumentError('upgrade must be a string') + } + + if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) { + throw new InvalidArgumentError('invalid headersTimeout') + } + + if (bodyTimeout != null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) { + throw new InvalidArgumentError('invalid bodyTimeout') + } + + if (reset != null && typeof reset !== 'boolean') { + throw new InvalidArgumentError('invalid reset') + } + + if (expectContinue != null && typeof expectContinue !== 'boolean') { + throw new InvalidArgumentError('invalid expectContinue') + } + + this.headersTimeout = headersTimeout + + this.bodyTimeout = bodyTimeout + + this.throwOnError = throwOnError === true + + this.method = method + + this.abort = null + + if (body == null) { + this.body = null + } else if (util.isStream(body)) { + this.body = body + + const rState = this.body._readableState + if (!rState || !rState.autoDestroy) { + this.endHandler = function autoDestroy () { + util.destroy(this) + } + this.body.on('end', this.endHandler) + } + + this.errorHandler = err => { + if (this.abort) { + this.abort(err) + } else { + this.error = err + } + } + this.body.on('error', this.errorHandler) + } else if (util.isBuffer(body)) { + this.body = body.byteLength ? body : null + } else if (ArrayBuffer.isView(body)) { + this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null + } else if (body instanceof ArrayBuffer) { + this.body = body.byteLength ? Buffer.from(body) : null + } else if (typeof body === 'string') { + this.body = body.length ? Buffer.from(body) : null + } else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) { + this.body = body + } else { + throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable') + } + + this.completed = false + + this.aborted = false + + this.upgrade = upgrade || null + + this.path = query ? util.buildURL(path, query) : path + + this.origin = origin + + this.idempotent = idempotent == null + ? method === 'HEAD' || method === 'GET' + : idempotent + + this.blocking = blocking == null ? false : blocking + + this.reset = reset == null ? null : reset + + this.host = null + + this.contentLength = null + + this.contentType = null + + this.headers = '' + + // Only for H2 + this.expectContinue = expectContinue != null ? expectContinue : false + + if (Array.isArray(headers)) { + if (headers.length % 2 !== 0) { + throw new InvalidArgumentError('headers array must be even') + } + for (let i = 0; i < headers.length; i += 2) { + processHeader(this, headers[i], headers[i + 1]) + } + } else if (headers && typeof headers === 'object') { + const keys = Object.keys(headers) + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + processHeader(this, key, headers[key]) + } + } else if (headers != null) { + throw new InvalidArgumentError('headers must be an object or an array') + } + + if (util.isFormDataLike(this.body)) { + if (util.nodeMajor < 16 || (util.nodeMajor === 16 && util.nodeMinor < 8)) { + throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.8 and newer.') + } + + if (!extractBody) { + extractBody = (__nccwpck_require__(4655).extractBody) + } + + const [bodyStream, contentType] = extractBody(body) + if (this.contentType == null) { + this.contentType = contentType + this.headers += `content-type: ${contentType}\r\n` + } + this.body = bodyStream.stream + this.contentLength = bodyStream.length + } else if (util.isBlobLike(body) && this.contentType == null && body.type) { + this.contentType = body.type + this.headers += `content-type: ${body.type}\r\n` + } + + util.validateHandler(handler, method, upgrade) + + this.servername = util.getServerName(this.host) + + this[kHandler] = handler + + if (channels.create.hasSubscribers) { + channels.create.publish({ request: this }) + } + } + + onBodySent (chunk) { + if (this[kHandler].onBodySent) { + try { + return this[kHandler].onBodySent(chunk) + } catch (err) { + this.abort(err) + } + } + } + + onRequestSent () { + if (channels.bodySent.hasSubscribers) { + channels.bodySent.publish({ request: this }) + } + + if (this[kHandler].onRequestSent) { + try { + return this[kHandler].onRequestSent() + } catch (err) { + this.abort(err) + } + } + } + + onConnect (abort) { + assert(!this.aborted) + assert(!this.completed) + + if (this.error) { + abort(this.error) + } else { + this.abort = abort + return this[kHandler].onConnect(abort) + } + } + + onHeaders (statusCode, headers, resume, statusText) { + assert(!this.aborted) + assert(!this.completed) + + if (channels.headers.hasSubscribers) { + channels.headers.publish({ request: this, response: { statusCode, headers, statusText } }) + } + + try { + return this[kHandler].onHeaders(statusCode, headers, resume, statusText) + } catch (err) { + this.abort(err) + } + } + + onData (chunk) { + assert(!this.aborted) + assert(!this.completed) + + try { + return this[kHandler].onData(chunk) + } catch (err) { + this.abort(err) + return false + } + } + + onUpgrade (statusCode, headers, socket) { + assert(!this.aborted) + assert(!this.completed) + + return this[kHandler].onUpgrade(statusCode, headers, socket) + } + + onComplete (trailers) { + this.onFinally() + + assert(!this.aborted) + + this.completed = true + if (channels.trailers.hasSubscribers) { + channels.trailers.publish({ request: this, trailers }) + } + + try { + return this[kHandler].onComplete(trailers) + } catch (err) { + // TODO (fix): This might be a bad idea? + this.onError(err) + } + } + + onError (error) { + this.onFinally() + + if (channels.error.hasSubscribers) { + channels.error.publish({ request: this, error }) + } + + if (this.aborted) { + return + } + this.aborted = true + + return this[kHandler].onError(error) + } + + onFinally () { + if (this.errorHandler) { + this.body.off('error', this.errorHandler) + this.errorHandler = null + } + + if (this.endHandler) { + this.body.off('end', this.endHandler) + this.endHandler = null + } + } + + // TODO: adjust to support H2 + addHeader (key, value) { + processHeader(this, key, value) + return this + } + + static [kHTTP1BuildRequest] (origin, opts, handler) { + // TODO: Migrate header parsing here, to make Requests + // HTTP agnostic + return new Request(origin, opts, handler) + } + + static [kHTTP2BuildRequest] (origin, opts, handler) { + const headers = opts.headers + opts = { ...opts, headers: null } + + const request = new Request(origin, opts, handler) + + request.headers = {} + + if (Array.isArray(headers)) { + if (headers.length % 2 !== 0) { + throw new InvalidArgumentError('headers array must be even') + } + for (let i = 0; i < headers.length; i += 2) { + processHeader(request, headers[i], headers[i + 1], true) + } + } else if (headers && typeof headers === 'object') { + const keys = Object.keys(headers) + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + processHeader(request, key, headers[key], true) + } + } else if (headers != null) { + throw new InvalidArgumentError('headers must be an object or an array') + } + + return request + } + + static [kHTTP2CopyHeaders] (raw) { + const rawHeaders = raw.split('\r\n') + const headers = {} + + for (const header of rawHeaders) { + const [key, value] = header.split(': ') + + if (value == null || value.length === 0) continue + + if (headers[key]) headers[key] += `,${value}` + else headers[key] = value + } + + return headers + } +} + +function processHeaderValue (key, val, skipAppend) { + if (val && typeof val === 'object') { + throw new InvalidArgumentError(`invalid ${key} header`) + } + + val = val != null ? `${val}` : '' + + if (headerCharRegex.exec(val) !== null) { + throw new InvalidArgumentError(`invalid ${key} header`) + } + + return skipAppend ? val : `${key}: ${val}\r\n` +} + +function processHeader (request, key, val, skipAppend = false) { + if (val && (typeof val === 'object' && !Array.isArray(val))) { + throw new InvalidArgumentError(`invalid ${key} header`) + } else if (val === undefined) { + return + } + + if ( + request.host === null && + key.length === 4 && + key.toLowerCase() === 'host' + ) { + if (headerCharRegex.exec(val) !== null) { + throw new InvalidArgumentError(`invalid ${key} header`) + } + // Consumed by Client + request.host = val + } else if ( + request.contentLength === null && + key.length === 14 && + key.toLowerCase() === 'content-length' + ) { + request.contentLength = parseInt(val, 10) + if (!Number.isFinite(request.contentLength)) { + throw new InvalidArgumentError('invalid content-length header') + } + } else if ( + request.contentType === null && + key.length === 12 && + key.toLowerCase() === 'content-type' + ) { + request.contentType = val + if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend) + else request.headers += processHeaderValue(key, val) + } else if ( + key.length === 17 && + key.toLowerCase() === 'transfer-encoding' + ) { + throw new InvalidArgumentError('invalid transfer-encoding header') + } else if ( + key.length === 10 && + key.toLowerCase() === 'connection' + ) { + const value = typeof val === 'string' ? val.toLowerCase() : null + if (value !== 'close' && value !== 'keep-alive') { + throw new InvalidArgumentError('invalid connection header') + } else if (value === 'close') { + request.reset = true + } + } else if ( + key.length === 10 && + key.toLowerCase() === 'keep-alive' + ) { + throw new InvalidArgumentError('invalid keep-alive header') + } else if ( + key.length === 7 && + key.toLowerCase() === 'upgrade' + ) { + throw new InvalidArgumentError('invalid upgrade header') + } else if ( + key.length === 6 && + key.toLowerCase() === 'expect' + ) { + throw new NotSupportedError('expect header not supported') + } else if (tokenRegExp.exec(key) === null) { + throw new InvalidArgumentError('invalid header key') + } else { + if (Array.isArray(val)) { + for (let i = 0; i < val.length; i++) { + if (skipAppend) { + if (request.headers[key]) request.headers[key] += `,${processHeaderValue(key, val[i], skipAppend)}` + else request.headers[key] = processHeaderValue(key, val[i], skipAppend) + } else { + request.headers += processHeaderValue(key, val[i]) + } + } + } else { + if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend) + else request.headers += processHeaderValue(key, val) + } + } +} + +module.exports = Request + + +/***/ }), + +/***/ 9583: +/***/ ((module) => { + +module.exports = { + kClose: Symbol('close'), + kDestroy: Symbol('destroy'), + kDispatch: Symbol('dispatch'), + kUrl: Symbol('url'), + kWriting: Symbol('writing'), + kResuming: Symbol('resuming'), + kQueue: Symbol('queue'), + kConnect: Symbol('connect'), + kConnecting: Symbol('connecting'), + kHeadersList: Symbol('headers list'), + kKeepAliveDefaultTimeout: Symbol('default keep alive timeout'), + kKeepAliveMaxTimeout: Symbol('max keep alive timeout'), + kKeepAliveTimeoutThreshold: Symbol('keep alive timeout threshold'), + kKeepAliveTimeoutValue: Symbol('keep alive timeout'), + kKeepAlive: Symbol('keep alive'), + kHeadersTimeout: Symbol('headers timeout'), + kBodyTimeout: Symbol('body timeout'), + kServerName: Symbol('server name'), + kLocalAddress: Symbol('local address'), + kHost: Symbol('host'), + kNoRef: Symbol('no ref'), + kBodyUsed: Symbol('used'), + kRunning: Symbol('running'), + kBlocking: Symbol('blocking'), + kPending: Symbol('pending'), + kSize: Symbol('size'), + kBusy: Symbol('busy'), + kQueued: Symbol('queued'), + kFree: Symbol('free'), + kConnected: Symbol('connected'), + kClosed: Symbol('closed'), + kNeedDrain: Symbol('need drain'), + kReset: Symbol('reset'), + kDestroyed: Symbol.for('nodejs.stream.destroyed'), + kMaxHeadersSize: Symbol('max headers size'), + kRunningIdx: Symbol('running index'), + kPendingIdx: Symbol('pending index'), + kError: Symbol('error'), + kClients: Symbol('clients'), + kClient: Symbol('client'), + kParser: Symbol('parser'), + kOnDestroyed: Symbol('destroy callbacks'), + kPipelining: Symbol('pipelining'), + kSocket: Symbol('socket'), + kHostHeader: Symbol('host header'), + kConnector: Symbol('connector'), + kStrictContentLength: Symbol('strict content length'), + kMaxRedirections: Symbol('maxRedirections'), + kMaxRequests: Symbol('maxRequestsPerClient'), + kProxy: Symbol('proxy agent options'), + kCounter: Symbol('socket request counter'), + kInterceptors: Symbol('dispatch interceptors'), + kMaxResponseSize: Symbol('max response size'), + kHTTP2Session: Symbol('http2Session'), + kHTTP2SessionState: Symbol('http2Session state'), + kHTTP2BuildRequest: Symbol('http2 build request'), + kHTTP1BuildRequest: Symbol('http1 build request'), + kHTTP2CopyHeaders: Symbol('http2 copy headers'), + kHTTPConnVersion: Symbol('http connection version'), + kRetryHandlerDefaultRetry: Symbol('retry agent default retry'), + kConstruct: Symbol('constructable') +} + + +/***/ }), + +/***/ 1436: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const assert = __nccwpck_require__(2613) +const { kDestroyed, kBodyUsed } = __nccwpck_require__(9583) +const { IncomingMessage } = __nccwpck_require__(8611) +const stream = __nccwpck_require__(2203) +const net = __nccwpck_require__(9278) +const { InvalidArgumentError } = __nccwpck_require__(1975) +const { Blob } = __nccwpck_require__(181) +const nodeUtil = __nccwpck_require__(9023) +const { stringify } = __nccwpck_require__(3480) +const { headerNameLowerCasedRecord } = __nccwpck_require__(5411) + +const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v)) + +function nop () {} + +function isStream (obj) { + return obj && typeof obj === 'object' && typeof obj.pipe === 'function' && typeof obj.on === 'function' +} + +// based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License) +function isBlobLike (object) { + return (Blob && object instanceof Blob) || ( + object && + typeof object === 'object' && + (typeof object.stream === 'function' || + typeof object.arrayBuffer === 'function') && + /^(Blob|File)$/.test(object[Symbol.toStringTag]) + ) +} + +function buildURL (url, queryParams) { + if (url.includes('?') || url.includes('#')) { + throw new Error('Query params cannot be passed when url already contains "?" or "#".') + } + + const stringified = stringify(queryParams) + + if (stringified) { + url += '?' + stringified + } + + return url +} + +function parseURL (url) { + if (typeof url === 'string') { + url = new URL(url) + + if (!/^https?:/.test(url.origin || url.protocol)) { + throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') + } + + return url + } + + if (!url || typeof url !== 'object') { + throw new InvalidArgumentError('Invalid URL: The URL argument must be a non-null object.') + } + + if (!/^https?:/.test(url.origin || url.protocol)) { + throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') + } + + if (!(url instanceof URL)) { + if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) { + throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.') + } + + if (url.path != null && typeof url.path !== 'string') { + throw new InvalidArgumentError('Invalid URL path: the path must be a string or null/undefined.') + } + + if (url.pathname != null && typeof url.pathname !== 'string') { + throw new InvalidArgumentError('Invalid URL pathname: the pathname must be a string or null/undefined.') + } + + if (url.hostname != null && typeof url.hostname !== 'string') { + throw new InvalidArgumentError('Invalid URL hostname: the hostname must be a string or null/undefined.') + } + + if (url.origin != null && typeof url.origin !== 'string') { + throw new InvalidArgumentError('Invalid URL origin: the origin must be a string or null/undefined.') + } + + const port = url.port != null + ? url.port + : (url.protocol === 'https:' ? 443 : 80) + let origin = url.origin != null + ? url.origin + : `${url.protocol}//${url.hostname}:${port}` + let path = url.path != null + ? url.path + : `${url.pathname || ''}${url.search || ''}` + + if (origin.endsWith('/')) { + origin = origin.substring(0, origin.length - 1) + } + + if (path && !path.startsWith('/')) { + path = `/${path}` + } + // new URL(path, origin) is unsafe when `path` contains an absolute URL + // From https://developer.mozilla.org/en-US/docs/Web/API/URL/URL: + // If first parameter is a relative URL, second param is required, and will be used as the base URL. + // If first parameter is an absolute URL, a given second param will be ignored. + url = new URL(origin + path) + } + + return url +} + +function parseOrigin (url) { + url = parseURL(url) + + if (url.pathname !== '/' || url.search || url.hash) { + throw new InvalidArgumentError('invalid url') + } + + return url +} + +function getHostname (host) { + if (host[0] === '[') { + const idx = host.indexOf(']') + + assert(idx !== -1) + return host.substring(1, idx) + } + + const idx = host.indexOf(':') + if (idx === -1) return host + + return host.substring(0, idx) +} + +// IP addresses are not valid server names per RFC6066 +// > Currently, the only server names supported are DNS hostnames +function getServerName (host) { + if (!host) { + return null + } + + assert.strictEqual(typeof host, 'string') + + const servername = getHostname(host) + if (net.isIP(servername)) { + return '' + } + + return servername +} + +function deepClone (obj) { + return JSON.parse(JSON.stringify(obj)) +} + +function isAsyncIterable (obj) { + return !!(obj != null && typeof obj[Symbol.asyncIterator] === 'function') +} + +function isIterable (obj) { + return !!(obj != null && (typeof obj[Symbol.iterator] === 'function' || typeof obj[Symbol.asyncIterator] === 'function')) +} + +function bodyLength (body) { + if (body == null) { + return 0 + } else if (isStream(body)) { + const state = body._readableState + return state && state.objectMode === false && state.ended === true && Number.isFinite(state.length) + ? state.length + : null + } else if (isBlobLike(body)) { + return body.size != null ? body.size : null + } else if (isBuffer(body)) { + return body.byteLength + } + + return null +} + +function isDestroyed (stream) { + return !stream || !!(stream.destroyed || stream[kDestroyed]) +} + +function isReadableAborted (stream) { + const state = stream && stream._readableState + return isDestroyed(stream) && state && !state.endEmitted +} + +function destroy (stream, err) { + if (stream == null || !isStream(stream) || isDestroyed(stream)) { + return + } + + if (typeof stream.destroy === 'function') { + if (Object.getPrototypeOf(stream).constructor === IncomingMessage) { + // See: https://github.com/nodejs/node/pull/38505/files + stream.socket = null + } + + stream.destroy(err) + } else if (err) { + process.nextTick((stream, err) => { + stream.emit('error', err) + }, stream, err) + } + + if (stream.destroyed !== true) { + stream[kDestroyed] = true + } +} + +const KEEPALIVE_TIMEOUT_EXPR = /timeout=(\d+)/ +function parseKeepAliveTimeout (val) { + const m = val.toString().match(KEEPALIVE_TIMEOUT_EXPR) + return m ? parseInt(m[1], 10) * 1000 : null +} + +/** + * Retrieves a header name and returns its lowercase value. + * @param {string | Buffer} value Header name + * @returns {string} + */ +function headerNameToString (value) { + return headerNameLowerCasedRecord[value] || value.toLowerCase() +} + +function parseHeaders (headers, obj = {}) { + // For H2 support + if (!Array.isArray(headers)) return headers + + for (let i = 0; i < headers.length; i += 2) { + const key = headers[i].toString().toLowerCase() + let val = obj[key] + + if (!val) { + if (Array.isArray(headers[i + 1])) { + obj[key] = headers[i + 1].map(x => x.toString('utf8')) + } else { + obj[key] = headers[i + 1].toString('utf8') + } + } else { + if (!Array.isArray(val)) { + val = [val] + obj[key] = val + } + val.push(headers[i + 1].toString('utf8')) + } + } + + // See https://github.com/nodejs/node/pull/46528 + if ('content-length' in obj && 'content-disposition' in obj) { + obj['content-disposition'] = Buffer.from(obj['content-disposition']).toString('latin1') + } + + return obj +} + +function parseRawHeaders (headers) { + const ret = [] + let hasContentLength = false + let contentDispositionIdx = -1 + + for (let n = 0; n < headers.length; n += 2) { + const key = headers[n + 0].toString() + const val = headers[n + 1].toString('utf8') + + if (key.length === 14 && (key === 'content-length' || key.toLowerCase() === 'content-length')) { + ret.push(key, val) + hasContentLength = true + } else if (key.length === 19 && (key === 'content-disposition' || key.toLowerCase() === 'content-disposition')) { + contentDispositionIdx = ret.push(key, val) - 1 + } else { + ret.push(key, val) + } + } + + // See https://github.com/nodejs/node/pull/46528 + if (hasContentLength && contentDispositionIdx !== -1) { + ret[contentDispositionIdx] = Buffer.from(ret[contentDispositionIdx]).toString('latin1') + } + + return ret +} + +function isBuffer (buffer) { + // See, https://github.com/mcollina/undici/pull/319 + return buffer instanceof Uint8Array || Buffer.isBuffer(buffer) +} + +function validateHandler (handler, method, upgrade) { + if (!handler || typeof handler !== 'object') { + throw new InvalidArgumentError('handler must be an object') + } + + if (typeof handler.onConnect !== 'function') { + throw new InvalidArgumentError('invalid onConnect method') + } + + if (typeof handler.onError !== 'function') { + throw new InvalidArgumentError('invalid onError method') + } + + if (typeof handler.onBodySent !== 'function' && handler.onBodySent !== undefined) { + throw new InvalidArgumentError('invalid onBodySent method') + } + + if (upgrade || method === 'CONNECT') { + if (typeof handler.onUpgrade !== 'function') { + throw new InvalidArgumentError('invalid onUpgrade method') + } + } else { + if (typeof handler.onHeaders !== 'function') { + throw new InvalidArgumentError('invalid onHeaders method') + } + + if (typeof handler.onData !== 'function') { + throw new InvalidArgumentError('invalid onData method') + } + + if (typeof handler.onComplete !== 'function') { + throw new InvalidArgumentError('invalid onComplete method') + } + } +} + +// A body is disturbed if it has been read from and it cannot +// be re-used without losing state or data. +function isDisturbed (body) { + return !!(body && ( + stream.isDisturbed + ? stream.isDisturbed(body) || body[kBodyUsed] // TODO (fix): Why is body[kBodyUsed] needed? + : body[kBodyUsed] || + body.readableDidRead || + (body._readableState && body._readableState.dataEmitted) || + isReadableAborted(body) + )) +} + +function isErrored (body) { + return !!(body && ( + stream.isErrored + ? stream.isErrored(body) + : /state: 'errored'/.test(nodeUtil.inspect(body) + ))) +} + +function isReadable (body) { + return !!(body && ( + stream.isReadable + ? stream.isReadable(body) + : /state: 'readable'/.test(nodeUtil.inspect(body) + ))) +} + +function getSocketInfo (socket) { + return { + localAddress: socket.localAddress, + localPort: socket.localPort, + remoteAddress: socket.remoteAddress, + remotePort: socket.remotePort, + remoteFamily: socket.remoteFamily, + timeout: socket.timeout, + bytesWritten: socket.bytesWritten, + bytesRead: socket.bytesRead + } +} + +async function * convertIterableToBuffer (iterable) { + for await (const chunk of iterable) { + yield Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk) + } +} + +let ReadableStream +function ReadableStreamFrom (iterable) { + if (!ReadableStream) { + ReadableStream = (__nccwpck_require__(3774).ReadableStream) + } + + if (ReadableStream.from) { + return ReadableStream.from(convertIterableToBuffer(iterable)) + } + + let iterator + return new ReadableStream( + { + async start () { + iterator = iterable[Symbol.asyncIterator]() + }, + async pull (controller) { + const { done, value } = await iterator.next() + if (done) { + queueMicrotask(() => { + controller.close() + }) + } else { + const buf = Buffer.isBuffer(value) ? value : Buffer.from(value) + controller.enqueue(new Uint8Array(buf)) + } + return controller.desiredSize > 0 + }, + async cancel (reason) { + await iterator.return() + } + }, + 0 + ) +} + +// The chunk should be a FormData instance and contains +// all the required methods. +function isFormDataLike (object) { + return ( + object && + typeof object === 'object' && + typeof object.append === 'function' && + typeof object.delete === 'function' && + typeof object.get === 'function' && + typeof object.getAll === 'function' && + typeof object.has === 'function' && + typeof object.set === 'function' && + object[Symbol.toStringTag] === 'FormData' + ) +} + +function throwIfAborted (signal) { + if (!signal) { return } + if (typeof signal.throwIfAborted === 'function') { + signal.throwIfAborted() + } else { + if (signal.aborted) { + // DOMException not available < v17.0.0 + const err = new Error('The operation was aborted') + err.name = 'AbortError' + throw err + } + } +} + +function addAbortListener (signal, listener) { + if ('addEventListener' in signal) { + signal.addEventListener('abort', listener, { once: true }) + return () => signal.removeEventListener('abort', listener) + } + signal.addListener('abort', listener) + return () => signal.removeListener('abort', listener) +} + +const hasToWellFormed = !!String.prototype.toWellFormed + +/** + * @param {string} val + */ +function toUSVString (val) { + if (hasToWellFormed) { + return `${val}`.toWellFormed() + } else if (nodeUtil.toUSVString) { + return nodeUtil.toUSVString(val) + } + + return `${val}` +} + +// Parsed accordingly to RFC 9110 +// https://www.rfc-editor.org/rfc/rfc9110#field.content-range +function parseRangeHeader (range) { + if (range == null || range === '') return { start: 0, end: null, size: null } + + const m = range ? range.match(/^bytes (\d+)-(\d+)\/(\d+)?$/) : null + return m + ? { + start: parseInt(m[1]), + end: m[2] ? parseInt(m[2]) : null, + size: m[3] ? parseInt(m[3]) : null + } + : null +} + +const kEnumerableProperty = Object.create(null) +kEnumerableProperty.enumerable = true + +module.exports = { + kEnumerableProperty, + nop, + isDisturbed, + isErrored, + isReadable, + toUSVString, + isReadableAborted, + isBlobLike, + parseOrigin, + parseURL, + getServerName, + isStream, + isIterable, + isAsyncIterable, + isDestroyed, + headerNameToString, + parseRawHeaders, + parseHeaders, + parseKeepAliveTimeout, + destroy, + bodyLength, + deepClone, + ReadableStreamFrom, + isBuffer, + validateHandler, + getSocketInfo, + isFormDataLike, + buildURL, + throwIfAborted, + addAbortListener, + parseRangeHeader, + nodeMajor, + nodeMinor, + nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13), + safeHTTPMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'] +} + + +/***/ }), + +/***/ 3301: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const Dispatcher = __nccwpck_require__(5903) +const { + ClientDestroyedError, + ClientClosedError, + InvalidArgumentError +} = __nccwpck_require__(1975) +const { kDestroy, kClose, kDispatch, kInterceptors } = __nccwpck_require__(9583) + +const kDestroyed = Symbol('destroyed') +const kClosed = Symbol('closed') +const kOnDestroyed = Symbol('onDestroyed') +const kOnClosed = Symbol('onClosed') +const kInterceptedDispatch = Symbol('Intercepted Dispatch') + +class DispatcherBase extends Dispatcher { + constructor () { + super() + + this[kDestroyed] = false + this[kOnDestroyed] = null + this[kClosed] = false + this[kOnClosed] = [] + } + + get destroyed () { + return this[kDestroyed] + } + + get closed () { + return this[kClosed] + } + + get interceptors () { + return this[kInterceptors] + } + + set interceptors (newInterceptors) { + if (newInterceptors) { + for (let i = newInterceptors.length - 1; i >= 0; i--) { + const interceptor = this[kInterceptors][i] + if (typeof interceptor !== 'function') { + throw new InvalidArgumentError('interceptor must be an function') + } + } + } + + this[kInterceptors] = newInterceptors + } + + close (callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + this.close((err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (this[kDestroyed]) { + queueMicrotask(() => callback(new ClientDestroyedError(), null)) + return + } + + if (this[kClosed]) { + if (this[kOnClosed]) { + this[kOnClosed].push(callback) + } else { + queueMicrotask(() => callback(null, null)) + } + return + } + + this[kClosed] = true + this[kOnClosed].push(callback) + + const onClosed = () => { + const callbacks = this[kOnClosed] + this[kOnClosed] = null + for (let i = 0; i < callbacks.length; i++) { + callbacks[i](null, null) + } + } + + // Should not error. + this[kClose]() + .then(() => this.destroy()) + .then(() => { + queueMicrotask(onClosed) + }) + } + + destroy (err, callback) { + if (typeof err === 'function') { + callback = err + err = null + } + + if (callback === undefined) { + return new Promise((resolve, reject) => { + this.destroy(err, (err, data) => { + return err ? /* istanbul ignore next: should never error */ reject(err) : resolve(data) + }) + }) + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (this[kDestroyed]) { + if (this[kOnDestroyed]) { + this[kOnDestroyed].push(callback) + } else { + queueMicrotask(() => callback(null, null)) + } + return + } + + if (!err) { + err = new ClientDestroyedError() + } + + this[kDestroyed] = true + this[kOnDestroyed] = this[kOnDestroyed] || [] + this[kOnDestroyed].push(callback) + + const onDestroyed = () => { + const callbacks = this[kOnDestroyed] + this[kOnDestroyed] = null + for (let i = 0; i < callbacks.length; i++) { + callbacks[i](null, null) + } + } + + // Should not error. + this[kDestroy](err).then(() => { + queueMicrotask(onDestroyed) + }) + } + + [kInterceptedDispatch] (opts, handler) { + if (!this[kInterceptors] || this[kInterceptors].length === 0) { + this[kInterceptedDispatch] = this[kDispatch] + return this[kDispatch](opts, handler) + } + + let dispatch = this[kDispatch].bind(this) + for (let i = this[kInterceptors].length - 1; i >= 0; i--) { + dispatch = this[kInterceptors][i](dispatch) + } + this[kInterceptedDispatch] = dispatch + return dispatch(opts, handler) + } + + dispatch (opts, handler) { + if (!handler || typeof handler !== 'object') { + throw new InvalidArgumentError('handler must be an object') + } + + try { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('opts must be an object.') + } + + if (this[kDestroyed] || this[kOnDestroyed]) { + throw new ClientDestroyedError() + } + + if (this[kClosed]) { + throw new ClientClosedError() + } + + return this[kInterceptedDispatch](opts, handler) + } catch (err) { + if (typeof handler.onError !== 'function') { + throw new InvalidArgumentError('invalid onError method') + } + + handler.onError(err) + + return false + } + } +} + +module.exports = DispatcherBase + + +/***/ }), + +/***/ 5903: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const EventEmitter = __nccwpck_require__(4434) + +class Dispatcher extends EventEmitter { + dispatch () { + throw new Error('not implemented') + } + + close () { + throw new Error('not implemented') + } + + destroy () { + throw new Error('not implemented') + } +} + +module.exports = Dispatcher + + +/***/ }), + +/***/ 4655: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const Busboy = __nccwpck_require__(9780) +const util = __nccwpck_require__(1436) +const { + ReadableStreamFrom, + isBlobLike, + isReadableStreamLike, + readableStreamClose, + createDeferredPromise, + fullyReadBody +} = __nccwpck_require__(3359) +const { FormData } = __nccwpck_require__(2813) +const { kState } = __nccwpck_require__(834) +const { webidl } = __nccwpck_require__(274) +const { DOMException, structuredClone } = __nccwpck_require__(450) +const { Blob, File: NativeFile } = __nccwpck_require__(181) +const { kBodyUsed } = __nccwpck_require__(9583) +const assert = __nccwpck_require__(2613) +const { isErrored } = __nccwpck_require__(1436) +const { isUint8Array, isArrayBuffer } = __nccwpck_require__(8253) +const { File: UndiciFile } = __nccwpck_require__(7085) +const { parseMIMEType, serializeAMimeType } = __nccwpck_require__(5294) + +let random +try { + const crypto = __nccwpck_require__(7598) + random = (max) => crypto.randomInt(0, max) +} catch { + random = (max) => Math.floor(Math.random(max)) +} + +let ReadableStream = globalThis.ReadableStream + +/** @type {globalThis['File']} */ +const File = NativeFile ?? UndiciFile +const textEncoder = new TextEncoder() +const textDecoder = new TextDecoder() + +// https://fetch.spec.whatwg.org/#concept-bodyinit-extract +function extractBody (object, keepalive = false) { + if (!ReadableStream) { + ReadableStream = (__nccwpck_require__(3774).ReadableStream) + } + + // 1. Let stream be null. + let stream = null + + // 2. If object is a ReadableStream object, then set stream to object. + if (object instanceof ReadableStream) { + stream = object + } else if (isBlobLike(object)) { + // 3. Otherwise, if object is a Blob object, set stream to the + // result of running object’s get stream. + stream = object.stream() + } else { + // 4. Otherwise, set stream to a new ReadableStream object, and set + // up stream. + stream = new ReadableStream({ + async pull (controller) { + controller.enqueue( + typeof source === 'string' ? textEncoder.encode(source) : source + ) + queueMicrotask(() => readableStreamClose(controller)) + }, + start () {}, + type: undefined + }) + } + + // 5. Assert: stream is a ReadableStream object. + assert(isReadableStreamLike(stream)) + + // 6. Let action be null. + let action = null + + // 7. Let source be null. + let source = null + + // 8. Let length be null. + let length = null + + // 9. Let type be null. + let type = null + + // 10. Switch on object: + if (typeof object === 'string') { + // Set source to the UTF-8 encoding of object. + // Note: setting source to a Uint8Array here breaks some mocking assumptions. + source = object + + // Set type to `text/plain;charset=UTF-8`. + type = 'text/plain;charset=UTF-8' + } else if (object instanceof URLSearchParams) { + // URLSearchParams + + // spec says to run application/x-www-form-urlencoded on body.list + // this is implemented in Node.js as apart of an URLSearchParams instance toString method + // See: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L490 + // and https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L1100 + + // Set source to the result of running the application/x-www-form-urlencoded serializer with object’s list. + source = object.toString() + + // Set type to `application/x-www-form-urlencoded;charset=UTF-8`. + type = 'application/x-www-form-urlencoded;charset=UTF-8' + } else if (isArrayBuffer(object)) { + // BufferSource/ArrayBuffer + + // Set source to a copy of the bytes held by object. + source = new Uint8Array(object.slice()) + } else if (ArrayBuffer.isView(object)) { + // BufferSource/ArrayBufferView + + // Set source to a copy of the bytes held by object. + source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength)) + } else if (util.isFormDataLike(object)) { + const boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, '0')}` + const prefix = `--${boundary}\r\nContent-Disposition: form-data` + + /*! formdata-polyfill. MIT License. Jimmy Wärting */ + const escape = (str) => + str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22') + const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n') + + // Set action to this step: run the multipart/form-data + // encoding algorithm, with object’s entry list and UTF-8. + // - This ensures that the body is immutable and can't be changed afterwords + // - That the content-length is calculated in advance. + // - And that all parts are pre-encoded and ready to be sent. + + const blobParts = [] + const rn = new Uint8Array([13, 10]) // '\r\n' + length = 0 + let hasUnknownSizeValue = false + + for (const [name, value] of object) { + if (typeof value === 'string') { + const chunk = textEncoder.encode(prefix + + `; name="${escape(normalizeLinefeeds(name))}"` + + `\r\n\r\n${normalizeLinefeeds(value)}\r\n`) + blobParts.push(chunk) + length += chunk.byteLength + } else { + const chunk = textEncoder.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` + + (value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' + + `Content-Type: ${ + value.type || 'application/octet-stream' + }\r\n\r\n`) + blobParts.push(chunk, value, rn) + if (typeof value.size === 'number') { + length += chunk.byteLength + value.size + rn.byteLength + } else { + hasUnknownSizeValue = true + } + } + } + + const chunk = textEncoder.encode(`--${boundary}--`) + blobParts.push(chunk) + length += chunk.byteLength + if (hasUnknownSizeValue) { + length = null + } + + // Set source to object. + source = object + + action = async function * () { + for (const part of blobParts) { + if (part.stream) { + yield * part.stream() + } else { + yield part + } + } + } + + // Set type to `multipart/form-data; boundary=`, + // followed by the multipart/form-data boundary string generated + // by the multipart/form-data encoding algorithm. + type = 'multipart/form-data; boundary=' + boundary + } else if (isBlobLike(object)) { + // Blob + + // Set source to object. + source = object + + // Set length to object’s size. + length = object.size + + // If object’s type attribute is not the empty byte sequence, set + // type to its value. + if (object.type) { + type = object.type + } + } else if (typeof object[Symbol.asyncIterator] === 'function') { + // If keepalive is true, then throw a TypeError. + if (keepalive) { + throw new TypeError('keepalive') + } + + // If object is disturbed or locked, then throw a TypeError. + if (util.isDisturbed(object) || object.locked) { + throw new TypeError( + 'Response body object should not be disturbed or locked' + ) + } + + stream = + object instanceof ReadableStream ? object : ReadableStreamFrom(object) + } + + // 11. If source is a byte sequence, then set action to a + // step that returns source and length to source’s length. + if (typeof source === 'string' || util.isBuffer(source)) { + length = Buffer.byteLength(source) + } + + // 12. If action is non-null, then run these steps in in parallel: + if (action != null) { + // Run action. + let iterator + stream = new ReadableStream({ + async start () { + iterator = action(object)[Symbol.asyncIterator]() + }, + async pull (controller) { + const { value, done } = await iterator.next() + if (done) { + // When running action is done, close stream. + queueMicrotask(() => { + controller.close() + }) + } else { + // Whenever one or more bytes are available and stream is not errored, + // enqueue a Uint8Array wrapping an ArrayBuffer containing the available + // bytes into stream. + if (!isErrored(stream)) { + controller.enqueue(new Uint8Array(value)) + } + } + return controller.desiredSize > 0 + }, + async cancel (reason) { + await iterator.return() + }, + type: undefined + }) + } + + // 13. Let body be a body whose stream is stream, source is source, + // and length is length. + const body = { stream, source, length } + + // 14. Return (body, type). + return [body, type] +} + +// https://fetch.spec.whatwg.org/#bodyinit-safely-extract +function safelyExtractBody (object, keepalive = false) { + if (!ReadableStream) { + // istanbul ignore next + ReadableStream = (__nccwpck_require__(3774).ReadableStream) + } + + // To safely extract a body and a `Content-Type` value from + // a byte sequence or BodyInit object object, run these steps: + + // 1. If object is a ReadableStream object, then: + if (object instanceof ReadableStream) { + // Assert: object is neither disturbed nor locked. + // istanbul ignore next + assert(!util.isDisturbed(object), 'The body has already been consumed.') + // istanbul ignore next + assert(!object.locked, 'The stream is locked.') + } + + // 2. Return the results of extracting object. + return extractBody(object, keepalive) +} + +function cloneBody (body) { + // To clone a body body, run these steps: + + // https://fetch.spec.whatwg.org/#concept-body-clone + + // 1. Let « out1, out2 » be the result of teeing body’s stream. + const [out1, out2] = body.stream.tee() + const out2Clone = structuredClone(out2, { transfer: [out2] }) + // This, for whatever reasons, unrefs out2Clone which allows + // the process to exit by itself. + const [, finalClone] = out2Clone.tee() + + // 2. Set body’s stream to out1. + body.stream = out1 + + // 3. Return a body whose stream is out2 and other members are copied from body. + return { + stream: finalClone, + length: body.length, + source: body.source + } +} + +async function * consumeBody (body) { + if (body) { + if (isUint8Array(body)) { + yield body + } else { + const stream = body.stream + + if (util.isDisturbed(stream)) { + throw new TypeError('The body has already been consumed.') + } + + if (stream.locked) { + throw new TypeError('The stream is locked.') + } + + // Compat. + stream[kBodyUsed] = true + + yield * stream + } + } +} + +function throwIfAborted (state) { + if (state.aborted) { + throw new DOMException('The operation was aborted.', 'AbortError') + } +} + +function bodyMixinMethods (instance) { + const methods = { + blob () { + // The blob() method steps are to return the result of + // running consume body with this and the following step + // given a byte sequence bytes: return a Blob whose + // contents are bytes and whose type attribute is this’s + // MIME type. + return specConsumeBody(this, (bytes) => { + let mimeType = bodyMimeType(this) + + if (mimeType === 'failure') { + mimeType = '' + } else if (mimeType) { + mimeType = serializeAMimeType(mimeType) + } + + // Return a Blob whose contents are bytes and type attribute + // is mimeType. + return new Blob([bytes], { type: mimeType }) + }, instance) + }, + + arrayBuffer () { + // The arrayBuffer() method steps are to return the result + // of running consume body with this and the following step + // given a byte sequence bytes: return a new ArrayBuffer + // whose contents are bytes. + return specConsumeBody(this, (bytes) => { + return new Uint8Array(bytes).buffer + }, instance) + }, + + text () { + // The text() method steps are to return the result of running + // consume body with this and UTF-8 decode. + return specConsumeBody(this, utf8DecodeBytes, instance) + }, + + json () { + // The json() method steps are to return the result of running + // consume body with this and parse JSON from bytes. + return specConsumeBody(this, parseJSONFromBytes, instance) + }, + + async formData () { + webidl.brandCheck(this, instance) + + throwIfAborted(this[kState]) + + const contentType = this.headers.get('Content-Type') + + // If mimeType’s essence is "multipart/form-data", then: + if (/multipart\/form-data/.test(contentType)) { + const headers = {} + for (const [key, value] of this.headers) headers[key.toLowerCase()] = value + + const responseFormData = new FormData() + + let busboy + + try { + busboy = new Busboy({ + headers, + preservePath: true + }) + } catch (err) { + throw new DOMException(`${err}`, 'AbortError') + } + + busboy.on('field', (name, value) => { + responseFormData.append(name, value) + }) + busboy.on('file', (name, value, filename, encoding, mimeType) => { + const chunks = [] + + if (encoding === 'base64' || encoding.toLowerCase() === 'base64') { + let base64chunk = '' + + value.on('data', (chunk) => { + base64chunk += chunk.toString().replace(/[\r\n]/gm, '') + + const end = base64chunk.length - base64chunk.length % 4 + chunks.push(Buffer.from(base64chunk.slice(0, end), 'base64')) + + base64chunk = base64chunk.slice(end) + }) + value.on('end', () => { + chunks.push(Buffer.from(base64chunk, 'base64')) + responseFormData.append(name, new File(chunks, filename, { type: mimeType })) + }) + } else { + value.on('data', (chunk) => { + chunks.push(chunk) + }) + value.on('end', () => { + responseFormData.append(name, new File(chunks, filename, { type: mimeType })) + }) + } + }) + + const busboyResolve = new Promise((resolve, reject) => { + busboy.on('finish', resolve) + busboy.on('error', (err) => reject(new TypeError(err))) + }) + + if (this.body !== null) for await (const chunk of consumeBody(this[kState].body)) busboy.write(chunk) + busboy.end() + await busboyResolve + + return responseFormData + } else if (/application\/x-www-form-urlencoded/.test(contentType)) { + // Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then: + + // 1. Let entries be the result of parsing bytes. + let entries + try { + let text = '' + // application/x-www-form-urlencoded parser will keep the BOM. + // https://url.spec.whatwg.org/#concept-urlencoded-parser + // Note that streaming decoder is stateful and cannot be reused + const streamingDecoder = new TextDecoder('utf-8', { ignoreBOM: true }) + + for await (const chunk of consumeBody(this[kState].body)) { + if (!isUint8Array(chunk)) { + throw new TypeError('Expected Uint8Array chunk') + } + text += streamingDecoder.decode(chunk, { stream: true }) + } + text += streamingDecoder.decode() + entries = new URLSearchParams(text) + } catch (err) { + // istanbul ignore next: Unclear when new URLSearchParams can fail on a string. + // 2. If entries is failure, then throw a TypeError. + throw Object.assign(new TypeError(), { cause: err }) + } + + // 3. Return a new FormData object whose entries are entries. + const formData = new FormData() + for (const [name, value] of entries) { + formData.append(name, value) + } + return formData + } else { + // Wait a tick before checking if the request has been aborted. + // Otherwise, a TypeError can be thrown when an AbortError should. + await Promise.resolve() + + throwIfAborted(this[kState]) + + // Otherwise, throw a TypeError. + throw webidl.errors.exception({ + header: `${instance.name}.formData`, + message: 'Could not parse content as FormData.' + }) + } + } + } + + return methods +} + +function mixinBody (prototype) { + Object.assign(prototype.prototype, bodyMixinMethods(prototype)) +} + +/** + * @see https://fetch.spec.whatwg.org/#concept-body-consume-body + * @param {Response|Request} object + * @param {(value: unknown) => unknown} convertBytesToJSValue + * @param {Response|Request} instance + */ +async function specConsumeBody (object, convertBytesToJSValue, instance) { + webidl.brandCheck(object, instance) + + throwIfAborted(object[kState]) + + // 1. If object is unusable, then return a promise rejected + // with a TypeError. + if (bodyUnusable(object[kState].body)) { + throw new TypeError('Body is unusable') + } + + // 2. Let promise be a new promise. + const promise = createDeferredPromise() + + // 3. Let errorSteps given error be to reject promise with error. + const errorSteps = (error) => promise.reject(error) + + // 4. Let successSteps given a byte sequence data be to resolve + // promise with the result of running convertBytesToJSValue + // with data. If that threw an exception, then run errorSteps + // with that exception. + const successSteps = (data) => { + try { + promise.resolve(convertBytesToJSValue(data)) + } catch (e) { + errorSteps(e) + } + } + + // 5. If object’s body is null, then run successSteps with an + // empty byte sequence. + if (object[kState].body == null) { + successSteps(new Uint8Array()) + return promise.promise + } + + // 6. Otherwise, fully read object’s body given successSteps, + // errorSteps, and object’s relevant global object. + await fullyReadBody(object[kState].body, successSteps, errorSteps) + + // 7. Return promise. + return promise.promise +} + +// https://fetch.spec.whatwg.org/#body-unusable +function bodyUnusable (body) { + // An object including the Body interface mixin is + // said to be unusable if its body is non-null and + // its body’s stream is disturbed or locked. + return body != null && (body.stream.locked || util.isDisturbed(body.stream)) +} + +/** + * @see https://encoding.spec.whatwg.org/#utf-8-decode + * @param {Buffer} buffer + */ +function utf8DecodeBytes (buffer) { + if (buffer.length === 0) { + return '' + } + + // 1. Let buffer be the result of peeking three bytes from + // ioQueue, converted to a byte sequence. + + // 2. If buffer is 0xEF 0xBB 0xBF, then read three + // bytes from ioQueue. (Do nothing with those bytes.) + if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) { + buffer = buffer.subarray(3) + } + + // 3. Process a queue with an instance of UTF-8’s + // decoder, ioQueue, output, and "replacement". + const output = textDecoder.decode(buffer) + + // 4. Return output. + return output +} + +/** + * @see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value + * @param {Uint8Array} bytes + */ +function parseJSONFromBytes (bytes) { + return JSON.parse(utf8DecodeBytes(bytes)) +} + +/** + * @see https://fetch.spec.whatwg.org/#concept-body-mime-type + * @param {import('./response').Response|import('./request').Request} object + */ +function bodyMimeType (object) { + const { headersList } = object[kState] + const contentType = headersList.get('content-type') + + if (contentType === null) { + return 'failure' + } + + return parseMIMEType(contentType) +} + +module.exports = { + extractBody, + safelyExtractBody, + cloneBody, + mixinBody +} + + +/***/ }), + +/***/ 450: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { MessageChannel, receiveMessageOnPort } = __nccwpck_require__(8167) + +const corsSafeListedMethods = ['GET', 'HEAD', 'POST'] +const corsSafeListedMethodsSet = new Set(corsSafeListedMethods) + +const nullBodyStatus = [101, 204, 205, 304] + +const redirectStatus = [301, 302, 303, 307, 308] +const redirectStatusSet = new Set(redirectStatus) + +// https://fetch.spec.whatwg.org/#block-bad-port +const badPorts = [ + '1', '7', '9', '11', '13', '15', '17', '19', '20', '21', '22', '23', '25', '37', '42', '43', '53', '69', '77', '79', + '87', '95', '101', '102', '103', '104', '109', '110', '111', '113', '115', '117', '119', '123', '135', '137', + '139', '143', '161', '179', '389', '427', '465', '512', '513', '514', '515', '526', '530', '531', '532', + '540', '548', '554', '556', '563', '587', '601', '636', '989', '990', '993', '995', '1719', '1720', '1723', + '2049', '3659', '4045', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6697', + '10080' +] + +const badPortsSet = new Set(badPorts) + +// https://w3c.github.io/webappsec-referrer-policy/#referrer-policies +const referrerPolicy = [ + '', + 'no-referrer', + 'no-referrer-when-downgrade', + 'same-origin', + 'origin', + 'strict-origin', + 'origin-when-cross-origin', + 'strict-origin-when-cross-origin', + 'unsafe-url' +] +const referrerPolicySet = new Set(referrerPolicy) + +const requestRedirect = ['follow', 'manual', 'error'] + +const safeMethods = ['GET', 'HEAD', 'OPTIONS', 'TRACE'] +const safeMethodsSet = new Set(safeMethods) + +const requestMode = ['navigate', 'same-origin', 'no-cors', 'cors'] + +const requestCredentials = ['omit', 'same-origin', 'include'] + +const requestCache = [ + 'default', + 'no-store', + 'reload', + 'no-cache', + 'force-cache', + 'only-if-cached' +] + +// https://fetch.spec.whatwg.org/#request-body-header-name +const requestBodyHeader = [ + 'content-encoding', + 'content-language', + 'content-location', + 'content-type', + // See https://github.com/nodejs/undici/issues/2021 + // 'Content-Length' is a forbidden header name, which is typically + // removed in the Headers implementation. However, undici doesn't + // filter out headers, so we add it here. + 'content-length' +] + +// https://fetch.spec.whatwg.org/#enumdef-requestduplex +const requestDuplex = [ + 'half' +] + +// http://fetch.spec.whatwg.org/#forbidden-method +const forbiddenMethods = ['CONNECT', 'TRACE', 'TRACK'] +const forbiddenMethodsSet = new Set(forbiddenMethods) + +const subresource = [ + 'audio', + 'audioworklet', + 'font', + 'image', + 'manifest', + 'paintworklet', + 'script', + 'style', + 'track', + 'video', + 'xslt', + '' +] +const subresourceSet = new Set(subresource) + +/** @type {globalThis['DOMException']} */ +const DOMException = globalThis.DOMException ?? (() => { + // DOMException was only made a global in Node v17.0.0, + // but fetch supports >= v16.8. + try { + atob('~') + } catch (err) { + return Object.getPrototypeOf(err).constructor + } +})() + +let channel + +/** @type {globalThis['structuredClone']} */ +const structuredClone = + globalThis.structuredClone ?? + // https://github.com/nodejs/node/blob/b27ae24dcc4251bad726d9d84baf678d1f707fed/lib/internal/structured_clone.js + // structuredClone was added in v17.0.0, but fetch supports v16.8 + function structuredClone (value, options = undefined) { + if (arguments.length === 0) { + throw new TypeError('missing argument') + } + + if (!channel) { + channel = new MessageChannel() + } + channel.port1.unref() + channel.port2.unref() + channel.port1.postMessage(value, options?.transfer) + return receiveMessageOnPort(channel.port2).message + } + +module.exports = { + DOMException, + structuredClone, + subresource, + forbiddenMethods, + requestBodyHeader, + referrerPolicy, + requestRedirect, + requestMode, + requestCredentials, + requestCache, + redirectStatus, + corsSafeListedMethods, + nullBodyStatus, + safeMethods, + badPorts, + requestDuplex, + subresourceSet, + badPortsSet, + redirectStatusSet, + corsSafeListedMethodsSet, + safeMethodsSet, + forbiddenMethodsSet, + referrerPolicySet +} + + +/***/ }), + +/***/ 5294: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const assert = __nccwpck_require__(2613) +const { atob } = __nccwpck_require__(181) +const { isomorphicDecode } = __nccwpck_require__(3359) + +const encoder = new TextEncoder() + +/** + * @see https://mimesniff.spec.whatwg.org/#http-token-code-point + */ +const HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+-.^_|~A-Za-z0-9]+$/ +const HTTP_WHITESPACE_REGEX = /(\u000A|\u000D|\u0009|\u0020)/ // eslint-disable-line +/** + * @see https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point + */ +const HTTP_QUOTED_STRING_TOKENS = /[\u0009|\u0020-\u007E|\u0080-\u00FF]/ // eslint-disable-line + +// https://fetch.spec.whatwg.org/#data-url-processor +/** @param {URL} dataURL */ +function dataURLProcessor (dataURL) { + // 1. Assert: dataURL’s scheme is "data". + assert(dataURL.protocol === 'data:') + + // 2. Let input be the result of running the URL + // serializer on dataURL with exclude fragment + // set to true. + let input = URLSerializer(dataURL, true) + + // 3. Remove the leading "data:" string from input. + input = input.slice(5) + + // 4. Let position point at the start of input. + const position = { position: 0 } + + // 5. Let mimeType be the result of collecting a + // sequence of code points that are not equal + // to U+002C (,), given position. + let mimeType = collectASequenceOfCodePointsFast( + ',', + input, + position + ) + + // 6. Strip leading and trailing ASCII whitespace + // from mimeType. + // Undici implementation note: we need to store the + // length because if the mimetype has spaces removed, + // the wrong amount will be sliced from the input in + // step #9 + const mimeTypeLength = mimeType.length + mimeType = removeASCIIWhitespace(mimeType, true, true) + + // 7. If position is past the end of input, then + // return failure + if (position.position >= input.length) { + return 'failure' + } + + // 8. Advance position by 1. + position.position++ + + // 9. Let encodedBody be the remainder of input. + const encodedBody = input.slice(mimeTypeLength + 1) + + // 10. Let body be the percent-decoding of encodedBody. + let body = stringPercentDecode(encodedBody) + + // 11. If mimeType ends with U+003B (;), followed by + // zero or more U+0020 SPACE, followed by an ASCII + // case-insensitive match for "base64", then: + if (/;(\u0020){0,}base64$/i.test(mimeType)) { + // 1. Let stringBody be the isomorphic decode of body. + const stringBody = isomorphicDecode(body) + + // 2. Set body to the forgiving-base64 decode of + // stringBody. + body = forgivingBase64(stringBody) + + // 3. If body is failure, then return failure. + if (body === 'failure') { + return 'failure' + } + + // 4. Remove the last 6 code points from mimeType. + mimeType = mimeType.slice(0, -6) + + // 5. Remove trailing U+0020 SPACE code points from mimeType, + // if any. + mimeType = mimeType.replace(/(\u0020)+$/, '') + + // 6. Remove the last U+003B (;) code point from mimeType. + mimeType = mimeType.slice(0, -1) + } + + // 12. If mimeType starts with U+003B (;), then prepend + // "text/plain" to mimeType. + if (mimeType.startsWith(';')) { + mimeType = 'text/plain' + mimeType + } + + // 13. Let mimeTypeRecord be the result of parsing + // mimeType. + let mimeTypeRecord = parseMIMEType(mimeType) + + // 14. If mimeTypeRecord is failure, then set + // mimeTypeRecord to text/plain;charset=US-ASCII. + if (mimeTypeRecord === 'failure') { + mimeTypeRecord = parseMIMEType('text/plain;charset=US-ASCII') + } + + // 15. Return a new data: URL struct whose MIME + // type is mimeTypeRecord and body is body. + // https://fetch.spec.whatwg.org/#data-url-struct + return { mimeType: mimeTypeRecord, body } +} + +// https://url.spec.whatwg.org/#concept-url-serializer +/** + * @param {URL} url + * @param {boolean} excludeFragment + */ +function URLSerializer (url, excludeFragment = false) { + if (!excludeFragment) { + return url.href + } + + const href = url.href + const hashLength = url.hash.length + + return hashLength === 0 ? href : href.substring(0, href.length - hashLength) +} + +// https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points +/** + * @param {(char: string) => boolean} condition + * @param {string} input + * @param {{ position: number }} position + */ +function collectASequenceOfCodePoints (condition, input, position) { + // 1. Let result be the empty string. + let result = '' + + // 2. While position doesn’t point past the end of input and the + // code point at position within input meets the condition condition: + while (position.position < input.length && condition(input[position.position])) { + // 1. Append that code point to the end of result. + result += input[position.position] + + // 2. Advance position by 1. + position.position++ + } + + // 3. Return result. + return result +} + +/** + * A faster collectASequenceOfCodePoints that only works when comparing a single character. + * @param {string} char + * @param {string} input + * @param {{ position: number }} position + */ +function collectASequenceOfCodePointsFast (char, input, position) { + const idx = input.indexOf(char, position.position) + const start = position.position + + if (idx === -1) { + position.position = input.length + return input.slice(start) + } + + position.position = idx + return input.slice(start, position.position) +} + +// https://url.spec.whatwg.org/#string-percent-decode +/** @param {string} input */ +function stringPercentDecode (input) { + // 1. Let bytes be the UTF-8 encoding of input. + const bytes = encoder.encode(input) + + // 2. Return the percent-decoding of bytes. + return percentDecode(bytes) +} + +// https://url.spec.whatwg.org/#percent-decode +/** @param {Uint8Array} input */ +function percentDecode (input) { + // 1. Let output be an empty byte sequence. + /** @type {number[]} */ + const output = [] + + // 2. For each byte byte in input: + for (let i = 0; i < input.length; i++) { + const byte = input[i] + + // 1. If byte is not 0x25 (%), then append byte to output. + if (byte !== 0x25) { + output.push(byte) + + // 2. Otherwise, if byte is 0x25 (%) and the next two bytes + // after byte in input are not in the ranges + // 0x30 (0) to 0x39 (9), 0x41 (A) to 0x46 (F), + // and 0x61 (a) to 0x66 (f), all inclusive, append byte + // to output. + } else if ( + byte === 0x25 && + !/^[0-9A-Fa-f]{2}$/i.test(String.fromCharCode(input[i + 1], input[i + 2])) + ) { + output.push(0x25) + + // 3. Otherwise: + } else { + // 1. Let bytePoint be the two bytes after byte in input, + // decoded, and then interpreted as hexadecimal number. + const nextTwoBytes = String.fromCharCode(input[i + 1], input[i + 2]) + const bytePoint = Number.parseInt(nextTwoBytes, 16) + + // 2. Append a byte whose value is bytePoint to output. + output.push(bytePoint) + + // 3. Skip the next two bytes in input. + i += 2 + } + } + + // 3. Return output. + return Uint8Array.from(output) +} + +// https://mimesniff.spec.whatwg.org/#parse-a-mime-type +/** @param {string} input */ +function parseMIMEType (input) { + // 1. Remove any leading and trailing HTTP whitespace + // from input. + input = removeHTTPWhitespace(input, true, true) + + // 2. Let position be a position variable for input, + // initially pointing at the start of input. + const position = { position: 0 } + + // 3. Let type be the result of collecting a sequence + // of code points that are not U+002F (/) from + // input, given position. + const type = collectASequenceOfCodePointsFast( + '/', + input, + position + ) + + // 4. If type is the empty string or does not solely + // contain HTTP token code points, then return failure. + // https://mimesniff.spec.whatwg.org/#http-token-code-point + if (type.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(type)) { + return 'failure' + } + + // 5. If position is past the end of input, then return + // failure + if (position.position > input.length) { + return 'failure' + } + + // 6. Advance position by 1. (This skips past U+002F (/).) + position.position++ + + // 7. Let subtype be the result of collecting a sequence of + // code points that are not U+003B (;) from input, given + // position. + let subtype = collectASequenceOfCodePointsFast( + ';', + input, + position + ) + + // 8. Remove any trailing HTTP whitespace from subtype. + subtype = removeHTTPWhitespace(subtype, false, true) + + // 9. If subtype is the empty string or does not solely + // contain HTTP token code points, then return failure. + if (subtype.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(subtype)) { + return 'failure' + } + + const typeLowercase = type.toLowerCase() + const subtypeLowercase = subtype.toLowerCase() + + // 10. Let mimeType be a new MIME type record whose type + // is type, in ASCII lowercase, and subtype is subtype, + // in ASCII lowercase. + // https://mimesniff.spec.whatwg.org/#mime-type + const mimeType = { + type: typeLowercase, + subtype: subtypeLowercase, + /** @type {Map} */ + parameters: new Map(), + // https://mimesniff.spec.whatwg.org/#mime-type-essence + essence: `${typeLowercase}/${subtypeLowercase}` + } + + // 11. While position is not past the end of input: + while (position.position < input.length) { + // 1. Advance position by 1. (This skips past U+003B (;).) + position.position++ + + // 2. Collect a sequence of code points that are HTTP + // whitespace from input given position. + collectASequenceOfCodePoints( + // https://fetch.spec.whatwg.org/#http-whitespace + char => HTTP_WHITESPACE_REGEX.test(char), + input, + position + ) + + // 3. Let parameterName be the result of collecting a + // sequence of code points that are not U+003B (;) + // or U+003D (=) from input, given position. + let parameterName = collectASequenceOfCodePoints( + (char) => char !== ';' && char !== '=', + input, + position + ) + + // 4. Set parameterName to parameterName, in ASCII + // lowercase. + parameterName = parameterName.toLowerCase() + + // 5. If position is not past the end of input, then: + if (position.position < input.length) { + // 1. If the code point at position within input is + // U+003B (;), then continue. + if (input[position.position] === ';') { + continue + } + + // 2. Advance position by 1. (This skips past U+003D (=).) + position.position++ + } + + // 6. If position is past the end of input, then break. + if (position.position > input.length) { + break + } + + // 7. Let parameterValue be null. + let parameterValue = null + + // 8. If the code point at position within input is + // U+0022 ("), then: + if (input[position.position] === '"') { + // 1. Set parameterValue to the result of collecting + // an HTTP quoted string from input, given position + // and the extract-value flag. + parameterValue = collectAnHTTPQuotedString(input, position, true) + + // 2. Collect a sequence of code points that are not + // U+003B (;) from input, given position. + collectASequenceOfCodePointsFast( + ';', + input, + position + ) + + // 9. Otherwise: + } else { + // 1. Set parameterValue to the result of collecting + // a sequence of code points that are not U+003B (;) + // from input, given position. + parameterValue = collectASequenceOfCodePointsFast( + ';', + input, + position + ) + + // 2. Remove any trailing HTTP whitespace from parameterValue. + parameterValue = removeHTTPWhitespace(parameterValue, false, true) + + // 3. If parameterValue is the empty string, then continue. + if (parameterValue.length === 0) { + continue + } + } + + // 10. If all of the following are true + // - parameterName is not the empty string + // - parameterName solely contains HTTP token code points + // - parameterValue solely contains HTTP quoted-string token code points + // - mimeType’s parameters[parameterName] does not exist + // then set mimeType’s parameters[parameterName] to parameterValue. + if ( + parameterName.length !== 0 && + HTTP_TOKEN_CODEPOINTS.test(parameterName) && + (parameterValue.length === 0 || HTTP_QUOTED_STRING_TOKENS.test(parameterValue)) && + !mimeType.parameters.has(parameterName) + ) { + mimeType.parameters.set(parameterName, parameterValue) + } + } + + // 12. Return mimeType. + return mimeType +} + +// https://infra.spec.whatwg.org/#forgiving-base64-decode +/** @param {string} data */ +function forgivingBase64 (data) { + // 1. Remove all ASCII whitespace from data. + data = data.replace(/[\u0009\u000A\u000C\u000D\u0020]/g, '') // eslint-disable-line + + // 2. If data’s code point length divides by 4 leaving + // no remainder, then: + if (data.length % 4 === 0) { + // 1. If data ends with one or two U+003D (=) code points, + // then remove them from data. + data = data.replace(/=?=$/, '') + } + + // 3. If data’s code point length divides by 4 leaving + // a remainder of 1, then return failure. + if (data.length % 4 === 1) { + return 'failure' + } + + // 4. If data contains a code point that is not one of + // U+002B (+) + // U+002F (/) + // ASCII alphanumeric + // then return failure. + if (/[^+/0-9A-Za-z]/.test(data)) { + return 'failure' + } + + const binary = atob(data) + const bytes = new Uint8Array(binary.length) + + for (let byte = 0; byte < binary.length; byte++) { + bytes[byte] = binary.charCodeAt(byte) + } + + return bytes +} + +// https://fetch.spec.whatwg.org/#collect-an-http-quoted-string +// tests: https://fetch.spec.whatwg.org/#example-http-quoted-string +/** + * @param {string} input + * @param {{ position: number }} position + * @param {boolean?} extractValue + */ +function collectAnHTTPQuotedString (input, position, extractValue) { + // 1. Let positionStart be position. + const positionStart = position.position + + // 2. Let value be the empty string. + let value = '' + + // 3. Assert: the code point at position within input + // is U+0022 ("). + assert(input[position.position] === '"') + + // 4. Advance position by 1. + position.position++ + + // 5. While true: + while (true) { + // 1. Append the result of collecting a sequence of code points + // that are not U+0022 (") or U+005C (\) from input, given + // position, to value. + value += collectASequenceOfCodePoints( + (char) => char !== '"' && char !== '\\', + input, + position + ) + + // 2. If position is past the end of input, then break. + if (position.position >= input.length) { + break + } + + // 3. Let quoteOrBackslash be the code point at position within + // input. + const quoteOrBackslash = input[position.position] + + // 4. Advance position by 1. + position.position++ + + // 5. If quoteOrBackslash is U+005C (\), then: + if (quoteOrBackslash === '\\') { + // 1. If position is past the end of input, then append + // U+005C (\) to value and break. + if (position.position >= input.length) { + value += '\\' + break + } + + // 2. Append the code point at position within input to value. + value += input[position.position] + + // 3. Advance position by 1. + position.position++ + + // 6. Otherwise: + } else { + // 1. Assert: quoteOrBackslash is U+0022 ("). + assert(quoteOrBackslash === '"') + + // 2. Break. + break + } + } + + // 6. If the extract-value flag is set, then return value. + if (extractValue) { + return value + } + + // 7. Return the code points from positionStart to position, + // inclusive, within input. + return input.slice(positionStart, position.position) +} + +/** + * @see https://mimesniff.spec.whatwg.org/#serialize-a-mime-type + */ +function serializeAMimeType (mimeType) { + assert(mimeType !== 'failure') + const { parameters, essence } = mimeType + + // 1. Let serialization be the concatenation of mimeType’s + // type, U+002F (/), and mimeType’s subtype. + let serialization = essence + + // 2. For each name → value of mimeType’s parameters: + for (let [name, value] of parameters.entries()) { + // 1. Append U+003B (;) to serialization. + serialization += ';' + + // 2. Append name to serialization. + serialization += name + + // 3. Append U+003D (=) to serialization. + serialization += '=' + + // 4. If value does not solely contain HTTP token code + // points or value is the empty string, then: + if (!HTTP_TOKEN_CODEPOINTS.test(value)) { + // 1. Precede each occurence of U+0022 (") or + // U+005C (\) in value with U+005C (\). + value = value.replace(/(\\|")/g, '\\$1') + + // 2. Prepend U+0022 (") to value. + value = '"' + value + + // 3. Append U+0022 (") to value. + value += '"' + } + + // 5. Append value to serialization. + serialization += value + } + + // 3. Return serialization. + return serialization +} + +/** + * @see https://fetch.spec.whatwg.org/#http-whitespace + * @param {string} char + */ +function isHTTPWhiteSpace (char) { + return char === '\r' || char === '\n' || char === '\t' || char === ' ' +} + +/** + * @see https://fetch.spec.whatwg.org/#http-whitespace + * @param {string} str + */ +function removeHTTPWhitespace (str, leading = true, trailing = true) { + let lead = 0 + let trail = str.length - 1 + + if (leading) { + for (; lead < str.length && isHTTPWhiteSpace(str[lead]); lead++); + } + + if (trailing) { + for (; trail > 0 && isHTTPWhiteSpace(str[trail]); trail--); + } + + return str.slice(lead, trail + 1) +} + +/** + * @see https://infra.spec.whatwg.org/#ascii-whitespace + * @param {string} char + */ +function isASCIIWhitespace (char) { + return char === '\r' || char === '\n' || char === '\t' || char === '\f' || char === ' ' +} + +/** + * @see https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace + */ +function removeASCIIWhitespace (str, leading = true, trailing = true) { + let lead = 0 + let trail = str.length - 1 + + if (leading) { + for (; lead < str.length && isASCIIWhitespace(str[lead]); lead++); + } + + if (trailing) { + for (; trail > 0 && isASCIIWhitespace(str[trail]); trail--); + } + + return str.slice(lead, trail + 1) +} + +module.exports = { + dataURLProcessor, + URLSerializer, + collectASequenceOfCodePoints, + collectASequenceOfCodePointsFast, + stringPercentDecode, + parseMIMEType, + collectAnHTTPQuotedString, + serializeAMimeType +} + + +/***/ }), + +/***/ 7085: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { Blob, File: NativeFile } = __nccwpck_require__(181) +const { types } = __nccwpck_require__(9023) +const { kState } = __nccwpck_require__(834) +const { isBlobLike } = __nccwpck_require__(3359) +const { webidl } = __nccwpck_require__(274) +const { parseMIMEType, serializeAMimeType } = __nccwpck_require__(5294) +const { kEnumerableProperty } = __nccwpck_require__(1436) +const encoder = new TextEncoder() + +class File extends Blob { + constructor (fileBits, fileName, options = {}) { + // The File constructor is invoked with two or three parameters, depending + // on whether the optional dictionary parameter is used. When the File() + // constructor is invoked, user agents must run the following steps: + webidl.argumentLengthCheck(arguments, 2, { header: 'File constructor' }) + + fileBits = webidl.converters['sequence'](fileBits) + fileName = webidl.converters.USVString(fileName) + options = webidl.converters.FilePropertyBag(options) + + // 1. Let bytes be the result of processing blob parts given fileBits and + // options. + // Note: Blob handles this for us + + // 2. Let n be the fileName argument to the constructor. + const n = fileName + + // 3. Process FilePropertyBag dictionary argument by running the following + // substeps: + + // 1. If the type member is provided and is not the empty string, let t + // be set to the type dictionary member. If t contains any characters + // outside the range U+0020 to U+007E, then set t to the empty string + // and return from these substeps. + // 2. Convert every character in t to ASCII lowercase. + let t = options.type + let d + + // eslint-disable-next-line no-labels + substep: { + if (t) { + t = parseMIMEType(t) + + if (t === 'failure') { + t = '' + // eslint-disable-next-line no-labels + break substep + } + + t = serializeAMimeType(t).toLowerCase() + } + + // 3. If the lastModified member is provided, let d be set to the + // lastModified dictionary member. If it is not provided, set d to the + // current date and time represented as the number of milliseconds since + // the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]). + d = options.lastModified + } + + // 4. Return a new File object F such that: + // F refers to the bytes byte sequence. + // F.size is set to the number of total bytes in bytes. + // F.name is set to n. + // F.type is set to t. + // F.lastModified is set to d. + + super(processBlobParts(fileBits, options), { type: t }) + this[kState] = { + name: n, + lastModified: d, + type: t + } + } + + get name () { + webidl.brandCheck(this, File) + + return this[kState].name + } + + get lastModified () { + webidl.brandCheck(this, File) + + return this[kState].lastModified + } + + get type () { + webidl.brandCheck(this, File) + + return this[kState].type + } +} + +class FileLike { + constructor (blobLike, fileName, options = {}) { + // TODO: argument idl type check + + // The File constructor is invoked with two or three parameters, depending + // on whether the optional dictionary parameter is used. When the File() + // constructor is invoked, user agents must run the following steps: + + // 1. Let bytes be the result of processing blob parts given fileBits and + // options. + + // 2. Let n be the fileName argument to the constructor. + const n = fileName + + // 3. Process FilePropertyBag dictionary argument by running the following + // substeps: + + // 1. If the type member is provided and is not the empty string, let t + // be set to the type dictionary member. If t contains any characters + // outside the range U+0020 to U+007E, then set t to the empty string + // and return from these substeps. + // TODO + const t = options.type + + // 2. Convert every character in t to ASCII lowercase. + // TODO + + // 3. If the lastModified member is provided, let d be set to the + // lastModified dictionary member. If it is not provided, set d to the + // current date and time represented as the number of milliseconds since + // the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]). + const d = options.lastModified ?? Date.now() + + // 4. Return a new File object F such that: + // F refers to the bytes byte sequence. + // F.size is set to the number of total bytes in bytes. + // F.name is set to n. + // F.type is set to t. + // F.lastModified is set to d. + + this[kState] = { + blobLike, + name: n, + type: t, + lastModified: d + } + } + + stream (...args) { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.stream(...args) + } + + arrayBuffer (...args) { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.arrayBuffer(...args) + } + + slice (...args) { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.slice(...args) + } + + text (...args) { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.text(...args) + } + + get size () { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.size + } + + get type () { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.type + } + + get name () { + webidl.brandCheck(this, FileLike) + + return this[kState].name + } + + get lastModified () { + webidl.brandCheck(this, FileLike) + + return this[kState].lastModified + } + + get [Symbol.toStringTag] () { + return 'File' + } +} + +Object.defineProperties(File.prototype, { + [Symbol.toStringTag]: { + value: 'File', + configurable: true + }, + name: kEnumerableProperty, + lastModified: kEnumerableProperty +}) + +webidl.converters.Blob = webidl.interfaceConverter(Blob) + +webidl.converters.BlobPart = function (V, opts) { + if (webidl.util.Type(V) === 'Object') { + if (isBlobLike(V)) { + return webidl.converters.Blob(V, { strict: false }) + } + + if ( + ArrayBuffer.isView(V) || + types.isAnyArrayBuffer(V) + ) { + return webidl.converters.BufferSource(V, opts) + } + } + + return webidl.converters.USVString(V, opts) +} + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.BlobPart +) + +// https://www.w3.org/TR/FileAPI/#dfn-FilePropertyBag +webidl.converters.FilePropertyBag = webidl.dictionaryConverter([ + { + key: 'lastModified', + converter: webidl.converters['long long'], + get defaultValue () { + return Date.now() + } + }, + { + key: 'type', + converter: webidl.converters.DOMString, + defaultValue: '' + }, + { + key: 'endings', + converter: (value) => { + value = webidl.converters.DOMString(value) + value = value.toLowerCase() + + if (value !== 'native') { + value = 'transparent' + } + + return value + }, + defaultValue: 'transparent' + } +]) + +/** + * @see https://www.w3.org/TR/FileAPI/#process-blob-parts + * @param {(NodeJS.TypedArray|Blob|string)[]} parts + * @param {{ type: string, endings: string }} options + */ +function processBlobParts (parts, options) { + // 1. Let bytes be an empty sequence of bytes. + /** @type {NodeJS.TypedArray[]} */ + const bytes = [] + + // 2. For each element in parts: + for (const element of parts) { + // 1. If element is a USVString, run the following substeps: + if (typeof element === 'string') { + // 1. Let s be element. + let s = element + + // 2. If the endings member of options is "native", set s + // to the result of converting line endings to native + // of element. + if (options.endings === 'native') { + s = convertLineEndingsNative(s) + } + + // 3. Append the result of UTF-8 encoding s to bytes. + bytes.push(encoder.encode(s)) + } else if ( + types.isAnyArrayBuffer(element) || + types.isTypedArray(element) + ) { + // 2. If element is a BufferSource, get a copy of the + // bytes held by the buffer source, and append those + // bytes to bytes. + if (!element.buffer) { // ArrayBuffer + bytes.push(new Uint8Array(element)) + } else { + bytes.push( + new Uint8Array(element.buffer, element.byteOffset, element.byteLength) + ) + } + } else if (isBlobLike(element)) { + // 3. If element is a Blob, append the bytes it represents + // to bytes. + bytes.push(element) + } + } + + // 3. Return bytes. + return bytes +} + +/** + * @see https://www.w3.org/TR/FileAPI/#convert-line-endings-to-native + * @param {string} s + */ +function convertLineEndingsNative (s) { + // 1. Let native line ending be be the code point U+000A LF. + let nativeLineEnding = '\n' + + // 2. If the underlying platform’s conventions are to + // represent newlines as a carriage return and line feed + // sequence, set native line ending to the code point + // U+000D CR followed by the code point U+000A LF. + if (process.platform === 'win32') { + nativeLineEnding = '\r\n' + } + + return s.replace(/\r?\n/g, nativeLineEnding) +} + +// If this function is moved to ./util.js, some tools (such as +// rollup) will warn about circular dependencies. See: +// https://github.com/nodejs/undici/issues/1629 +function isFileLike (object) { + return ( + (NativeFile && object instanceof NativeFile) || + object instanceof File || ( + object && + (typeof object.stream === 'function' || + typeof object.arrayBuffer === 'function') && + object[Symbol.toStringTag] === 'File' + ) + ) +} + +module.exports = { File, FileLike, isFileLike } + + +/***/ }), + +/***/ 2813: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { isBlobLike, toUSVString, makeIterator } = __nccwpck_require__(3359) +const { kState } = __nccwpck_require__(834) +const { File: UndiciFile, FileLike, isFileLike } = __nccwpck_require__(7085) +const { webidl } = __nccwpck_require__(274) +const { Blob, File: NativeFile } = __nccwpck_require__(181) + +/** @type {globalThis['File']} */ +const File = NativeFile ?? UndiciFile + +// https://xhr.spec.whatwg.org/#formdata +class FormData { + constructor (form) { + if (form !== undefined) { + throw webidl.errors.conversionFailed({ + prefix: 'FormData constructor', + argument: 'Argument 1', + types: ['undefined'] + }) + } + + this[kState] = [] + } + + append (name, value, filename = undefined) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.append' }) + + if (arguments.length === 3 && !isBlobLike(value)) { + throw new TypeError( + "Failed to execute 'append' on 'FormData': parameter 2 is not of type 'Blob'" + ) + } + + // 1. Let value be value if given; otherwise blobValue. + + name = webidl.converters.USVString(name) + value = isBlobLike(value) + ? webidl.converters.Blob(value, { strict: false }) + : webidl.converters.USVString(value) + filename = arguments.length === 3 + ? webidl.converters.USVString(filename) + : undefined + + // 2. Let entry be the result of creating an entry with + // name, value, and filename if given. + const entry = makeEntry(name, value, filename) + + // 3. Append entry to this’s entry list. + this[kState].push(entry) + } + + delete (name) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.delete' }) + + name = webidl.converters.USVString(name) + + // The delete(name) method steps are to remove all entries whose name + // is name from this’s entry list. + this[kState] = this[kState].filter(entry => entry.name !== name) + } + + get (name) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.get' }) + + name = webidl.converters.USVString(name) + + // 1. If there is no entry whose name is name in this’s entry list, + // then return null. + const idx = this[kState].findIndex((entry) => entry.name === name) + if (idx === -1) { + return null + } + + // 2. Return the value of the first entry whose name is name from + // this’s entry list. + return this[kState][idx].value + } + + getAll (name) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.getAll' }) + + name = webidl.converters.USVString(name) + + // 1. If there is no entry whose name is name in this’s entry list, + // then return the empty list. + // 2. Return the values of all entries whose name is name, in order, + // from this’s entry list. + return this[kState] + .filter((entry) => entry.name === name) + .map((entry) => entry.value) + } + + has (name) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.has' }) + + name = webidl.converters.USVString(name) + + // The has(name) method steps are to return true if there is an entry + // whose name is name in this’s entry list; otherwise false. + return this[kState].findIndex((entry) => entry.name === name) !== -1 + } + + set (name, value, filename = undefined) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.set' }) + + if (arguments.length === 3 && !isBlobLike(value)) { + throw new TypeError( + "Failed to execute 'set' on 'FormData': parameter 2 is not of type 'Blob'" + ) + } + + // The set(name, value) and set(name, blobValue, filename) method steps + // are: + + // 1. Let value be value if given; otherwise blobValue. + + name = webidl.converters.USVString(name) + value = isBlobLike(value) + ? webidl.converters.Blob(value, { strict: false }) + : webidl.converters.USVString(value) + filename = arguments.length === 3 + ? toUSVString(filename) + : undefined + + // 2. Let entry be the result of creating an entry with name, value, and + // filename if given. + const entry = makeEntry(name, value, filename) + + // 3. If there are entries in this’s entry list whose name is name, then + // replace the first such entry with entry and remove the others. + const idx = this[kState].findIndex((entry) => entry.name === name) + if (idx !== -1) { + this[kState] = [ + ...this[kState].slice(0, idx), + entry, + ...this[kState].slice(idx + 1).filter((entry) => entry.name !== name) + ] + } else { + // 4. Otherwise, append entry to this’s entry list. + this[kState].push(entry) + } + } + + entries () { + webidl.brandCheck(this, FormData) + + return makeIterator( + () => this[kState].map(pair => [pair.name, pair.value]), + 'FormData', + 'key+value' + ) + } + + keys () { + webidl.brandCheck(this, FormData) + + return makeIterator( + () => this[kState].map(pair => [pair.name, pair.value]), + 'FormData', + 'key' + ) + } + + values () { + webidl.brandCheck(this, FormData) + + return makeIterator( + () => this[kState].map(pair => [pair.name, pair.value]), + 'FormData', + 'value' + ) + } + + /** + * @param {(value: string, key: string, self: FormData) => void} callbackFn + * @param {unknown} thisArg + */ + forEach (callbackFn, thisArg = globalThis) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.forEach' }) + + if (typeof callbackFn !== 'function') { + throw new TypeError( + "Failed to execute 'forEach' on 'FormData': parameter 1 is not of type 'Function'." + ) + } + + for (const [key, value] of this) { + callbackFn.apply(thisArg, [value, key, this]) + } + } +} + +FormData.prototype[Symbol.iterator] = FormData.prototype.entries + +Object.defineProperties(FormData.prototype, { + [Symbol.toStringTag]: { + value: 'FormData', + configurable: true + } +}) + +/** + * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry + * @param {string} name + * @param {string|Blob} value + * @param {?string} filename + * @returns + */ +function makeEntry (name, value, filename) { + // 1. Set name to the result of converting name into a scalar value string. + // "To convert a string into a scalar value string, replace any surrogates + // with U+FFFD." + // see: https://nodejs.org/dist/latest-v18.x/docs/api/buffer.html#buftostringencoding-start-end + name = Buffer.from(name).toString('utf8') + + // 2. If value is a string, then set value to the result of converting + // value into a scalar value string. + if (typeof value === 'string') { + value = Buffer.from(value).toString('utf8') + } else { + // 3. Otherwise: + + // 1. If value is not a File object, then set value to a new File object, + // representing the same bytes, whose name attribute value is "blob" + if (!isFileLike(value)) { + value = value instanceof Blob + ? new File([value], 'blob', { type: value.type }) + : new FileLike(value, 'blob', { type: value.type }) + } + + // 2. If filename is given, then set value to a new File object, + // representing the same bytes, whose name attribute is filename. + if (filename !== undefined) { + /** @type {FilePropertyBag} */ + const options = { + type: value.type, + lastModified: value.lastModified + } + + value = (NativeFile && value instanceof NativeFile) || value instanceof UndiciFile + ? new File([value], filename, options) + : new FileLike(value, filename, options) + } + } + + // 4. Return an entry whose name is name and whose value is value. + return { name, value } +} + +module.exports = { FormData } + + +/***/ }), + +/***/ 960: +/***/ ((module) => { + +"use strict"; + + +// In case of breaking changes, increase the version +// number to avoid conflicts. +const globalOrigin = Symbol.for('undici.globalOrigin.1') + +function getGlobalOrigin () { + return globalThis[globalOrigin] +} + +function setGlobalOrigin (newOrigin) { + if (newOrigin === undefined) { + Object.defineProperty(globalThis, globalOrigin, { + value: undefined, + writable: true, + enumerable: false, + configurable: false + }) + + return + } + + const parsedURL = new URL(newOrigin) + + if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:') { + throw new TypeError(`Only http & https urls are allowed, received ${parsedURL.protocol}`) + } + + Object.defineProperty(globalThis, globalOrigin, { + value: parsedURL, + writable: true, + enumerable: false, + configurable: false + }) +} + +module.exports = { + getGlobalOrigin, + setGlobalOrigin +} + + +/***/ }), + +/***/ 161: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +// https://github.com/Ethan-Arrowood/undici-fetch + + + +const { kHeadersList, kConstruct } = __nccwpck_require__(9583) +const { kGuard } = __nccwpck_require__(834) +const { kEnumerableProperty } = __nccwpck_require__(1436) +const { + makeIterator, + isValidHeaderName, + isValidHeaderValue +} = __nccwpck_require__(3359) +const { webidl } = __nccwpck_require__(274) +const assert = __nccwpck_require__(2613) + +const kHeadersMap = Symbol('headers map') +const kHeadersSortedMap = Symbol('headers map sorted') + +/** + * @param {number} code + */ +function isHTTPWhiteSpaceCharCode (code) { + return code === 0x00a || code === 0x00d || code === 0x009 || code === 0x020 +} + +/** + * @see https://fetch.spec.whatwg.org/#concept-header-value-normalize + * @param {string} potentialValue + */ +function headerValueNormalize (potentialValue) { + // To normalize a byte sequence potentialValue, remove + // any leading and trailing HTTP whitespace bytes from + // potentialValue. + let i = 0; let j = potentialValue.length + + while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(j - 1))) --j + while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(i))) ++i + + return i === 0 && j === potentialValue.length ? potentialValue : potentialValue.substring(i, j) +} + +function fill (headers, object) { + // To fill a Headers object headers with a given object object, run these steps: + + // 1. If object is a sequence, then for each header in object: + // Note: webidl conversion to array has already been done. + if (Array.isArray(object)) { + for (let i = 0; i < object.length; ++i) { + const header = object[i] + // 1. If header does not contain exactly two items, then throw a TypeError. + if (header.length !== 2) { + throw webidl.errors.exception({ + header: 'Headers constructor', + message: `expected name/value pair to be length 2, found ${header.length}.` + }) + } + + // 2. Append (header’s first item, header’s second item) to headers. + appendHeader(headers, header[0], header[1]) + } + } else if (typeof object === 'object' && object !== null) { + // Note: null should throw + + // 2. Otherwise, object is a record, then for each key → value in object, + // append (key, value) to headers + const keys = Object.keys(object) + for (let i = 0; i < keys.length; ++i) { + appendHeader(headers, keys[i], object[keys[i]]) + } + } else { + throw webidl.errors.conversionFailed({ + prefix: 'Headers constructor', + argument: 'Argument 1', + types: ['sequence>', 'record'] + }) + } +} + +/** + * @see https://fetch.spec.whatwg.org/#concept-headers-append + */ +function appendHeader (headers, name, value) { + // 1. Normalize value. + value = headerValueNormalize(value) + + // 2. If name is not a header name or value is not a + // header value, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.append', + value: name, + type: 'header name' + }) + } else if (!isValidHeaderValue(value)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.append', + value, + type: 'header value' + }) + } + + // 3. If headers’s guard is "immutable", then throw a TypeError. + // 4. Otherwise, if headers’s guard is "request" and name is a + // forbidden header name, return. + // Note: undici does not implement forbidden header names + if (headers[kGuard] === 'immutable') { + throw new TypeError('immutable') + } else if (headers[kGuard] === 'request-no-cors') { + // 5. Otherwise, if headers’s guard is "request-no-cors": + // TODO + } + + // 6. Otherwise, if headers’s guard is "response" and name is a + // forbidden response-header name, return. + + // 7. Append (name, value) to headers’s header list. + return headers[kHeadersList].append(name, value) + + // 8. If headers’s guard is "request-no-cors", then remove + // privileged no-CORS request headers from headers +} + +class HeadersList { + /** @type {[string, string][]|null} */ + cookies = null + + constructor (init) { + if (init instanceof HeadersList) { + this[kHeadersMap] = new Map(init[kHeadersMap]) + this[kHeadersSortedMap] = init[kHeadersSortedMap] + this.cookies = init.cookies === null ? null : [...init.cookies] + } else { + this[kHeadersMap] = new Map(init) + this[kHeadersSortedMap] = null + } + } + + // https://fetch.spec.whatwg.org/#header-list-contains + contains (name) { + // A header list list contains a header name name if list + // contains a header whose name is a byte-case-insensitive + // match for name. + name = name.toLowerCase() + + return this[kHeadersMap].has(name) + } + + clear () { + this[kHeadersMap].clear() + this[kHeadersSortedMap] = null + this.cookies = null + } + + // https://fetch.spec.whatwg.org/#concept-header-list-append + append (name, value) { + this[kHeadersSortedMap] = null + + // 1. If list contains name, then set name to the first such + // header’s name. + const lowercaseName = name.toLowerCase() + const exists = this[kHeadersMap].get(lowercaseName) + + // 2. Append (name, value) to list. + if (exists) { + const delimiter = lowercaseName === 'cookie' ? '; ' : ', ' + this[kHeadersMap].set(lowercaseName, { + name: exists.name, + value: `${exists.value}${delimiter}${value}` + }) + } else { + this[kHeadersMap].set(lowercaseName, { name, value }) + } + + if (lowercaseName === 'set-cookie') { + this.cookies ??= [] + this.cookies.push(value) + } + } + + // https://fetch.spec.whatwg.org/#concept-header-list-set + set (name, value) { + this[kHeadersSortedMap] = null + const lowercaseName = name.toLowerCase() + + if (lowercaseName === 'set-cookie') { + this.cookies = [value] + } + + // 1. If list contains name, then set the value of + // the first such header to value and remove the + // others. + // 2. Otherwise, append header (name, value) to list. + this[kHeadersMap].set(lowercaseName, { name, value }) + } + + // https://fetch.spec.whatwg.org/#concept-header-list-delete + delete (name) { + this[kHeadersSortedMap] = null + + name = name.toLowerCase() + + if (name === 'set-cookie') { + this.cookies = null + } + + this[kHeadersMap].delete(name) + } + + // https://fetch.spec.whatwg.org/#concept-header-list-get + get (name) { + const value = this[kHeadersMap].get(name.toLowerCase()) + + // 1. If list does not contain name, then return null. + // 2. Return the values of all headers in list whose name + // is a byte-case-insensitive match for name, + // separated from each other by 0x2C 0x20, in order. + return value === undefined ? null : value.value + } + + * [Symbol.iterator] () { + // use the lowercased name + for (const [name, { value }] of this[kHeadersMap]) { + yield [name, value] + } + } + + get entries () { + const headers = {} + + if (this[kHeadersMap].size) { + for (const { name, value } of this[kHeadersMap].values()) { + headers[name] = value + } + } + + return headers + } +} + +// https://fetch.spec.whatwg.org/#headers-class +class Headers { + constructor (init = undefined) { + if (init === kConstruct) { + return + } + this[kHeadersList] = new HeadersList() + + // The new Headers(init) constructor steps are: + + // 1. Set this’s guard to "none". + this[kGuard] = 'none' + + // 2. If init is given, then fill this with init. + if (init !== undefined) { + init = webidl.converters.HeadersInit(init) + fill(this, init) + } + } + + // https://fetch.spec.whatwg.org/#dom-headers-append + append (name, value) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.append' }) + + name = webidl.converters.ByteString(name) + value = webidl.converters.ByteString(value) + + return appendHeader(this, name, value) + } + + // https://fetch.spec.whatwg.org/#dom-headers-delete + delete (name) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.delete' }) + + name = webidl.converters.ByteString(name) + + // 1. If name is not a header name, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.delete', + value: name, + type: 'header name' + }) + } + + // 2. If this’s guard is "immutable", then throw a TypeError. + // 3. Otherwise, if this’s guard is "request" and name is a + // forbidden header name, return. + // 4. Otherwise, if this’s guard is "request-no-cors", name + // is not a no-CORS-safelisted request-header name, and + // name is not a privileged no-CORS request-header name, + // return. + // 5. Otherwise, if this’s guard is "response" and name is + // a forbidden response-header name, return. + // Note: undici does not implement forbidden header names + if (this[kGuard] === 'immutable') { + throw new TypeError('immutable') + } else if (this[kGuard] === 'request-no-cors') { + // TODO + } + + // 6. If this’s header list does not contain name, then + // return. + if (!this[kHeadersList].contains(name)) { + return + } + + // 7. Delete name from this’s header list. + // 8. If this’s guard is "request-no-cors", then remove + // privileged no-CORS request headers from this. + this[kHeadersList].delete(name) + } + + // https://fetch.spec.whatwg.org/#dom-headers-get + get (name) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.get' }) + + name = webidl.converters.ByteString(name) + + // 1. If name is not a header name, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.get', + value: name, + type: 'header name' + }) + } + + // 2. Return the result of getting name from this’s header + // list. + return this[kHeadersList].get(name) + } + + // https://fetch.spec.whatwg.org/#dom-headers-has + has (name) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.has' }) + + name = webidl.converters.ByteString(name) + + // 1. If name is not a header name, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.has', + value: name, + type: 'header name' + }) + } + + // 2. Return true if this’s header list contains name; + // otherwise false. + return this[kHeadersList].contains(name) + } + + // https://fetch.spec.whatwg.org/#dom-headers-set + set (name, value) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.set' }) + + name = webidl.converters.ByteString(name) + value = webidl.converters.ByteString(value) + + // 1. Normalize value. + value = headerValueNormalize(value) + + // 2. If name is not a header name or value is not a + // header value, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.set', + value: name, + type: 'header name' + }) + } else if (!isValidHeaderValue(value)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.set', + value, + type: 'header value' + }) + } + + // 3. If this’s guard is "immutable", then throw a TypeError. + // 4. Otherwise, if this’s guard is "request" and name is a + // forbidden header name, return. + // 5. Otherwise, if this’s guard is "request-no-cors" and + // name/value is not a no-CORS-safelisted request-header, + // return. + // 6. Otherwise, if this’s guard is "response" and name is a + // forbidden response-header name, return. + // Note: undici does not implement forbidden header names + if (this[kGuard] === 'immutable') { + throw new TypeError('immutable') + } else if (this[kGuard] === 'request-no-cors') { + // TODO + } + + // 7. Set (name, value) in this’s header list. + // 8. If this’s guard is "request-no-cors", then remove + // privileged no-CORS request headers from this + this[kHeadersList].set(name, value) + } + + // https://fetch.spec.whatwg.org/#dom-headers-getsetcookie + getSetCookie () { + webidl.brandCheck(this, Headers) + + // 1. If this’s header list does not contain `Set-Cookie`, then return « ». + // 2. Return the values of all headers in this’s header list whose name is + // a byte-case-insensitive match for `Set-Cookie`, in order. + + const list = this[kHeadersList].cookies + + if (list) { + return [...list] + } + + return [] + } + + // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine + get [kHeadersSortedMap] () { + if (this[kHeadersList][kHeadersSortedMap]) { + return this[kHeadersList][kHeadersSortedMap] + } + + // 1. Let headers be an empty list of headers with the key being the name + // and value the value. + const headers = [] + + // 2. Let names be the result of convert header names to a sorted-lowercase + // set with all the names of the headers in list. + const names = [...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1) + const cookies = this[kHeadersList].cookies + + // 3. For each name of names: + for (let i = 0; i < names.length; ++i) { + const [name, value] = names[i] + // 1. If name is `set-cookie`, then: + if (name === 'set-cookie') { + // 1. Let values be a list of all values of headers in list whose name + // is a byte-case-insensitive match for name, in order. + + // 2. For each value of values: + // 1. Append (name, value) to headers. + for (let j = 0; j < cookies.length; ++j) { + headers.push([name, cookies[j]]) + } + } else { + // 2. Otherwise: + + // 1. Let value be the result of getting name from list. + + // 2. Assert: value is non-null. + assert(value !== null) + + // 3. Append (name, value) to headers. + headers.push([name, value]) + } + } + + this[kHeadersList][kHeadersSortedMap] = headers + + // 4. Return headers. + return headers + } + + keys () { + webidl.brandCheck(this, Headers) + + if (this[kGuard] === 'immutable') { + const value = this[kHeadersSortedMap] + return makeIterator(() => value, 'Headers', + 'key') + } + + return makeIterator( + () => [...this[kHeadersSortedMap].values()], + 'Headers', + 'key' + ) + } + + values () { + webidl.brandCheck(this, Headers) + + if (this[kGuard] === 'immutable') { + const value = this[kHeadersSortedMap] + return makeIterator(() => value, 'Headers', + 'value') + } + + return makeIterator( + () => [...this[kHeadersSortedMap].values()], + 'Headers', + 'value' + ) + } + + entries () { + webidl.brandCheck(this, Headers) + + if (this[kGuard] === 'immutable') { + const value = this[kHeadersSortedMap] + return makeIterator(() => value, 'Headers', + 'key+value') + } + + return makeIterator( + () => [...this[kHeadersSortedMap].values()], + 'Headers', + 'key+value' + ) + } + + /** + * @param {(value: string, key: string, self: Headers) => void} callbackFn + * @param {unknown} thisArg + */ + forEach (callbackFn, thisArg = globalThis) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.forEach' }) + + if (typeof callbackFn !== 'function') { + throw new TypeError( + "Failed to execute 'forEach' on 'Headers': parameter 1 is not of type 'Function'." + ) + } + + for (const [key, value] of this) { + callbackFn.apply(thisArg, [value, key, this]) + } + } + + [Symbol.for('nodejs.util.inspect.custom')] () { + webidl.brandCheck(this, Headers) + + return this[kHeadersList] + } +} + +Headers.prototype[Symbol.iterator] = Headers.prototype.entries + +Object.defineProperties(Headers.prototype, { + append: kEnumerableProperty, + delete: kEnumerableProperty, + get: kEnumerableProperty, + has: kEnumerableProperty, + set: kEnumerableProperty, + getSetCookie: kEnumerableProperty, + keys: kEnumerableProperty, + values: kEnumerableProperty, + entries: kEnumerableProperty, + forEach: kEnumerableProperty, + [Symbol.iterator]: { enumerable: false }, + [Symbol.toStringTag]: { + value: 'Headers', + configurable: true + } +}) + +webidl.converters.HeadersInit = function (V) { + if (webidl.util.Type(V) === 'Object') { + if (V[Symbol.iterator]) { + return webidl.converters['sequence>'](V) + } + + return webidl.converters['record'](V) + } + + throw webidl.errors.conversionFailed({ + prefix: 'Headers constructor', + argument: 'Argument 1', + types: ['sequence>', 'record'] + }) +} + +module.exports = { + fill, + Headers, + HeadersList +} + + +/***/ }), + +/***/ 1279: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +// https://github.com/Ethan-Arrowood/undici-fetch + + + +const { + Response, + makeNetworkError, + makeAppropriateNetworkError, + filterResponse, + makeResponse +} = __nccwpck_require__(2440) +const { Headers } = __nccwpck_require__(161) +const { Request, makeRequest } = __nccwpck_require__(1558) +const zlib = __nccwpck_require__(3106) +const { + bytesMatch, + makePolicyContainer, + clonePolicyContainer, + requestBadPort, + TAOCheck, + appendRequestOriginHeader, + responseLocationURL, + requestCurrentURL, + setRequestReferrerPolicyOnRedirect, + tryUpgradeRequestToAPotentiallyTrustworthyURL, + createOpaqueTimingInfo, + appendFetchMetadata, + corsCheck, + crossOriginResourcePolicyCheck, + determineRequestsReferrer, + coarsenedSharedCurrentTime, + createDeferredPromise, + isBlobLike, + sameOrigin, + isCancelled, + isAborted, + isErrorLike, + fullyReadBody, + readableStreamClose, + isomorphicEncode, + urlIsLocal, + urlIsHttpHttpsScheme, + urlHasHttpsScheme +} = __nccwpck_require__(3359) +const { kState, kHeaders, kGuard, kRealm } = __nccwpck_require__(834) +const assert = __nccwpck_require__(2613) +const { safelyExtractBody } = __nccwpck_require__(4655) +const { + redirectStatusSet, + nullBodyStatus, + safeMethodsSet, + requestBodyHeader, + subresourceSet, + DOMException +} = __nccwpck_require__(450) +const { kHeadersList } = __nccwpck_require__(9583) +const EE = __nccwpck_require__(4434) +const { Readable, pipeline } = __nccwpck_require__(2203) +const { addAbortListener, isErrored, isReadable, nodeMajor, nodeMinor } = __nccwpck_require__(1436) +const { dataURLProcessor, serializeAMimeType } = __nccwpck_require__(5294) +const { TransformStream } = __nccwpck_require__(3774) +const { getGlobalDispatcher } = __nccwpck_require__(8377) +const { webidl } = __nccwpck_require__(274) +const { STATUS_CODES } = __nccwpck_require__(8611) +const GET_OR_HEAD = ['GET', 'HEAD'] + +/** @type {import('buffer').resolveObjectURL} */ +let resolveObjectURL +let ReadableStream = globalThis.ReadableStream + +class Fetch extends EE { + constructor (dispatcher) { + super() + + this.dispatcher = dispatcher + this.connection = null + this.dump = false + this.state = 'ongoing' + // 2 terminated listeners get added per request, + // but only 1 gets removed. If there are 20 redirects, + // 21 listeners will be added. + // See https://github.com/nodejs/undici/issues/1711 + // TODO (fix): Find and fix root cause for leaked listener. + this.setMaxListeners(21) + } + + terminate (reason) { + if (this.state !== 'ongoing') { + return + } + + this.state = 'terminated' + this.connection?.destroy(reason) + this.emit('terminated', reason) + } + + // https://fetch.spec.whatwg.org/#fetch-controller-abort + abort (error) { + if (this.state !== 'ongoing') { + return + } + + // 1. Set controller’s state to "aborted". + this.state = 'aborted' + + // 2. Let fallbackError be an "AbortError" DOMException. + // 3. Set error to fallbackError if it is not given. + if (!error) { + error = new DOMException('The operation was aborted.', 'AbortError') + } + + // 4. Let serializedError be StructuredSerialize(error). + // If that threw an exception, catch it, and let + // serializedError be StructuredSerialize(fallbackError). + + // 5. Set controller’s serialized abort reason to serializedError. + this.serializedAbortReason = error + + this.connection?.destroy(error) + this.emit('terminated', error) + } +} + +// https://fetch.spec.whatwg.org/#fetch-method +function fetch (input, init = {}) { + webidl.argumentLengthCheck(arguments, 1, { header: 'globalThis.fetch' }) + + // 1. Let p be a new promise. + const p = createDeferredPromise() + + // 2. Let requestObject be the result of invoking the initial value of + // Request as constructor with input and init as arguments. If this throws + // an exception, reject p with it and return p. + let requestObject + + try { + requestObject = new Request(input, init) + } catch (e) { + p.reject(e) + return p.promise + } + + // 3. Let request be requestObject’s request. + const request = requestObject[kState] + + // 4. If requestObject’s signal’s aborted flag is set, then: + if (requestObject.signal.aborted) { + // 1. Abort the fetch() call with p, request, null, and + // requestObject’s signal’s abort reason. + abortFetch(p, request, null, requestObject.signal.reason) + + // 2. Return p. + return p.promise + } + + // 5. Let globalObject be request’s client’s global object. + const globalObject = request.client.globalObject + + // 6. If globalObject is a ServiceWorkerGlobalScope object, then set + // request’s service-workers mode to "none". + if (globalObject?.constructor?.name === 'ServiceWorkerGlobalScope') { + request.serviceWorkers = 'none' + } + + // 7. Let responseObject be null. + let responseObject = null + + // 8. Let relevantRealm be this’s relevant Realm. + const relevantRealm = null + + // 9. Let locallyAborted be false. + let locallyAborted = false + + // 10. Let controller be null. + let controller = null + + // 11. Add the following abort steps to requestObject’s signal: + addAbortListener( + requestObject.signal, + () => { + // 1. Set locallyAborted to true. + locallyAborted = true + + // 2. Assert: controller is non-null. + assert(controller != null) + + // 3. Abort controller with requestObject’s signal’s abort reason. + controller.abort(requestObject.signal.reason) + + // 4. Abort the fetch() call with p, request, responseObject, + // and requestObject’s signal’s abort reason. + abortFetch(p, request, responseObject, requestObject.signal.reason) + } + ) + + // 12. Let handleFetchDone given response response be to finalize and + // report timing with response, globalObject, and "fetch". + const handleFetchDone = (response) => + finalizeAndReportTiming(response, 'fetch') + + // 13. Set controller to the result of calling fetch given request, + // with processResponseEndOfBody set to handleFetchDone, and processResponse + // given response being these substeps: + + const processResponse = (response) => { + // 1. If locallyAborted is true, terminate these substeps. + if (locallyAborted) { + return Promise.resolve() + } + + // 2. If response’s aborted flag is set, then: + if (response.aborted) { + // 1. Let deserializedError be the result of deserialize a serialized + // abort reason given controller’s serialized abort reason and + // relevantRealm. + + // 2. Abort the fetch() call with p, request, responseObject, and + // deserializedError. + + abortFetch(p, request, responseObject, controller.serializedAbortReason) + return Promise.resolve() + } + + // 3. If response is a network error, then reject p with a TypeError + // and terminate these substeps. + if (response.type === 'error') { + p.reject( + Object.assign(new TypeError('fetch failed'), { cause: response.error }) + ) + return Promise.resolve() + } + + // 4. Set responseObject to the result of creating a Response object, + // given response, "immutable", and relevantRealm. + responseObject = new Response() + responseObject[kState] = response + responseObject[kRealm] = relevantRealm + responseObject[kHeaders][kHeadersList] = response.headersList + responseObject[kHeaders][kGuard] = 'immutable' + responseObject[kHeaders][kRealm] = relevantRealm + + // 5. Resolve p with responseObject. + p.resolve(responseObject) + } + + controller = fetching({ + request, + processResponseEndOfBody: handleFetchDone, + processResponse, + dispatcher: init.dispatcher ?? getGlobalDispatcher() // undici + }) + + // 14. Return p. + return p.promise +} + +// https://fetch.spec.whatwg.org/#finalize-and-report-timing +function finalizeAndReportTiming (response, initiatorType = 'other') { + // 1. If response is an aborted network error, then return. + if (response.type === 'error' && response.aborted) { + return + } + + // 2. If response’s URL list is null or empty, then return. + if (!response.urlList?.length) { + return + } + + // 3. Let originalURL be response’s URL list[0]. + const originalURL = response.urlList[0] + + // 4. Let timingInfo be response’s timing info. + let timingInfo = response.timingInfo + + // 5. Let cacheState be response’s cache state. + let cacheState = response.cacheState + + // 6. If originalURL’s scheme is not an HTTP(S) scheme, then return. + if (!urlIsHttpHttpsScheme(originalURL)) { + return + } + + // 7. If timingInfo is null, then return. + if (timingInfo === null) { + return + } + + // 8. If response’s timing allow passed flag is not set, then: + if (!response.timingAllowPassed) { + // 1. Set timingInfo to a the result of creating an opaque timing info for timingInfo. + timingInfo = createOpaqueTimingInfo({ + startTime: timingInfo.startTime + }) + + // 2. Set cacheState to the empty string. + cacheState = '' + } + + // 9. Set timingInfo’s end time to the coarsened shared current time + // given global’s relevant settings object’s cross-origin isolated + // capability. + // TODO: given global’s relevant settings object’s cross-origin isolated + // capability? + timingInfo.endTime = coarsenedSharedCurrentTime() + + // 10. Set response’s timing info to timingInfo. + response.timingInfo = timingInfo + + // 11. Mark resource timing for timingInfo, originalURL, initiatorType, + // global, and cacheState. + markResourceTiming( + timingInfo, + originalURL, + initiatorType, + globalThis, + cacheState + ) +} + +// https://w3c.github.io/resource-timing/#dfn-mark-resource-timing +function markResourceTiming (timingInfo, originalURL, initiatorType, globalThis, cacheState) { + if (nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 2)) { + performance.markResourceTiming(timingInfo, originalURL.href, initiatorType, globalThis, cacheState) + } +} + +// https://fetch.spec.whatwg.org/#abort-fetch +function abortFetch (p, request, responseObject, error) { + // Note: AbortSignal.reason was added in node v17.2.0 + // which would give us an undefined error to reject with. + // Remove this once node v16 is no longer supported. + if (!error) { + error = new DOMException('The operation was aborted.', 'AbortError') + } + + // 1. Reject promise with error. + p.reject(error) + + // 2. If request’s body is not null and is readable, then cancel request’s + // body with error. + if (request.body != null && isReadable(request.body?.stream)) { + request.body.stream.cancel(error).catch((err) => { + if (err.code === 'ERR_INVALID_STATE') { + // Node bug? + return + } + throw err + }) + } + + // 3. If responseObject is null, then return. + if (responseObject == null) { + return + } + + // 4. Let response be responseObject’s response. + const response = responseObject[kState] + + // 5. If response’s body is not null and is readable, then error response’s + // body with error. + if (response.body != null && isReadable(response.body?.stream)) { + response.body.stream.cancel(error).catch((err) => { + if (err.code === 'ERR_INVALID_STATE') { + // Node bug? + return + } + throw err + }) + } +} + +// https://fetch.spec.whatwg.org/#fetching +function fetching ({ + request, + processRequestBodyChunkLength, + processRequestEndOfBody, + processResponse, + processResponseEndOfBody, + processResponseConsumeBody, + useParallelQueue = false, + dispatcher // undici +}) { + // 1. Let taskDestination be null. + let taskDestination = null + + // 2. Let crossOriginIsolatedCapability be false. + let crossOriginIsolatedCapability = false + + // 3. If request’s client is non-null, then: + if (request.client != null) { + // 1. Set taskDestination to request’s client’s global object. + taskDestination = request.client.globalObject + + // 2. Set crossOriginIsolatedCapability to request’s client’s cross-origin + // isolated capability. + crossOriginIsolatedCapability = + request.client.crossOriginIsolatedCapability + } + + // 4. If useParallelQueue is true, then set taskDestination to the result of + // starting a new parallel queue. + // TODO + + // 5. Let timingInfo be a new fetch timing info whose start time and + // post-redirect start time are the coarsened shared current time given + // crossOriginIsolatedCapability. + const currenTime = coarsenedSharedCurrentTime(crossOriginIsolatedCapability) + const timingInfo = createOpaqueTimingInfo({ + startTime: currenTime + }) + + // 6. Let fetchParams be a new fetch params whose + // request is request, + // timing info is timingInfo, + // process request body chunk length is processRequestBodyChunkLength, + // process request end-of-body is processRequestEndOfBody, + // process response is processResponse, + // process response consume body is processResponseConsumeBody, + // process response end-of-body is processResponseEndOfBody, + // task destination is taskDestination, + // and cross-origin isolated capability is crossOriginIsolatedCapability. + const fetchParams = { + controller: new Fetch(dispatcher), + request, + timingInfo, + processRequestBodyChunkLength, + processRequestEndOfBody, + processResponse, + processResponseConsumeBody, + processResponseEndOfBody, + taskDestination, + crossOriginIsolatedCapability + } + + // 7. If request’s body is a byte sequence, then set request’s body to + // request’s body as a body. + // NOTE: Since fetching is only called from fetch, body should already be + // extracted. + assert(!request.body || request.body.stream) + + // 8. If request’s window is "client", then set request’s window to request’s + // client, if request’s client’s global object is a Window object; otherwise + // "no-window". + if (request.window === 'client') { + // TODO: What if request.client is null? + request.window = + request.client?.globalObject?.constructor?.name === 'Window' + ? request.client + : 'no-window' + } + + // 9. If request’s origin is "client", then set request’s origin to request’s + // client’s origin. + if (request.origin === 'client') { + // TODO: What if request.client is null? + request.origin = request.client?.origin + } + + // 10. If all of the following conditions are true: + // TODO + + // 11. If request’s policy container is "client", then: + if (request.policyContainer === 'client') { + // 1. If request’s client is non-null, then set request’s policy + // container to a clone of request’s client’s policy container. [HTML] + if (request.client != null) { + request.policyContainer = clonePolicyContainer( + request.client.policyContainer + ) + } else { + // 2. Otherwise, set request’s policy container to a new policy + // container. + request.policyContainer = makePolicyContainer() + } + } + + // 12. If request’s header list does not contain `Accept`, then: + if (!request.headersList.contains('accept')) { + // 1. Let value be `*/*`. + const value = '*/*' + + // 2. A user agent should set value to the first matching statement, if + // any, switching on request’s destination: + // "document" + // "frame" + // "iframe" + // `text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8` + // "image" + // `image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5` + // "style" + // `text/css,*/*;q=0.1` + // TODO + + // 3. Append `Accept`/value to request’s header list. + request.headersList.append('accept', value) + } + + // 13. If request’s header list does not contain `Accept-Language`, then + // user agents should append `Accept-Language`/an appropriate value to + // request’s header list. + if (!request.headersList.contains('accept-language')) { + request.headersList.append('accept-language', '*') + } + + // 14. If request’s priority is null, then use request’s initiator and + // destination appropriately in setting request’s priority to a + // user-agent-defined object. + if (request.priority === null) { + // TODO + } + + // 15. If request is a subresource request, then: + if (subresourceSet.has(request.destination)) { + // TODO + } + + // 16. Run main fetch given fetchParams. + mainFetch(fetchParams) + .catch(err => { + fetchParams.controller.terminate(err) + }) + + // 17. Return fetchParam's controller + return fetchParams.controller +} + +// https://fetch.spec.whatwg.org/#concept-main-fetch +async function mainFetch (fetchParams, recursive = false) { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let response be null. + let response = null + + // 3. If request’s local-URLs-only flag is set and request’s current URL is + // not local, then set response to a network error. + if (request.localURLsOnly && !urlIsLocal(requestCurrentURL(request))) { + response = makeNetworkError('local URLs only') + } + + // 4. Run report Content Security Policy violations for request. + // TODO + + // 5. Upgrade request to a potentially trustworthy URL, if appropriate. + tryUpgradeRequestToAPotentiallyTrustworthyURL(request) + + // 6. If should request be blocked due to a bad port, should fetching request + // be blocked as mixed content, or should request be blocked by Content + // Security Policy returns blocked, then set response to a network error. + if (requestBadPort(request) === 'blocked') { + response = makeNetworkError('bad port') + } + // TODO: should fetching request be blocked as mixed content? + // TODO: should request be blocked by Content Security Policy? + + // 7. If request’s referrer policy is the empty string, then set request’s + // referrer policy to request’s policy container’s referrer policy. + if (request.referrerPolicy === '') { + request.referrerPolicy = request.policyContainer.referrerPolicy + } + + // 8. If request’s referrer is not "no-referrer", then set request’s + // referrer to the result of invoking determine request’s referrer. + if (request.referrer !== 'no-referrer') { + request.referrer = determineRequestsReferrer(request) + } + + // 9. Set request’s current URL’s scheme to "https" if all of the following + // conditions are true: + // - request’s current URL’s scheme is "http" + // - request’s current URL’s host is a domain + // - Matching request’s current URL’s host per Known HSTS Host Domain Name + // Matching results in either a superdomain match with an asserted + // includeSubDomains directive or a congruent match (with or without an + // asserted includeSubDomains directive). [HSTS] + // TODO + + // 10. If recursive is false, then run the remaining steps in parallel. + // TODO + + // 11. If response is null, then set response to the result of running + // the steps corresponding to the first matching statement: + if (response === null) { + response = await (async () => { + const currentURL = requestCurrentURL(request) + + if ( + // - request’s current URL’s origin is same origin with request’s origin, + // and request’s response tainting is "basic" + (sameOrigin(currentURL, request.url) && request.responseTainting === 'basic') || + // request’s current URL’s scheme is "data" + (currentURL.protocol === 'data:') || + // - request’s mode is "navigate" or "websocket" + (request.mode === 'navigate' || request.mode === 'websocket') + ) { + // 1. Set request’s response tainting to "basic". + request.responseTainting = 'basic' + + // 2. Return the result of running scheme fetch given fetchParams. + return await schemeFetch(fetchParams) + } + + // request’s mode is "same-origin" + if (request.mode === 'same-origin') { + // 1. Return a network error. + return makeNetworkError('request mode cannot be "same-origin"') + } + + // request’s mode is "no-cors" + if (request.mode === 'no-cors') { + // 1. If request’s redirect mode is not "follow", then return a network + // error. + if (request.redirect !== 'follow') { + return makeNetworkError( + 'redirect mode cannot be "follow" for "no-cors" request' + ) + } + + // 2. Set request’s response tainting to "opaque". + request.responseTainting = 'opaque' + + // 3. Return the result of running scheme fetch given fetchParams. + return await schemeFetch(fetchParams) + } + + // request’s current URL’s scheme is not an HTTP(S) scheme + if (!urlIsHttpHttpsScheme(requestCurrentURL(request))) { + // Return a network error. + return makeNetworkError('URL scheme must be a HTTP(S) scheme') + } + + // - request’s use-CORS-preflight flag is set + // - request’s unsafe-request flag is set and either request’s method is + // not a CORS-safelisted method or CORS-unsafe request-header names with + // request’s header list is not empty + // 1. Set request’s response tainting to "cors". + // 2. Let corsWithPreflightResponse be the result of running HTTP fetch + // given fetchParams and true. + // 3. If corsWithPreflightResponse is a network error, then clear cache + // entries using request. + // 4. Return corsWithPreflightResponse. + // TODO + + // Otherwise + // 1. Set request’s response tainting to "cors". + request.responseTainting = 'cors' + + // 2. Return the result of running HTTP fetch given fetchParams. + return await httpFetch(fetchParams) + })() + } + + // 12. If recursive is true, then return response. + if (recursive) { + return response + } + + // 13. If response is not a network error and response is not a filtered + // response, then: + if (response.status !== 0 && !response.internalResponse) { + // If request’s response tainting is "cors", then: + if (request.responseTainting === 'cors') { + // 1. Let headerNames be the result of extracting header list values + // given `Access-Control-Expose-Headers` and response’s header list. + // TODO + // 2. If request’s credentials mode is not "include" and headerNames + // contains `*`, then set response’s CORS-exposed header-name list to + // all unique header names in response’s header list. + // TODO + // 3. Otherwise, if headerNames is not null or failure, then set + // response’s CORS-exposed header-name list to headerNames. + // TODO + } + + // Set response to the following filtered response with response as its + // internal response, depending on request’s response tainting: + if (request.responseTainting === 'basic') { + response = filterResponse(response, 'basic') + } else if (request.responseTainting === 'cors') { + response = filterResponse(response, 'cors') + } else if (request.responseTainting === 'opaque') { + response = filterResponse(response, 'opaque') + } else { + assert(false) + } + } + + // 14. Let internalResponse be response, if response is a network error, + // and response’s internal response otherwise. + let internalResponse = + response.status === 0 ? response : response.internalResponse + + // 15. If internalResponse’s URL list is empty, then set it to a clone of + // request’s URL list. + if (internalResponse.urlList.length === 0) { + internalResponse.urlList.push(...request.urlList) + } + + // 16. If request’s timing allow failed flag is unset, then set + // internalResponse’s timing allow passed flag. + if (!request.timingAllowFailed) { + response.timingAllowPassed = true + } + + // 17. If response is not a network error and any of the following returns + // blocked + // - should internalResponse to request be blocked as mixed content + // - should internalResponse to request be blocked by Content Security Policy + // - should internalResponse to request be blocked due to its MIME type + // - should internalResponse to request be blocked due to nosniff + // TODO + + // 18. If response’s type is "opaque", internalResponse’s status is 206, + // internalResponse’s range-requested flag is set, and request’s header + // list does not contain `Range`, then set response and internalResponse + // to a network error. + if ( + response.type === 'opaque' && + internalResponse.status === 206 && + internalResponse.rangeRequested && + !request.headers.contains('range') + ) { + response = internalResponse = makeNetworkError() + } + + // 19. If response is not a network error and either request’s method is + // `HEAD` or `CONNECT`, or internalResponse’s status is a null body status, + // set internalResponse’s body to null and disregard any enqueuing toward + // it (if any). + if ( + response.status !== 0 && + (request.method === 'HEAD' || + request.method === 'CONNECT' || + nullBodyStatus.includes(internalResponse.status)) + ) { + internalResponse.body = null + fetchParams.controller.dump = true + } + + // 20. If request’s integrity metadata is not the empty string, then: + if (request.integrity) { + // 1. Let processBodyError be this step: run fetch finale given fetchParams + // and a network error. + const processBodyError = (reason) => + fetchFinale(fetchParams, makeNetworkError(reason)) + + // 2. If request’s response tainting is "opaque", or response’s body is null, + // then run processBodyError and abort these steps. + if (request.responseTainting === 'opaque' || response.body == null) { + processBodyError(response.error) + return + } + + // 3. Let processBody given bytes be these steps: + const processBody = (bytes) => { + // 1. If bytes do not match request’s integrity metadata, + // then run processBodyError and abort these steps. [SRI] + if (!bytesMatch(bytes, request.integrity)) { + processBodyError('integrity mismatch') + return + } + + // 2. Set response’s body to bytes as a body. + response.body = safelyExtractBody(bytes)[0] + + // 3. Run fetch finale given fetchParams and response. + fetchFinale(fetchParams, response) + } + + // 4. Fully read response’s body given processBody and processBodyError. + await fullyReadBody(response.body, processBody, processBodyError) + } else { + // 21. Otherwise, run fetch finale given fetchParams and response. + fetchFinale(fetchParams, response) + } +} + +// https://fetch.spec.whatwg.org/#concept-scheme-fetch +// given a fetch params fetchParams +function schemeFetch (fetchParams) { + // Note: since the connection is destroyed on redirect, which sets fetchParams to a + // cancelled state, we do not want this condition to trigger *unless* there have been + // no redirects. See https://github.com/nodejs/undici/issues/1776 + // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams. + if (isCancelled(fetchParams) && fetchParams.request.redirectCount === 0) { + return Promise.resolve(makeAppropriateNetworkError(fetchParams)) + } + + // 2. Let request be fetchParams’s request. + const { request } = fetchParams + + const { protocol: scheme } = requestCurrentURL(request) + + // 3. Switch on request’s current URL’s scheme and run the associated steps: + switch (scheme) { + case 'about:': { + // If request’s current URL’s path is the string "blank", then return a new response + // whose status message is `OK`, header list is « (`Content-Type`, `text/html;charset=utf-8`) », + // and body is the empty byte sequence as a body. + + // Otherwise, return a network error. + return Promise.resolve(makeNetworkError('about scheme is not supported')) + } + case 'blob:': { + if (!resolveObjectURL) { + resolveObjectURL = (__nccwpck_require__(181).resolveObjectURL) + } + + // 1. Let blobURLEntry be request’s current URL’s blob URL entry. + const blobURLEntry = requestCurrentURL(request) + + // https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L52-L56 + // Buffer.resolveObjectURL does not ignore URL queries. + if (blobURLEntry.search.length !== 0) { + return Promise.resolve(makeNetworkError('NetworkError when attempting to fetch resource.')) + } + + const blobURLEntryObject = resolveObjectURL(blobURLEntry.toString()) + + // 2. If request’s method is not `GET`, blobURLEntry is null, or blobURLEntry’s + // object is not a Blob object, then return a network error. + if (request.method !== 'GET' || !isBlobLike(blobURLEntryObject)) { + return Promise.resolve(makeNetworkError('invalid method')) + } + + // 3. Let bodyWithType be the result of safely extracting blobURLEntry’s object. + const bodyWithType = safelyExtractBody(blobURLEntryObject) + + // 4. Let body be bodyWithType’s body. + const body = bodyWithType[0] + + // 5. Let length be body’s length, serialized and isomorphic encoded. + const length = isomorphicEncode(`${body.length}`) + + // 6. Let type be bodyWithType’s type if it is non-null; otherwise the empty byte sequence. + const type = bodyWithType[1] ?? '' + + // 7. Return a new response whose status message is `OK`, header list is + // « (`Content-Length`, length), (`Content-Type`, type) », and body is body. + const response = makeResponse({ + statusText: 'OK', + headersList: [ + ['content-length', { name: 'Content-Length', value: length }], + ['content-type', { name: 'Content-Type', value: type }] + ] + }) + + response.body = body + + return Promise.resolve(response) + } + case 'data:': { + // 1. Let dataURLStruct be the result of running the + // data: URL processor on request’s current URL. + const currentURL = requestCurrentURL(request) + const dataURLStruct = dataURLProcessor(currentURL) + + // 2. If dataURLStruct is failure, then return a + // network error. + if (dataURLStruct === 'failure') { + return Promise.resolve(makeNetworkError('failed to fetch the data URL')) + } + + // 3. Let mimeType be dataURLStruct’s MIME type, serialized. + const mimeType = serializeAMimeType(dataURLStruct.mimeType) + + // 4. Return a response whose status message is `OK`, + // header list is « (`Content-Type`, mimeType) », + // and body is dataURLStruct’s body as a body. + return Promise.resolve(makeResponse({ + statusText: 'OK', + headersList: [ + ['content-type', { name: 'Content-Type', value: mimeType }] + ], + body: safelyExtractBody(dataURLStruct.body)[0] + })) + } + case 'file:': { + // For now, unfortunate as it is, file URLs are left as an exercise for the reader. + // When in doubt, return a network error. + return Promise.resolve(makeNetworkError('not implemented... yet...')) + } + case 'http:': + case 'https:': { + // Return the result of running HTTP fetch given fetchParams. + + return httpFetch(fetchParams) + .catch((err) => makeNetworkError(err)) + } + default: { + return Promise.resolve(makeNetworkError('unknown scheme')) + } + } +} + +// https://fetch.spec.whatwg.org/#finalize-response +function finalizeResponse (fetchParams, response) { + // 1. Set fetchParams’s request’s done flag. + fetchParams.request.done = true + + // 2, If fetchParams’s process response done is not null, then queue a fetch + // task to run fetchParams’s process response done given response, with + // fetchParams’s task destination. + if (fetchParams.processResponseDone != null) { + queueMicrotask(() => fetchParams.processResponseDone(response)) + } +} + +// https://fetch.spec.whatwg.org/#fetch-finale +function fetchFinale (fetchParams, response) { + // 1. If response is a network error, then: + if (response.type === 'error') { + // 1. Set response’s URL list to « fetchParams’s request’s URL list[0] ». + response.urlList = [fetchParams.request.urlList[0]] + + // 2. Set response’s timing info to the result of creating an opaque timing + // info for fetchParams’s timing info. + response.timingInfo = createOpaqueTimingInfo({ + startTime: fetchParams.timingInfo.startTime + }) + } + + // 2. Let processResponseEndOfBody be the following steps: + const processResponseEndOfBody = () => { + // 1. Set fetchParams’s request’s done flag. + fetchParams.request.done = true + + // If fetchParams’s process response end-of-body is not null, + // then queue a fetch task to run fetchParams’s process response + // end-of-body given response with fetchParams’s task destination. + if (fetchParams.processResponseEndOfBody != null) { + queueMicrotask(() => fetchParams.processResponseEndOfBody(response)) + } + } + + // 3. If fetchParams’s process response is non-null, then queue a fetch task + // to run fetchParams’s process response given response, with fetchParams’s + // task destination. + if (fetchParams.processResponse != null) { + queueMicrotask(() => fetchParams.processResponse(response)) + } + + // 4. If response’s body is null, then run processResponseEndOfBody. + if (response.body == null) { + processResponseEndOfBody() + } else { + // 5. Otherwise: + + // 1. Let transformStream be a new a TransformStream. + + // 2. Let identityTransformAlgorithm be an algorithm which, given chunk, + // enqueues chunk in transformStream. + const identityTransformAlgorithm = (chunk, controller) => { + controller.enqueue(chunk) + } + + // 3. Set up transformStream with transformAlgorithm set to identityTransformAlgorithm + // and flushAlgorithm set to processResponseEndOfBody. + const transformStream = new TransformStream({ + start () {}, + transform: identityTransformAlgorithm, + flush: processResponseEndOfBody + }, { + size () { + return 1 + } + }, { + size () { + return 1 + } + }) + + // 4. Set response’s body to the result of piping response’s body through transformStream. + response.body = { stream: response.body.stream.pipeThrough(transformStream) } + } + + // 6. If fetchParams’s process response consume body is non-null, then: + if (fetchParams.processResponseConsumeBody != null) { + // 1. Let processBody given nullOrBytes be this step: run fetchParams’s + // process response consume body given response and nullOrBytes. + const processBody = (nullOrBytes) => fetchParams.processResponseConsumeBody(response, nullOrBytes) + + // 2. Let processBodyError be this step: run fetchParams’s process + // response consume body given response and failure. + const processBodyError = (failure) => fetchParams.processResponseConsumeBody(response, failure) + + // 3. If response’s body is null, then queue a fetch task to run processBody + // given null, with fetchParams’s task destination. + if (response.body == null) { + queueMicrotask(() => processBody(null)) + } else { + // 4. Otherwise, fully read response’s body given processBody, processBodyError, + // and fetchParams’s task destination. + return fullyReadBody(response.body, processBody, processBodyError) + } + return Promise.resolve() + } +} + +// https://fetch.spec.whatwg.org/#http-fetch +async function httpFetch (fetchParams) { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let response be null. + let response = null + + // 3. Let actualResponse be null. + let actualResponse = null + + // 4. Let timingInfo be fetchParams’s timing info. + const timingInfo = fetchParams.timingInfo + + // 5. If request’s service-workers mode is "all", then: + if (request.serviceWorkers === 'all') { + // TODO + } + + // 6. If response is null, then: + if (response === null) { + // 1. If makeCORSPreflight is true and one of these conditions is true: + // TODO + + // 2. If request’s redirect mode is "follow", then set request’s + // service-workers mode to "none". + if (request.redirect === 'follow') { + request.serviceWorkers = 'none' + } + + // 3. Set response and actualResponse to the result of running + // HTTP-network-or-cache fetch given fetchParams. + actualResponse = response = await httpNetworkOrCacheFetch(fetchParams) + + // 4. If request’s response tainting is "cors" and a CORS check + // for request and response returns failure, then return a network error. + if ( + request.responseTainting === 'cors' && + corsCheck(request, response) === 'failure' + ) { + return makeNetworkError('cors failure') + } + + // 5. If the TAO check for request and response returns failure, then set + // request’s timing allow failed flag. + if (TAOCheck(request, response) === 'failure') { + request.timingAllowFailed = true + } + } + + // 7. If either request’s response tainting or response’s type + // is "opaque", and the cross-origin resource policy check with + // request’s origin, request’s client, request’s destination, + // and actualResponse returns blocked, then return a network error. + if ( + (request.responseTainting === 'opaque' || response.type === 'opaque') && + crossOriginResourcePolicyCheck( + request.origin, + request.client, + request.destination, + actualResponse + ) === 'blocked' + ) { + return makeNetworkError('blocked') + } + + // 8. If actualResponse’s status is a redirect status, then: + if (redirectStatusSet.has(actualResponse.status)) { + // 1. If actualResponse’s status is not 303, request’s body is not null, + // and the connection uses HTTP/2, then user agents may, and are even + // encouraged to, transmit an RST_STREAM frame. + // See, https://github.com/whatwg/fetch/issues/1288 + if (request.redirect !== 'manual') { + fetchParams.controller.connection.destroy() + } + + // 2. Switch on request’s redirect mode: + if (request.redirect === 'error') { + // Set response to a network error. + response = makeNetworkError('unexpected redirect') + } else if (request.redirect === 'manual') { + // Set response to an opaque-redirect filtered response whose internal + // response is actualResponse. + // NOTE(spec): On the web this would return an `opaqueredirect` response, + // but that doesn't make sense server side. + // See https://github.com/nodejs/undici/issues/1193. + response = actualResponse + } else if (request.redirect === 'follow') { + // Set response to the result of running HTTP-redirect fetch given + // fetchParams and response. + response = await httpRedirectFetch(fetchParams, response) + } else { + assert(false) + } + } + + // 9. Set response’s timing info to timingInfo. + response.timingInfo = timingInfo + + // 10. Return response. + return response +} + +// https://fetch.spec.whatwg.org/#http-redirect-fetch +function httpRedirectFetch (fetchParams, response) { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let actualResponse be response, if response is not a filtered response, + // and response’s internal response otherwise. + const actualResponse = response.internalResponse + ? response.internalResponse + : response + + // 3. Let locationURL be actualResponse’s location URL given request’s current + // URL’s fragment. + let locationURL + + try { + locationURL = responseLocationURL( + actualResponse, + requestCurrentURL(request).hash + ) + + // 4. If locationURL is null, then return response. + if (locationURL == null) { + return response + } + } catch (err) { + // 5. If locationURL is failure, then return a network error. + return Promise.resolve(makeNetworkError(err)) + } + + // 6. If locationURL’s scheme is not an HTTP(S) scheme, then return a network + // error. + if (!urlIsHttpHttpsScheme(locationURL)) { + return Promise.resolve(makeNetworkError('URL scheme must be a HTTP(S) scheme')) + } + + // 7. If request’s redirect count is 20, then return a network error. + if (request.redirectCount === 20) { + return Promise.resolve(makeNetworkError('redirect count exceeded')) + } + + // 8. Increase request’s redirect count by 1. + request.redirectCount += 1 + + // 9. If request’s mode is "cors", locationURL includes credentials, and + // request’s origin is not same origin with locationURL’s origin, then return + // a network error. + if ( + request.mode === 'cors' && + (locationURL.username || locationURL.password) && + !sameOrigin(request, locationURL) + ) { + return Promise.resolve(makeNetworkError('cross origin not allowed for request mode "cors"')) + } + + // 10. If request’s response tainting is "cors" and locationURL includes + // credentials, then return a network error. + if ( + request.responseTainting === 'cors' && + (locationURL.username || locationURL.password) + ) { + return Promise.resolve(makeNetworkError( + 'URL cannot contain credentials for request mode "cors"' + )) + } + + // 11. If actualResponse’s status is not 303, request’s body is non-null, + // and request’s body’s source is null, then return a network error. + if ( + actualResponse.status !== 303 && + request.body != null && + request.body.source == null + ) { + return Promise.resolve(makeNetworkError()) + } + + // 12. If one of the following is true + // - actualResponse’s status is 301 or 302 and request’s method is `POST` + // - actualResponse’s status is 303 and request’s method is not `GET` or `HEAD` + if ( + ([301, 302].includes(actualResponse.status) && request.method === 'POST') || + (actualResponse.status === 303 && + !GET_OR_HEAD.includes(request.method)) + ) { + // then: + // 1. Set request’s method to `GET` and request’s body to null. + request.method = 'GET' + request.body = null + + // 2. For each headerName of request-body-header name, delete headerName from + // request’s header list. + for (const headerName of requestBodyHeader) { + request.headersList.delete(headerName) + } + } + + // 13. If request’s current URL’s origin is not same origin with locationURL’s + // origin, then for each headerName of CORS non-wildcard request-header name, + // delete headerName from request’s header list. + if (!sameOrigin(requestCurrentURL(request), locationURL)) { + // https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name + request.headersList.delete('authorization') + + // https://fetch.spec.whatwg.org/#authentication-entries + request.headersList.delete('proxy-authorization', true) + + // "Cookie" and "Host" are forbidden request-headers, which undici doesn't implement. + request.headersList.delete('cookie') + request.headersList.delete('host') + } + + // 14. If request’s body is non-null, then set request’s body to the first return + // value of safely extracting request’s body’s source. + if (request.body != null) { + assert(request.body.source != null) + request.body = safelyExtractBody(request.body.source)[0] + } + + // 15. Let timingInfo be fetchParams’s timing info. + const timingInfo = fetchParams.timingInfo + + // 16. Set timingInfo’s redirect end time and post-redirect start time to the + // coarsened shared current time given fetchParams’s cross-origin isolated + // capability. + timingInfo.redirectEndTime = timingInfo.postRedirectStartTime = + coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability) + + // 17. If timingInfo’s redirect start time is 0, then set timingInfo’s + // redirect start time to timingInfo’s start time. + if (timingInfo.redirectStartTime === 0) { + timingInfo.redirectStartTime = timingInfo.startTime + } + + // 18. Append locationURL to request’s URL list. + request.urlList.push(locationURL) + + // 19. Invoke set request’s referrer policy on redirect on request and + // actualResponse. + setRequestReferrerPolicyOnRedirect(request, actualResponse) + + // 20. Return the result of running main fetch given fetchParams and true. + return mainFetch(fetchParams, true) +} + +// https://fetch.spec.whatwg.org/#http-network-or-cache-fetch +async function httpNetworkOrCacheFetch ( + fetchParams, + isAuthenticationFetch = false, + isNewConnectionFetch = false +) { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let httpFetchParams be null. + let httpFetchParams = null + + // 3. Let httpRequest be null. + let httpRequest = null + + // 4. Let response be null. + let response = null + + // 5. Let storedResponse be null. + // TODO: cache + + // 6. Let httpCache be null. + const httpCache = null + + // 7. Let the revalidatingFlag be unset. + const revalidatingFlag = false + + // 8. Run these steps, but abort when the ongoing fetch is terminated: + + // 1. If request’s window is "no-window" and request’s redirect mode is + // "error", then set httpFetchParams to fetchParams and httpRequest to + // request. + if (request.window === 'no-window' && request.redirect === 'error') { + httpFetchParams = fetchParams + httpRequest = request + } else { + // Otherwise: + + // 1. Set httpRequest to a clone of request. + httpRequest = makeRequest(request) + + // 2. Set httpFetchParams to a copy of fetchParams. + httpFetchParams = { ...fetchParams } + + // 3. Set httpFetchParams’s request to httpRequest. + httpFetchParams.request = httpRequest + } + + // 3. Let includeCredentials be true if one of + const includeCredentials = + request.credentials === 'include' || + (request.credentials === 'same-origin' && + request.responseTainting === 'basic') + + // 4. Let contentLength be httpRequest’s body’s length, if httpRequest’s + // body is non-null; otherwise null. + const contentLength = httpRequest.body ? httpRequest.body.length : null + + // 5. Let contentLengthHeaderValue be null. + let contentLengthHeaderValue = null + + // 6. If httpRequest’s body is null and httpRequest’s method is `POST` or + // `PUT`, then set contentLengthHeaderValue to `0`. + if ( + httpRequest.body == null && + ['POST', 'PUT'].includes(httpRequest.method) + ) { + contentLengthHeaderValue = '0' + } + + // 7. If contentLength is non-null, then set contentLengthHeaderValue to + // contentLength, serialized and isomorphic encoded. + if (contentLength != null) { + contentLengthHeaderValue = isomorphicEncode(`${contentLength}`) + } + + // 8. If contentLengthHeaderValue is non-null, then append + // `Content-Length`/contentLengthHeaderValue to httpRequest’s header + // list. + if (contentLengthHeaderValue != null) { + httpRequest.headersList.append('content-length', contentLengthHeaderValue) + } + + // 9. If contentLengthHeaderValue is non-null, then append (`Content-Length`, + // contentLengthHeaderValue) to httpRequest’s header list. + + // 10. If contentLength is non-null and httpRequest’s keepalive is true, + // then: + if (contentLength != null && httpRequest.keepalive) { + // NOTE: keepalive is a noop outside of browser context. + } + + // 11. If httpRequest’s referrer is a URL, then append + // `Referer`/httpRequest’s referrer, serialized and isomorphic encoded, + // to httpRequest’s header list. + if (httpRequest.referrer instanceof URL) { + httpRequest.headersList.append('referer', isomorphicEncode(httpRequest.referrer.href)) + } + + // 12. Append a request `Origin` header for httpRequest. + appendRequestOriginHeader(httpRequest) + + // 13. Append the Fetch metadata headers for httpRequest. [FETCH-METADATA] + appendFetchMetadata(httpRequest) + + // 14. If httpRequest’s header list does not contain `User-Agent`, then + // user agents should append `User-Agent`/default `User-Agent` value to + // httpRequest’s header list. + if (!httpRequest.headersList.contains('user-agent')) { + httpRequest.headersList.append('user-agent', typeof esbuildDetection === 'undefined' ? 'undici' : 'node') + } + + // 15. If httpRequest’s cache mode is "default" and httpRequest’s header + // list contains `If-Modified-Since`, `If-None-Match`, + // `If-Unmodified-Since`, `If-Match`, or `If-Range`, then set + // httpRequest’s cache mode to "no-store". + if ( + httpRequest.cache === 'default' && + (httpRequest.headersList.contains('if-modified-since') || + httpRequest.headersList.contains('if-none-match') || + httpRequest.headersList.contains('if-unmodified-since') || + httpRequest.headersList.contains('if-match') || + httpRequest.headersList.contains('if-range')) + ) { + httpRequest.cache = 'no-store' + } + + // 16. If httpRequest’s cache mode is "no-cache", httpRequest’s prevent + // no-cache cache-control header modification flag is unset, and + // httpRequest’s header list does not contain `Cache-Control`, then append + // `Cache-Control`/`max-age=0` to httpRequest’s header list. + if ( + httpRequest.cache === 'no-cache' && + !httpRequest.preventNoCacheCacheControlHeaderModification && + !httpRequest.headersList.contains('cache-control') + ) { + httpRequest.headersList.append('cache-control', 'max-age=0') + } + + // 17. If httpRequest’s cache mode is "no-store" or "reload", then: + if (httpRequest.cache === 'no-store' || httpRequest.cache === 'reload') { + // 1. If httpRequest’s header list does not contain `Pragma`, then append + // `Pragma`/`no-cache` to httpRequest’s header list. + if (!httpRequest.headersList.contains('pragma')) { + httpRequest.headersList.append('pragma', 'no-cache') + } + + // 2. If httpRequest’s header list does not contain `Cache-Control`, + // then append `Cache-Control`/`no-cache` to httpRequest’s header list. + if (!httpRequest.headersList.contains('cache-control')) { + httpRequest.headersList.append('cache-control', 'no-cache') + } + } + + // 18. If httpRequest’s header list contains `Range`, then append + // `Accept-Encoding`/`identity` to httpRequest’s header list. + if (httpRequest.headersList.contains('range')) { + httpRequest.headersList.append('accept-encoding', 'identity') + } + + // 19. Modify httpRequest’s header list per HTTP. Do not append a given + // header if httpRequest’s header list contains that header’s name. + // TODO: https://github.com/whatwg/fetch/issues/1285#issuecomment-896560129 + if (!httpRequest.headersList.contains('accept-encoding')) { + if (urlHasHttpsScheme(requestCurrentURL(httpRequest))) { + httpRequest.headersList.append('accept-encoding', 'br, gzip, deflate') + } else { + httpRequest.headersList.append('accept-encoding', 'gzip, deflate') + } + } + + httpRequest.headersList.delete('host') + + // 20. If includeCredentials is true, then: + if (includeCredentials) { + // 1. If the user agent is not configured to block cookies for httpRequest + // (see section 7 of [COOKIES]), then: + // TODO: credentials + // 2. If httpRequest’s header list does not contain `Authorization`, then: + // TODO: credentials + } + + // 21. If there’s a proxy-authentication entry, use it as appropriate. + // TODO: proxy-authentication + + // 22. Set httpCache to the result of determining the HTTP cache + // partition, given httpRequest. + // TODO: cache + + // 23. If httpCache is null, then set httpRequest’s cache mode to + // "no-store". + if (httpCache == null) { + httpRequest.cache = 'no-store' + } + + // 24. If httpRequest’s cache mode is neither "no-store" nor "reload", + // then: + if (httpRequest.mode !== 'no-store' && httpRequest.mode !== 'reload') { + // TODO: cache + } + + // 9. If aborted, then return the appropriate network error for fetchParams. + // TODO + + // 10. If response is null, then: + if (response == null) { + // 1. If httpRequest’s cache mode is "only-if-cached", then return a + // network error. + if (httpRequest.mode === 'only-if-cached') { + return makeNetworkError('only if cached') + } + + // 2. Let forwardResponse be the result of running HTTP-network fetch + // given httpFetchParams, includeCredentials, and isNewConnectionFetch. + const forwardResponse = await httpNetworkFetch( + httpFetchParams, + includeCredentials, + isNewConnectionFetch + ) + + // 3. If httpRequest’s method is unsafe and forwardResponse’s status is + // in the range 200 to 399, inclusive, invalidate appropriate stored + // responses in httpCache, as per the "Invalidation" chapter of HTTP + // Caching, and set storedResponse to null. [HTTP-CACHING] + if ( + !safeMethodsSet.has(httpRequest.method) && + forwardResponse.status >= 200 && + forwardResponse.status <= 399 + ) { + // TODO: cache + } + + // 4. If the revalidatingFlag is set and forwardResponse’s status is 304, + // then: + if (revalidatingFlag && forwardResponse.status === 304) { + // TODO: cache + } + + // 5. If response is null, then: + if (response == null) { + // 1. Set response to forwardResponse. + response = forwardResponse + + // 2. Store httpRequest and forwardResponse in httpCache, as per the + // "Storing Responses in Caches" chapter of HTTP Caching. [HTTP-CACHING] + // TODO: cache + } + } + + // 11. Set response’s URL list to a clone of httpRequest’s URL list. + response.urlList = [...httpRequest.urlList] + + // 12. If httpRequest’s header list contains `Range`, then set response’s + // range-requested flag. + if (httpRequest.headersList.contains('range')) { + response.rangeRequested = true + } + + // 13. Set response’s request-includes-credentials to includeCredentials. + response.requestIncludesCredentials = includeCredentials + + // 14. If response’s status is 401, httpRequest’s response tainting is not + // "cors", includeCredentials is true, and request’s window is an environment + // settings object, then: + // TODO + + // 15. If response’s status is 407, then: + if (response.status === 407) { + // 1. If request’s window is "no-window", then return a network error. + if (request.window === 'no-window') { + return makeNetworkError() + } + + // 2. ??? + + // 3. If fetchParams is canceled, then return the appropriate network error for fetchParams. + if (isCancelled(fetchParams)) { + return makeAppropriateNetworkError(fetchParams) + } + + // 4. Prompt the end user as appropriate in request’s window and store + // the result as a proxy-authentication entry. [HTTP-AUTH] + // TODO: Invoke some kind of callback? + + // 5. Set response to the result of running HTTP-network-or-cache fetch given + // fetchParams. + // TODO + return makeNetworkError('proxy authentication required') + } + + // 16. If all of the following are true + if ( + // response’s status is 421 + response.status === 421 && + // isNewConnectionFetch is false + !isNewConnectionFetch && + // request’s body is null, or request’s body is non-null and request’s body’s source is non-null + (request.body == null || request.body.source != null) + ) { + // then: + + // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams. + if (isCancelled(fetchParams)) { + return makeAppropriateNetworkError(fetchParams) + } + + // 2. Set response to the result of running HTTP-network-or-cache + // fetch given fetchParams, isAuthenticationFetch, and true. + + // TODO (spec): The spec doesn't specify this but we need to cancel + // the active response before we can start a new one. + // https://github.com/whatwg/fetch/issues/1293 + fetchParams.controller.connection.destroy() + + response = await httpNetworkOrCacheFetch( + fetchParams, + isAuthenticationFetch, + true + ) + } + + // 17. If isAuthenticationFetch is true, then create an authentication entry + if (isAuthenticationFetch) { + // TODO + } + + // 18. Return response. + return response +} + +// https://fetch.spec.whatwg.org/#http-network-fetch +async function httpNetworkFetch ( + fetchParams, + includeCredentials = false, + forceNewConnection = false +) { + assert(!fetchParams.controller.connection || fetchParams.controller.connection.destroyed) + + fetchParams.controller.connection = { + abort: null, + destroyed: false, + destroy (err) { + if (!this.destroyed) { + this.destroyed = true + this.abort?.(err ?? new DOMException('The operation was aborted.', 'AbortError')) + } + } + } + + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let response be null. + let response = null + + // 3. Let timingInfo be fetchParams’s timing info. + const timingInfo = fetchParams.timingInfo + + // 4. Let httpCache be the result of determining the HTTP cache partition, + // given request. + // TODO: cache + const httpCache = null + + // 5. If httpCache is null, then set request’s cache mode to "no-store". + if (httpCache == null) { + request.cache = 'no-store' + } + + // 6. Let networkPartitionKey be the result of determining the network + // partition key given request. + // TODO + + // 7. Let newConnection be "yes" if forceNewConnection is true; otherwise + // "no". + const newConnection = forceNewConnection ? 'yes' : 'no' // eslint-disable-line no-unused-vars + + // 8. Switch on request’s mode: + if (request.mode === 'websocket') { + // Let connection be the result of obtaining a WebSocket connection, + // given request’s current URL. + // TODO + } else { + // Let connection be the result of obtaining a connection, given + // networkPartitionKey, request’s current URL’s origin, + // includeCredentials, and forceNewConnection. + // TODO + } + + // 9. Run these steps, but abort when the ongoing fetch is terminated: + + // 1. If connection is failure, then return a network error. + + // 2. Set timingInfo’s final connection timing info to the result of + // calling clamp and coarsen connection timing info with connection’s + // timing info, timingInfo’s post-redirect start time, and fetchParams’s + // cross-origin isolated capability. + + // 3. If connection is not an HTTP/2 connection, request’s body is non-null, + // and request’s body’s source is null, then append (`Transfer-Encoding`, + // `chunked`) to request’s header list. + + // 4. Set timingInfo’s final network-request start time to the coarsened + // shared current time given fetchParams’s cross-origin isolated + // capability. + + // 5. Set response to the result of making an HTTP request over connection + // using request with the following caveats: + + // - Follow the relevant requirements from HTTP. [HTTP] [HTTP-SEMANTICS] + // [HTTP-COND] [HTTP-CACHING] [HTTP-AUTH] + + // - If request’s body is non-null, and request’s body’s source is null, + // then the user agent may have a buffer of up to 64 kibibytes and store + // a part of request’s body in that buffer. If the user agent reads from + // request’s body beyond that buffer’s size and the user agent needs to + // resend request, then instead return a network error. + + // - Set timingInfo’s final network-response start time to the coarsened + // shared current time given fetchParams’s cross-origin isolated capability, + // immediately after the user agent’s HTTP parser receives the first byte + // of the response (e.g., frame header bytes for HTTP/2 or response status + // line for HTTP/1.x). + + // - Wait until all the headers are transmitted. + + // - Any responses whose status is in the range 100 to 199, inclusive, + // and is not 101, are to be ignored, except for the purposes of setting + // timingInfo’s final network-response start time above. + + // - If request’s header list contains `Transfer-Encoding`/`chunked` and + // response is transferred via HTTP/1.0 or older, then return a network + // error. + + // - If the HTTP request results in a TLS client certificate dialog, then: + + // 1. If request’s window is an environment settings object, make the + // dialog available in request’s window. + + // 2. Otherwise, return a network error. + + // To transmit request’s body body, run these steps: + let requestBody = null + // 1. If body is null and fetchParams’s process request end-of-body is + // non-null, then queue a fetch task given fetchParams’s process request + // end-of-body and fetchParams’s task destination. + if (request.body == null && fetchParams.processRequestEndOfBody) { + queueMicrotask(() => fetchParams.processRequestEndOfBody()) + } else if (request.body != null) { + // 2. Otherwise, if body is non-null: + + // 1. Let processBodyChunk given bytes be these steps: + const processBodyChunk = async function * (bytes) { + // 1. If the ongoing fetch is terminated, then abort these steps. + if (isCancelled(fetchParams)) { + return + } + + // 2. Run this step in parallel: transmit bytes. + yield bytes + + // 3. If fetchParams’s process request body is non-null, then run + // fetchParams’s process request body given bytes’s length. + fetchParams.processRequestBodyChunkLength?.(bytes.byteLength) + } + + // 2. Let processEndOfBody be these steps: + const processEndOfBody = () => { + // 1. If fetchParams is canceled, then abort these steps. + if (isCancelled(fetchParams)) { + return + } + + // 2. If fetchParams’s process request end-of-body is non-null, + // then run fetchParams’s process request end-of-body. + if (fetchParams.processRequestEndOfBody) { + fetchParams.processRequestEndOfBody() + } + } + + // 3. Let processBodyError given e be these steps: + const processBodyError = (e) => { + // 1. If fetchParams is canceled, then abort these steps. + if (isCancelled(fetchParams)) { + return + } + + // 2. If e is an "AbortError" DOMException, then abort fetchParams’s controller. + if (e.name === 'AbortError') { + fetchParams.controller.abort() + } else { + fetchParams.controller.terminate(e) + } + } + + // 4. Incrementally read request’s body given processBodyChunk, processEndOfBody, + // processBodyError, and fetchParams’s task destination. + requestBody = (async function * () { + try { + for await (const bytes of request.body.stream) { + yield * processBodyChunk(bytes) + } + processEndOfBody() + } catch (err) { + processBodyError(err) + } + })() + } + + try { + // socket is only provided for websockets + const { body, status, statusText, headersList, socket } = await dispatch({ body: requestBody }) + + if (socket) { + response = makeResponse({ status, statusText, headersList, socket }) + } else { + const iterator = body[Symbol.asyncIterator]() + fetchParams.controller.next = () => iterator.next() + + response = makeResponse({ status, statusText, headersList }) + } + } catch (err) { + // 10. If aborted, then: + if (err.name === 'AbortError') { + // 1. If connection uses HTTP/2, then transmit an RST_STREAM frame. + fetchParams.controller.connection.destroy() + + // 2. Return the appropriate network error for fetchParams. + return makeAppropriateNetworkError(fetchParams, err) + } + + return makeNetworkError(err) + } + + // 11. Let pullAlgorithm be an action that resumes the ongoing fetch + // if it is suspended. + const pullAlgorithm = () => { + fetchParams.controller.resume() + } + + // 12. Let cancelAlgorithm be an algorithm that aborts fetchParams’s + // controller with reason, given reason. + const cancelAlgorithm = (reason) => { + fetchParams.controller.abort(reason) + } + + // 13. Let highWaterMark be a non-negative, non-NaN number, chosen by + // the user agent. + // TODO + + // 14. Let sizeAlgorithm be an algorithm that accepts a chunk object + // and returns a non-negative, non-NaN, non-infinite number, chosen by the user agent. + // TODO + + // 15. Let stream be a new ReadableStream. + // 16. Set up stream with pullAlgorithm set to pullAlgorithm, + // cancelAlgorithm set to cancelAlgorithm, highWaterMark set to + // highWaterMark, and sizeAlgorithm set to sizeAlgorithm. + if (!ReadableStream) { + ReadableStream = (__nccwpck_require__(3774).ReadableStream) + } + + const stream = new ReadableStream( + { + async start (controller) { + fetchParams.controller.controller = controller + }, + async pull (controller) { + await pullAlgorithm(controller) + }, + async cancel (reason) { + await cancelAlgorithm(reason) + } + }, + { + highWaterMark: 0, + size () { + return 1 + } + } + ) + + // 17. Run these steps, but abort when the ongoing fetch is terminated: + + // 1. Set response’s body to a new body whose stream is stream. + response.body = { stream } + + // 2. If response is not a network error and request’s cache mode is + // not "no-store", then update response in httpCache for request. + // TODO + + // 3. If includeCredentials is true and the user agent is not configured + // to block cookies for request (see section 7 of [COOKIES]), then run the + // "set-cookie-string" parsing algorithm (see section 5.2 of [COOKIES]) on + // the value of each header whose name is a byte-case-insensitive match for + // `Set-Cookie` in response’s header list, if any, and request’s current URL. + // TODO + + // 18. If aborted, then: + // TODO + + // 19. Run these steps in parallel: + + // 1. Run these steps, but abort when fetchParams is canceled: + fetchParams.controller.on('terminated', onAborted) + fetchParams.controller.resume = async () => { + // 1. While true + while (true) { + // 1-3. See onData... + + // 4. Set bytes to the result of handling content codings given + // codings and bytes. + let bytes + let isFailure + try { + const { done, value } = await fetchParams.controller.next() + + if (isAborted(fetchParams)) { + break + } + + bytes = done ? undefined : value + } catch (err) { + if (fetchParams.controller.ended && !timingInfo.encodedBodySize) { + // zlib doesn't like empty streams. + bytes = undefined + } else { + bytes = err + + // err may be propagated from the result of calling readablestream.cancel, + // which might not be an error. https://github.com/nodejs/undici/issues/2009 + isFailure = true + } + } + + if (bytes === undefined) { + // 2. Otherwise, if the bytes transmission for response’s message + // body is done normally and stream is readable, then close + // stream, finalize response for fetchParams and response, and + // abort these in-parallel steps. + readableStreamClose(fetchParams.controller.controller) + + finalizeResponse(fetchParams, response) + + return + } + + // 5. Increase timingInfo’s decoded body size by bytes’s length. + timingInfo.decodedBodySize += bytes?.byteLength ?? 0 + + // 6. If bytes is failure, then terminate fetchParams’s controller. + if (isFailure) { + fetchParams.controller.terminate(bytes) + return + } + + // 7. Enqueue a Uint8Array wrapping an ArrayBuffer containing bytes + // into stream. + fetchParams.controller.controller.enqueue(new Uint8Array(bytes)) + + // 8. If stream is errored, then terminate the ongoing fetch. + if (isErrored(stream)) { + fetchParams.controller.terminate() + return + } + + // 9. If stream doesn’t need more data ask the user agent to suspend + // the ongoing fetch. + if (!fetchParams.controller.controller.desiredSize) { + return + } + } + } + + // 2. If aborted, then: + function onAborted (reason) { + // 2. If fetchParams is aborted, then: + if (isAborted(fetchParams)) { + // 1. Set response’s aborted flag. + response.aborted = true + + // 2. If stream is readable, then error stream with the result of + // deserialize a serialized abort reason given fetchParams’s + // controller’s serialized abort reason and an + // implementation-defined realm. + if (isReadable(stream)) { + fetchParams.controller.controller.error( + fetchParams.controller.serializedAbortReason + ) + } + } else { + // 3. Otherwise, if stream is readable, error stream with a TypeError. + if (isReadable(stream)) { + fetchParams.controller.controller.error(new TypeError('terminated', { + cause: isErrorLike(reason) ? reason : undefined + })) + } + } + + // 4. If connection uses HTTP/2, then transmit an RST_STREAM frame. + // 5. Otherwise, the user agent should close connection unless it would be bad for performance to do so. + fetchParams.controller.connection.destroy() + } + + // 20. Return response. + return response + + async function dispatch ({ body }) { + const url = requestCurrentURL(request) + /** @type {import('../..').Agent} */ + const agent = fetchParams.controller.dispatcher + + return new Promise((resolve, reject) => agent.dispatch( + { + path: url.pathname + url.search, + origin: url.origin, + method: request.method, + body: fetchParams.controller.dispatcher.isMockActive ? request.body && (request.body.source || request.body.stream) : body, + headers: request.headersList.entries, + maxRedirections: 0, + upgrade: request.mode === 'websocket' ? 'websocket' : undefined + }, + { + body: null, + abort: null, + + onConnect (abort) { + // TODO (fix): Do we need connection here? + const { connection } = fetchParams.controller + + if (connection.destroyed) { + abort(new DOMException('The operation was aborted.', 'AbortError')) + } else { + fetchParams.controller.on('terminated', abort) + this.abort = connection.abort = abort + } + }, + + onHeaders (status, headersList, resume, statusText) { + if (status < 200) { + return + } + + let codings = [] + let location = '' + + const headers = new Headers() + + // For H2, the headers are a plain JS object + // We distinguish between them and iterate accordingly + if (Array.isArray(headersList)) { + for (let n = 0; n < headersList.length; n += 2) { + const key = headersList[n + 0].toString('latin1') + const val = headersList[n + 1].toString('latin1') + if (key.toLowerCase() === 'content-encoding') { + // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1 + // "All content-coding values are case-insensitive..." + codings = val.toLowerCase().split(',').map((x) => x.trim()) + } else if (key.toLowerCase() === 'location') { + location = val + } + + headers[kHeadersList].append(key, val) + } + } else { + const keys = Object.keys(headersList) + for (const key of keys) { + const val = headersList[key] + if (key.toLowerCase() === 'content-encoding') { + // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1 + // "All content-coding values are case-insensitive..." + codings = val.toLowerCase().split(',').map((x) => x.trim()).reverse() + } else if (key.toLowerCase() === 'location') { + location = val + } + + headers[kHeadersList].append(key, val) + } + } + + this.body = new Readable({ read: resume }) + + const decoders = [] + + const willFollow = request.redirect === 'follow' && + location && + redirectStatusSet.has(status) + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding + if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) { + for (const coding of codings) { + // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2 + if (coding === 'x-gzip' || coding === 'gzip') { + decoders.push(zlib.createGunzip({ + // Be less strict when decoding compressed responses, since sometimes + // servers send slightly invalid responses that are still accepted + // by common browsers. + // Always using Z_SYNC_FLUSH is what cURL does. + flush: zlib.constants.Z_SYNC_FLUSH, + finishFlush: zlib.constants.Z_SYNC_FLUSH + })) + } else if (coding === 'deflate') { + decoders.push(zlib.createInflate()) + } else if (coding === 'br') { + decoders.push(zlib.createBrotliDecompress()) + } else { + decoders.length = 0 + break + } + } + } + + resolve({ + status, + statusText, + headersList: headers[kHeadersList], + body: decoders.length + ? pipeline(this.body, ...decoders, () => { }) + : this.body.on('error', () => {}) + }) + + return true + }, + + onData (chunk) { + if (fetchParams.controller.dump) { + return + } + + // 1. If one or more bytes have been transmitted from response’s + // message body, then: + + // 1. Let bytes be the transmitted bytes. + const bytes = chunk + + // 2. Let codings be the result of extracting header list values + // given `Content-Encoding` and response’s header list. + // See pullAlgorithm. + + // 3. Increase timingInfo’s encoded body size by bytes’s length. + timingInfo.encodedBodySize += bytes.byteLength + + // 4. See pullAlgorithm... + + return this.body.push(bytes) + }, + + onComplete () { + if (this.abort) { + fetchParams.controller.off('terminated', this.abort) + } + + fetchParams.controller.ended = true + + this.body.push(null) + }, + + onError (error) { + if (this.abort) { + fetchParams.controller.off('terminated', this.abort) + } + + this.body?.destroy(error) + + fetchParams.controller.terminate(error) + + reject(error) + }, + + onUpgrade (status, headersList, socket) { + if (status !== 101) { + return + } + + const headers = new Headers() + + for (let n = 0; n < headersList.length; n += 2) { + const key = headersList[n + 0].toString('latin1') + const val = headersList[n + 1].toString('latin1') + + headers[kHeadersList].append(key, val) + } + + resolve({ + status, + statusText: STATUS_CODES[status], + headersList: headers[kHeadersList], + socket + }) + + return true + } + } + )) + } +} + +module.exports = { + fetch, + Fetch, + fetching, + finalizeAndReportTiming +} + + +/***/ }), + +/***/ 1558: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +/* globals AbortController */ + + + +const { extractBody, mixinBody, cloneBody } = __nccwpck_require__(4655) +const { Headers, fill: fillHeaders, HeadersList } = __nccwpck_require__(161) +const { FinalizationRegistry } = __nccwpck_require__(5254)() +const util = __nccwpck_require__(1436) +const { + isValidHTTPToken, + sameOrigin, + normalizeMethod, + makePolicyContainer, + normalizeMethodRecord +} = __nccwpck_require__(3359) +const { + forbiddenMethodsSet, + corsSafeListedMethodsSet, + referrerPolicy, + requestRedirect, + requestMode, + requestCredentials, + requestCache, + requestDuplex +} = __nccwpck_require__(450) +const { kEnumerableProperty } = util +const { kHeaders, kSignal, kState, kGuard, kRealm } = __nccwpck_require__(834) +const { webidl } = __nccwpck_require__(274) +const { getGlobalOrigin } = __nccwpck_require__(960) +const { URLSerializer } = __nccwpck_require__(5294) +const { kHeadersList, kConstruct } = __nccwpck_require__(9583) +const assert = __nccwpck_require__(2613) +const { getMaxListeners, setMaxListeners, getEventListeners, defaultMaxListeners } = __nccwpck_require__(4434) + +let TransformStream = globalThis.TransformStream + +const kAbortController = Symbol('abortController') + +const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => { + signal.removeEventListener('abort', abort) +}) + +// https://fetch.spec.whatwg.org/#request-class +class Request { + // https://fetch.spec.whatwg.org/#dom-request + constructor (input, init = {}) { + if (input === kConstruct) { + return + } + + webidl.argumentLengthCheck(arguments, 1, { header: 'Request constructor' }) + + input = webidl.converters.RequestInfo(input) + init = webidl.converters.RequestInit(init) + + // https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object + this[kRealm] = { + settingsObject: { + baseUrl: getGlobalOrigin(), + get origin () { + return this.baseUrl?.origin + }, + policyContainer: makePolicyContainer() + } + } + + // 1. Let request be null. + let request = null + + // 2. Let fallbackMode be null. + let fallbackMode = null + + // 3. Let baseURL be this’s relevant settings object’s API base URL. + const baseUrl = this[kRealm].settingsObject.baseUrl + + // 4. Let signal be null. + let signal = null + + // 5. If input is a string, then: + if (typeof input === 'string') { + // 1. Let parsedURL be the result of parsing input with baseURL. + // 2. If parsedURL is failure, then throw a TypeError. + let parsedURL + try { + parsedURL = new URL(input, baseUrl) + } catch (err) { + throw new TypeError('Failed to parse URL from ' + input, { cause: err }) + } + + // 3. If parsedURL includes credentials, then throw a TypeError. + if (parsedURL.username || parsedURL.password) { + throw new TypeError( + 'Request cannot be constructed from a URL that includes credentials: ' + + input + ) + } + + // 4. Set request to a new request whose URL is parsedURL. + request = makeRequest({ urlList: [parsedURL] }) + + // 5. Set fallbackMode to "cors". + fallbackMode = 'cors' + } else { + // 6. Otherwise: + + // 7. Assert: input is a Request object. + assert(input instanceof Request) + + // 8. Set request to input’s request. + request = input[kState] + + // 9. Set signal to input’s signal. + signal = input[kSignal] + } + + // 7. Let origin be this’s relevant settings object’s origin. + const origin = this[kRealm].settingsObject.origin + + // 8. Let window be "client". + let window = 'client' + + // 9. If request’s window is an environment settings object and its origin + // is same origin with origin, then set window to request’s window. + if ( + request.window?.constructor?.name === 'EnvironmentSettingsObject' && + sameOrigin(request.window, origin) + ) { + window = request.window + } + + // 10. If init["window"] exists and is non-null, then throw a TypeError. + if (init.window != null) { + throw new TypeError(`'window' option '${window}' must be null`) + } + + // 11. If init["window"] exists, then set window to "no-window". + if ('window' in init) { + window = 'no-window' + } + + // 12. Set request to a new request with the following properties: + request = makeRequest({ + // URL request’s URL. + // undici implementation note: this is set as the first item in request's urlList in makeRequest + // method request’s method. + method: request.method, + // header list A copy of request’s header list. + // undici implementation note: headersList is cloned in makeRequest + headersList: request.headersList, + // unsafe-request flag Set. + unsafeRequest: request.unsafeRequest, + // client This’s relevant settings object. + client: this[kRealm].settingsObject, + // window window. + window, + // priority request’s priority. + priority: request.priority, + // origin request’s origin. The propagation of the origin is only significant for navigation requests + // being handled by a service worker. In this scenario a request can have an origin that is different + // from the current client. + origin: request.origin, + // referrer request’s referrer. + referrer: request.referrer, + // referrer policy request’s referrer policy. + referrerPolicy: request.referrerPolicy, + // mode request’s mode. + mode: request.mode, + // credentials mode request’s credentials mode. + credentials: request.credentials, + // cache mode request’s cache mode. + cache: request.cache, + // redirect mode request’s redirect mode. + redirect: request.redirect, + // integrity metadata request’s integrity metadata. + integrity: request.integrity, + // keepalive request’s keepalive. + keepalive: request.keepalive, + // reload-navigation flag request’s reload-navigation flag. + reloadNavigation: request.reloadNavigation, + // history-navigation flag request’s history-navigation flag. + historyNavigation: request.historyNavigation, + // URL list A clone of request’s URL list. + urlList: [...request.urlList] + }) + + const initHasKey = Object.keys(init).length !== 0 + + // 13. If init is not empty, then: + if (initHasKey) { + // 1. If request’s mode is "navigate", then set it to "same-origin". + if (request.mode === 'navigate') { + request.mode = 'same-origin' + } + + // 2. Unset request’s reload-navigation flag. + request.reloadNavigation = false + + // 3. Unset request’s history-navigation flag. + request.historyNavigation = false + + // 4. Set request’s origin to "client". + request.origin = 'client' + + // 5. Set request’s referrer to "client" + request.referrer = 'client' + + // 6. Set request’s referrer policy to the empty string. + request.referrerPolicy = '' + + // 7. Set request’s URL to request’s current URL. + request.url = request.urlList[request.urlList.length - 1] + + // 8. Set request’s URL list to « request’s URL ». + request.urlList = [request.url] + } + + // 14. If init["referrer"] exists, then: + if (init.referrer !== undefined) { + // 1. Let referrer be init["referrer"]. + const referrer = init.referrer + + // 2. If referrer is the empty string, then set request’s referrer to "no-referrer". + if (referrer === '') { + request.referrer = 'no-referrer' + } else { + // 1. Let parsedReferrer be the result of parsing referrer with + // baseURL. + // 2. If parsedReferrer is failure, then throw a TypeError. + let parsedReferrer + try { + parsedReferrer = new URL(referrer, baseUrl) + } catch (err) { + throw new TypeError(`Referrer "${referrer}" is not a valid URL.`, { cause: err }) + } + + // 3. If one of the following is true + // - parsedReferrer’s scheme is "about" and path is the string "client" + // - parsedReferrer’s origin is not same origin with origin + // then set request’s referrer to "client". + if ( + (parsedReferrer.protocol === 'about:' && parsedReferrer.hostname === 'client') || + (origin && !sameOrigin(parsedReferrer, this[kRealm].settingsObject.baseUrl)) + ) { + request.referrer = 'client' + } else { + // 4. Otherwise, set request’s referrer to parsedReferrer. + request.referrer = parsedReferrer + } + } + } + + // 15. If init["referrerPolicy"] exists, then set request’s referrer policy + // to it. + if (init.referrerPolicy !== undefined) { + request.referrerPolicy = init.referrerPolicy + } + + // 16. Let mode be init["mode"] if it exists, and fallbackMode otherwise. + let mode + if (init.mode !== undefined) { + mode = init.mode + } else { + mode = fallbackMode + } + + // 17. If mode is "navigate", then throw a TypeError. + if (mode === 'navigate') { + throw webidl.errors.exception({ + header: 'Request constructor', + message: 'invalid request mode navigate.' + }) + } + + // 18. If mode is non-null, set request’s mode to mode. + if (mode != null) { + request.mode = mode + } + + // 19. If init["credentials"] exists, then set request’s credentials mode + // to it. + if (init.credentials !== undefined) { + request.credentials = init.credentials + } + + // 18. If init["cache"] exists, then set request’s cache mode to it. + if (init.cache !== undefined) { + request.cache = init.cache + } + + // 21. If request’s cache mode is "only-if-cached" and request’s mode is + // not "same-origin", then throw a TypeError. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + throw new TypeError( + "'only-if-cached' can be set only with 'same-origin' mode" + ) + } + + // 22. If init["redirect"] exists, then set request’s redirect mode to it. + if (init.redirect !== undefined) { + request.redirect = init.redirect + } + + // 23. If init["integrity"] exists, then set request’s integrity metadata to it. + if (init.integrity != null) { + request.integrity = String(init.integrity) + } + + // 24. If init["keepalive"] exists, then set request’s keepalive to it. + if (init.keepalive !== undefined) { + request.keepalive = Boolean(init.keepalive) + } + + // 25. If init["method"] exists, then: + if (init.method !== undefined) { + // 1. Let method be init["method"]. + let method = init.method + + // 2. If method is not a method or method is a forbidden method, then + // throw a TypeError. + if (!isValidHTTPToken(method)) { + throw new TypeError(`'${method}' is not a valid HTTP method.`) + } + + if (forbiddenMethodsSet.has(method.toUpperCase())) { + throw new TypeError(`'${method}' HTTP method is unsupported.`) + } + + // 3. Normalize method. + method = normalizeMethodRecord[method] ?? normalizeMethod(method) + + // 4. Set request’s method to method. + request.method = method + } + + // 26. If init["signal"] exists, then set signal to it. + if (init.signal !== undefined) { + signal = init.signal + } + + // 27. Set this’s request to request. + this[kState] = request + + // 28. Set this’s signal to a new AbortSignal object with this’s relevant + // Realm. + // TODO: could this be simplified with AbortSignal.any + // (https://dom.spec.whatwg.org/#dom-abortsignal-any) + const ac = new AbortController() + this[kSignal] = ac.signal + this[kSignal][kRealm] = this[kRealm] + + // 29. If signal is not null, then make this’s signal follow signal. + if (signal != null) { + if ( + !signal || + typeof signal.aborted !== 'boolean' || + typeof signal.addEventListener !== 'function' + ) { + throw new TypeError( + "Failed to construct 'Request': member signal is not of type AbortSignal." + ) + } + + if (signal.aborted) { + ac.abort(signal.reason) + } else { + // Keep a strong ref to ac while request object + // is alive. This is needed to prevent AbortController + // from being prematurely garbage collected. + // See, https://github.com/nodejs/undici/issues/1926. + this[kAbortController] = ac + + const acRef = new WeakRef(ac) + const abort = function () { + const ac = acRef.deref() + if (ac !== undefined) { + ac.abort(this.reason) + } + } + + // Third-party AbortControllers may not work with these. + // See, https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619. + try { + // If the max amount of listeners is equal to the default, increase it + // This is only available in node >= v19.9.0 + if (typeof getMaxListeners === 'function' && getMaxListeners(signal) === defaultMaxListeners) { + setMaxListeners(100, signal) + } else if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) { + setMaxListeners(100, signal) + } + } catch {} + + util.addAbortListener(signal, abort) + requestFinalizer.register(ac, { signal, abort }) + } + } + + // 30. Set this’s headers to a new Headers object with this’s relevant + // Realm, whose header list is request’s header list and guard is + // "request". + this[kHeaders] = new Headers(kConstruct) + this[kHeaders][kHeadersList] = request.headersList + this[kHeaders][kGuard] = 'request' + this[kHeaders][kRealm] = this[kRealm] + + // 31. If this’s request’s mode is "no-cors", then: + if (mode === 'no-cors') { + // 1. If this’s request’s method is not a CORS-safelisted method, + // then throw a TypeError. + if (!corsSafeListedMethodsSet.has(request.method)) { + throw new TypeError( + `'${request.method} is unsupported in no-cors mode.` + ) + } + + // 2. Set this’s headers’s guard to "request-no-cors". + this[kHeaders][kGuard] = 'request-no-cors' + } + + // 32. If init is not empty, then: + if (initHasKey) { + /** @type {HeadersList} */ + const headersList = this[kHeaders][kHeadersList] + // 1. Let headers be a copy of this’s headers and its associated header + // list. + // 2. If init["headers"] exists, then set headers to init["headers"]. + const headers = init.headers !== undefined ? init.headers : new HeadersList(headersList) + + // 3. Empty this’s headers’s header list. + headersList.clear() + + // 4. If headers is a Headers object, then for each header in its header + // list, append header’s name/header’s value to this’s headers. + if (headers instanceof HeadersList) { + for (const [key, val] of headers) { + headersList.append(key, val) + } + // Note: Copy the `set-cookie` meta-data. + headersList.cookies = headers.cookies + } else { + // 5. Otherwise, fill this’s headers with headers. + fillHeaders(this[kHeaders], headers) + } + } + + // 33. Let inputBody be input’s request’s body if input is a Request + // object; otherwise null. + const inputBody = input instanceof Request ? input[kState].body : null + + // 34. If either init["body"] exists and is non-null or inputBody is + // non-null, and request’s method is `GET` or `HEAD`, then throw a + // TypeError. + if ( + (init.body != null || inputBody != null) && + (request.method === 'GET' || request.method === 'HEAD') + ) { + throw new TypeError('Request with GET/HEAD method cannot have body.') + } + + // 35. Let initBody be null. + let initBody = null + + // 36. If init["body"] exists and is non-null, then: + if (init.body != null) { + // 1. Let Content-Type be null. + // 2. Set initBody and Content-Type to the result of extracting + // init["body"], with keepalive set to request’s keepalive. + const [extractedBody, contentType] = extractBody( + init.body, + request.keepalive + ) + initBody = extractedBody + + // 3, If Content-Type is non-null and this’s headers’s header list does + // not contain `Content-Type`, then append `Content-Type`/Content-Type to + // this’s headers. + if (contentType && !this[kHeaders][kHeadersList].contains('content-type')) { + this[kHeaders].append('content-type', contentType) + } + } + + // 37. Let inputOrInitBody be initBody if it is non-null; otherwise + // inputBody. + const inputOrInitBody = initBody ?? inputBody + + // 38. If inputOrInitBody is non-null and inputOrInitBody’s source is + // null, then: + if (inputOrInitBody != null && inputOrInitBody.source == null) { + // 1. If initBody is non-null and init["duplex"] does not exist, + // then throw a TypeError. + if (initBody != null && init.duplex == null) { + throw new TypeError('RequestInit: duplex option is required when sending a body.') + } + + // 2. If this’s request’s mode is neither "same-origin" nor "cors", + // then throw a TypeError. + if (request.mode !== 'same-origin' && request.mode !== 'cors') { + throw new TypeError( + 'If request is made from ReadableStream, mode should be "same-origin" or "cors"' + ) + } + + // 3. Set this’s request’s use-CORS-preflight flag. + request.useCORSPreflightFlag = true + } + + // 39. Let finalBody be inputOrInitBody. + let finalBody = inputOrInitBody + + // 40. If initBody is null and inputBody is non-null, then: + if (initBody == null && inputBody != null) { + // 1. If input is unusable, then throw a TypeError. + if (util.isDisturbed(inputBody.stream) || inputBody.stream.locked) { + throw new TypeError( + 'Cannot construct a Request with a Request object that has already been used.' + ) + } + + // 2. Set finalBody to the result of creating a proxy for inputBody. + if (!TransformStream) { + TransformStream = (__nccwpck_require__(3774).TransformStream) + } + + // https://streams.spec.whatwg.org/#readablestream-create-a-proxy + const identityTransform = new TransformStream() + inputBody.stream.pipeThrough(identityTransform) + finalBody = { + source: inputBody.source, + length: inputBody.length, + stream: identityTransform.readable + } + } + + // 41. Set this’s request’s body to finalBody. + this[kState].body = finalBody + } + + // Returns request’s HTTP method, which is "GET" by default. + get method () { + webidl.brandCheck(this, Request) + + // The method getter steps are to return this’s request’s method. + return this[kState].method + } + + // Returns the URL of request as a string. + get url () { + webidl.brandCheck(this, Request) + + // The url getter steps are to return this’s request’s URL, serialized. + return URLSerializer(this[kState].url) + } + + // Returns a Headers object consisting of the headers associated with request. + // Note that headers added in the network layer by the user agent will not + // be accounted for in this object, e.g., the "Host" header. + get headers () { + webidl.brandCheck(this, Request) + + // The headers getter steps are to return this’s headers. + return this[kHeaders] + } + + // Returns the kind of resource requested by request, e.g., "document" + // or "script". + get destination () { + webidl.brandCheck(this, Request) + + // The destination getter are to return this’s request’s destination. + return this[kState].destination + } + + // Returns the referrer of request. Its value can be a same-origin URL if + // explicitly set in init, the empty string to indicate no referrer, and + // "about:client" when defaulting to the global’s default. This is used + // during fetching to determine the value of the `Referer` header of the + // request being made. + get referrer () { + webidl.brandCheck(this, Request) + + // 1. If this’s request’s referrer is "no-referrer", then return the + // empty string. + if (this[kState].referrer === 'no-referrer') { + return '' + } + + // 2. If this’s request’s referrer is "client", then return + // "about:client". + if (this[kState].referrer === 'client') { + return 'about:client' + } + + // Return this’s request’s referrer, serialized. + return this[kState].referrer.toString() + } + + // Returns the referrer policy associated with request. + // This is used during fetching to compute the value of the request’s + // referrer. + get referrerPolicy () { + webidl.brandCheck(this, Request) + + // The referrerPolicy getter steps are to return this’s request’s referrer policy. + return this[kState].referrerPolicy + } + + // Returns the mode associated with request, which is a string indicating + // whether the request will use CORS, or will be restricted to same-origin + // URLs. + get mode () { + webidl.brandCheck(this, Request) + + // The mode getter steps are to return this’s request’s mode. + return this[kState].mode + } + + // Returns the credentials mode associated with request, + // which is a string indicating whether credentials will be sent with the + // request always, never, or only when sent to a same-origin URL. + get credentials () { + // The credentials getter steps are to return this’s request’s credentials mode. + return this[kState].credentials + } + + // Returns the cache mode associated with request, + // which is a string indicating how the request will + // interact with the browser’s cache when fetching. + get cache () { + webidl.brandCheck(this, Request) + + // The cache getter steps are to return this’s request’s cache mode. + return this[kState].cache + } + + // Returns the redirect mode associated with request, + // which is a string indicating how redirects for the + // request will be handled during fetching. A request + // will follow redirects by default. + get redirect () { + webidl.brandCheck(this, Request) + + // The redirect getter steps are to return this’s request’s redirect mode. + return this[kState].redirect + } + + // Returns request’s subresource integrity metadata, which is a + // cryptographic hash of the resource being fetched. Its value + // consists of multiple hashes separated by whitespace. [SRI] + get integrity () { + webidl.brandCheck(this, Request) + + // The integrity getter steps are to return this’s request’s integrity + // metadata. + return this[kState].integrity + } + + // Returns a boolean indicating whether or not request can outlive the + // global in which it was created. + get keepalive () { + webidl.brandCheck(this, Request) + + // The keepalive getter steps are to return this’s request’s keepalive. + return this[kState].keepalive + } + + // Returns a boolean indicating whether or not request is for a reload + // navigation. + get isReloadNavigation () { + webidl.brandCheck(this, Request) + + // The isReloadNavigation getter steps are to return true if this’s + // request’s reload-navigation flag is set; otherwise false. + return this[kState].reloadNavigation + } + + // Returns a boolean indicating whether or not request is for a history + // navigation (a.k.a. back-foward navigation). + get isHistoryNavigation () { + webidl.brandCheck(this, Request) + + // The isHistoryNavigation getter steps are to return true if this’s request’s + // history-navigation flag is set; otherwise false. + return this[kState].historyNavigation + } + + // Returns the signal associated with request, which is an AbortSignal + // object indicating whether or not request has been aborted, and its + // abort event handler. + get signal () { + webidl.brandCheck(this, Request) + + // The signal getter steps are to return this’s signal. + return this[kSignal] + } + + get body () { + webidl.brandCheck(this, Request) + + return this[kState].body ? this[kState].body.stream : null + } + + get bodyUsed () { + webidl.brandCheck(this, Request) + + return !!this[kState].body && util.isDisturbed(this[kState].body.stream) + } + + get duplex () { + webidl.brandCheck(this, Request) + + return 'half' + } + + // Returns a clone of request. + clone () { + webidl.brandCheck(this, Request) + + // 1. If this is unusable, then throw a TypeError. + if (this.bodyUsed || this.body?.locked) { + throw new TypeError('unusable') + } + + // 2. Let clonedRequest be the result of cloning this’s request. + const clonedRequest = cloneRequest(this[kState]) + + // 3. Let clonedRequestObject be the result of creating a Request object, + // given clonedRequest, this’s headers’s guard, and this’s relevant Realm. + const clonedRequestObject = new Request(kConstruct) + clonedRequestObject[kState] = clonedRequest + clonedRequestObject[kRealm] = this[kRealm] + clonedRequestObject[kHeaders] = new Headers(kConstruct) + clonedRequestObject[kHeaders][kHeadersList] = clonedRequest.headersList + clonedRequestObject[kHeaders][kGuard] = this[kHeaders][kGuard] + clonedRequestObject[kHeaders][kRealm] = this[kHeaders][kRealm] + + // 4. Make clonedRequestObject’s signal follow this’s signal. + const ac = new AbortController() + if (this.signal.aborted) { + ac.abort(this.signal.reason) + } else { + util.addAbortListener( + this.signal, + () => { + ac.abort(this.signal.reason) + } + ) + } + clonedRequestObject[kSignal] = ac.signal + + // 4. Return clonedRequestObject. + return clonedRequestObject + } +} + +mixinBody(Request) + +function makeRequest (init) { + // https://fetch.spec.whatwg.org/#requests + const request = { + method: 'GET', + localURLsOnly: false, + unsafeRequest: false, + body: null, + client: null, + reservedClient: null, + replacesClientId: '', + window: 'client', + keepalive: false, + serviceWorkers: 'all', + initiator: '', + destination: '', + priority: null, + origin: 'client', + policyContainer: 'client', + referrer: 'client', + referrerPolicy: '', + mode: 'no-cors', + useCORSPreflightFlag: false, + credentials: 'same-origin', + useCredentials: false, + cache: 'default', + redirect: 'follow', + integrity: '', + cryptoGraphicsNonceMetadata: '', + parserMetadata: '', + reloadNavigation: false, + historyNavigation: false, + userActivation: false, + taintedOrigin: false, + redirectCount: 0, + responseTainting: 'basic', + preventNoCacheCacheControlHeaderModification: false, + done: false, + timingAllowFailed: false, + ...init, + headersList: init.headersList + ? new HeadersList(init.headersList) + : new HeadersList() + } + request.url = request.urlList[0] + return request +} + +// https://fetch.spec.whatwg.org/#concept-request-clone +function cloneRequest (request) { + // To clone a request request, run these steps: + + // 1. Let newRequest be a copy of request, except for its body. + const newRequest = makeRequest({ ...request, body: null }) + + // 2. If request’s body is non-null, set newRequest’s body to the + // result of cloning request’s body. + if (request.body != null) { + newRequest.body = cloneBody(request.body) + } + + // 3. Return newRequest. + return newRequest +} + +Object.defineProperties(Request.prototype, { + method: kEnumerableProperty, + url: kEnumerableProperty, + headers: kEnumerableProperty, + redirect: kEnumerableProperty, + clone: kEnumerableProperty, + signal: kEnumerableProperty, + duplex: kEnumerableProperty, + destination: kEnumerableProperty, + body: kEnumerableProperty, + bodyUsed: kEnumerableProperty, + isHistoryNavigation: kEnumerableProperty, + isReloadNavigation: kEnumerableProperty, + keepalive: kEnumerableProperty, + integrity: kEnumerableProperty, + cache: kEnumerableProperty, + credentials: kEnumerableProperty, + attribute: kEnumerableProperty, + referrerPolicy: kEnumerableProperty, + referrer: kEnumerableProperty, + mode: kEnumerableProperty, + [Symbol.toStringTag]: { + value: 'Request', + configurable: true + } +}) + +webidl.converters.Request = webidl.interfaceConverter( + Request +) + +// https://fetch.spec.whatwg.org/#requestinfo +webidl.converters.RequestInfo = function (V) { + if (typeof V === 'string') { + return webidl.converters.USVString(V) + } + + if (V instanceof Request) { + return webidl.converters.Request(V) + } + + return webidl.converters.USVString(V) +} + +webidl.converters.AbortSignal = webidl.interfaceConverter( + AbortSignal +) + +// https://fetch.spec.whatwg.org/#requestinit +webidl.converters.RequestInit = webidl.dictionaryConverter([ + { + key: 'method', + converter: webidl.converters.ByteString + }, + { + key: 'headers', + converter: webidl.converters.HeadersInit + }, + { + key: 'body', + converter: webidl.nullableConverter( + webidl.converters.BodyInit + ) + }, + { + key: 'referrer', + converter: webidl.converters.USVString + }, + { + key: 'referrerPolicy', + converter: webidl.converters.DOMString, + // https://w3c.github.io/webappsec-referrer-policy/#referrer-policy + allowedValues: referrerPolicy + }, + { + key: 'mode', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#concept-request-mode + allowedValues: requestMode + }, + { + key: 'credentials', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestcredentials + allowedValues: requestCredentials + }, + { + key: 'cache', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestcache + allowedValues: requestCache + }, + { + key: 'redirect', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestredirect + allowedValues: requestRedirect + }, + { + key: 'integrity', + converter: webidl.converters.DOMString + }, + { + key: 'keepalive', + converter: webidl.converters.boolean + }, + { + key: 'signal', + converter: webidl.nullableConverter( + (signal) => webidl.converters.AbortSignal( + signal, + { strict: false } + ) + ) + }, + { + key: 'window', + converter: webidl.converters.any + }, + { + key: 'duplex', + converter: webidl.converters.DOMString, + allowedValues: requestDuplex + } +]) + +module.exports = { Request, makeRequest } + + +/***/ }), + +/***/ 2440: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { Headers, HeadersList, fill } = __nccwpck_require__(161) +const { extractBody, cloneBody, mixinBody } = __nccwpck_require__(4655) +const util = __nccwpck_require__(1436) +const { kEnumerableProperty } = util +const { + isValidReasonPhrase, + isCancelled, + isAborted, + isBlobLike, + serializeJavascriptValueToJSONString, + isErrorLike, + isomorphicEncode +} = __nccwpck_require__(3359) +const { + redirectStatusSet, + nullBodyStatus, + DOMException +} = __nccwpck_require__(450) +const { kState, kHeaders, kGuard, kRealm } = __nccwpck_require__(834) +const { webidl } = __nccwpck_require__(274) +const { FormData } = __nccwpck_require__(2813) +const { getGlobalOrigin } = __nccwpck_require__(960) +const { URLSerializer } = __nccwpck_require__(5294) +const { kHeadersList, kConstruct } = __nccwpck_require__(9583) +const assert = __nccwpck_require__(2613) +const { types } = __nccwpck_require__(9023) + +const ReadableStream = globalThis.ReadableStream || (__nccwpck_require__(3774).ReadableStream) +const textEncoder = new TextEncoder('utf-8') + +// https://fetch.spec.whatwg.org/#response-class +class Response { + // Creates network error Response. + static error () { + // TODO + const relevantRealm = { settingsObject: {} } + + // The static error() method steps are to return the result of creating a + // Response object, given a new network error, "immutable", and this’s + // relevant Realm. + const responseObject = new Response() + responseObject[kState] = makeNetworkError() + responseObject[kRealm] = relevantRealm + responseObject[kHeaders][kHeadersList] = responseObject[kState].headersList + responseObject[kHeaders][kGuard] = 'immutable' + responseObject[kHeaders][kRealm] = relevantRealm + return responseObject + } + + // https://fetch.spec.whatwg.org/#dom-response-json + static json (data, init = {}) { + webidl.argumentLengthCheck(arguments, 1, { header: 'Response.json' }) + + if (init !== null) { + init = webidl.converters.ResponseInit(init) + } + + // 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data. + const bytes = textEncoder.encode( + serializeJavascriptValueToJSONString(data) + ) + + // 2. Let body be the result of extracting bytes. + const body = extractBody(bytes) + + // 3. Let responseObject be the result of creating a Response object, given a new response, + // "response", and this’s relevant Realm. + const relevantRealm = { settingsObject: {} } + const responseObject = new Response() + responseObject[kRealm] = relevantRealm + responseObject[kHeaders][kGuard] = 'response' + responseObject[kHeaders][kRealm] = relevantRealm + + // 4. Perform initialize a response given responseObject, init, and (body, "application/json"). + initializeResponse(responseObject, init, { body: body[0], type: 'application/json' }) + + // 5. Return responseObject. + return responseObject + } + + // Creates a redirect Response that redirects to url with status status. + static redirect (url, status = 302) { + const relevantRealm = { settingsObject: {} } + + webidl.argumentLengthCheck(arguments, 1, { header: 'Response.redirect' }) + + url = webidl.converters.USVString(url) + status = webidl.converters['unsigned short'](status) + + // 1. Let parsedURL be the result of parsing url with current settings + // object’s API base URL. + // 2. If parsedURL is failure, then throw a TypeError. + // TODO: base-URL? + let parsedURL + try { + parsedURL = new URL(url, getGlobalOrigin()) + } catch (err) { + throw Object.assign(new TypeError('Failed to parse URL from ' + url), { + cause: err + }) + } + + // 3. If status is not a redirect status, then throw a RangeError. + if (!redirectStatusSet.has(status)) { + throw new RangeError('Invalid status code ' + status) + } + + // 4. Let responseObject be the result of creating a Response object, + // given a new response, "immutable", and this’s relevant Realm. + const responseObject = new Response() + responseObject[kRealm] = relevantRealm + responseObject[kHeaders][kGuard] = 'immutable' + responseObject[kHeaders][kRealm] = relevantRealm + + // 5. Set responseObject’s response’s status to status. + responseObject[kState].status = status + + // 6. Let value be parsedURL, serialized and isomorphic encoded. + const value = isomorphicEncode(URLSerializer(parsedURL)) + + // 7. Append `Location`/value to responseObject’s response’s header list. + responseObject[kState].headersList.append('location', value) + + // 8. Return responseObject. + return responseObject + } + + // https://fetch.spec.whatwg.org/#dom-response + constructor (body = null, init = {}) { + if (body !== null) { + body = webidl.converters.BodyInit(body) + } + + init = webidl.converters.ResponseInit(init) + + // TODO + this[kRealm] = { settingsObject: {} } + + // 1. Set this’s response to a new response. + this[kState] = makeResponse({}) + + // 2. Set this’s headers to a new Headers object with this’s relevant + // Realm, whose header list is this’s response’s header list and guard + // is "response". + this[kHeaders] = new Headers(kConstruct) + this[kHeaders][kGuard] = 'response' + this[kHeaders][kHeadersList] = this[kState].headersList + this[kHeaders][kRealm] = this[kRealm] + + // 3. Let bodyWithType be null. + let bodyWithType = null + + // 4. If body is non-null, then set bodyWithType to the result of extracting body. + if (body != null) { + const [extractedBody, type] = extractBody(body) + bodyWithType = { body: extractedBody, type } + } + + // 5. Perform initialize a response given this, init, and bodyWithType. + initializeResponse(this, init, bodyWithType) + } + + // Returns response’s type, e.g., "cors". + get type () { + webidl.brandCheck(this, Response) + + // The type getter steps are to return this’s response’s type. + return this[kState].type + } + + // Returns response’s URL, if it has one; otherwise the empty string. + get url () { + webidl.brandCheck(this, Response) + + const urlList = this[kState].urlList + + // The url getter steps are to return the empty string if this’s + // response’s URL is null; otherwise this’s response’s URL, + // serialized with exclude fragment set to true. + const url = urlList[urlList.length - 1] ?? null + + if (url === null) { + return '' + } + + return URLSerializer(url, true) + } + + // Returns whether response was obtained through a redirect. + get redirected () { + webidl.brandCheck(this, Response) + + // The redirected getter steps are to return true if this’s response’s URL + // list has more than one item; otherwise false. + return this[kState].urlList.length > 1 + } + + // Returns response’s status. + get status () { + webidl.brandCheck(this, Response) + + // The status getter steps are to return this’s response’s status. + return this[kState].status + } + + // Returns whether response’s status is an ok status. + get ok () { + webidl.brandCheck(this, Response) + + // The ok getter steps are to return true if this’s response’s status is an + // ok status; otherwise false. + return this[kState].status >= 200 && this[kState].status <= 299 + } + + // Returns response’s status message. + get statusText () { + webidl.brandCheck(this, Response) + + // The statusText getter steps are to return this’s response’s status + // message. + return this[kState].statusText + } + + // Returns response’s headers as Headers. + get headers () { + webidl.brandCheck(this, Response) + + // The headers getter steps are to return this’s headers. + return this[kHeaders] + } + + get body () { + webidl.brandCheck(this, Response) + + return this[kState].body ? this[kState].body.stream : null + } + + get bodyUsed () { + webidl.brandCheck(this, Response) + + return !!this[kState].body && util.isDisturbed(this[kState].body.stream) + } + + // Returns a clone of response. + clone () { + webidl.brandCheck(this, Response) + + // 1. If this is unusable, then throw a TypeError. + if (this.bodyUsed || (this.body && this.body.locked)) { + throw webidl.errors.exception({ + header: 'Response.clone', + message: 'Body has already been consumed.' + }) + } + + // 2. Let clonedResponse be the result of cloning this’s response. + const clonedResponse = cloneResponse(this[kState]) + + // 3. Return the result of creating a Response object, given + // clonedResponse, this’s headers’s guard, and this’s relevant Realm. + const clonedResponseObject = new Response() + clonedResponseObject[kState] = clonedResponse + clonedResponseObject[kRealm] = this[kRealm] + clonedResponseObject[kHeaders][kHeadersList] = clonedResponse.headersList + clonedResponseObject[kHeaders][kGuard] = this[kHeaders][kGuard] + clonedResponseObject[kHeaders][kRealm] = this[kHeaders][kRealm] + + return clonedResponseObject + } +} + +mixinBody(Response) + +Object.defineProperties(Response.prototype, { + type: kEnumerableProperty, + url: kEnumerableProperty, + status: kEnumerableProperty, + ok: kEnumerableProperty, + redirected: kEnumerableProperty, + statusText: kEnumerableProperty, + headers: kEnumerableProperty, + clone: kEnumerableProperty, + body: kEnumerableProperty, + bodyUsed: kEnumerableProperty, + [Symbol.toStringTag]: { + value: 'Response', + configurable: true + } +}) + +Object.defineProperties(Response, { + json: kEnumerableProperty, + redirect: kEnumerableProperty, + error: kEnumerableProperty +}) + +// https://fetch.spec.whatwg.org/#concept-response-clone +function cloneResponse (response) { + // To clone a response response, run these steps: + + // 1. If response is a filtered response, then return a new identical + // filtered response whose internal response is a clone of response’s + // internal response. + if (response.internalResponse) { + return filterResponse( + cloneResponse(response.internalResponse), + response.type + ) + } + + // 2. Let newResponse be a copy of response, except for its body. + const newResponse = makeResponse({ ...response, body: null }) + + // 3. If response’s body is non-null, then set newResponse’s body to the + // result of cloning response’s body. + if (response.body != null) { + newResponse.body = cloneBody(response.body) + } + + // 4. Return newResponse. + return newResponse +} + +function makeResponse (init) { + return { + aborted: false, + rangeRequested: false, + timingAllowPassed: false, + requestIncludesCredentials: false, + type: 'default', + status: 200, + timingInfo: null, + cacheState: '', + statusText: '', + ...init, + headersList: init.headersList + ? new HeadersList(init.headersList) + : new HeadersList(), + urlList: init.urlList ? [...init.urlList] : [] + } +} + +function makeNetworkError (reason) { + const isError = isErrorLike(reason) + return makeResponse({ + type: 'error', + status: 0, + error: isError + ? reason + : new Error(reason ? String(reason) : reason), + aborted: reason && reason.name === 'AbortError' + }) +} + +function makeFilteredResponse (response, state) { + state = { + internalResponse: response, + ...state + } + + return new Proxy(response, { + get (target, p) { + return p in state ? state[p] : target[p] + }, + set (target, p, value) { + assert(!(p in state)) + target[p] = value + return true + } + }) +} + +// https://fetch.spec.whatwg.org/#concept-filtered-response +function filterResponse (response, type) { + // Set response to the following filtered response with response as its + // internal response, depending on request’s response tainting: + if (type === 'basic') { + // A basic filtered response is a filtered response whose type is "basic" + // and header list excludes any headers in internal response’s header list + // whose name is a forbidden response-header name. + + // Note: undici does not implement forbidden response-header names + return makeFilteredResponse(response, { + type: 'basic', + headersList: response.headersList + }) + } else if (type === 'cors') { + // A CORS filtered response is a filtered response whose type is "cors" + // and header list excludes any headers in internal response’s header + // list whose name is not a CORS-safelisted response-header name, given + // internal response’s CORS-exposed header-name list. + + // Note: undici does not implement CORS-safelisted response-header names + return makeFilteredResponse(response, { + type: 'cors', + headersList: response.headersList + }) + } else if (type === 'opaque') { + // An opaque filtered response is a filtered response whose type is + // "opaque", URL list is the empty list, status is 0, status message + // is the empty byte sequence, header list is empty, and body is null. + + return makeFilteredResponse(response, { + type: 'opaque', + urlList: Object.freeze([]), + status: 0, + statusText: '', + body: null + }) + } else if (type === 'opaqueredirect') { + // An opaque-redirect filtered response is a filtered response whose type + // is "opaqueredirect", status is 0, status message is the empty byte + // sequence, header list is empty, and body is null. + + return makeFilteredResponse(response, { + type: 'opaqueredirect', + status: 0, + statusText: '', + headersList: [], + body: null + }) + } else { + assert(false) + } +} + +// https://fetch.spec.whatwg.org/#appropriate-network-error +function makeAppropriateNetworkError (fetchParams, err = null) { + // 1. Assert: fetchParams is canceled. + assert(isCancelled(fetchParams)) + + // 2. Return an aborted network error if fetchParams is aborted; + // otherwise return a network error. + return isAborted(fetchParams) + ? makeNetworkError(Object.assign(new DOMException('The operation was aborted.', 'AbortError'), { cause: err })) + : makeNetworkError(Object.assign(new DOMException('Request was cancelled.'), { cause: err })) +} + +// https://whatpr.org/fetch/1392.html#initialize-a-response +function initializeResponse (response, init, body) { + // 1. If init["status"] is not in the range 200 to 599, inclusive, then + // throw a RangeError. + if (init.status !== null && (init.status < 200 || init.status > 599)) { + throw new RangeError('init["status"] must be in the range of 200 to 599, inclusive.') + } + + // 2. If init["statusText"] does not match the reason-phrase token production, + // then throw a TypeError. + if ('statusText' in init && init.statusText != null) { + // See, https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2: + // reason-phrase = *( HTAB / SP / VCHAR / obs-text ) + if (!isValidReasonPhrase(String(init.statusText))) { + throw new TypeError('Invalid statusText') + } + } + + // 3. Set response’s response’s status to init["status"]. + if ('status' in init && init.status != null) { + response[kState].status = init.status + } + + // 4. Set response’s response’s status message to init["statusText"]. + if ('statusText' in init && init.statusText != null) { + response[kState].statusText = init.statusText + } + + // 5. If init["headers"] exists, then fill response’s headers with init["headers"]. + if ('headers' in init && init.headers != null) { + fill(response[kHeaders], init.headers) + } + + // 6. If body was given, then: + if (body) { + // 1. If response's status is a null body status, then throw a TypeError. + if (nullBodyStatus.includes(response.status)) { + throw webidl.errors.exception({ + header: 'Response constructor', + message: 'Invalid response status code ' + response.status + }) + } + + // 2. Set response's body to body's body. + response[kState].body = body.body + + // 3. If body's type is non-null and response's header list does not contain + // `Content-Type`, then append (`Content-Type`, body's type) to response's header list. + if (body.type != null && !response[kState].headersList.contains('Content-Type')) { + response[kState].headersList.append('content-type', body.type) + } + } +} + +webidl.converters.ReadableStream = webidl.interfaceConverter( + ReadableStream +) + +webidl.converters.FormData = webidl.interfaceConverter( + FormData +) + +webidl.converters.URLSearchParams = webidl.interfaceConverter( + URLSearchParams +) + +// https://fetch.spec.whatwg.org/#typedefdef-xmlhttprequestbodyinit +webidl.converters.XMLHttpRequestBodyInit = function (V) { + if (typeof V === 'string') { + return webidl.converters.USVString(V) + } + + if (isBlobLike(V)) { + return webidl.converters.Blob(V, { strict: false }) + } + + if (types.isArrayBuffer(V) || types.isTypedArray(V) || types.isDataView(V)) { + return webidl.converters.BufferSource(V) + } + + if (util.isFormDataLike(V)) { + return webidl.converters.FormData(V, { strict: false }) + } + + if (V instanceof URLSearchParams) { + return webidl.converters.URLSearchParams(V) + } + + return webidl.converters.DOMString(V) +} + +// https://fetch.spec.whatwg.org/#bodyinit +webidl.converters.BodyInit = function (V) { + if (V instanceof ReadableStream) { + return webidl.converters.ReadableStream(V) + } + + // Note: the spec doesn't include async iterables, + // this is an undici extension. + if (V?.[Symbol.asyncIterator]) { + return V + } + + return webidl.converters.XMLHttpRequestBodyInit(V) +} + +webidl.converters.ResponseInit = webidl.dictionaryConverter([ + { + key: 'status', + converter: webidl.converters['unsigned short'], + defaultValue: 200 + }, + { + key: 'statusText', + converter: webidl.converters.ByteString, + defaultValue: '' + }, + { + key: 'headers', + converter: webidl.converters.HeadersInit + } +]) + +module.exports = { + makeNetworkError, + makeResponse, + makeAppropriateNetworkError, + filterResponse, + Response, + cloneResponse +} + + +/***/ }), + +/***/ 834: +/***/ ((module) => { + +"use strict"; + + +module.exports = { + kUrl: Symbol('url'), + kHeaders: Symbol('headers'), + kSignal: Symbol('signal'), + kState: Symbol('state'), + kGuard: Symbol('guard'), + kRealm: Symbol('realm') +} + + +/***/ }), + +/***/ 3359: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = __nccwpck_require__(450) +const { getGlobalOrigin } = __nccwpck_require__(960) +const { performance } = __nccwpck_require__(2987) +const { isBlobLike, toUSVString, ReadableStreamFrom } = __nccwpck_require__(1436) +const assert = __nccwpck_require__(2613) +const { isUint8Array } = __nccwpck_require__(8253) + +let supportedHashes = [] + +// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable +/** @type {import('crypto')|undefined} */ +let crypto + +try { + crypto = __nccwpck_require__(6982) + const possibleRelevantHashes = ['sha256', 'sha384', 'sha512'] + supportedHashes = crypto.getHashes().filter((hash) => possibleRelevantHashes.includes(hash)) +/* c8 ignore next 3 */ +} catch { +} + +function responseURL (response) { + // https://fetch.spec.whatwg.org/#responses + // A response has an associated URL. It is a pointer to the last URL + // in response’s URL list and null if response’s URL list is empty. + const urlList = response.urlList + const length = urlList.length + return length === 0 ? null : urlList[length - 1].toString() +} + +// https://fetch.spec.whatwg.org/#concept-response-location-url +function responseLocationURL (response, requestFragment) { + // 1. If response’s status is not a redirect status, then return null. + if (!redirectStatusSet.has(response.status)) { + return null + } + + // 2. Let location be the result of extracting header list values given + // `Location` and response’s header list. + let location = response.headersList.get('location') + + // 3. If location is a header value, then set location to the result of + // parsing location with response’s URL. + if (location !== null && isValidHeaderValue(location)) { + location = new URL(location, responseURL(response)) + } + + // 4. If location is a URL whose fragment is null, then set location’s + // fragment to requestFragment. + if (location && !location.hash) { + location.hash = requestFragment + } + + // 5. Return location. + return location +} + +/** @returns {URL} */ +function requestCurrentURL (request) { + return request.urlList[request.urlList.length - 1] +} + +function requestBadPort (request) { + // 1. Let url be request’s current URL. + const url = requestCurrentURL(request) + + // 2. If url’s scheme is an HTTP(S) scheme and url’s port is a bad port, + // then return blocked. + if (urlIsHttpHttpsScheme(url) && badPortsSet.has(url.port)) { + return 'blocked' + } + + // 3. Return allowed. + return 'allowed' +} + +function isErrorLike (object) { + return object instanceof Error || ( + object?.constructor?.name === 'Error' || + object?.constructor?.name === 'DOMException' + ) +} + +// Check whether |statusText| is a ByteString and +// matches the Reason-Phrase token production. +// RFC 2616: https://tools.ietf.org/html/rfc2616 +// RFC 7230: https://tools.ietf.org/html/rfc7230 +// "reason-phrase = *( HTAB / SP / VCHAR / obs-text )" +// https://github.com/chromium/chromium/blob/94.0.4604.1/third_party/blink/renderer/core/fetch/response.cc#L116 +function isValidReasonPhrase (statusText) { + for (let i = 0; i < statusText.length; ++i) { + const c = statusText.charCodeAt(i) + if ( + !( + ( + c === 0x09 || // HTAB + (c >= 0x20 && c <= 0x7e) || // SP / VCHAR + (c >= 0x80 && c <= 0xff) + ) // obs-text + ) + ) { + return false + } + } + return true +} + +/** + * @see https://tools.ietf.org/html/rfc7230#section-3.2.6 + * @param {number} c + */ +function isTokenCharCode (c) { + switch (c) { + case 0x22: + case 0x28: + case 0x29: + case 0x2c: + case 0x2f: + case 0x3a: + case 0x3b: + case 0x3c: + case 0x3d: + case 0x3e: + case 0x3f: + case 0x40: + case 0x5b: + case 0x5c: + case 0x5d: + case 0x7b: + case 0x7d: + // DQUOTE and "(),/:;<=>?@[\]{}" + return false + default: + // VCHAR %x21-7E + return c >= 0x21 && c <= 0x7e + } +} + +/** + * @param {string} characters + */ +function isValidHTTPToken (characters) { + if (characters.length === 0) { + return false + } + for (let i = 0; i < characters.length; ++i) { + if (!isTokenCharCode(characters.charCodeAt(i))) { + return false + } + } + return true +} + +/** + * @see https://fetch.spec.whatwg.org/#header-name + * @param {string} potentialValue + */ +function isValidHeaderName (potentialValue) { + return isValidHTTPToken(potentialValue) +} + +/** + * @see https://fetch.spec.whatwg.org/#header-value + * @param {string} potentialValue + */ +function isValidHeaderValue (potentialValue) { + // - Has no leading or trailing HTTP tab or space bytes. + // - Contains no 0x00 (NUL) or HTTP newline bytes. + if ( + potentialValue.startsWith('\t') || + potentialValue.startsWith(' ') || + potentialValue.endsWith('\t') || + potentialValue.endsWith(' ') + ) { + return false + } + + if ( + potentialValue.includes('\0') || + potentialValue.includes('\r') || + potentialValue.includes('\n') + ) { + return false + } + + return true +} + +// https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect +function setRequestReferrerPolicyOnRedirect (request, actualResponse) { + // Given a request request and a response actualResponse, this algorithm + // updates request’s referrer policy according to the Referrer-Policy + // header (if any) in actualResponse. + + // 1. Let policy be the result of executing § 8.1 Parse a referrer policy + // from a Referrer-Policy header on actualResponse. + + // 8.1 Parse a referrer policy from a Referrer-Policy header + // 1. Let policy-tokens be the result of extracting header list values given `Referrer-Policy` and response’s header list. + const { headersList } = actualResponse + // 2. Let policy be the empty string. + // 3. For each token in policy-tokens, if token is a referrer policy and token is not the empty string, then set policy to token. + // 4. Return policy. + const policyHeader = (headersList.get('referrer-policy') ?? '').split(',') + + // Note: As the referrer-policy can contain multiple policies + // separated by comma, we need to loop through all of them + // and pick the first valid one. + // Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#specify_a_fallback_policy + let policy = '' + if (policyHeader.length > 0) { + // The right-most policy takes precedence. + // The left-most policy is the fallback. + for (let i = policyHeader.length; i !== 0; i--) { + const token = policyHeader[i - 1].trim() + if (referrerPolicyTokens.has(token)) { + policy = token + break + } + } + } + + // 2. If policy is not the empty string, then set request’s referrer policy to policy. + if (policy !== '') { + request.referrerPolicy = policy + } +} + +// https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check +function crossOriginResourcePolicyCheck () { + // TODO + return 'allowed' +} + +// https://fetch.spec.whatwg.org/#concept-cors-check +function corsCheck () { + // TODO + return 'success' +} + +// https://fetch.spec.whatwg.org/#concept-tao-check +function TAOCheck () { + // TODO + return 'success' +} + +function appendFetchMetadata (httpRequest) { + // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-dest-header + // TODO + + // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-mode-header + + // 1. Assert: r’s url is a potentially trustworthy URL. + // TODO + + // 2. Let header be a Structured Header whose value is a token. + let header = null + + // 3. Set header’s value to r’s mode. + header = httpRequest.mode + + // 4. Set a structured field value `Sec-Fetch-Mode`/header in r’s header list. + httpRequest.headersList.set('sec-fetch-mode', header) + + // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-site-header + // TODO + + // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-user-header + // TODO +} + +// https://fetch.spec.whatwg.org/#append-a-request-origin-header +function appendRequestOriginHeader (request) { + // 1. Let serializedOrigin be the result of byte-serializing a request origin with request. + let serializedOrigin = request.origin + + // 2. If request’s response tainting is "cors" or request’s mode is "websocket", then append (`Origin`, serializedOrigin) to request’s header list. + if (request.responseTainting === 'cors' || request.mode === 'websocket') { + if (serializedOrigin) { + request.headersList.append('origin', serializedOrigin) + } + + // 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then: + } else if (request.method !== 'GET' && request.method !== 'HEAD') { + // 1. Switch on request’s referrer policy: + switch (request.referrerPolicy) { + case 'no-referrer': + // Set serializedOrigin to `null`. + serializedOrigin = null + break + case 'no-referrer-when-downgrade': + case 'strict-origin': + case 'strict-origin-when-cross-origin': + // If request’s origin is a tuple origin, its scheme is "https", and request’s current URL’s scheme is not "https", then set serializedOrigin to `null`. + if (request.origin && urlHasHttpsScheme(request.origin) && !urlHasHttpsScheme(requestCurrentURL(request))) { + serializedOrigin = null + } + break + case 'same-origin': + // If request’s origin is not same origin with request’s current URL’s origin, then set serializedOrigin to `null`. + if (!sameOrigin(request, requestCurrentURL(request))) { + serializedOrigin = null + } + break + default: + // Do nothing. + } + + if (serializedOrigin) { + // 2. Append (`Origin`, serializedOrigin) to request’s header list. + request.headersList.append('origin', serializedOrigin) + } + } +} + +function coarsenedSharedCurrentTime (crossOriginIsolatedCapability) { + // TODO + return performance.now() +} + +// https://fetch.spec.whatwg.org/#create-an-opaque-timing-info +function createOpaqueTimingInfo (timingInfo) { + return { + startTime: timingInfo.startTime ?? 0, + redirectStartTime: 0, + redirectEndTime: 0, + postRedirectStartTime: timingInfo.startTime ?? 0, + finalServiceWorkerStartTime: 0, + finalNetworkResponseStartTime: 0, + finalNetworkRequestStartTime: 0, + endTime: 0, + encodedBodySize: 0, + decodedBodySize: 0, + finalConnectionTimingInfo: null + } +} + +// https://html.spec.whatwg.org/multipage/origin.html#policy-container +function makePolicyContainer () { + // Note: the fetch spec doesn't make use of embedder policy or CSP list + return { + referrerPolicy: 'strict-origin-when-cross-origin' + } +} + +// https://html.spec.whatwg.org/multipage/origin.html#clone-a-policy-container +function clonePolicyContainer (policyContainer) { + return { + referrerPolicy: policyContainer.referrerPolicy + } +} + +// https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer +function determineRequestsReferrer (request) { + // 1. Let policy be request's referrer policy. + const policy = request.referrerPolicy + + // Note: policy cannot (shouldn't) be null or an empty string. + assert(policy) + + // 2. Let environment be request’s client. + + let referrerSource = null + + // 3. Switch on request’s referrer: + if (request.referrer === 'client') { + // Note: node isn't a browser and doesn't implement document/iframes, + // so we bypass this step and replace it with our own. + + const globalOrigin = getGlobalOrigin() + + if (!globalOrigin || globalOrigin.origin === 'null') { + return 'no-referrer' + } + + // note: we need to clone it as it's mutated + referrerSource = new URL(globalOrigin) + } else if (request.referrer instanceof URL) { + // Let referrerSource be request’s referrer. + referrerSource = request.referrer + } + + // 4. Let request’s referrerURL be the result of stripping referrerSource for + // use as a referrer. + let referrerURL = stripURLForReferrer(referrerSource) + + // 5. Let referrerOrigin be the result of stripping referrerSource for use as + // a referrer, with the origin-only flag set to true. + const referrerOrigin = stripURLForReferrer(referrerSource, true) + + // 6. If the result of serializing referrerURL is a string whose length is + // greater than 4096, set referrerURL to referrerOrigin. + if (referrerURL.toString().length > 4096) { + referrerURL = referrerOrigin + } + + const areSameOrigin = sameOrigin(request, referrerURL) + const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerURL) && + !isURLPotentiallyTrustworthy(request.url) + + // 8. Execute the switch statements corresponding to the value of policy: + switch (policy) { + case 'origin': return referrerOrigin != null ? referrerOrigin : stripURLForReferrer(referrerSource, true) + case 'unsafe-url': return referrerURL + case 'same-origin': + return areSameOrigin ? referrerOrigin : 'no-referrer' + case 'origin-when-cross-origin': + return areSameOrigin ? referrerURL : referrerOrigin + case 'strict-origin-when-cross-origin': { + const currentURL = requestCurrentURL(request) + + // 1. If the origin of referrerURL and the origin of request’s current + // URL are the same, then return referrerURL. + if (sameOrigin(referrerURL, currentURL)) { + return referrerURL + } + + // 2. If referrerURL is a potentially trustworthy URL and request’s + // current URL is not a potentially trustworthy URL, then return no + // referrer. + if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) { + return 'no-referrer' + } + + // 3. Return referrerOrigin. + return referrerOrigin + } + case 'strict-origin': // eslint-disable-line + /** + * 1. If referrerURL is a potentially trustworthy URL and + * request’s current URL is not a potentially trustworthy URL, + * then return no referrer. + * 2. Return referrerOrigin + */ + case 'no-referrer-when-downgrade': // eslint-disable-line + /** + * 1. If referrerURL is a potentially trustworthy URL and + * request’s current URL is not a potentially trustworthy URL, + * then return no referrer. + * 2. Return referrerOrigin + */ + + default: // eslint-disable-line + return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin + } +} + +/** + * @see https://w3c.github.io/webappsec-referrer-policy/#strip-url + * @param {URL} url + * @param {boolean|undefined} originOnly + */ +function stripURLForReferrer (url, originOnly) { + // 1. Assert: url is a URL. + assert(url instanceof URL) + + // 2. If url’s scheme is a local scheme, then return no referrer. + if (url.protocol === 'file:' || url.protocol === 'about:' || url.protocol === 'blank:') { + return 'no-referrer' + } + + // 3. Set url’s username to the empty string. + url.username = '' + + // 4. Set url’s password to the empty string. + url.password = '' + + // 5. Set url’s fragment to null. + url.hash = '' + + // 6. If the origin-only flag is true, then: + if (originOnly) { + // 1. Set url’s path to « the empty string ». + url.pathname = '' + + // 2. Set url’s query to null. + url.search = '' + } + + // 7. Return url. + return url +} + +function isURLPotentiallyTrustworthy (url) { + if (!(url instanceof URL)) { + return false + } + + // If child of about, return true + if (url.href === 'about:blank' || url.href === 'about:srcdoc') { + return true + } + + // If scheme is data, return true + if (url.protocol === 'data:') return true + + // If file, return true + if (url.protocol === 'file:') return true + + return isOriginPotentiallyTrustworthy(url.origin) + + function isOriginPotentiallyTrustworthy (origin) { + // If origin is explicitly null, return false + if (origin == null || origin === 'null') return false + + const originAsURL = new URL(origin) + + // If secure, return true + if (originAsURL.protocol === 'https:' || originAsURL.protocol === 'wss:') { + return true + } + + // If localhost or variants, return true + if (/^127(?:\.[0-9]+){0,2}\.[0-9]+$|^\[(?:0*:)*?:?0*1\]$/.test(originAsURL.hostname) || + (originAsURL.hostname === 'localhost' || originAsURL.hostname.includes('localhost.')) || + (originAsURL.hostname.endsWith('.localhost'))) { + return true + } + + // If any other, return false + return false + } +} + +/** + * @see https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist + * @param {Uint8Array} bytes + * @param {string} metadataList + */ +function bytesMatch (bytes, metadataList) { + // If node is not built with OpenSSL support, we cannot check + // a request's integrity, so allow it by default (the spec will + // allow requests if an invalid hash is given, as precedence). + /* istanbul ignore if: only if node is built with --without-ssl */ + if (crypto === undefined) { + return true + } + + // 1. Let parsedMetadata be the result of parsing metadataList. + const parsedMetadata = parseMetadata(metadataList) + + // 2. If parsedMetadata is no metadata, return true. + if (parsedMetadata === 'no metadata') { + return true + } + + // 3. If response is not eligible for integrity validation, return false. + // TODO + + // 4. If parsedMetadata is the empty set, return true. + if (parsedMetadata.length === 0) { + return true + } + + // 5. Let metadata be the result of getting the strongest + // metadata from parsedMetadata. + const strongest = getStrongestMetadata(parsedMetadata) + const metadata = filterMetadataListByAlgorithm(parsedMetadata, strongest) + + // 6. For each item in metadata: + for (const item of metadata) { + // 1. Let algorithm be the alg component of item. + const algorithm = item.algo + + // 2. Let expectedValue be the val component of item. + const expectedValue = item.hash + + // See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e + // "be liberal with padding". This is annoying, and it's not even in the spec. + + // 3. Let actualValue be the result of applying algorithm to bytes. + let actualValue = crypto.createHash(algorithm).update(bytes).digest('base64') + + if (actualValue[actualValue.length - 1] === '=') { + if (actualValue[actualValue.length - 2] === '=') { + actualValue = actualValue.slice(0, -2) + } else { + actualValue = actualValue.slice(0, -1) + } + } + + // 4. If actualValue is a case-sensitive match for expectedValue, + // return true. + if (compareBase64Mixed(actualValue, expectedValue)) { + return true + } + } + + // 7. Return false. + return false +} + +// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options +// https://www.w3.org/TR/CSP2/#source-list-syntax +// https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1 +const parseHashWithOptions = /(?sha256|sha384|sha512)-((?[A-Za-z0-9+/]+|[A-Za-z0-9_-]+)={0,2}(?:\s|$)( +[!-~]*)?)?/i + +/** + * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata + * @param {string} metadata + */ +function parseMetadata (metadata) { + // 1. Let result be the empty set. + /** @type {{ algo: string, hash: string }[]} */ + const result = [] + + // 2. Let empty be equal to true. + let empty = true + + // 3. For each token returned by splitting metadata on spaces: + for (const token of metadata.split(' ')) { + // 1. Set empty to false. + empty = false + + // 2. Parse token as a hash-with-options. + const parsedToken = parseHashWithOptions.exec(token) + + // 3. If token does not parse, continue to the next token. + if ( + parsedToken === null || + parsedToken.groups === undefined || + parsedToken.groups.algo === undefined + ) { + // Note: Chromium blocks the request at this point, but Firefox + // gives a warning that an invalid integrity was given. The + // correct behavior is to ignore these, and subsequently not + // check the integrity of the resource. + continue + } + + // 4. Let algorithm be the hash-algo component of token. + const algorithm = parsedToken.groups.algo.toLowerCase() + + // 5. If algorithm is a hash function recognized by the user + // agent, add the parsed token to result. + if (supportedHashes.includes(algorithm)) { + result.push(parsedToken.groups) + } + } + + // 4. Return no metadata if empty is true, otherwise return result. + if (empty === true) { + return 'no metadata' + } + + return result +} + +/** + * @param {{ algo: 'sha256' | 'sha384' | 'sha512' }[]} metadataList + */ +function getStrongestMetadata (metadataList) { + // Let algorithm be the algo component of the first item in metadataList. + // Can be sha256 + let algorithm = metadataList[0].algo + // If the algorithm is sha512, then it is the strongest + // and we can return immediately + if (algorithm[3] === '5') { + return algorithm + } + + for (let i = 1; i < metadataList.length; ++i) { + const metadata = metadataList[i] + // If the algorithm is sha512, then it is the strongest + // and we can break the loop immediately + if (metadata.algo[3] === '5') { + algorithm = 'sha512' + break + // If the algorithm is sha384, then a potential sha256 or sha384 is ignored + } else if (algorithm[3] === '3') { + continue + // algorithm is sha256, check if algorithm is sha384 and if so, set it as + // the strongest + } else if (metadata.algo[3] === '3') { + algorithm = 'sha384' + } + } + return algorithm +} + +function filterMetadataListByAlgorithm (metadataList, algorithm) { + if (metadataList.length === 1) { + return metadataList + } + + let pos = 0 + for (let i = 0; i < metadataList.length; ++i) { + if (metadataList[i].algo === algorithm) { + metadataList[pos++] = metadataList[i] + } + } + + metadataList.length = pos + + return metadataList +} + +/** + * Compares two base64 strings, allowing for base64url + * in the second string. + * +* @param {string} actualValue always base64 + * @param {string} expectedValue base64 or base64url + * @returns {boolean} + */ +function compareBase64Mixed (actualValue, expectedValue) { + if (actualValue.length !== expectedValue.length) { + return false + } + for (let i = 0; i < actualValue.length; ++i) { + if (actualValue[i] !== expectedValue[i]) { + if ( + (actualValue[i] === '+' && expectedValue[i] === '-') || + (actualValue[i] === '/' && expectedValue[i] === '_') + ) { + continue + } + return false + } + } + + return true +} + +// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request +function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) { + // TODO +} + +/** + * @link {https://html.spec.whatwg.org/multipage/origin.html#same-origin} + * @param {URL} A + * @param {URL} B + */ +function sameOrigin (A, B) { + // 1. If A and B are the same opaque origin, then return true. + if (A.origin === B.origin && A.origin === 'null') { + return true + } + + // 2. If A and B are both tuple origins and their schemes, + // hosts, and port are identical, then return true. + if (A.protocol === B.protocol && A.hostname === B.hostname && A.port === B.port) { + return true + } + + // 3. Return false. + return false +} + +function createDeferredPromise () { + let res + let rej + const promise = new Promise((resolve, reject) => { + res = resolve + rej = reject + }) + + return { promise, resolve: res, reject: rej } +} + +function isAborted (fetchParams) { + return fetchParams.controller.state === 'aborted' +} + +function isCancelled (fetchParams) { + return fetchParams.controller.state === 'aborted' || + fetchParams.controller.state === 'terminated' +} + +const normalizeMethodRecord = { + delete: 'DELETE', + DELETE: 'DELETE', + get: 'GET', + GET: 'GET', + head: 'HEAD', + HEAD: 'HEAD', + options: 'OPTIONS', + OPTIONS: 'OPTIONS', + post: 'POST', + POST: 'POST', + put: 'PUT', + PUT: 'PUT' +} + +// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`. +Object.setPrototypeOf(normalizeMethodRecord, null) + +/** + * @see https://fetch.spec.whatwg.org/#concept-method-normalize + * @param {string} method + */ +function normalizeMethod (method) { + return normalizeMethodRecord[method.toLowerCase()] ?? method +} + +// https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string +function serializeJavascriptValueToJSONString (value) { + // 1. Let result be ? Call(%JSON.stringify%, undefined, « value »). + const result = JSON.stringify(value) + + // 2. If result is undefined, then throw a TypeError. + if (result === undefined) { + throw new TypeError('Value is not JSON serializable') + } + + // 3. Assert: result is a string. + assert(typeof result === 'string') + + // 4. Return result. + return result +} + +// https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object +const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())) + +/** + * @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object + * @param {() => unknown[]} iterator + * @param {string} name name of the instance + * @param {'key'|'value'|'key+value'} kind + */ +function makeIterator (iterator, name, kind) { + const object = { + index: 0, + kind, + target: iterator + } + + const i = { + next () { + // 1. Let interface be the interface for which the iterator prototype object exists. + + // 2. Let thisValue be the this value. + + // 3. Let object be ? ToObject(thisValue). + + // 4. If object is a platform object, then perform a security + // check, passing: + + // 5. If object is not a default iterator object for interface, + // then throw a TypeError. + if (Object.getPrototypeOf(this) !== i) { + throw new TypeError( + `'next' called on an object that does not implement interface ${name} Iterator.` + ) + } + + // 6. Let index be object’s index. + // 7. Let kind be object’s kind. + // 8. Let values be object’s target's value pairs to iterate over. + const { index, kind, target } = object + const values = target() + + // 9. Let len be the length of values. + const len = values.length + + // 10. If index is greater than or equal to len, then return + // CreateIterResultObject(undefined, true). + if (index >= len) { + return { value: undefined, done: true } + } + + // 11. Let pair be the entry in values at index index. + const pair = values[index] + + // 12. Set object’s index to index + 1. + object.index = index + 1 + + // 13. Return the iterator result for pair and kind. + return iteratorResult(pair, kind) + }, + // The class string of an iterator prototype object for a given interface is the + // result of concatenating the identifier of the interface and the string " Iterator". + [Symbol.toStringTag]: `${name} Iterator` + } + + // The [[Prototype]] internal slot of an iterator prototype object must be %IteratorPrototype%. + Object.setPrototypeOf(i, esIteratorPrototype) + // esIteratorPrototype needs to be the prototype of i + // which is the prototype of an empty object. Yes, it's confusing. + return Object.setPrototypeOf({}, i) +} + +// https://webidl.spec.whatwg.org/#iterator-result +function iteratorResult (pair, kind) { + let result + + // 1. Let result be a value determined by the value of kind: + switch (kind) { + case 'key': { + // 1. Let idlKey be pair’s key. + // 2. Let key be the result of converting idlKey to an + // ECMAScript value. + // 3. result is key. + result = pair[0] + break + } + case 'value': { + // 1. Let idlValue be pair’s value. + // 2. Let value be the result of converting idlValue to + // an ECMAScript value. + // 3. result is value. + result = pair[1] + break + } + case 'key+value': { + // 1. Let idlKey be pair’s key. + // 2. Let idlValue be pair’s value. + // 3. Let key be the result of converting idlKey to an + // ECMAScript value. + // 4. Let value be the result of converting idlValue to + // an ECMAScript value. + // 5. Let array be ! ArrayCreate(2). + // 6. Call ! CreateDataProperty(array, "0", key). + // 7. Call ! CreateDataProperty(array, "1", value). + // 8. result is array. + result = pair + break + } + } + + // 2. Return CreateIterResultObject(result, false). + return { value: result, done: false } +} + +/** + * @see https://fetch.spec.whatwg.org/#body-fully-read + */ +async function fullyReadBody (body, processBody, processBodyError) { + // 1. If taskDestination is null, then set taskDestination to + // the result of starting a new parallel queue. + + // 2. Let successSteps given a byte sequence bytes be to queue a + // fetch task to run processBody given bytes, with taskDestination. + const successSteps = processBody + + // 3. Let errorSteps be to queue a fetch task to run processBodyError, + // with taskDestination. + const errorSteps = processBodyError + + // 4. Let reader be the result of getting a reader for body’s stream. + // If that threw an exception, then run errorSteps with that + // exception and return. + let reader + + try { + reader = body.stream.getReader() + } catch (e) { + errorSteps(e) + return + } + + // 5. Read all bytes from reader, given successSteps and errorSteps. + try { + const result = await readAllBytes(reader) + successSteps(result) + } catch (e) { + errorSteps(e) + } +} + +/** @type {ReadableStream} */ +let ReadableStream = globalThis.ReadableStream + +function isReadableStreamLike (stream) { + if (!ReadableStream) { + ReadableStream = (__nccwpck_require__(3774).ReadableStream) + } + + return stream instanceof ReadableStream || ( + stream[Symbol.toStringTag] === 'ReadableStream' && + typeof stream.tee === 'function' + ) +} + +const MAXIMUM_ARGUMENT_LENGTH = 65535 + +/** + * @see https://infra.spec.whatwg.org/#isomorphic-decode + * @param {number[]|Uint8Array} input + */ +function isomorphicDecode (input) { + // 1. To isomorphic decode a byte sequence input, return a string whose code point + // length is equal to input’s length and whose code points have the same values + // as the values of input’s bytes, in the same order. + + if (input.length < MAXIMUM_ARGUMENT_LENGTH) { + return String.fromCharCode(...input) + } + + return input.reduce((previous, current) => previous + String.fromCharCode(current), '') +} + +/** + * @param {ReadableStreamController} controller + */ +function readableStreamClose (controller) { + try { + controller.close() + } catch (err) { + // TODO: add comment explaining why this error occurs. + if (!err.message.includes('Controller is already closed')) { + throw err + } + } +} + +/** + * @see https://infra.spec.whatwg.org/#isomorphic-encode + * @param {string} input + */ +function isomorphicEncode (input) { + // 1. Assert: input contains no code points greater than U+00FF. + for (let i = 0; i < input.length; i++) { + assert(input.charCodeAt(i) <= 0xFF) + } + + // 2. Return a byte sequence whose length is equal to input’s code + // point length and whose bytes have the same values as the + // values of input’s code points, in the same order + return input +} + +/** + * @see https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-all-bytes + * @see https://streams.spec.whatwg.org/#read-loop + * @param {ReadableStreamDefaultReader} reader + */ +async function readAllBytes (reader) { + const bytes = [] + let byteLength = 0 + + while (true) { + const { done, value: chunk } = await reader.read() + + if (done) { + // 1. Call successSteps with bytes. + return Buffer.concat(bytes, byteLength) + } + + // 1. If chunk is not a Uint8Array object, call failureSteps + // with a TypeError and abort these steps. + if (!isUint8Array(chunk)) { + throw new TypeError('Received non-Uint8Array chunk') + } + + // 2. Append the bytes represented by chunk to bytes. + bytes.push(chunk) + byteLength += chunk.length + + // 3. Read-loop given reader, bytes, successSteps, and failureSteps. + } +} + +/** + * @see https://fetch.spec.whatwg.org/#is-local + * @param {URL} url + */ +function urlIsLocal (url) { + assert('protocol' in url) // ensure it's a url object + + const protocol = url.protocol + + return protocol === 'about:' || protocol === 'blob:' || protocol === 'data:' +} + +/** + * @param {string|URL} url + */ +function urlHasHttpsScheme (url) { + if (typeof url === 'string') { + return url.startsWith('https:') + } + + return url.protocol === 'https:' +} + +/** + * @see https://fetch.spec.whatwg.org/#http-scheme + * @param {URL} url + */ +function urlIsHttpHttpsScheme (url) { + assert('protocol' in url) // ensure it's a url object + + const protocol = url.protocol + + return protocol === 'http:' || protocol === 'https:' +} + +/** + * Fetch supports node >= 16.8.0, but Object.hasOwn was added in v16.9.0. + */ +const hasOwn = Object.hasOwn || ((dict, key) => Object.prototype.hasOwnProperty.call(dict, key)) + +module.exports = { + isAborted, + isCancelled, + createDeferredPromise, + ReadableStreamFrom, + toUSVString, + tryUpgradeRequestToAPotentiallyTrustworthyURL, + coarsenedSharedCurrentTime, + determineRequestsReferrer, + makePolicyContainer, + clonePolicyContainer, + appendFetchMetadata, + appendRequestOriginHeader, + TAOCheck, + corsCheck, + crossOriginResourcePolicyCheck, + createOpaqueTimingInfo, + setRequestReferrerPolicyOnRedirect, + isValidHTTPToken, + requestBadPort, + requestCurrentURL, + responseURL, + responseLocationURL, + isBlobLike, + isURLPotentiallyTrustworthy, + isValidReasonPhrase, + sameOrigin, + normalizeMethod, + serializeJavascriptValueToJSONString, + makeIterator, + isValidHeaderName, + isValidHeaderValue, + hasOwn, + isErrorLike, + fullyReadBody, + bytesMatch, + isReadableStreamLike, + readableStreamClose, + isomorphicEncode, + isomorphicDecode, + urlIsLocal, + urlHasHttpsScheme, + urlIsHttpHttpsScheme, + readAllBytes, + normalizeMethodRecord, + parseMetadata +} + + +/***/ }), + +/***/ 274: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { types } = __nccwpck_require__(9023) +const { hasOwn, toUSVString } = __nccwpck_require__(3359) + +/** @type {import('../../types/webidl').Webidl} */ +const webidl = {} +webidl.converters = {} +webidl.util = {} +webidl.errors = {} + +webidl.errors.exception = function (message) { + return new TypeError(`${message.header}: ${message.message}`) +} + +webidl.errors.conversionFailed = function (context) { + const plural = context.types.length === 1 ? '' : ' one of' + const message = + `${context.argument} could not be converted to` + + `${plural}: ${context.types.join(', ')}.` + + return webidl.errors.exception({ + header: context.prefix, + message + }) +} + +webidl.errors.invalidArgument = function (context) { + return webidl.errors.exception({ + header: context.prefix, + message: `"${context.value}" is an invalid ${context.type}.` + }) +} + +// https://webidl.spec.whatwg.org/#implements +webidl.brandCheck = function (V, I, opts = undefined) { + if (opts?.strict !== false && !(V instanceof I)) { + throw new TypeError('Illegal invocation') + } else { + return V?.[Symbol.toStringTag] === I.prototype[Symbol.toStringTag] + } +} + +webidl.argumentLengthCheck = function ({ length }, min, ctx) { + if (length < min) { + throw webidl.errors.exception({ + message: `${min} argument${min !== 1 ? 's' : ''} required, ` + + `but${length ? ' only' : ''} ${length} found.`, + ...ctx + }) + } +} + +webidl.illegalConstructor = function () { + throw webidl.errors.exception({ + header: 'TypeError', + message: 'Illegal constructor' + }) +} + +// https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values +webidl.util.Type = function (V) { + switch (typeof V) { + case 'undefined': return 'Undefined' + case 'boolean': return 'Boolean' + case 'string': return 'String' + case 'symbol': return 'Symbol' + case 'number': return 'Number' + case 'bigint': return 'BigInt' + case 'function': + case 'object': { + if (V === null) { + return 'Null' + } + + return 'Object' + } + } +} + +// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint +webidl.util.ConvertToInt = function (V, bitLength, signedness, opts = {}) { + let upperBound + let lowerBound + + // 1. If bitLength is 64, then: + if (bitLength === 64) { + // 1. Let upperBound be 2^53 − 1. + upperBound = Math.pow(2, 53) - 1 + + // 2. If signedness is "unsigned", then let lowerBound be 0. + if (signedness === 'unsigned') { + lowerBound = 0 + } else { + // 3. Otherwise let lowerBound be −2^53 + 1. + lowerBound = Math.pow(-2, 53) + 1 + } + } else if (signedness === 'unsigned') { + // 2. Otherwise, if signedness is "unsigned", then: + + // 1. Let lowerBound be 0. + lowerBound = 0 + + // 2. Let upperBound be 2^bitLength − 1. + upperBound = Math.pow(2, bitLength) - 1 + } else { + // 3. Otherwise: + + // 1. Let lowerBound be -2^bitLength − 1. + lowerBound = Math.pow(-2, bitLength) - 1 + + // 2. Let upperBound be 2^bitLength − 1 − 1. + upperBound = Math.pow(2, bitLength - 1) - 1 + } + + // 4. Let x be ? ToNumber(V). + let x = Number(V) + + // 5. If x is −0, then set x to +0. + if (x === 0) { + x = 0 + } + + // 6. If the conversion is to an IDL type associated + // with the [EnforceRange] extended attribute, then: + if (opts.enforceRange === true) { + // 1. If x is NaN, +∞, or −∞, then throw a TypeError. + if ( + Number.isNaN(x) || + x === Number.POSITIVE_INFINITY || + x === Number.NEGATIVE_INFINITY + ) { + throw webidl.errors.exception({ + header: 'Integer conversion', + message: `Could not convert ${V} to an integer.` + }) + } + + // 2. Set x to IntegerPart(x). + x = webidl.util.IntegerPart(x) + + // 3. If x < lowerBound or x > upperBound, then + // throw a TypeError. + if (x < lowerBound || x > upperBound) { + throw webidl.errors.exception({ + header: 'Integer conversion', + message: `Value must be between ${lowerBound}-${upperBound}, got ${x}.` + }) + } + + // 4. Return x. + return x + } + + // 7. If x is not NaN and the conversion is to an IDL + // type associated with the [Clamp] extended + // attribute, then: + if (!Number.isNaN(x) && opts.clamp === true) { + // 1. Set x to min(max(x, lowerBound), upperBound). + x = Math.min(Math.max(x, lowerBound), upperBound) + + // 2. Round x to the nearest integer, choosing the + // even integer if it lies halfway between two, + // and choosing +0 rather than −0. + if (Math.floor(x) % 2 === 0) { + x = Math.floor(x) + } else { + x = Math.ceil(x) + } + + // 3. Return x. + return x + } + + // 8. If x is NaN, +0, +∞, or −∞, then return +0. + if ( + Number.isNaN(x) || + (x === 0 && Object.is(0, x)) || + x === Number.POSITIVE_INFINITY || + x === Number.NEGATIVE_INFINITY + ) { + return 0 + } + + // 9. Set x to IntegerPart(x). + x = webidl.util.IntegerPart(x) + + // 10. Set x to x modulo 2^bitLength. + x = x % Math.pow(2, bitLength) + + // 11. If signedness is "signed" and x ≥ 2^bitLength − 1, + // then return x − 2^bitLength. + if (signedness === 'signed' && x >= Math.pow(2, bitLength) - 1) { + return x - Math.pow(2, bitLength) + } + + // 12. Otherwise, return x. + return x +} + +// https://webidl.spec.whatwg.org/#abstract-opdef-integerpart +webidl.util.IntegerPart = function (n) { + // 1. Let r be floor(abs(n)). + const r = Math.floor(Math.abs(n)) + + // 2. If n < 0, then return -1 × r. + if (n < 0) { + return -1 * r + } + + // 3. Otherwise, return r. + return r +} + +// https://webidl.spec.whatwg.org/#es-sequence +webidl.sequenceConverter = function (converter) { + return (V) => { + // 1. If Type(V) is not Object, throw a TypeError. + if (webidl.util.Type(V) !== 'Object') { + throw webidl.errors.exception({ + header: 'Sequence', + message: `Value of type ${webidl.util.Type(V)} is not an Object.` + }) + } + + // 2. Let method be ? GetMethod(V, @@iterator). + /** @type {Generator} */ + const method = V?.[Symbol.iterator]?.() + const seq = [] + + // 3. If method is undefined, throw a TypeError. + if ( + method === undefined || + typeof method.next !== 'function' + ) { + throw webidl.errors.exception({ + header: 'Sequence', + message: 'Object is not an iterator.' + }) + } + + // https://webidl.spec.whatwg.org/#create-sequence-from-iterable + while (true) { + const { done, value } = method.next() + + if (done) { + break + } + + seq.push(converter(value)) + } + + return seq + } +} + +// https://webidl.spec.whatwg.org/#es-to-record +webidl.recordConverter = function (keyConverter, valueConverter) { + return (O) => { + // 1. If Type(O) is not Object, throw a TypeError. + if (webidl.util.Type(O) !== 'Object') { + throw webidl.errors.exception({ + header: 'Record', + message: `Value of type ${webidl.util.Type(O)} is not an Object.` + }) + } + + // 2. Let result be a new empty instance of record. + const result = {} + + if (!types.isProxy(O)) { + // Object.keys only returns enumerable properties + const keys = Object.keys(O) + + for (const key of keys) { + // 1. Let typedKey be key converted to an IDL value of type K. + const typedKey = keyConverter(key) + + // 2. Let value be ? Get(O, key). + // 3. Let typedValue be value converted to an IDL value of type V. + const typedValue = valueConverter(O[key]) + + // 4. Set result[typedKey] to typedValue. + result[typedKey] = typedValue + } + + // 5. Return result. + return result + } + + // 3. Let keys be ? O.[[OwnPropertyKeys]](). + const keys = Reflect.ownKeys(O) + + // 4. For each key of keys. + for (const key of keys) { + // 1. Let desc be ? O.[[GetOwnProperty]](key). + const desc = Reflect.getOwnPropertyDescriptor(O, key) + + // 2. If desc is not undefined and desc.[[Enumerable]] is true: + if (desc?.enumerable) { + // 1. Let typedKey be key converted to an IDL value of type K. + const typedKey = keyConverter(key) + + // 2. Let value be ? Get(O, key). + // 3. Let typedValue be value converted to an IDL value of type V. + const typedValue = valueConverter(O[key]) + + // 4. Set result[typedKey] to typedValue. + result[typedKey] = typedValue + } + } + + // 5. Return result. + return result + } +} + +webidl.interfaceConverter = function (i) { + return (V, opts = {}) => { + if (opts.strict !== false && !(V instanceof i)) { + throw webidl.errors.exception({ + header: i.name, + message: `Expected ${V} to be an instance of ${i.name}.` + }) + } + + return V + } +} + +webidl.dictionaryConverter = function (converters) { + return (dictionary) => { + const type = webidl.util.Type(dictionary) + const dict = {} + + if (type === 'Null' || type === 'Undefined') { + return dict + } else if (type !== 'Object') { + throw webidl.errors.exception({ + header: 'Dictionary', + message: `Expected ${dictionary} to be one of: Null, Undefined, Object.` + }) + } + + for (const options of converters) { + const { key, defaultValue, required, converter } = options + + if (required === true) { + if (!hasOwn(dictionary, key)) { + throw webidl.errors.exception({ + header: 'Dictionary', + message: `Missing required key "${key}".` + }) + } + } + + let value = dictionary[key] + const hasDefault = hasOwn(options, 'defaultValue') + + // Only use defaultValue if value is undefined and + // a defaultValue options was provided. + if (hasDefault && value !== null) { + value = value ?? defaultValue + } + + // A key can be optional and have no default value. + // When this happens, do not perform a conversion, + // and do not assign the key a value. + if (required || hasDefault || value !== undefined) { + value = converter(value) + + if ( + options.allowedValues && + !options.allowedValues.includes(value) + ) { + throw webidl.errors.exception({ + header: 'Dictionary', + message: `${value} is not an accepted type. Expected one of ${options.allowedValues.join(', ')}.` + }) + } + + dict[key] = value + } + } + + return dict + } +} + +webidl.nullableConverter = function (converter) { + return (V) => { + if (V === null) { + return V + } + + return converter(V) + } +} + +// https://webidl.spec.whatwg.org/#es-DOMString +webidl.converters.DOMString = function (V, opts = {}) { + // 1. If V is null and the conversion is to an IDL type + // associated with the [LegacyNullToEmptyString] + // extended attribute, then return the DOMString value + // that represents the empty string. + if (V === null && opts.legacyNullToEmptyString) { + return '' + } + + // 2. Let x be ? ToString(V). + if (typeof V === 'symbol') { + throw new TypeError('Could not convert argument of type symbol to string.') + } + + // 3. Return the IDL DOMString value that represents the + // same sequence of code units as the one the + // ECMAScript String value x represents. + return String(V) +} + +// https://webidl.spec.whatwg.org/#es-ByteString +webidl.converters.ByteString = function (V) { + // 1. Let x be ? ToString(V). + // Note: DOMString converter perform ? ToString(V) + const x = webidl.converters.DOMString(V) + + // 2. If the value of any element of x is greater than + // 255, then throw a TypeError. + for (let index = 0; index < x.length; index++) { + if (x.charCodeAt(index) > 255) { + throw new TypeError( + 'Cannot convert argument to a ByteString because the character at ' + + `index ${index} has a value of ${x.charCodeAt(index)} which is greater than 255.` + ) + } + } + + // 3. Return an IDL ByteString value whose length is the + // length of x, and where the value of each element is + // the value of the corresponding element of x. + return x +} + +// https://webidl.spec.whatwg.org/#es-USVString +webidl.converters.USVString = toUSVString + +// https://webidl.spec.whatwg.org/#es-boolean +webidl.converters.boolean = function (V) { + // 1. Let x be the result of computing ToBoolean(V). + const x = Boolean(V) + + // 2. Return the IDL boolean value that is the one that represents + // the same truth value as the ECMAScript Boolean value x. + return x +} + +// https://webidl.spec.whatwg.org/#es-any +webidl.converters.any = function (V) { + return V +} + +// https://webidl.spec.whatwg.org/#es-long-long +webidl.converters['long long'] = function (V) { + // 1. Let x be ? ConvertToInt(V, 64, "signed"). + const x = webidl.util.ConvertToInt(V, 64, 'signed') + + // 2. Return the IDL long long value that represents + // the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#es-unsigned-long-long +webidl.converters['unsigned long long'] = function (V) { + // 1. Let x be ? ConvertToInt(V, 64, "unsigned"). + const x = webidl.util.ConvertToInt(V, 64, 'unsigned') + + // 2. Return the IDL unsigned long long value that + // represents the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#es-unsigned-long +webidl.converters['unsigned long'] = function (V) { + // 1. Let x be ? ConvertToInt(V, 32, "unsigned"). + const x = webidl.util.ConvertToInt(V, 32, 'unsigned') + + // 2. Return the IDL unsigned long value that + // represents the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#es-unsigned-short +webidl.converters['unsigned short'] = function (V, opts) { + // 1. Let x be ? ConvertToInt(V, 16, "unsigned"). + const x = webidl.util.ConvertToInt(V, 16, 'unsigned', opts) + + // 2. Return the IDL unsigned short value that represents + // the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#idl-ArrayBuffer +webidl.converters.ArrayBuffer = function (V, opts = {}) { + // 1. If Type(V) is not Object, or V does not have an + // [[ArrayBufferData]] internal slot, then throw a + // TypeError. + // see: https://tc39.es/ecma262/#sec-properties-of-the-arraybuffer-instances + // see: https://tc39.es/ecma262/#sec-properties-of-the-sharedarraybuffer-instances + if ( + webidl.util.Type(V) !== 'Object' || + !types.isAnyArrayBuffer(V) + ) { + throw webidl.errors.conversionFailed({ + prefix: `${V}`, + argument: `${V}`, + types: ['ArrayBuffer'] + }) + } + + // 2. If the conversion is not to an IDL type associated + // with the [AllowShared] extended attribute, and + // IsSharedArrayBuffer(V) is true, then throw a + // TypeError. + if (opts.allowShared === false && types.isSharedArrayBuffer(V)) { + throw webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'SharedArrayBuffer is not allowed.' + }) + } + + // 3. If the conversion is not to an IDL type associated + // with the [AllowResizable] extended attribute, and + // IsResizableArrayBuffer(V) is true, then throw a + // TypeError. + // Note: resizable ArrayBuffers are currently a proposal. + + // 4. Return the IDL ArrayBuffer value that is a + // reference to the same object as V. + return V +} + +webidl.converters.TypedArray = function (V, T, opts = {}) { + // 1. Let T be the IDL type V is being converted to. + + // 2. If Type(V) is not Object, or V does not have a + // [[TypedArrayName]] internal slot with a value + // equal to T’s name, then throw a TypeError. + if ( + webidl.util.Type(V) !== 'Object' || + !types.isTypedArray(V) || + V.constructor.name !== T.name + ) { + throw webidl.errors.conversionFailed({ + prefix: `${T.name}`, + argument: `${V}`, + types: [T.name] + }) + } + + // 3. If the conversion is not to an IDL type associated + // with the [AllowShared] extended attribute, and + // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is + // true, then throw a TypeError. + if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { + throw webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'SharedArrayBuffer is not allowed.' + }) + } + + // 4. If the conversion is not to an IDL type associated + // with the [AllowResizable] extended attribute, and + // IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is + // true, then throw a TypeError. + // Note: resizable array buffers are currently a proposal + + // 5. Return the IDL value of type T that is a reference + // to the same object as V. + return V +} + +webidl.converters.DataView = function (V, opts = {}) { + // 1. If Type(V) is not Object, or V does not have a + // [[DataView]] internal slot, then throw a TypeError. + if (webidl.util.Type(V) !== 'Object' || !types.isDataView(V)) { + throw webidl.errors.exception({ + header: 'DataView', + message: 'Object is not a DataView.' + }) + } + + // 2. If the conversion is not to an IDL type associated + // with the [AllowShared] extended attribute, and + // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is true, + // then throw a TypeError. + if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { + throw webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'SharedArrayBuffer is not allowed.' + }) + } + + // 3. If the conversion is not to an IDL type associated + // with the [AllowResizable] extended attribute, and + // IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is + // true, then throw a TypeError. + // Note: resizable ArrayBuffers are currently a proposal + + // 4. Return the IDL DataView value that is a reference + // to the same object as V. + return V +} + +// https://webidl.spec.whatwg.org/#BufferSource +webidl.converters.BufferSource = function (V, opts = {}) { + if (types.isAnyArrayBuffer(V)) { + return webidl.converters.ArrayBuffer(V, opts) + } + + if (types.isTypedArray(V)) { + return webidl.converters.TypedArray(V, V.constructor) + } + + if (types.isDataView(V)) { + return webidl.converters.DataView(V, opts) + } + + throw new TypeError(`Could not convert ${V} to a BufferSource.`) +} + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.ByteString +) + +webidl.converters['sequence>'] = webidl.sequenceConverter( + webidl.converters['sequence'] +) + +webidl.converters['record'] = webidl.recordConverter( + webidl.converters.ByteString, + webidl.converters.ByteString +) + +module.exports = { + webidl +} + + +/***/ }), + +/***/ 3744: +/***/ ((module) => { + +"use strict"; + + +/** + * @see https://encoding.spec.whatwg.org/#concept-encoding-get + * @param {string|undefined} label + */ +function getEncoding (label) { + if (!label) { + return 'failure' + } + + // 1. Remove any leading and trailing ASCII whitespace from label. + // 2. If label is an ASCII case-insensitive match for any of the + // labels listed in the table below, then return the + // corresponding encoding; otherwise return failure. + switch (label.trim().toLowerCase()) { + case 'unicode-1-1-utf-8': + case 'unicode11utf8': + case 'unicode20utf8': + case 'utf-8': + case 'utf8': + case 'x-unicode20utf8': + return 'UTF-8' + case '866': + case 'cp866': + case 'csibm866': + case 'ibm866': + return 'IBM866' + case 'csisolatin2': + case 'iso-8859-2': + case 'iso-ir-101': + case 'iso8859-2': + case 'iso88592': + case 'iso_8859-2': + case 'iso_8859-2:1987': + case 'l2': + case 'latin2': + return 'ISO-8859-2' + case 'csisolatin3': + case 'iso-8859-3': + case 'iso-ir-109': + case 'iso8859-3': + case 'iso88593': + case 'iso_8859-3': + case 'iso_8859-3:1988': + case 'l3': + case 'latin3': + return 'ISO-8859-3' + case 'csisolatin4': + case 'iso-8859-4': + case 'iso-ir-110': + case 'iso8859-4': + case 'iso88594': + case 'iso_8859-4': + case 'iso_8859-4:1988': + case 'l4': + case 'latin4': + return 'ISO-8859-4' + case 'csisolatincyrillic': + case 'cyrillic': + case 'iso-8859-5': + case 'iso-ir-144': + case 'iso8859-5': + case 'iso88595': + case 'iso_8859-5': + case 'iso_8859-5:1988': + return 'ISO-8859-5' + case 'arabic': + case 'asmo-708': + case 'csiso88596e': + case 'csiso88596i': + case 'csisolatinarabic': + case 'ecma-114': + case 'iso-8859-6': + case 'iso-8859-6-e': + case 'iso-8859-6-i': + case 'iso-ir-127': + case 'iso8859-6': + case 'iso88596': + case 'iso_8859-6': + case 'iso_8859-6:1987': + return 'ISO-8859-6' + case 'csisolatingreek': + case 'ecma-118': + case 'elot_928': + case 'greek': + case 'greek8': + case 'iso-8859-7': + case 'iso-ir-126': + case 'iso8859-7': + case 'iso88597': + case 'iso_8859-7': + case 'iso_8859-7:1987': + case 'sun_eu_greek': + return 'ISO-8859-7' + case 'csiso88598e': + case 'csisolatinhebrew': + case 'hebrew': + case 'iso-8859-8': + case 'iso-8859-8-e': + case 'iso-ir-138': + case 'iso8859-8': + case 'iso88598': + case 'iso_8859-8': + case 'iso_8859-8:1988': + case 'visual': + return 'ISO-8859-8' + case 'csiso88598i': + case 'iso-8859-8-i': + case 'logical': + return 'ISO-8859-8-I' + case 'csisolatin6': + case 'iso-8859-10': + case 'iso-ir-157': + case 'iso8859-10': + case 'iso885910': + case 'l6': + case 'latin6': + return 'ISO-8859-10' + case 'iso-8859-13': + case 'iso8859-13': + case 'iso885913': + return 'ISO-8859-13' + case 'iso-8859-14': + case 'iso8859-14': + case 'iso885914': + return 'ISO-8859-14' + case 'csisolatin9': + case 'iso-8859-15': + case 'iso8859-15': + case 'iso885915': + case 'iso_8859-15': + case 'l9': + return 'ISO-8859-15' + case 'iso-8859-16': + return 'ISO-8859-16' + case 'cskoi8r': + case 'koi': + case 'koi8': + case 'koi8-r': + case 'koi8_r': + return 'KOI8-R' + case 'koi8-ru': + case 'koi8-u': + return 'KOI8-U' + case 'csmacintosh': + case 'mac': + case 'macintosh': + case 'x-mac-roman': + return 'macintosh' + case 'iso-8859-11': + case 'iso8859-11': + case 'iso885911': + case 'tis-620': + case 'windows-874': + return 'windows-874' + case 'cp1250': + case 'windows-1250': + case 'x-cp1250': + return 'windows-1250' + case 'cp1251': + case 'windows-1251': + case 'x-cp1251': + return 'windows-1251' + case 'ansi_x3.4-1968': + case 'ascii': + case 'cp1252': + case 'cp819': + case 'csisolatin1': + case 'ibm819': + case 'iso-8859-1': + case 'iso-ir-100': + case 'iso8859-1': + case 'iso88591': + case 'iso_8859-1': + case 'iso_8859-1:1987': + case 'l1': + case 'latin1': + case 'us-ascii': + case 'windows-1252': + case 'x-cp1252': + return 'windows-1252' + case 'cp1253': + case 'windows-1253': + case 'x-cp1253': + return 'windows-1253' + case 'cp1254': + case 'csisolatin5': + case 'iso-8859-9': + case 'iso-ir-148': + case 'iso8859-9': + case 'iso88599': + case 'iso_8859-9': + case 'iso_8859-9:1989': + case 'l5': + case 'latin5': + case 'windows-1254': + case 'x-cp1254': + return 'windows-1254' + case 'cp1255': + case 'windows-1255': + case 'x-cp1255': + return 'windows-1255' + case 'cp1256': + case 'windows-1256': + case 'x-cp1256': + return 'windows-1256' + case 'cp1257': + case 'windows-1257': + case 'x-cp1257': + return 'windows-1257' + case 'cp1258': + case 'windows-1258': + case 'x-cp1258': + return 'windows-1258' + case 'x-mac-cyrillic': + case 'x-mac-ukrainian': + return 'x-mac-cyrillic' + case 'chinese': + case 'csgb2312': + case 'csiso58gb231280': + case 'gb2312': + case 'gb_2312': + case 'gb_2312-80': + case 'gbk': + case 'iso-ir-58': + case 'x-gbk': + return 'GBK' + case 'gb18030': + return 'gb18030' + case 'big5': + case 'big5-hkscs': + case 'cn-big5': + case 'csbig5': + case 'x-x-big5': + return 'Big5' + case 'cseucpkdfmtjapanese': + case 'euc-jp': + case 'x-euc-jp': + return 'EUC-JP' + case 'csiso2022jp': + case 'iso-2022-jp': + return 'ISO-2022-JP' + case 'csshiftjis': + case 'ms932': + case 'ms_kanji': + case 'shift-jis': + case 'shift_jis': + case 'sjis': + case 'windows-31j': + case 'x-sjis': + return 'Shift_JIS' + case 'cseuckr': + case 'csksc56011987': + case 'euc-kr': + case 'iso-ir-149': + case 'korean': + case 'ks_c_5601-1987': + case 'ks_c_5601-1989': + case 'ksc5601': + case 'ksc_5601': + case 'windows-949': + return 'EUC-KR' + case 'csiso2022kr': + case 'hz-gb-2312': + case 'iso-2022-cn': + case 'iso-2022-cn-ext': + case 'iso-2022-kr': + case 'replacement': + return 'replacement' + case 'unicodefffe': + case 'utf-16be': + return 'UTF-16BE' + case 'csunicode': + case 'iso-10646-ucs-2': + case 'ucs-2': + case 'unicode': + case 'unicodefeff': + case 'utf-16': + case 'utf-16le': + return 'UTF-16LE' + case 'x-user-defined': + return 'x-user-defined' + default: return 'failure' + } +} + +module.exports = { + getEncoding +} + + +/***/ }), + +/***/ 1428: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + staticPropertyDescriptors, + readOperation, + fireAProgressEvent +} = __nccwpck_require__(3201) +const { + kState, + kError, + kResult, + kEvents, + kAborted +} = __nccwpck_require__(1056) +const { webidl } = __nccwpck_require__(274) +const { kEnumerableProperty } = __nccwpck_require__(1436) + +class FileReader extends EventTarget { + constructor () { + super() + + this[kState] = 'empty' + this[kResult] = null + this[kError] = null + this[kEvents] = { + loadend: null, + error: null, + abort: null, + load: null, + progress: null, + loadstart: null + } + } + + /** + * @see https://w3c.github.io/FileAPI/#dfn-readAsArrayBuffer + * @param {import('buffer').Blob} blob + */ + readAsArrayBuffer (blob) { + webidl.brandCheck(this, FileReader) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsArrayBuffer' }) + + blob = webidl.converters.Blob(blob, { strict: false }) + + // The readAsArrayBuffer(blob) method, when invoked, + // must initiate a read operation for blob with ArrayBuffer. + readOperation(this, blob, 'ArrayBuffer') + } + + /** + * @see https://w3c.github.io/FileAPI/#readAsBinaryString + * @param {import('buffer').Blob} blob + */ + readAsBinaryString (blob) { + webidl.brandCheck(this, FileReader) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsBinaryString' }) + + blob = webidl.converters.Blob(blob, { strict: false }) + + // The readAsBinaryString(blob) method, when invoked, + // must initiate a read operation for blob with BinaryString. + readOperation(this, blob, 'BinaryString') + } + + /** + * @see https://w3c.github.io/FileAPI/#readAsDataText + * @param {import('buffer').Blob} blob + * @param {string?} encoding + */ + readAsText (blob, encoding = undefined) { + webidl.brandCheck(this, FileReader) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsText' }) + + blob = webidl.converters.Blob(blob, { strict: false }) + + if (encoding !== undefined) { + encoding = webidl.converters.DOMString(encoding) + } + + // The readAsText(blob, encoding) method, when invoked, + // must initiate a read operation for blob with Text and encoding. + readOperation(this, blob, 'Text', encoding) + } + + /** + * @see https://w3c.github.io/FileAPI/#dfn-readAsDataURL + * @param {import('buffer').Blob} blob + */ + readAsDataURL (blob) { + webidl.brandCheck(this, FileReader) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsDataURL' }) + + blob = webidl.converters.Blob(blob, { strict: false }) + + // The readAsDataURL(blob) method, when invoked, must + // initiate a read operation for blob with DataURL. + readOperation(this, blob, 'DataURL') + } + + /** + * @see https://w3c.github.io/FileAPI/#dfn-abort + */ + abort () { + // 1. If this's state is "empty" or if this's state is + // "done" set this's result to null and terminate + // this algorithm. + if (this[kState] === 'empty' || this[kState] === 'done') { + this[kResult] = null + return + } + + // 2. If this's state is "loading" set this's state to + // "done" and set this's result to null. + if (this[kState] === 'loading') { + this[kState] = 'done' + this[kResult] = null + } + + // 3. If there are any tasks from this on the file reading + // task source in an affiliated task queue, then remove + // those tasks from that task queue. + this[kAborted] = true + + // 4. Terminate the algorithm for the read method being processed. + // TODO + + // 5. Fire a progress event called abort at this. + fireAProgressEvent('abort', this) + + // 6. If this's state is not "loading", fire a progress + // event called loadend at this. + if (this[kState] !== 'loading') { + fireAProgressEvent('loadend', this) + } + } + + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-readystate + */ + get readyState () { + webidl.brandCheck(this, FileReader) + + switch (this[kState]) { + case 'empty': return this.EMPTY + case 'loading': return this.LOADING + case 'done': return this.DONE + } + } + + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-result + */ + get result () { + webidl.brandCheck(this, FileReader) + + // The result attribute’s getter, when invoked, must return + // this's result. + return this[kResult] + } + + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-error + */ + get error () { + webidl.brandCheck(this, FileReader) + + // The error attribute’s getter, when invoked, must return + // this's error. + return this[kError] + } + + get onloadend () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].loadend + } + + set onloadend (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].loadend) { + this.removeEventListener('loadend', this[kEvents].loadend) + } + + if (typeof fn === 'function') { + this[kEvents].loadend = fn + this.addEventListener('loadend', fn) + } else { + this[kEvents].loadend = null + } + } + + get onerror () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].error + } + + set onerror (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].error) { + this.removeEventListener('error', this[kEvents].error) + } + + if (typeof fn === 'function') { + this[kEvents].error = fn + this.addEventListener('error', fn) + } else { + this[kEvents].error = null + } + } + + get onloadstart () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].loadstart + } + + set onloadstart (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].loadstart) { + this.removeEventListener('loadstart', this[kEvents].loadstart) + } + + if (typeof fn === 'function') { + this[kEvents].loadstart = fn + this.addEventListener('loadstart', fn) + } else { + this[kEvents].loadstart = null + } + } + + get onprogress () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].progress + } + + set onprogress (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].progress) { + this.removeEventListener('progress', this[kEvents].progress) + } + + if (typeof fn === 'function') { + this[kEvents].progress = fn + this.addEventListener('progress', fn) + } else { + this[kEvents].progress = null + } + } + + get onload () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].load + } + + set onload (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].load) { + this.removeEventListener('load', this[kEvents].load) + } + + if (typeof fn === 'function') { + this[kEvents].load = fn + this.addEventListener('load', fn) + } else { + this[kEvents].load = null + } + } + + get onabort () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].abort + } + + set onabort (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].abort) { + this.removeEventListener('abort', this[kEvents].abort) + } + + if (typeof fn === 'function') { + this[kEvents].abort = fn + this.addEventListener('abort', fn) + } else { + this[kEvents].abort = null + } + } +} + +// https://w3c.github.io/FileAPI/#dom-filereader-empty +FileReader.EMPTY = FileReader.prototype.EMPTY = 0 +// https://w3c.github.io/FileAPI/#dom-filereader-loading +FileReader.LOADING = FileReader.prototype.LOADING = 1 +// https://w3c.github.io/FileAPI/#dom-filereader-done +FileReader.DONE = FileReader.prototype.DONE = 2 + +Object.defineProperties(FileReader.prototype, { + EMPTY: staticPropertyDescriptors, + LOADING: staticPropertyDescriptors, + DONE: staticPropertyDescriptors, + readAsArrayBuffer: kEnumerableProperty, + readAsBinaryString: kEnumerableProperty, + readAsText: kEnumerableProperty, + readAsDataURL: kEnumerableProperty, + abort: kEnumerableProperty, + readyState: kEnumerableProperty, + result: kEnumerableProperty, + error: kEnumerableProperty, + onloadstart: kEnumerableProperty, + onprogress: kEnumerableProperty, + onload: kEnumerableProperty, + onabort: kEnumerableProperty, + onerror: kEnumerableProperty, + onloadend: kEnumerableProperty, + [Symbol.toStringTag]: { + value: 'FileReader', + writable: false, + enumerable: false, + configurable: true + } +}) + +Object.defineProperties(FileReader, { + EMPTY: staticPropertyDescriptors, + LOADING: staticPropertyDescriptors, + DONE: staticPropertyDescriptors +}) + +module.exports = { + FileReader +} + + +/***/ }), + +/***/ 1828: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { webidl } = __nccwpck_require__(274) + +const kState = Symbol('ProgressEvent state') + +/** + * @see https://xhr.spec.whatwg.org/#progressevent + */ +class ProgressEvent extends Event { + constructor (type, eventInitDict = {}) { + type = webidl.converters.DOMString(type) + eventInitDict = webidl.converters.ProgressEventInit(eventInitDict ?? {}) + + super(type, eventInitDict) + + this[kState] = { + lengthComputable: eventInitDict.lengthComputable, + loaded: eventInitDict.loaded, + total: eventInitDict.total + } + } + + get lengthComputable () { + webidl.brandCheck(this, ProgressEvent) + + return this[kState].lengthComputable + } + + get loaded () { + webidl.brandCheck(this, ProgressEvent) + + return this[kState].loaded + } + + get total () { + webidl.brandCheck(this, ProgressEvent) + + return this[kState].total + } +} + +webidl.converters.ProgressEventInit = webidl.dictionaryConverter([ + { + key: 'lengthComputable', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'loaded', + converter: webidl.converters['unsigned long long'], + defaultValue: 0 + }, + { + key: 'total', + converter: webidl.converters['unsigned long long'], + defaultValue: 0 + }, + { + key: 'bubbles', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'cancelable', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'composed', + converter: webidl.converters.boolean, + defaultValue: false + } +]) + +module.exports = { + ProgressEvent +} + + +/***/ }), + +/***/ 1056: +/***/ ((module) => { + +"use strict"; + + +module.exports = { + kState: Symbol('FileReader state'), + kResult: Symbol('FileReader result'), + kError: Symbol('FileReader error'), + kLastProgressEventFired: Symbol('FileReader last progress event fired timestamp'), + kEvents: Symbol('FileReader events'), + kAborted: Symbol('FileReader aborted') +} + + +/***/ }), + +/***/ 3201: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + kState, + kError, + kResult, + kAborted, + kLastProgressEventFired +} = __nccwpck_require__(1056) +const { ProgressEvent } = __nccwpck_require__(1828) +const { getEncoding } = __nccwpck_require__(3744) +const { DOMException } = __nccwpck_require__(450) +const { serializeAMimeType, parseMIMEType } = __nccwpck_require__(5294) +const { types } = __nccwpck_require__(9023) +const { StringDecoder } = __nccwpck_require__(3193) +const { btoa } = __nccwpck_require__(181) + +/** @type {PropertyDescriptor} */ +const staticPropertyDescriptors = { + enumerable: true, + writable: false, + configurable: false +} + +/** + * @see https://w3c.github.io/FileAPI/#readOperation + * @param {import('./filereader').FileReader} fr + * @param {import('buffer').Blob} blob + * @param {string} type + * @param {string?} encodingName + */ +function readOperation (fr, blob, type, encodingName) { + // 1. If fr’s state is "loading", throw an InvalidStateError + // DOMException. + if (fr[kState] === 'loading') { + throw new DOMException('Invalid state', 'InvalidStateError') + } + + // 2. Set fr’s state to "loading". + fr[kState] = 'loading' + + // 3. Set fr’s result to null. + fr[kResult] = null + + // 4. Set fr’s error to null. + fr[kError] = null + + // 5. Let stream be the result of calling get stream on blob. + /** @type {import('stream/web').ReadableStream} */ + const stream = blob.stream() + + // 6. Let reader be the result of getting a reader from stream. + const reader = stream.getReader() + + // 7. Let bytes be an empty byte sequence. + /** @type {Uint8Array[]} */ + const bytes = [] + + // 8. Let chunkPromise be the result of reading a chunk from + // stream with reader. + let chunkPromise = reader.read() + + // 9. Let isFirstChunk be true. + let isFirstChunk = true + + // 10. In parallel, while true: + // Note: "In parallel" just means non-blocking + // Note 2: readOperation itself cannot be async as double + // reading the body would then reject the promise, instead + // of throwing an error. + ;(async () => { + while (!fr[kAborted]) { + // 1. Wait for chunkPromise to be fulfilled or rejected. + try { + const { done, value } = await chunkPromise + + // 2. If chunkPromise is fulfilled, and isFirstChunk is + // true, queue a task to fire a progress event called + // loadstart at fr. + if (isFirstChunk && !fr[kAborted]) { + queueMicrotask(() => { + fireAProgressEvent('loadstart', fr) + }) + } + + // 3. Set isFirstChunk to false. + isFirstChunk = false + + // 4. If chunkPromise is fulfilled with an object whose + // done property is false and whose value property is + // a Uint8Array object, run these steps: + if (!done && types.isUint8Array(value)) { + // 1. Let bs be the byte sequence represented by the + // Uint8Array object. + + // 2. Append bs to bytes. + bytes.push(value) + + // 3. If roughly 50ms have passed since these steps + // were last invoked, queue a task to fire a + // progress event called progress at fr. + if ( + ( + fr[kLastProgressEventFired] === undefined || + Date.now() - fr[kLastProgressEventFired] >= 50 + ) && + !fr[kAborted] + ) { + fr[kLastProgressEventFired] = Date.now() + queueMicrotask(() => { + fireAProgressEvent('progress', fr) + }) + } + + // 4. Set chunkPromise to the result of reading a + // chunk from stream with reader. + chunkPromise = reader.read() + } else if (done) { + // 5. Otherwise, if chunkPromise is fulfilled with an + // object whose done property is true, queue a task + // to run the following steps and abort this algorithm: + queueMicrotask(() => { + // 1. Set fr’s state to "done". + fr[kState] = 'done' + + // 2. Let result be the result of package data given + // bytes, type, blob’s type, and encodingName. + try { + const result = packageData(bytes, type, blob.type, encodingName) + + // 4. Else: + + if (fr[kAborted]) { + return + } + + // 1. Set fr’s result to result. + fr[kResult] = result + + // 2. Fire a progress event called load at the fr. + fireAProgressEvent('load', fr) + } catch (error) { + // 3. If package data threw an exception error: + + // 1. Set fr’s error to error. + fr[kError] = error + + // 2. Fire a progress event called error at fr. + fireAProgressEvent('error', fr) + } + + // 5. If fr’s state is not "loading", fire a progress + // event called loadend at the fr. + if (fr[kState] !== 'loading') { + fireAProgressEvent('loadend', fr) + } + }) + + break + } + } catch (error) { + if (fr[kAborted]) { + return + } + + // 6. Otherwise, if chunkPromise is rejected with an + // error error, queue a task to run the following + // steps and abort this algorithm: + queueMicrotask(() => { + // 1. Set fr’s state to "done". + fr[kState] = 'done' + + // 2. Set fr’s error to error. + fr[kError] = error + + // 3. Fire a progress event called error at fr. + fireAProgressEvent('error', fr) + + // 4. If fr’s state is not "loading", fire a progress + // event called loadend at fr. + if (fr[kState] !== 'loading') { + fireAProgressEvent('loadend', fr) + } + }) + + break + } + } + })() +} + +/** + * @see https://w3c.github.io/FileAPI/#fire-a-progress-event + * @see https://dom.spec.whatwg.org/#concept-event-fire + * @param {string} e The name of the event + * @param {import('./filereader').FileReader} reader + */ +function fireAProgressEvent (e, reader) { + // The progress event e does not bubble. e.bubbles must be false + // The progress event e is NOT cancelable. e.cancelable must be false + const event = new ProgressEvent(e, { + bubbles: false, + cancelable: false + }) + + reader.dispatchEvent(event) +} + +/** + * @see https://w3c.github.io/FileAPI/#blob-package-data + * @param {Uint8Array[]} bytes + * @param {string} type + * @param {string?} mimeType + * @param {string?} encodingName + */ +function packageData (bytes, type, mimeType, encodingName) { + // 1. A Blob has an associated package data algorithm, given + // bytes, a type, a optional mimeType, and a optional + // encodingName, which switches on type and runs the + // associated steps: + + switch (type) { + case 'DataURL': { + // 1. Return bytes as a DataURL [RFC2397] subject to + // the considerations below: + // * Use mimeType as part of the Data URL if it is + // available in keeping with the Data URL + // specification [RFC2397]. + // * If mimeType is not available return a Data URL + // without a media-type. [RFC2397]. + + // https://datatracker.ietf.org/doc/html/rfc2397#section-3 + // dataurl := "data:" [ mediatype ] [ ";base64" ] "," data + // mediatype := [ type "/" subtype ] *( ";" parameter ) + // data := *urlchar + // parameter := attribute "=" value + let dataURL = 'data:' + + const parsed = parseMIMEType(mimeType || 'application/octet-stream') + + if (parsed !== 'failure') { + dataURL += serializeAMimeType(parsed) + } + + dataURL += ';base64,' + + const decoder = new StringDecoder('latin1') + + for (const chunk of bytes) { + dataURL += btoa(decoder.write(chunk)) + } + + dataURL += btoa(decoder.end()) + + return dataURL + } + case 'Text': { + // 1. Let encoding be failure + let encoding = 'failure' + + // 2. If the encodingName is present, set encoding to the + // result of getting an encoding from encodingName. + if (encodingName) { + encoding = getEncoding(encodingName) + } + + // 3. If encoding is failure, and mimeType is present: + if (encoding === 'failure' && mimeType) { + // 1. Let type be the result of parse a MIME type + // given mimeType. + const type = parseMIMEType(mimeType) + + // 2. If type is not failure, set encoding to the result + // of getting an encoding from type’s parameters["charset"]. + if (type !== 'failure') { + encoding = getEncoding(type.parameters.get('charset')) + } + } + + // 4. If encoding is failure, then set encoding to UTF-8. + if (encoding === 'failure') { + encoding = 'UTF-8' + } + + // 5. Decode bytes using fallback encoding encoding, and + // return the result. + return decode(bytes, encoding) + } + case 'ArrayBuffer': { + // Return a new ArrayBuffer whose contents are bytes. + const sequence = combineByteSequences(bytes) + + return sequence.buffer + } + case 'BinaryString': { + // Return bytes as a binary string, in which every byte + // is represented by a code unit of equal value [0..255]. + let binaryString = '' + + const decoder = new StringDecoder('latin1') + + for (const chunk of bytes) { + binaryString += decoder.write(chunk) + } + + binaryString += decoder.end() + + return binaryString + } + } +} + +/** + * @see https://encoding.spec.whatwg.org/#decode + * @param {Uint8Array[]} ioQueue + * @param {string} encoding + */ +function decode (ioQueue, encoding) { + const bytes = combineByteSequences(ioQueue) + + // 1. Let BOMEncoding be the result of BOM sniffing ioQueue. + const BOMEncoding = BOMSniffing(bytes) + + let slice = 0 + + // 2. If BOMEncoding is non-null: + if (BOMEncoding !== null) { + // 1. Set encoding to BOMEncoding. + encoding = BOMEncoding + + // 2. Read three bytes from ioQueue, if BOMEncoding is + // UTF-8; otherwise read two bytes. + // (Do nothing with those bytes.) + slice = BOMEncoding === 'UTF-8' ? 3 : 2 + } + + // 3. Process a queue with an instance of encoding’s + // decoder, ioQueue, output, and "replacement". + + // 4. Return output. + + const sliced = bytes.slice(slice) + return new TextDecoder(encoding).decode(sliced) +} + +/** + * @see https://encoding.spec.whatwg.org/#bom-sniff + * @param {Uint8Array} ioQueue + */ +function BOMSniffing (ioQueue) { + // 1. Let BOM be the result of peeking 3 bytes from ioQueue, + // converted to a byte sequence. + const [a, b, c] = ioQueue + + // 2. For each of the rows in the table below, starting with + // the first one and going down, if BOM starts with the + // bytes given in the first column, then return the + // encoding given in the cell in the second column of that + // row. Otherwise, return null. + if (a === 0xEF && b === 0xBB && c === 0xBF) { + return 'UTF-8' + } else if (a === 0xFE && b === 0xFF) { + return 'UTF-16BE' + } else if (a === 0xFF && b === 0xFE) { + return 'UTF-16LE' + } + + return null +} + +/** + * @param {Uint8Array[]} sequences + */ +function combineByteSequences (sequences) { + const size = sequences.reduce((a, b) => { + return a + b.byteLength + }, 0) + + let offset = 0 + + return sequences.reduce((a, b) => { + a.set(b, offset) + offset += b.byteLength + return a + }, new Uint8Array(size)) +} + +module.exports = { + staticPropertyDescriptors, + readOperation, + fireAProgressEvent +} + + +/***/ }), + +/***/ 8377: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +// We include a version number for the Dispatcher API. In case of breaking changes, +// this version number must be increased to avoid conflicts. +const globalDispatcher = Symbol.for('undici.globalDispatcher.1') +const { InvalidArgumentError } = __nccwpck_require__(1975) +const Agent = __nccwpck_require__(2841) + +if (getGlobalDispatcher() === undefined) { + setGlobalDispatcher(new Agent()) +} + +function setGlobalDispatcher (agent) { + if (!agent || typeof agent.dispatch !== 'function') { + throw new InvalidArgumentError('Argument agent must implement Agent') + } + Object.defineProperty(globalThis, globalDispatcher, { + value: agent, + writable: true, + enumerable: false, + configurable: false + }) +} + +function getGlobalDispatcher () { + return globalThis[globalDispatcher] +} + +module.exports = { + setGlobalDispatcher, + getGlobalDispatcher +} + + +/***/ }), + +/***/ 3276: +/***/ ((module) => { + +"use strict"; + + +module.exports = class DecoratorHandler { + constructor (handler) { + this.handler = handler + } + + onConnect (...args) { + return this.handler.onConnect(...args) + } + + onError (...args) { + return this.handler.onError(...args) + } + + onUpgrade (...args) { + return this.handler.onUpgrade(...args) + } + + onHeaders (...args) { + return this.handler.onHeaders(...args) + } + + onData (...args) { + return this.handler.onData(...args) + } + + onComplete (...args) { + return this.handler.onComplete(...args) + } + + onBodySent (...args) { + return this.handler.onBodySent(...args) + } +} + + +/***/ }), + +/***/ 6303: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const util = __nccwpck_require__(1436) +const { kBodyUsed } = __nccwpck_require__(9583) +const assert = __nccwpck_require__(2613) +const { InvalidArgumentError } = __nccwpck_require__(1975) +const EE = __nccwpck_require__(4434) + +const redirectableStatusCodes = [300, 301, 302, 303, 307, 308] + +const kBody = Symbol('body') + +class BodyAsyncIterable { + constructor (body) { + this[kBody] = body + this[kBodyUsed] = false + } + + async * [Symbol.asyncIterator] () { + assert(!this[kBodyUsed], 'disturbed') + this[kBodyUsed] = true + yield * this[kBody] + } +} + +class RedirectHandler { + constructor (dispatch, maxRedirections, opts, handler) { + if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) { + throw new InvalidArgumentError('maxRedirections must be a positive number') + } + + util.validateHandler(handler, opts.method, opts.upgrade) + + this.dispatch = dispatch + this.location = null + this.abort = null + this.opts = { ...opts, maxRedirections: 0 } // opts must be a copy + this.maxRedirections = maxRedirections + this.handler = handler + this.history = [] + + if (util.isStream(this.opts.body)) { + // TODO (fix): Provide some way for the user to cache the file to e.g. /tmp + // so that it can be dispatched again? + // TODO (fix): Do we need 100-expect support to provide a way to do this properly? + if (util.bodyLength(this.opts.body) === 0) { + this.opts.body + .on('data', function () { + assert(false) + }) + } + + if (typeof this.opts.body.readableDidRead !== 'boolean') { + this.opts.body[kBodyUsed] = false + EE.prototype.on.call(this.opts.body, 'data', function () { + this[kBodyUsed] = true + }) + } + } else if (this.opts.body && typeof this.opts.body.pipeTo === 'function') { + // TODO (fix): We can't access ReadableStream internal state + // to determine whether or not it has been disturbed. This is just + // a workaround. + this.opts.body = new BodyAsyncIterable(this.opts.body) + } else if ( + this.opts.body && + typeof this.opts.body !== 'string' && + !ArrayBuffer.isView(this.opts.body) && + util.isIterable(this.opts.body) + ) { + // TODO: Should we allow re-using iterable if !this.opts.idempotent + // or through some other flag? + this.opts.body = new BodyAsyncIterable(this.opts.body) + } + } + + onConnect (abort) { + this.abort = abort + this.handler.onConnect(abort, { history: this.history }) + } + + onUpgrade (statusCode, headers, socket) { + this.handler.onUpgrade(statusCode, headers, socket) + } + + onError (error) { + this.handler.onError(error) + } + + onHeaders (statusCode, headers, resume, statusText) { + this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body) + ? null + : parseLocation(statusCode, headers) + + if (this.opts.origin) { + this.history.push(new URL(this.opts.path, this.opts.origin)) + } + + if (!this.location) { + return this.handler.onHeaders(statusCode, headers, resume, statusText) + } + + const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin))) + const path = search ? `${pathname}${search}` : pathname + + // Remove headers referring to the original URL. + // By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers. + // https://tools.ietf.org/html/rfc7231#section-6.4 + this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin) + this.opts.path = path + this.opts.origin = origin + this.opts.maxRedirections = 0 + this.opts.query = null + + // https://tools.ietf.org/html/rfc7231#section-6.4.4 + // In case of HTTP 303, always replace method to be either HEAD or GET + if (statusCode === 303 && this.opts.method !== 'HEAD') { + this.opts.method = 'GET' + this.opts.body = null + } + } + + onData (chunk) { + if (this.location) { + /* + https://tools.ietf.org/html/rfc7231#section-6.4 + + TLDR: undici always ignores 3xx response bodies. + + Redirection is used to serve the requested resource from another URL, so it is assumes that + no body is generated (and thus can be ignored). Even though generating a body is not prohibited. + + For status 301, 302, 303, 307 and 308 (the latter from RFC 7238), the specs mention that the body usually + (which means it's optional and not mandated) contain just an hyperlink to the value of + the Location response header, so the body can be ignored safely. + + For status 300, which is "Multiple Choices", the spec mentions both generating a Location + response header AND a response body with the other possible location to follow. + Since the spec explicitily chooses not to specify a format for such body and leave it to + servers and browsers implementors, we ignore the body as there is no specified way to eventually parse it. + */ + } else { + return this.handler.onData(chunk) + } + } + + onComplete (trailers) { + if (this.location) { + /* + https://tools.ietf.org/html/rfc7231#section-6.4 + + TLDR: undici always ignores 3xx response trailers as they are not expected in case of redirections + and neither are useful if present. + + See comment on onData method above for more detailed informations. + */ + + this.location = null + this.abort = null + + this.dispatch(this.opts, this) + } else { + this.handler.onComplete(trailers) + } + } + + onBodySent (chunk) { + if (this.handler.onBodySent) { + this.handler.onBodySent(chunk) + } + } +} + +function parseLocation (statusCode, headers) { + if (redirectableStatusCodes.indexOf(statusCode) === -1) { + return null + } + + for (let i = 0; i < headers.length; i += 2) { + if (headers[i].toString().toLowerCase() === 'location') { + return headers[i + 1] + } + } +} + +// https://tools.ietf.org/html/rfc7231#section-6.4.4 +function shouldRemoveHeader (header, removeContent, unknownOrigin) { + if (header.length === 4) { + return util.headerNameToString(header) === 'host' + } + if (removeContent && util.headerNameToString(header).startsWith('content-')) { + return true + } + if (unknownOrigin && (header.length === 13 || header.length === 6 || header.length === 19)) { + const name = util.headerNameToString(header) + return name === 'authorization' || name === 'cookie' || name === 'proxy-authorization' + } + return false +} + +// https://tools.ietf.org/html/rfc7231#section-6.4 +function cleanRequestHeaders (headers, removeContent, unknownOrigin) { + const ret = [] + if (Array.isArray(headers)) { + for (let i = 0; i < headers.length; i += 2) { + if (!shouldRemoveHeader(headers[i], removeContent, unknownOrigin)) { + ret.push(headers[i], headers[i + 1]) + } + } + } else if (headers && typeof headers === 'object') { + for (const key of Object.keys(headers)) { + if (!shouldRemoveHeader(key, removeContent, unknownOrigin)) { + ret.push(key, headers[key]) + } + } + } else { + assert(headers == null, 'headers must be an object or an array') + } + return ret +} + +module.exports = RedirectHandler + + +/***/ }), + +/***/ 3441: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const assert = __nccwpck_require__(2613) + +const { kRetryHandlerDefaultRetry } = __nccwpck_require__(9583) +const { RequestRetryError } = __nccwpck_require__(1975) +const { isDisturbed, parseHeaders, parseRangeHeader } = __nccwpck_require__(1436) + +function calculateRetryAfterHeader (retryAfter) { + const current = Date.now() + const diff = new Date(retryAfter).getTime() - current + + return diff +} + +class RetryHandler { + constructor (opts, handlers) { + const { retryOptions, ...dispatchOpts } = opts + const { + // Retry scoped + retry: retryFn, + maxRetries, + maxTimeout, + minTimeout, + timeoutFactor, + // Response scoped + methods, + errorCodes, + retryAfter, + statusCodes + } = retryOptions ?? {} + + this.dispatch = handlers.dispatch + this.handler = handlers.handler + this.opts = dispatchOpts + this.abort = null + this.aborted = false + this.retryOpts = { + retry: retryFn ?? RetryHandler[kRetryHandlerDefaultRetry], + retryAfter: retryAfter ?? true, + maxTimeout: maxTimeout ?? 30 * 1000, // 30s, + timeout: minTimeout ?? 500, // .5s + timeoutFactor: timeoutFactor ?? 2, + maxRetries: maxRetries ?? 5, + // What errors we should retry + methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'], + // Indicates which errors to retry + statusCodes: statusCodes ?? [500, 502, 503, 504, 429], + // List of errors to retry + errorCodes: errorCodes ?? [ + 'ECONNRESET', + 'ECONNREFUSED', + 'ENOTFOUND', + 'ENETDOWN', + 'ENETUNREACH', + 'EHOSTDOWN', + 'EHOSTUNREACH', + 'EPIPE' + ] + } + + this.retryCount = 0 + this.start = 0 + this.end = null + this.etag = null + this.resume = null + + // Handle possible onConnect duplication + this.handler.onConnect(reason => { + this.aborted = true + if (this.abort) { + this.abort(reason) + } else { + this.reason = reason + } + }) + } + + onRequestSent () { + if (this.handler.onRequestSent) { + this.handler.onRequestSent() + } + } + + onUpgrade (statusCode, headers, socket) { + if (this.handler.onUpgrade) { + this.handler.onUpgrade(statusCode, headers, socket) + } + } + + onConnect (abort) { + if (this.aborted) { + abort(this.reason) + } else { + this.abort = abort + } + } + + onBodySent (chunk) { + if (this.handler.onBodySent) return this.handler.onBodySent(chunk) + } + + static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) { + const { statusCode, code, headers } = err + const { method, retryOptions } = opts + const { + maxRetries, + timeout, + maxTimeout, + timeoutFactor, + statusCodes, + errorCodes, + methods + } = retryOptions + let { counter, currentTimeout } = state + + currentTimeout = + currentTimeout != null && currentTimeout > 0 ? currentTimeout : timeout + + // Any code that is not a Undici's originated and allowed to retry + if ( + code && + code !== 'UND_ERR_REQ_RETRY' && + code !== 'UND_ERR_SOCKET' && + !errorCodes.includes(code) + ) { + cb(err) + return + } + + // If a set of method are provided and the current method is not in the list + if (Array.isArray(methods) && !methods.includes(method)) { + cb(err) + return + } + + // If a set of status code are provided and the current status code is not in the list + if ( + statusCode != null && + Array.isArray(statusCodes) && + !statusCodes.includes(statusCode) + ) { + cb(err) + return + } + + // If we reached the max number of retries + if (counter > maxRetries) { + cb(err) + return + } + + let retryAfterHeader = headers != null && headers['retry-after'] + if (retryAfterHeader) { + retryAfterHeader = Number(retryAfterHeader) + retryAfterHeader = isNaN(retryAfterHeader) + ? calculateRetryAfterHeader(retryAfterHeader) + : retryAfterHeader * 1e3 // Retry-After is in seconds + } + + const retryTimeout = + retryAfterHeader > 0 + ? Math.min(retryAfterHeader, maxTimeout) + : Math.min(currentTimeout * timeoutFactor ** counter, maxTimeout) + + state.currentTimeout = retryTimeout + + setTimeout(() => cb(null), retryTimeout) + } + + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + const headers = parseHeaders(rawHeaders) + + this.retryCount += 1 + + if (statusCode >= 300) { + this.abort( + new RequestRetryError('Request failed', statusCode, { + headers, + count: this.retryCount + }) + ) + return false + } + + // Checkpoint for resume from where we left it + if (this.resume != null) { + this.resume = null + + if (statusCode !== 206) { + return true + } + + const contentRange = parseRangeHeader(headers['content-range']) + // If no content range + if (!contentRange) { + this.abort( + new RequestRetryError('Content-Range mismatch', statusCode, { + headers, + count: this.retryCount + }) + ) + return false + } + + // Let's start with a weak etag check + if (this.etag != null && this.etag !== headers.etag) { + this.abort( + new RequestRetryError('ETag mismatch', statusCode, { + headers, + count: this.retryCount + }) + ) + return false + } + + const { start, size, end = size } = contentRange + + assert(this.start === start, 'content-range mismatch') + assert(this.end == null || this.end === end, 'content-range mismatch') + + this.resume = resume + return true + } + + if (this.end == null) { + if (statusCode === 206) { + // First time we receive 206 + const range = parseRangeHeader(headers['content-range']) + + if (range == null) { + return this.handler.onHeaders( + statusCode, + rawHeaders, + resume, + statusMessage + ) + } + + const { start, size, end = size } = range + + assert( + start != null && Number.isFinite(start) && this.start !== start, + 'content-range mismatch' + ) + assert(Number.isFinite(start)) + assert( + end != null && Number.isFinite(end) && this.end !== end, + 'invalid content-length' + ) + + this.start = start + this.end = end + } + + // We make our best to checkpoint the body for further range headers + if (this.end == null) { + const contentLength = headers['content-length'] + this.end = contentLength != null ? Number(contentLength) : null + } + + assert(Number.isFinite(this.start)) + assert( + this.end == null || Number.isFinite(this.end), + 'invalid content-length' + ) + + this.resume = resume + this.etag = headers.etag != null ? headers.etag : null + + return this.handler.onHeaders( + statusCode, + rawHeaders, + resume, + statusMessage + ) + } + + const err = new RequestRetryError('Request failed', statusCode, { + headers, + count: this.retryCount + }) + + this.abort(err) + + return false + } + + onData (chunk) { + this.start += chunk.length + + return this.handler.onData(chunk) + } + + onComplete (rawTrailers) { + this.retryCount = 0 + return this.handler.onComplete(rawTrailers) + } + + onError (err) { + if (this.aborted || isDisturbed(this.opts.body)) { + return this.handler.onError(err) + } + + this.retryOpts.retry( + err, + { + state: { counter: this.retryCount++, currentTimeout: this.retryAfter }, + opts: { retryOptions: this.retryOpts, ...this.opts } + }, + onRetry.bind(this) + ) + + function onRetry (err) { + if (err != null || this.aborted || isDisturbed(this.opts.body)) { + return this.handler.onError(err) + } + + if (this.start !== 0) { + this.opts = { + ...this.opts, + headers: { + ...this.opts.headers, + range: `bytes=${this.start}-${this.end ?? ''}` + } + } + } + + try { + this.dispatch(this.opts, this) + } catch (err) { + this.handler.onError(err) + } + } + } +} + +module.exports = RetryHandler + + +/***/ }), + +/***/ 1475: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const RedirectHandler = __nccwpck_require__(6303) + +function createRedirectInterceptor ({ maxRedirections: defaultMaxRedirections }) { + return (dispatch) => { + return function Intercept (opts, handler) { + const { maxRedirections = defaultMaxRedirections } = opts + + if (!maxRedirections) { + return dispatch(opts, handler) + } + + const redirectHandler = new RedirectHandler(dispatch, maxRedirections, opts, handler) + opts = { ...opts, maxRedirections: 0 } // Stop sub dispatcher from also redirecting. + return dispatch(opts, redirectHandler) + } + } +} + +module.exports = createRedirectInterceptor + + +/***/ }), + +/***/ 3564: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SPECIAL_HEADERS = exports.HEADER_STATE = exports.MINOR = exports.MAJOR = exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS = exports.TOKEN = exports.STRICT_TOKEN = exports.HEX = exports.URL_CHAR = exports.STRICT_URL_CHAR = exports.USERINFO_CHARS = exports.MARK = exports.ALPHANUM = exports.NUM = exports.HEX_MAP = exports.NUM_MAP = exports.ALPHA = exports.FINISH = exports.H_METHOD_MAP = exports.METHOD_MAP = exports.METHODS_RTSP = exports.METHODS_ICE = exports.METHODS_HTTP = exports.METHODS = exports.LENIENT_FLAGS = exports.FLAGS = exports.TYPE = exports.ERROR = void 0; +const utils_1 = __nccwpck_require__(432); +// C headers +var ERROR; +(function (ERROR) { + ERROR[ERROR["OK"] = 0] = "OK"; + ERROR[ERROR["INTERNAL"] = 1] = "INTERNAL"; + ERROR[ERROR["STRICT"] = 2] = "STRICT"; + ERROR[ERROR["LF_EXPECTED"] = 3] = "LF_EXPECTED"; + ERROR[ERROR["UNEXPECTED_CONTENT_LENGTH"] = 4] = "UNEXPECTED_CONTENT_LENGTH"; + ERROR[ERROR["CLOSED_CONNECTION"] = 5] = "CLOSED_CONNECTION"; + ERROR[ERROR["INVALID_METHOD"] = 6] = "INVALID_METHOD"; + ERROR[ERROR["INVALID_URL"] = 7] = "INVALID_URL"; + ERROR[ERROR["INVALID_CONSTANT"] = 8] = "INVALID_CONSTANT"; + ERROR[ERROR["INVALID_VERSION"] = 9] = "INVALID_VERSION"; + ERROR[ERROR["INVALID_HEADER_TOKEN"] = 10] = "INVALID_HEADER_TOKEN"; + ERROR[ERROR["INVALID_CONTENT_LENGTH"] = 11] = "INVALID_CONTENT_LENGTH"; + ERROR[ERROR["INVALID_CHUNK_SIZE"] = 12] = "INVALID_CHUNK_SIZE"; + ERROR[ERROR["INVALID_STATUS"] = 13] = "INVALID_STATUS"; + ERROR[ERROR["INVALID_EOF_STATE"] = 14] = "INVALID_EOF_STATE"; + ERROR[ERROR["INVALID_TRANSFER_ENCODING"] = 15] = "INVALID_TRANSFER_ENCODING"; + ERROR[ERROR["CB_MESSAGE_BEGIN"] = 16] = "CB_MESSAGE_BEGIN"; + ERROR[ERROR["CB_HEADERS_COMPLETE"] = 17] = "CB_HEADERS_COMPLETE"; + ERROR[ERROR["CB_MESSAGE_COMPLETE"] = 18] = "CB_MESSAGE_COMPLETE"; + ERROR[ERROR["CB_CHUNK_HEADER"] = 19] = "CB_CHUNK_HEADER"; + ERROR[ERROR["CB_CHUNK_COMPLETE"] = 20] = "CB_CHUNK_COMPLETE"; + ERROR[ERROR["PAUSED"] = 21] = "PAUSED"; + ERROR[ERROR["PAUSED_UPGRADE"] = 22] = "PAUSED_UPGRADE"; + ERROR[ERROR["PAUSED_H2_UPGRADE"] = 23] = "PAUSED_H2_UPGRADE"; + ERROR[ERROR["USER"] = 24] = "USER"; +})(ERROR = exports.ERROR || (exports.ERROR = {})); +var TYPE; +(function (TYPE) { + TYPE[TYPE["BOTH"] = 0] = "BOTH"; + TYPE[TYPE["REQUEST"] = 1] = "REQUEST"; + TYPE[TYPE["RESPONSE"] = 2] = "RESPONSE"; +})(TYPE = exports.TYPE || (exports.TYPE = {})); +var FLAGS; +(function (FLAGS) { + FLAGS[FLAGS["CONNECTION_KEEP_ALIVE"] = 1] = "CONNECTION_KEEP_ALIVE"; + FLAGS[FLAGS["CONNECTION_CLOSE"] = 2] = "CONNECTION_CLOSE"; + FLAGS[FLAGS["CONNECTION_UPGRADE"] = 4] = "CONNECTION_UPGRADE"; + FLAGS[FLAGS["CHUNKED"] = 8] = "CHUNKED"; + FLAGS[FLAGS["UPGRADE"] = 16] = "UPGRADE"; + FLAGS[FLAGS["CONTENT_LENGTH"] = 32] = "CONTENT_LENGTH"; + FLAGS[FLAGS["SKIPBODY"] = 64] = "SKIPBODY"; + FLAGS[FLAGS["TRAILING"] = 128] = "TRAILING"; + // 1 << 8 is unused + FLAGS[FLAGS["TRANSFER_ENCODING"] = 512] = "TRANSFER_ENCODING"; +})(FLAGS = exports.FLAGS || (exports.FLAGS = {})); +var LENIENT_FLAGS; +(function (LENIENT_FLAGS) { + LENIENT_FLAGS[LENIENT_FLAGS["HEADERS"] = 1] = "HEADERS"; + LENIENT_FLAGS[LENIENT_FLAGS["CHUNKED_LENGTH"] = 2] = "CHUNKED_LENGTH"; + LENIENT_FLAGS[LENIENT_FLAGS["KEEP_ALIVE"] = 4] = "KEEP_ALIVE"; +})(LENIENT_FLAGS = exports.LENIENT_FLAGS || (exports.LENIENT_FLAGS = {})); +var METHODS; +(function (METHODS) { + METHODS[METHODS["DELETE"] = 0] = "DELETE"; + METHODS[METHODS["GET"] = 1] = "GET"; + METHODS[METHODS["HEAD"] = 2] = "HEAD"; + METHODS[METHODS["POST"] = 3] = "POST"; + METHODS[METHODS["PUT"] = 4] = "PUT"; + /* pathological */ + METHODS[METHODS["CONNECT"] = 5] = "CONNECT"; + METHODS[METHODS["OPTIONS"] = 6] = "OPTIONS"; + METHODS[METHODS["TRACE"] = 7] = "TRACE"; + /* WebDAV */ + METHODS[METHODS["COPY"] = 8] = "COPY"; + METHODS[METHODS["LOCK"] = 9] = "LOCK"; + METHODS[METHODS["MKCOL"] = 10] = "MKCOL"; + METHODS[METHODS["MOVE"] = 11] = "MOVE"; + METHODS[METHODS["PROPFIND"] = 12] = "PROPFIND"; + METHODS[METHODS["PROPPATCH"] = 13] = "PROPPATCH"; + METHODS[METHODS["SEARCH"] = 14] = "SEARCH"; + METHODS[METHODS["UNLOCK"] = 15] = "UNLOCK"; + METHODS[METHODS["BIND"] = 16] = "BIND"; + METHODS[METHODS["REBIND"] = 17] = "REBIND"; + METHODS[METHODS["UNBIND"] = 18] = "UNBIND"; + METHODS[METHODS["ACL"] = 19] = "ACL"; + /* subversion */ + METHODS[METHODS["REPORT"] = 20] = "REPORT"; + METHODS[METHODS["MKACTIVITY"] = 21] = "MKACTIVITY"; + METHODS[METHODS["CHECKOUT"] = 22] = "CHECKOUT"; + METHODS[METHODS["MERGE"] = 23] = "MERGE"; + /* upnp */ + METHODS[METHODS["M-SEARCH"] = 24] = "M-SEARCH"; + METHODS[METHODS["NOTIFY"] = 25] = "NOTIFY"; + METHODS[METHODS["SUBSCRIBE"] = 26] = "SUBSCRIBE"; + METHODS[METHODS["UNSUBSCRIBE"] = 27] = "UNSUBSCRIBE"; + /* RFC-5789 */ + METHODS[METHODS["PATCH"] = 28] = "PATCH"; + METHODS[METHODS["PURGE"] = 29] = "PURGE"; + /* CalDAV */ + METHODS[METHODS["MKCALENDAR"] = 30] = "MKCALENDAR"; + /* RFC-2068, section 19.6.1.2 */ + METHODS[METHODS["LINK"] = 31] = "LINK"; + METHODS[METHODS["UNLINK"] = 32] = "UNLINK"; + /* icecast */ + METHODS[METHODS["SOURCE"] = 33] = "SOURCE"; + /* RFC-7540, section 11.6 */ + METHODS[METHODS["PRI"] = 34] = "PRI"; + /* RFC-2326 RTSP */ + METHODS[METHODS["DESCRIBE"] = 35] = "DESCRIBE"; + METHODS[METHODS["ANNOUNCE"] = 36] = "ANNOUNCE"; + METHODS[METHODS["SETUP"] = 37] = "SETUP"; + METHODS[METHODS["PLAY"] = 38] = "PLAY"; + METHODS[METHODS["PAUSE"] = 39] = "PAUSE"; + METHODS[METHODS["TEARDOWN"] = 40] = "TEARDOWN"; + METHODS[METHODS["GET_PARAMETER"] = 41] = "GET_PARAMETER"; + METHODS[METHODS["SET_PARAMETER"] = 42] = "SET_PARAMETER"; + METHODS[METHODS["REDIRECT"] = 43] = "REDIRECT"; + METHODS[METHODS["RECORD"] = 44] = "RECORD"; + /* RAOP */ + METHODS[METHODS["FLUSH"] = 45] = "FLUSH"; +})(METHODS = exports.METHODS || (exports.METHODS = {})); +exports.METHODS_HTTP = [ + METHODS.DELETE, + METHODS.GET, + METHODS.HEAD, + METHODS.POST, + METHODS.PUT, + METHODS.CONNECT, + METHODS.OPTIONS, + METHODS.TRACE, + METHODS.COPY, + METHODS.LOCK, + METHODS.MKCOL, + METHODS.MOVE, + METHODS.PROPFIND, + METHODS.PROPPATCH, + METHODS.SEARCH, + METHODS.UNLOCK, + METHODS.BIND, + METHODS.REBIND, + METHODS.UNBIND, + METHODS.ACL, + METHODS.REPORT, + METHODS.MKACTIVITY, + METHODS.CHECKOUT, + METHODS.MERGE, + METHODS['M-SEARCH'], + METHODS.NOTIFY, + METHODS.SUBSCRIBE, + METHODS.UNSUBSCRIBE, + METHODS.PATCH, + METHODS.PURGE, + METHODS.MKCALENDAR, + METHODS.LINK, + METHODS.UNLINK, + METHODS.PRI, + // TODO(indutny): should we allow it with HTTP? + METHODS.SOURCE, +]; +exports.METHODS_ICE = [ + METHODS.SOURCE, +]; +exports.METHODS_RTSP = [ + METHODS.OPTIONS, + METHODS.DESCRIBE, + METHODS.ANNOUNCE, + METHODS.SETUP, + METHODS.PLAY, + METHODS.PAUSE, + METHODS.TEARDOWN, + METHODS.GET_PARAMETER, + METHODS.SET_PARAMETER, + METHODS.REDIRECT, + METHODS.RECORD, + METHODS.FLUSH, + // For AirPlay + METHODS.GET, + METHODS.POST, +]; +exports.METHOD_MAP = utils_1.enumToMap(METHODS); +exports.H_METHOD_MAP = {}; +Object.keys(exports.METHOD_MAP).forEach((key) => { + if (/^H/.test(key)) { + exports.H_METHOD_MAP[key] = exports.METHOD_MAP[key]; + } +}); +var FINISH; +(function (FINISH) { + FINISH[FINISH["SAFE"] = 0] = "SAFE"; + FINISH[FINISH["SAFE_WITH_CB"] = 1] = "SAFE_WITH_CB"; + FINISH[FINISH["UNSAFE"] = 2] = "UNSAFE"; +})(FINISH = exports.FINISH || (exports.FINISH = {})); +exports.ALPHA = []; +for (let i = 'A'.charCodeAt(0); i <= 'Z'.charCodeAt(0); i++) { + // Upper case + exports.ALPHA.push(String.fromCharCode(i)); + // Lower case + exports.ALPHA.push(String.fromCharCode(i + 0x20)); +} +exports.NUM_MAP = { + 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, + 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, +}; +exports.HEX_MAP = { + 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, + 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, + A: 0XA, B: 0XB, C: 0XC, D: 0XD, E: 0XE, F: 0XF, + a: 0xa, b: 0xb, c: 0xc, d: 0xd, e: 0xe, f: 0xf, +}; +exports.NUM = [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', +]; +exports.ALPHANUM = exports.ALPHA.concat(exports.NUM); +exports.MARK = ['-', '_', '.', '!', '~', '*', '\'', '(', ')']; +exports.USERINFO_CHARS = exports.ALPHANUM + .concat(exports.MARK) + .concat(['%', ';', ':', '&', '=', '+', '$', ',']); +// TODO(indutny): use RFC +exports.STRICT_URL_CHAR = [ + '!', '"', '$', '%', '&', '\'', + '(', ')', '*', '+', ',', '-', '.', '/', + ':', ';', '<', '=', '>', + '@', '[', '\\', ']', '^', '_', + '`', + '{', '|', '}', '~', +].concat(exports.ALPHANUM); +exports.URL_CHAR = exports.STRICT_URL_CHAR + .concat(['\t', '\f']); +// All characters with 0x80 bit set to 1 +for (let i = 0x80; i <= 0xff; i++) { + exports.URL_CHAR.push(i); +} +exports.HEX = exports.NUM.concat(['a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F']); +/* Tokens as defined by rfc 2616. Also lowercases them. + * token = 1* + * separators = "(" | ")" | "<" | ">" | "@" + * | "," | ";" | ":" | "\" | <"> + * | "/" | "[" | "]" | "?" | "=" + * | "{" | "}" | SP | HT + */ +exports.STRICT_TOKEN = [ + '!', '#', '$', '%', '&', '\'', + '*', '+', '-', '.', + '^', '_', '`', + '|', '~', +].concat(exports.ALPHANUM); +exports.TOKEN = exports.STRICT_TOKEN.concat([' ']); +/* + * Verify that a char is a valid visible (printable) US-ASCII + * character or %x80-FF + */ +exports.HEADER_CHARS = ['\t']; +for (let i = 32; i <= 255; i++) { + if (i !== 127) { + exports.HEADER_CHARS.push(i); + } +} +// ',' = \x44 +exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS.filter((c) => c !== 44); +exports.MAJOR = exports.NUM_MAP; +exports.MINOR = exports.MAJOR; +var HEADER_STATE; +(function (HEADER_STATE) { + HEADER_STATE[HEADER_STATE["GENERAL"] = 0] = "GENERAL"; + HEADER_STATE[HEADER_STATE["CONNECTION"] = 1] = "CONNECTION"; + HEADER_STATE[HEADER_STATE["CONTENT_LENGTH"] = 2] = "CONTENT_LENGTH"; + HEADER_STATE[HEADER_STATE["TRANSFER_ENCODING"] = 3] = "TRANSFER_ENCODING"; + HEADER_STATE[HEADER_STATE["UPGRADE"] = 4] = "UPGRADE"; + HEADER_STATE[HEADER_STATE["CONNECTION_KEEP_ALIVE"] = 5] = "CONNECTION_KEEP_ALIVE"; + HEADER_STATE[HEADER_STATE["CONNECTION_CLOSE"] = 6] = "CONNECTION_CLOSE"; + HEADER_STATE[HEADER_STATE["CONNECTION_UPGRADE"] = 7] = "CONNECTION_UPGRADE"; + HEADER_STATE[HEADER_STATE["TRANSFER_ENCODING_CHUNKED"] = 8] = "TRANSFER_ENCODING_CHUNKED"; +})(HEADER_STATE = exports.HEADER_STATE || (exports.HEADER_STATE = {})); +exports.SPECIAL_HEADERS = { + 'connection': HEADER_STATE.CONNECTION, + 'content-length': HEADER_STATE.CONTENT_LENGTH, + 'proxy-connection': HEADER_STATE.CONNECTION, + 'transfer-encoding': HEADER_STATE.TRANSFER_ENCODING, + 'upgrade': HEADER_STATE.UPGRADE, +}; +//# sourceMappingURL=constants.js.map + +/***/ }), + +/***/ 5506: +/***/ ((module) => { + +module.exports = 'AGFzbQEAAAABMAhgAX8Bf2ADf39/AX9gBH9/f38Bf2AAAGADf39/AGABfwBgAn9/AGAGf39/f39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQACA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAA0ZFAwMEAAAFAAAAAAAABQEFAAUFBQAABgAAAAAGBgYGAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAABAQcAAAUFAwABBAUBcAESEgUDAQACBggBfwFBgNQECwfRBSIGbWVtb3J5AgALX2luaXRpYWxpemUACRlfX2luZGlyZWN0X2Z1bmN0aW9uX3RhYmxlAQALbGxodHRwX2luaXQAChhsbGh0dHBfc2hvdWxkX2tlZXBfYWxpdmUAQQxsbGh0dHBfYWxsb2MADAZtYWxsb2MARgtsbGh0dHBfZnJlZQANBGZyZWUASA9sbGh0dHBfZ2V0X3R5cGUADhVsbGh0dHBfZ2V0X2h0dHBfbWFqb3IADxVsbGh0dHBfZ2V0X2h0dHBfbWlub3IAEBFsbGh0dHBfZ2V0X21ldGhvZAARFmxsaHR0cF9nZXRfc3RhdHVzX2NvZGUAEhJsbGh0dHBfZ2V0X3VwZ3JhZGUAEwxsbGh0dHBfcmVzZXQAFA5sbGh0dHBfZXhlY3V0ZQAVFGxsaHR0cF9zZXR0aW5nc19pbml0ABYNbGxodHRwX2ZpbmlzaAAXDGxsaHR0cF9wYXVzZQAYDWxsaHR0cF9yZXN1bWUAGRtsbGh0dHBfcmVzdW1lX2FmdGVyX3VwZ3JhZGUAGhBsbGh0dHBfZ2V0X2Vycm5vABsXbGxodHRwX2dldF9lcnJvcl9yZWFzb24AHBdsbGh0dHBfc2V0X2Vycm9yX3JlYXNvbgAdFGxsaHR0cF9nZXRfZXJyb3JfcG9zAB4RbGxodHRwX2Vycm5vX25hbWUAHxJsbGh0dHBfbWV0aG9kX25hbWUAIBJsbGh0dHBfc3RhdHVzX25hbWUAIRpsbGh0dHBfc2V0X2xlbmllbnRfaGVhZGVycwAiIWxsaHR0cF9zZXRfbGVuaWVudF9jaHVua2VkX2xlbmd0aAAjHWxsaHR0cF9zZXRfbGVuaWVudF9rZWVwX2FsaXZlACQkbGxodHRwX3NldF9sZW5pZW50X3RyYW5zZmVyX2VuY29kaW5nACUYbGxodHRwX21lc3NhZ2VfbmVlZHNfZW9mAD8JFwEAQQELEQECAwQFCwYHNTk3MS8tJyspCsLgAkUCAAsIABCIgICAAAsZACAAEMKAgIAAGiAAIAI2AjggACABOgAoCxwAIAAgAC8BMiAALQAuIAAQwYCAgAAQgICAgAALKgEBf0HAABDGgICAACIBEMKAgIAAGiABQYCIgIAANgI4IAEgADoAKCABCwoAIAAQyICAgAALBwAgAC0AKAsHACAALQAqCwcAIAAtACsLBwAgAC0AKQsHACAALwEyCwcAIAAtAC4LRQEEfyAAKAIYIQEgAC0ALSECIAAtACghAyAAKAI4IQQgABDCgICAABogACAENgI4IAAgAzoAKCAAIAI6AC0gACABNgIYCxEAIAAgASABIAJqEMOAgIAACxAAIABBAEHcABDMgICAABoLZwEBf0EAIQECQCAAKAIMDQACQAJAAkACQCAALQAvDgMBAAMCCyAAKAI4IgFFDQAgASgCLCIBRQ0AIAAgARGAgICAAAAiAQ0DC0EADwsQyoCAgAAACyAAQcOWgIAANgIQQQ4hAQsgAQseAAJAIAAoAgwNACAAQdGbgIAANgIQIABBFTYCDAsLFgACQCAAKAIMQRVHDQAgAEEANgIMCwsWAAJAIAAoAgxBFkcNACAAQQA2AgwLCwcAIAAoAgwLBwAgACgCEAsJACAAIAE2AhALBwAgACgCFAsiAAJAIABBJEkNABDKgICAAAALIABBAnRBoLOAgABqKAIACyIAAkAgAEEuSQ0AEMqAgIAAAAsgAEECdEGwtICAAGooAgAL7gsBAX9B66iAgAAhAQJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIABBnH9qDvQDY2IAAWFhYWFhYQIDBAVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhBgcICQoLDA0OD2FhYWFhEGFhYWFhYWFhYWFhEWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYRITFBUWFxgZGhthYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2YTc4OTphYWFhYWFhYTthYWE8YWFhYT0+P2FhYWFhYWFhQGFhQWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYUJDREVGR0hJSktMTU5PUFFSU2FhYWFhYWFhVFVWV1hZWlthXF1hYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFeYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhX2BhC0Hhp4CAAA8LQaShgIAADwtBy6yAgAAPC0H+sYCAAA8LQcCkgIAADwtBq6SAgAAPC0GNqICAAA8LQeKmgIAADwtBgLCAgAAPC0G5r4CAAA8LQdekgIAADwtB75+AgAAPC0Hhn4CAAA8LQfqfgIAADwtB8qCAgAAPC0Gor4CAAA8LQa6ygIAADwtBiLCAgAAPC0Hsp4CAAA8LQYKigIAADwtBjp2AgAAPC0HQroCAAA8LQcqjgIAADwtBxbKAgAAPC0HfnICAAA8LQdKcgIAADwtBxKCAgAAPC0HXoICAAA8LQaKfgIAADwtB7a6AgAAPC0GrsICAAA8LQdSlgIAADwtBzK6AgAAPC0H6roCAAA8LQfyrgIAADwtB0rCAgAAPC0HxnYCAAA8LQbuggIAADwtB96uAgAAPC0GQsYCAAA8LQdexgIAADwtBoq2AgAAPC0HUp4CAAA8LQeCrgIAADwtBn6yAgAAPC0HrsYCAAA8LQdWfgIAADwtByrGAgAAPC0HepYCAAA8LQdSegIAADwtB9JyAgAAPC0GnsoCAAA8LQbGdgIAADwtBoJ2AgAAPC0G5sYCAAA8LQbywgIAADwtBkqGAgAAPC0GzpoCAAA8LQemsgIAADwtBrJ6AgAAPC0HUq4CAAA8LQfemgIAADwtBgKaAgAAPC0GwoYCAAA8LQf6egIAADwtBjaOAgAAPC0GJrYCAAA8LQfeigIAADwtBoLGAgAAPC0Gun4CAAA8LQcalgIAADwtB6J6AgAAPC0GTooCAAA8LQcKvgIAADwtBw52AgAAPC0GLrICAAA8LQeGdgIAADwtBja+AgAAPC0HqoYCAAA8LQbStgIAADwtB0q+AgAAPC0HfsoCAAA8LQdKygIAADwtB8LCAgAAPC0GpooCAAA8LQfmjgIAADwtBmZ6AgAAPC0G1rICAAA8LQZuwgIAADwtBkrKAgAAPC0G2q4CAAA8LQcKigIAADwtB+LKAgAAPC0GepYCAAA8LQdCigIAADwtBup6AgAAPC0GBnoCAAA8LEMqAgIAAAAtB1qGAgAAhAQsgAQsWACAAIAAtAC1B/gFxIAFBAEdyOgAtCxkAIAAgAC0ALUH9AXEgAUEAR0EBdHI6AC0LGQAgACAALQAtQfsBcSABQQBHQQJ0cjoALQsZACAAIAAtAC1B9wFxIAFBAEdBA3RyOgAtCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAgAiBEUNACAAIAQRgICAgAAAIQMLIAMLSQECf0EAIQMCQCAAKAI4IgRFDQAgBCgCBCIERQ0AIAAgASACIAFrIAQRgYCAgAAAIgNBf0cNACAAQcaRgIAANgIQQRghAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIwIgRFDQAgACAEEYCAgIAAACEDCyADC0kBAn9BACEDAkAgACgCOCIERQ0AIAQoAggiBEUNACAAIAEgAiABayAEEYGAgIAAACIDQX9HDQAgAEH2ioCAADYCEEEYIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCNCIERQ0AIAAgBBGAgICAAAAhAwsgAwtJAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIMIgRFDQAgACABIAIgAWsgBBGBgICAAAAiA0F/Rw0AIABB7ZqAgAA2AhBBGCEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAjgiBEUNACAAIAQRgICAgAAAIQMLIAMLSQECf0EAIQMCQCAAKAI4IgRFDQAgBCgCECIERQ0AIAAgASACIAFrIAQRgYCAgAAAIgNBf0cNACAAQZWQgIAANgIQQRghAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAI8IgRFDQAgACAEEYCAgIAAACEDCyADC0kBAn9BACEDAkAgACgCOCIERQ0AIAQoAhQiBEUNACAAIAEgAiABayAEEYGAgIAAACIDQX9HDQAgAEGqm4CAADYCEEEYIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCQCIERQ0AIAAgBBGAgICAAAAhAwsgAwtJAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIYIgRFDQAgACABIAIgAWsgBBGBgICAAAAiA0F/Rw0AIABB7ZOAgAA2AhBBGCEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAkQiBEUNACAAIAQRgICAgAAAIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCJCIERQ0AIAAgBBGAgICAAAAhAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIsIgRFDQAgACAEEYCAgIAAACEDCyADC0kBAn9BACEDAkAgACgCOCIERQ0AIAQoAigiBEUNACAAIAEgAiABayAEEYGAgIAAACIDQX9HDQAgAEH2iICAADYCEEEYIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCUCIERQ0AIAAgBBGAgICAAAAhAwsgAwtJAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIcIgRFDQAgACABIAIgAWsgBBGBgICAAAAiA0F/Rw0AIABBwpmAgAA2AhBBGCEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAkgiBEUNACAAIAQRgICAgAAAIQMLIAMLSQECf0EAIQMCQCAAKAI4IgRFDQAgBCgCICIERQ0AIAAgASACIAFrIAQRgYCAgAAAIgNBf0cNACAAQZSUgIAANgIQQRghAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAJMIgRFDQAgACAEEYCAgIAAACEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAlQiBEUNACAAIAQRgICAgAAAIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCWCIERQ0AIAAgBBGAgICAAAAhAwsgAwtFAQF/AkACQCAALwEwQRRxQRRHDQBBASEDIAAtAChBAUYNASAALwEyQeUARiEDDAELIAAtAClBBUYhAwsgACADOgAuQQAL/gEBA39BASEDAkAgAC8BMCIEQQhxDQAgACkDIEIAUiEDCwJAAkAgAC0ALkUNAEEBIQUgAC0AKUEFRg0BQQEhBSAEQcAAcUUgA3FBAUcNAQtBACEFIARBwABxDQBBAiEFIARB//8DcSIDQQhxDQACQCADQYAEcUUNAAJAIAAtAChBAUcNACAALQAtQQpxDQBBBQ8LQQQPCwJAIANBIHENAAJAIAAtAChBAUYNACAALwEyQf//A3EiAEGcf2pB5ABJDQAgAEHMAUYNACAAQbACRg0AQQQhBSAEQShxRQ0CIANBiARxQYAERg0CC0EADwtBAEEDIAApAyBQGyEFCyAFC2IBAn9BACEBAkAgAC0AKEEBRg0AIAAvATJB//8DcSICQZx/akHkAEkNACACQcwBRg0AIAJBsAJGDQAgAC8BMCIAQcAAcQ0AQQEhASAAQYgEcUGABEYNACAAQShxRSEBCyABC6cBAQN/AkACQAJAIAAtACpFDQAgAC0AK0UNAEEAIQMgAC8BMCIEQQJxRQ0BDAILQQAhAyAALwEwIgRBAXFFDQELQQEhAyAALQAoQQFGDQAgAC8BMkH//wNxIgVBnH9qQeQASQ0AIAVBzAFGDQAgBUGwAkYNACAEQcAAcQ0AQQAhAyAEQYgEcUGABEYNACAEQShxQQBHIQMLIABBADsBMCAAQQA6AC8gAwuZAQECfwJAAkACQCAALQAqRQ0AIAAtACtFDQBBACEBIAAvATAiAkECcUUNAQwCC0EAIQEgAC8BMCICQQFxRQ0BC0EBIQEgAC0AKEEBRg0AIAAvATJB//8DcSIAQZx/akHkAEkNACAAQcwBRg0AIABBsAJGDQAgAkHAAHENAEEAIQEgAkGIBHFBgARGDQAgAkEocUEARyEBCyABC1kAIABBGGpCADcDACAAQgA3AwAgAEE4akIANwMAIABBMGpCADcDACAAQShqQgA3AwAgAEEgakIANwMAIABBEGpCADcDACAAQQhqQgA3AwAgAEHdATYCHEEAC3sBAX8CQCAAKAIMIgMNAAJAIAAoAgRFDQAgACABNgIECwJAIAAgASACEMSAgIAAIgMNACAAKAIMDwsgACADNgIcQQAhAyAAKAIEIgFFDQAgACABIAIgACgCCBGBgICAAAAiAUUNACAAIAI2AhQgACABNgIMIAEhAwsgAwvk8wEDDn8DfgR/I4CAgIAAQRBrIgMkgICAgAAgASEEIAEhBSABIQYgASEHIAEhCCABIQkgASEKIAEhCyABIQwgASENIAEhDiABIQ8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgACgCHCIQQX9qDt0B2gEB2QECAwQFBgcICQoLDA0O2AEPENcBERLWARMUFRYXGBkaG+AB3wEcHR7VAR8gISIjJCXUASYnKCkqKyzTAdIBLS7RAdABLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVG2wFHSElKzwHOAUvNAUzMAU1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1+f4ABgQGCAYMBhAGFAYYBhwGIAYkBigGLAYwBjQGOAY8BkAGRAZIBkwGUAZUBlgGXAZgBmQGaAZsBnAGdAZ4BnwGgAaEBogGjAaQBpQGmAacBqAGpAaoBqwGsAa0BrgGvAbABsQGyAbMBtAG1AbYBtwHLAcoBuAHJAbkByAG6AbsBvAG9Ab4BvwHAAcEBwgHDAcQBxQHGAQDcAQtBACEQDMYBC0EOIRAMxQELQQ0hEAzEAQtBDyEQDMMBC0EQIRAMwgELQRMhEAzBAQtBFCEQDMABC0EVIRAMvwELQRYhEAy+AQtBFyEQDL0BC0EYIRAMvAELQRkhEAy7AQtBGiEQDLoBC0EbIRAMuQELQRwhEAy4AQtBCCEQDLcBC0EdIRAMtgELQSAhEAy1AQtBHyEQDLQBC0EHIRAMswELQSEhEAyyAQtBIiEQDLEBC0EeIRAMsAELQSMhEAyvAQtBEiEQDK4BC0ERIRAMrQELQSQhEAysAQtBJSEQDKsBC0EmIRAMqgELQSchEAypAQtBwwEhEAyoAQtBKSEQDKcBC0ErIRAMpgELQSwhEAylAQtBLSEQDKQBC0EuIRAMowELQS8hEAyiAQtBxAEhEAyhAQtBMCEQDKABC0E0IRAMnwELQQwhEAyeAQtBMSEQDJ0BC0EyIRAMnAELQTMhEAybAQtBOSEQDJoBC0E1IRAMmQELQcUBIRAMmAELQQshEAyXAQtBOiEQDJYBC0E2IRAMlQELQQohEAyUAQtBNyEQDJMBC0E4IRAMkgELQTwhEAyRAQtBOyEQDJABC0E9IRAMjwELQQkhEAyOAQtBKCEQDI0BC0E+IRAMjAELQT8hEAyLAQtBwAAhEAyKAQtBwQAhEAyJAQtBwgAhEAyIAQtBwwAhEAyHAQtBxAAhEAyGAQtBxQAhEAyFAQtBxgAhEAyEAQtBKiEQDIMBC0HHACEQDIIBC0HIACEQDIEBC0HJACEQDIABC0HKACEQDH8LQcsAIRAMfgtBzQAhEAx9C0HMACEQDHwLQc4AIRAMewtBzwAhEAx6C0HQACEQDHkLQdEAIRAMeAtB0gAhEAx3C0HTACEQDHYLQdQAIRAMdQtB1gAhEAx0C0HVACEQDHMLQQYhEAxyC0HXACEQDHELQQUhEAxwC0HYACEQDG8LQQQhEAxuC0HZACEQDG0LQdoAIRAMbAtB2wAhEAxrC0HcACEQDGoLQQMhEAxpC0HdACEQDGgLQd4AIRAMZwtB3wAhEAxmC0HhACEQDGULQeAAIRAMZAtB4gAhEAxjC0HjACEQDGILQQIhEAxhC0HkACEQDGALQeUAIRAMXwtB5gAhEAxeC0HnACEQDF0LQegAIRAMXAtB6QAhEAxbC0HqACEQDFoLQesAIRAMWQtB7AAhEAxYC0HtACEQDFcLQe4AIRAMVgtB7wAhEAxVC0HwACEQDFQLQfEAIRAMUwtB8gAhEAxSC0HzACEQDFELQfQAIRAMUAtB9QAhEAxPC0H2ACEQDE4LQfcAIRAMTQtB+AAhEAxMC0H5ACEQDEsLQfoAIRAMSgtB+wAhEAxJC0H8ACEQDEgLQf0AIRAMRwtB/gAhEAxGC0H/ACEQDEULQYABIRAMRAtBgQEhEAxDC0GCASEQDEILQYMBIRAMQQtBhAEhEAxAC0GFASEQDD8LQYYBIRAMPgtBhwEhEAw9C0GIASEQDDwLQYkBIRAMOwtBigEhEAw6C0GLASEQDDkLQYwBIRAMOAtBjQEhEAw3C0GOASEQDDYLQY8BIRAMNQtBkAEhEAw0C0GRASEQDDMLQZIBIRAMMgtBkwEhEAwxC0GUASEQDDALQZUBIRAMLwtBlgEhEAwuC0GXASEQDC0LQZgBIRAMLAtBmQEhEAwrC0GaASEQDCoLQZsBIRAMKQtBnAEhEAwoC0GdASEQDCcLQZ4BIRAMJgtBnwEhEAwlC0GgASEQDCQLQaEBIRAMIwtBogEhEAwiC0GjASEQDCELQaQBIRAMIAtBpQEhEAwfC0GmASEQDB4LQacBIRAMHQtBqAEhEAwcC0GpASEQDBsLQaoBIRAMGgtBqwEhEAwZC0GsASEQDBgLQa0BIRAMFwtBrgEhEAwWC0EBIRAMFQtBrwEhEAwUC0GwASEQDBMLQbEBIRAMEgtBswEhEAwRC0GyASEQDBALQbQBIRAMDwtBtQEhEAwOC0G2ASEQDA0LQbcBIRAMDAtBuAEhEAwLC0G5ASEQDAoLQboBIRAMCQtBuwEhEAwIC0HGASEQDAcLQbwBIRAMBgtBvQEhEAwFC0G+ASEQDAQLQb8BIRAMAwtBwAEhEAwCC0HCASEQDAELQcEBIRALA0ACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAQDscBAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxweHyAhIyUoP0BBREVGR0hJSktMTU9QUVJT3gNXWVtcXWBiZWZnaGlqa2xtb3BxcnN0dXZ3eHl6e3x9foABggGFAYYBhwGJAYsBjAGNAY4BjwGQAZEBlAGVAZYBlwGYAZkBmgGbAZwBnQGeAZ8BoAGhAaIBowGkAaUBpgGnAagBqQGqAasBrAGtAa4BrwGwAbEBsgGzAbQBtQG2AbcBuAG5AboBuwG8Ab0BvgG/AcABwQHCAcMBxAHFAcYBxwHIAckBygHLAcwBzQHOAc8B0AHRAdIB0wHUAdUB1gHXAdgB2QHaAdsB3AHdAd4B4AHhAeIB4wHkAeUB5gHnAegB6QHqAesB7AHtAe4B7wHwAfEB8gHzAZkCpAKwAv4C/gILIAEiBCACRw3zAUHdASEQDP8DCyABIhAgAkcN3QFBwwEhEAz+AwsgASIBIAJHDZABQfcAIRAM/QMLIAEiASACRw2GAUHvACEQDPwDCyABIgEgAkcNf0HqACEQDPsDCyABIgEgAkcNe0HoACEQDPoDCyABIgEgAkcNeEHmACEQDPkDCyABIgEgAkcNGkEYIRAM+AMLIAEiASACRw0UQRIhEAz3AwsgASIBIAJHDVlBxQAhEAz2AwsgASIBIAJHDUpBPyEQDPUDCyABIgEgAkcNSEE8IRAM9AMLIAEiASACRw1BQTEhEAzzAwsgAC0ALkEBRg3rAwyHAgsgACABIgEgAhDAgICAAEEBRw3mASAAQgA3AyAM5wELIAAgASIBIAIQtICAgAAiEA3nASABIQEM9QILAkAgASIBIAJHDQBBBiEQDPADCyAAIAFBAWoiASACELuAgIAAIhAN6AEgASEBDDELIABCADcDIEESIRAM1QMLIAEiECACRw0rQR0hEAztAwsCQCABIgEgAkYNACABQQFqIQFBECEQDNQDC0EHIRAM7AMLIABCACAAKQMgIhEgAiABIhBrrSISfSITIBMgEVYbNwMgIBEgElYiFEUN5QFBCCEQDOsDCwJAIAEiASACRg0AIABBiYCAgAA2AgggACABNgIEIAEhAUEUIRAM0gMLQQkhEAzqAwsgASEBIAApAyBQDeQBIAEhAQzyAgsCQCABIgEgAkcNAEELIRAM6QMLIAAgAUEBaiIBIAIQtoCAgAAiEA3lASABIQEM8gILIAAgASIBIAIQuICAgAAiEA3lASABIQEM8gILIAAgASIBIAIQuICAgAAiEA3mASABIQEMDQsgACABIgEgAhC6gICAACIQDecBIAEhAQzwAgsCQCABIgEgAkcNAEEPIRAM5QMLIAEtAAAiEEE7Rg0IIBBBDUcN6AEgAUEBaiEBDO8CCyAAIAEiASACELqAgIAAIhAN6AEgASEBDPICCwNAAkAgAS0AAEHwtYCAAGotAAAiEEEBRg0AIBBBAkcN6wEgACgCBCEQIABBADYCBCAAIBAgAUEBaiIBELmAgIAAIhAN6gEgASEBDPQCCyABQQFqIgEgAkcNAAtBEiEQDOIDCyAAIAEiASACELqAgIAAIhAN6QEgASEBDAoLIAEiASACRw0GQRshEAzgAwsCQCABIgEgAkcNAEEWIRAM4AMLIABBioCAgAA2AgggACABNgIEIAAgASACELiAgIAAIhAN6gEgASEBQSAhEAzGAwsCQCABIgEgAkYNAANAAkAgAS0AAEHwt4CAAGotAAAiEEECRg0AAkAgEEF/ag4E5QHsAQDrAewBCyABQQFqIQFBCCEQDMgDCyABQQFqIgEgAkcNAAtBFSEQDN8DC0EVIRAM3gMLA0ACQCABLQAAQfC5gIAAai0AACIQQQJGDQAgEEF/ag4E3gHsAeAB6wHsAQsgAUEBaiIBIAJHDQALQRghEAzdAwsCQCABIgEgAkYNACAAQYuAgIAANgIIIAAgATYCBCABIQFBByEQDMQDC0EZIRAM3AMLIAFBAWohAQwCCwJAIAEiFCACRw0AQRohEAzbAwsgFCEBAkAgFC0AAEFzag4U3QLuAu4C7gLuAu4C7gLuAu4C7gLuAu4C7gLuAu4C7gLuAu4C7gIA7gILQQAhECAAQQA2AhwgAEGvi4CAADYCECAAQQI2AgwgACAUQQFqNgIUDNoDCwJAIAEtAAAiEEE7Rg0AIBBBDUcN6AEgAUEBaiEBDOUCCyABQQFqIQELQSIhEAy/AwsCQCABIhAgAkcNAEEcIRAM2AMLQgAhESAQIQEgEC0AAEFQag435wHmAQECAwQFBgcIAAAAAAAAAAkKCwwNDgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADxAREhMUAAtBHiEQDL0DC0ICIREM5QELQgMhEQzkAQtCBCERDOMBC0IFIREM4gELQgYhEQzhAQtCByERDOABC0IIIREM3wELQgkhEQzeAQtCCiERDN0BC0ILIREM3AELQgwhEQzbAQtCDSERDNoBC0IOIREM2QELQg8hEQzYAQtCCiERDNcBC0ILIREM1gELQgwhEQzVAQtCDSERDNQBC0IOIREM0wELQg8hEQzSAQtCACERAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAQLQAAQVBqDjflAeQBAAECAwQFBgfmAeYB5gHmAeYB5gHmAQgJCgsMDeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gEODxAREhPmAQtCAiERDOQBC0IDIREM4wELQgQhEQziAQtCBSERDOEBC0IGIREM4AELQgchEQzfAQtCCCERDN4BC0IJIREM3QELQgohEQzcAQtCCyERDNsBC0IMIREM2gELQg0hEQzZAQtCDiERDNgBC0IPIREM1wELQgohEQzWAQtCCyERDNUBC0IMIREM1AELQg0hEQzTAQtCDiERDNIBC0IPIREM0QELIABCACAAKQMgIhEgAiABIhBrrSISfSITIBMgEVYbNwMgIBEgElYiFEUN0gFBHyEQDMADCwJAIAEiASACRg0AIABBiYCAgAA2AgggACABNgIEIAEhAUEkIRAMpwMLQSAhEAy/AwsgACABIhAgAhC+gICAAEF/ag4FtgEAxQIB0QHSAQtBESEQDKQDCyAAQQE6AC8gECEBDLsDCyABIgEgAkcN0gFBJCEQDLsDCyABIg0gAkcNHkHGACEQDLoDCyAAIAEiASACELKAgIAAIhAN1AEgASEBDLUBCyABIhAgAkcNJkHQACEQDLgDCwJAIAEiASACRw0AQSghEAy4AwsgAEEANgIEIABBjICAgAA2AgggACABIAEQsYCAgAAiEA3TASABIQEM2AELAkAgASIQIAJHDQBBKSEQDLcDCyAQLQAAIgFBIEYNFCABQQlHDdMBIBBBAWohAQwVCwJAIAEiASACRg0AIAFBAWohAQwXC0EqIRAMtQMLAkAgASIQIAJHDQBBKyEQDLUDCwJAIBAtAAAiAUEJRg0AIAFBIEcN1QELIAAtACxBCEYN0wEgECEBDJEDCwJAIAEiASACRw0AQSwhEAy0AwsgAS0AAEEKRw3VASABQQFqIQEMyQILIAEiDiACRw3VAUEvIRAMsgMLA0ACQCABLQAAIhBBIEYNAAJAIBBBdmoOBADcAdwBANoBCyABIQEM4AELIAFBAWoiASACRw0AC0ExIRAMsQMLQTIhECABIhQgAkYNsAMgAiAUayAAKAIAIgFqIRUgFCABa0EDaiEWAkADQCAULQAAIhdBIHIgFyAXQb9/akH/AXFBGkkbQf8BcSABQfC7gIAAai0AAEcNAQJAIAFBA0cNAEEGIQEMlgMLIAFBAWohASAUQQFqIhQgAkcNAAsgACAVNgIADLEDCyAAQQA2AgAgFCEBDNkBC0EzIRAgASIUIAJGDa8DIAIgFGsgACgCACIBaiEVIBQgAWtBCGohFgJAA0AgFC0AACIXQSByIBcgF0G/f2pB/wFxQRpJG0H/AXEgAUH0u4CAAGotAABHDQECQCABQQhHDQBBBSEBDJUDCyABQQFqIQEgFEEBaiIUIAJHDQALIAAgFTYCAAywAwsgAEEANgIAIBQhAQzYAQtBNCEQIAEiFCACRg2uAyACIBRrIAAoAgAiAWohFSAUIAFrQQVqIRYCQANAIBQtAAAiF0EgciAXIBdBv39qQf8BcUEaSRtB/wFxIAFB0MKAgABqLQAARw0BAkAgAUEFRw0AQQchAQyUAwsgAUEBaiEBIBRBAWoiFCACRw0ACyAAIBU2AgAMrwMLIABBADYCACAUIQEM1wELAkAgASIBIAJGDQADQAJAIAEtAABBgL6AgABqLQAAIhBBAUYNACAQQQJGDQogASEBDN0BCyABQQFqIgEgAkcNAAtBMCEQDK4DC0EwIRAMrQMLAkAgASIBIAJGDQADQAJAIAEtAAAiEEEgRg0AIBBBdmoOBNkB2gHaAdkB2gELIAFBAWoiASACRw0AC0E4IRAMrQMLQTghEAysAwsDQAJAIAEtAAAiEEEgRg0AIBBBCUcNAwsgAUEBaiIBIAJHDQALQTwhEAyrAwsDQAJAIAEtAAAiEEEgRg0AAkACQCAQQXZqDgTaAQEB2gEACyAQQSxGDdsBCyABIQEMBAsgAUEBaiIBIAJHDQALQT8hEAyqAwsgASEBDNsBC0HAACEQIAEiFCACRg2oAyACIBRrIAAoAgAiAWohFiAUIAFrQQZqIRcCQANAIBQtAABBIHIgAUGAwICAAGotAABHDQEgAUEGRg2OAyABQQFqIQEgFEEBaiIUIAJHDQALIAAgFjYCAAypAwsgAEEANgIAIBQhAQtBNiEQDI4DCwJAIAEiDyACRw0AQcEAIRAMpwMLIABBjICAgAA2AgggACAPNgIEIA8hASAALQAsQX9qDgTNAdUB1wHZAYcDCyABQQFqIQEMzAELAkAgASIBIAJGDQADQAJAIAEtAAAiEEEgciAQIBBBv39qQf8BcUEaSRtB/wFxIhBBCUYNACAQQSBGDQACQAJAAkACQCAQQZ1/ag4TAAMDAwMDAwMBAwMDAwMDAwMDAgMLIAFBAWohAUExIRAMkQMLIAFBAWohAUEyIRAMkAMLIAFBAWohAUEzIRAMjwMLIAEhAQzQAQsgAUEBaiIBIAJHDQALQTUhEAylAwtBNSEQDKQDCwJAIAEiASACRg0AA0ACQCABLQAAQYC8gIAAai0AAEEBRg0AIAEhAQzTAQsgAUEBaiIBIAJHDQALQT0hEAykAwtBPSEQDKMDCyAAIAEiASACELCAgIAAIhAN1gEgASEBDAELIBBBAWohAQtBPCEQDIcDCwJAIAEiASACRw0AQcIAIRAMoAMLAkADQAJAIAEtAABBd2oOGAAC/gL+AoQD/gL+Av4C/gL+Av4C/gL+Av4C/gL+Av4C/gL+Av4C/gL+Av4CAP4CCyABQQFqIgEgAkcNAAtBwgAhEAygAwsgAUEBaiEBIAAtAC1BAXFFDb0BIAEhAQtBLCEQDIUDCyABIgEgAkcN0wFBxAAhEAydAwsDQAJAIAEtAABBkMCAgABqLQAAQQFGDQAgASEBDLcCCyABQQFqIgEgAkcNAAtBxQAhEAycAwsgDS0AACIQQSBGDbMBIBBBOkcNgQMgACgCBCEBIABBADYCBCAAIAEgDRCvgICAACIBDdABIA1BAWohAQyzAgtBxwAhECABIg0gAkYNmgMgAiANayAAKAIAIgFqIRYgDSABa0EFaiEXA0AgDS0AACIUQSByIBQgFEG/f2pB/wFxQRpJG0H/AXEgAUGQwoCAAGotAABHDYADIAFBBUYN9AIgAUEBaiEBIA1BAWoiDSACRw0ACyAAIBY2AgAMmgMLQcgAIRAgASINIAJGDZkDIAIgDWsgACgCACIBaiEWIA0gAWtBCWohFwNAIA0tAAAiFEEgciAUIBRBv39qQf8BcUEaSRtB/wFxIAFBlsKAgABqLQAARw3/AgJAIAFBCUcNAEECIQEM9QILIAFBAWohASANQQFqIg0gAkcNAAsgACAWNgIADJkDCwJAIAEiDSACRw0AQckAIRAMmQMLAkACQCANLQAAIgFBIHIgASABQb9/akH/AXFBGkkbQf8BcUGSf2oOBwCAA4ADgAOAA4ADAYADCyANQQFqIQFBPiEQDIADCyANQQFqIQFBPyEQDP8CC0HKACEQIAEiDSACRg2XAyACIA1rIAAoAgAiAWohFiANIAFrQQFqIRcDQCANLQAAIhRBIHIgFCAUQb9/akH/AXFBGkkbQf8BcSABQaDCgIAAai0AAEcN/QIgAUEBRg3wAiABQQFqIQEgDUEBaiINIAJHDQALIAAgFjYCAAyXAwtBywAhECABIg0gAkYNlgMgAiANayAAKAIAIgFqIRYgDSABa0EOaiEXA0AgDS0AACIUQSByIBQgFEG/f2pB/wFxQRpJG0H/AXEgAUGiwoCAAGotAABHDfwCIAFBDkYN8AIgAUEBaiEBIA1BAWoiDSACRw0ACyAAIBY2AgAMlgMLQcwAIRAgASINIAJGDZUDIAIgDWsgACgCACIBaiEWIA0gAWtBD2ohFwNAIA0tAAAiFEEgciAUIBRBv39qQf8BcUEaSRtB/wFxIAFBwMKAgABqLQAARw37AgJAIAFBD0cNAEEDIQEM8QILIAFBAWohASANQQFqIg0gAkcNAAsgACAWNgIADJUDC0HNACEQIAEiDSACRg2UAyACIA1rIAAoAgAiAWohFiANIAFrQQVqIRcDQCANLQAAIhRBIHIgFCAUQb9/akH/AXFBGkkbQf8BcSABQdDCgIAAai0AAEcN+gICQCABQQVHDQBBBCEBDPACCyABQQFqIQEgDUEBaiINIAJHDQALIAAgFjYCAAyUAwsCQCABIg0gAkcNAEHOACEQDJQDCwJAAkACQAJAIA0tAAAiAUEgciABIAFBv39qQf8BcUEaSRtB/wFxQZ1/ag4TAP0C/QL9Av0C/QL9Av0C/QL9Av0C/QL9AgH9Av0C/QICA/0CCyANQQFqIQFBwQAhEAz9AgsgDUEBaiEBQcIAIRAM/AILIA1BAWohAUHDACEQDPsCCyANQQFqIQFBxAAhEAz6AgsCQCABIgEgAkYNACAAQY2AgIAANgIIIAAgATYCBCABIQFBxQAhEAz6AgtBzwAhEAySAwsgECEBAkACQCAQLQAAQXZqDgQBqAKoAgCoAgsgEEEBaiEBC0EnIRAM+AILAkAgASIBIAJHDQBB0QAhEAyRAwsCQCABLQAAQSBGDQAgASEBDI0BCyABQQFqIQEgAC0ALUEBcUUNxwEgASEBDIwBCyABIhcgAkcNyAFB0gAhEAyPAwtB0wAhECABIhQgAkYNjgMgAiAUayAAKAIAIgFqIRYgFCABa0EBaiEXA0AgFC0AACABQdbCgIAAai0AAEcNzAEgAUEBRg3HASABQQFqIQEgFEEBaiIUIAJHDQALIAAgFjYCAAyOAwsCQCABIgEgAkcNAEHVACEQDI4DCyABLQAAQQpHDcwBIAFBAWohAQzHAQsCQCABIgEgAkcNAEHWACEQDI0DCwJAAkAgAS0AAEF2ag4EAM0BzQEBzQELIAFBAWohAQzHAQsgAUEBaiEBQcoAIRAM8wILIAAgASIBIAIQroCAgAAiEA3LASABIQFBzQAhEAzyAgsgAC0AKUEiRg2FAwymAgsCQCABIgEgAkcNAEHbACEQDIoDC0EAIRRBASEXQQEhFkEAIRACQAJAAkACQAJAAkACQAJAAkAgAS0AAEFQag4K1AHTAQABAgMEBQYI1QELQQIhEAwGC0EDIRAMBQtBBCEQDAQLQQUhEAwDC0EGIRAMAgtBByEQDAELQQghEAtBACEXQQAhFkEAIRQMzAELQQkhEEEBIRRBACEXQQAhFgzLAQsCQCABIgEgAkcNAEHdACEQDIkDCyABLQAAQS5HDcwBIAFBAWohAQymAgsgASIBIAJHDcwBQd8AIRAMhwMLAkAgASIBIAJGDQAgAEGOgICAADYCCCAAIAE2AgQgASEBQdAAIRAM7gILQeAAIRAMhgMLQeEAIRAgASIBIAJGDYUDIAIgAWsgACgCACIUaiEWIAEgFGtBA2ohFwNAIAEtAAAgFEHiwoCAAGotAABHDc0BIBRBA0YNzAEgFEEBaiEUIAFBAWoiASACRw0ACyAAIBY2AgAMhQMLQeIAIRAgASIBIAJGDYQDIAIgAWsgACgCACIUaiEWIAEgFGtBAmohFwNAIAEtAAAgFEHmwoCAAGotAABHDcwBIBRBAkYNzgEgFEEBaiEUIAFBAWoiASACRw0ACyAAIBY2AgAMhAMLQeMAIRAgASIBIAJGDYMDIAIgAWsgACgCACIUaiEWIAEgFGtBA2ohFwNAIAEtAAAgFEHpwoCAAGotAABHDcsBIBRBA0YNzgEgFEEBaiEUIAFBAWoiASACRw0ACyAAIBY2AgAMgwMLAkAgASIBIAJHDQBB5QAhEAyDAwsgACABQQFqIgEgAhCogICAACIQDc0BIAEhAUHWACEQDOkCCwJAIAEiASACRg0AA0ACQCABLQAAIhBBIEYNAAJAAkACQCAQQbh/ag4LAAHPAc8BzwHPAc8BzwHPAc8BAs8BCyABQQFqIQFB0gAhEAztAgsgAUEBaiEBQdMAIRAM7AILIAFBAWohAUHUACEQDOsCCyABQQFqIgEgAkcNAAtB5AAhEAyCAwtB5AAhEAyBAwsDQAJAIAEtAABB8MKAgABqLQAAIhBBAUYNACAQQX5qDgPPAdAB0QHSAQsgAUEBaiIBIAJHDQALQeYAIRAMgAMLAkAgASIBIAJGDQAgAUEBaiEBDAMLQecAIRAM/wILA0ACQCABLQAAQfDEgIAAai0AACIQQQFGDQACQCAQQX5qDgTSAdMB1AEA1QELIAEhAUHXACEQDOcCCyABQQFqIgEgAkcNAAtB6AAhEAz+AgsCQCABIgEgAkcNAEHpACEQDP4CCwJAIAEtAAAiEEF2ag4augHVAdUBvAHVAdUB1QHVAdUB1QHVAdUB1QHVAdUB1QHVAdUB1QHVAdUB1QHKAdUB1QEA0wELIAFBAWohAQtBBiEQDOMCCwNAAkAgAS0AAEHwxoCAAGotAABBAUYNACABIQEMngILIAFBAWoiASACRw0AC0HqACEQDPsCCwJAIAEiASACRg0AIAFBAWohAQwDC0HrACEQDPoCCwJAIAEiASACRw0AQewAIRAM+gILIAFBAWohAQwBCwJAIAEiASACRw0AQe0AIRAM+QILIAFBAWohAQtBBCEQDN4CCwJAIAEiFCACRw0AQe4AIRAM9wILIBQhAQJAAkACQCAULQAAQfDIgIAAai0AAEF/ag4H1AHVAdYBAJwCAQLXAQsgFEEBaiEBDAoLIBRBAWohAQzNAQtBACEQIABBADYCHCAAQZuSgIAANgIQIABBBzYCDCAAIBRBAWo2AhQM9gILAkADQAJAIAEtAABB8MiAgABqLQAAIhBBBEYNAAJAAkAgEEF/ag4H0gHTAdQB2QEABAHZAQsgASEBQdoAIRAM4AILIAFBAWohAUHcACEQDN8CCyABQQFqIgEgAkcNAAtB7wAhEAz2AgsgAUEBaiEBDMsBCwJAIAEiFCACRw0AQfAAIRAM9QILIBQtAABBL0cN1AEgFEEBaiEBDAYLAkAgASIUIAJHDQBB8QAhEAz0AgsCQCAULQAAIgFBL0cNACAUQQFqIQFB3QAhEAzbAgsgAUF2aiIEQRZLDdMBQQEgBHRBiYCAAnFFDdMBDMoCCwJAIAEiASACRg0AIAFBAWohAUHeACEQDNoCC0HyACEQDPICCwJAIAEiFCACRw0AQfQAIRAM8gILIBQhAQJAIBQtAABB8MyAgABqLQAAQX9qDgPJApQCANQBC0HhACEQDNgCCwJAIAEiFCACRg0AA0ACQCAULQAAQfDKgIAAai0AACIBQQNGDQACQCABQX9qDgLLAgDVAQsgFCEBQd8AIRAM2gILIBRBAWoiFCACRw0AC0HzACEQDPECC0HzACEQDPACCwJAIAEiASACRg0AIABBj4CAgAA2AgggACABNgIEIAEhAUHgACEQDNcCC0H1ACEQDO8CCwJAIAEiASACRw0AQfYAIRAM7wILIABBj4CAgAA2AgggACABNgIEIAEhAQtBAyEQDNQCCwNAIAEtAABBIEcNwwIgAUEBaiIBIAJHDQALQfcAIRAM7AILAkAgASIBIAJHDQBB+AAhEAzsAgsgAS0AAEEgRw3OASABQQFqIQEM7wELIAAgASIBIAIQrICAgAAiEA3OASABIQEMjgILAkAgASIEIAJHDQBB+gAhEAzqAgsgBC0AAEHMAEcN0QEgBEEBaiEBQRMhEAzPAQsCQCABIgQgAkcNAEH7ACEQDOkCCyACIARrIAAoAgAiAWohFCAEIAFrQQVqIRADQCAELQAAIAFB8M6AgABqLQAARw3QASABQQVGDc4BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQfsAIRAM6AILAkAgASIEIAJHDQBB/AAhEAzoAgsCQAJAIAQtAABBvX9qDgwA0QHRAdEB0QHRAdEB0QHRAdEB0QEB0QELIARBAWohAUHmACEQDM8CCyAEQQFqIQFB5wAhEAzOAgsCQCABIgQgAkcNAEH9ACEQDOcCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHtz4CAAGotAABHDc8BIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEH9ACEQDOcCCyAAQQA2AgAgEEEBaiEBQRAhEAzMAQsCQCABIgQgAkcNAEH+ACEQDOYCCyACIARrIAAoAgAiAWohFCAEIAFrQQVqIRACQANAIAQtAAAgAUH2zoCAAGotAABHDc4BIAFBBUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEH+ACEQDOYCCyAAQQA2AgAgEEEBaiEBQRYhEAzLAQsCQCABIgQgAkcNAEH/ACEQDOUCCyACIARrIAAoAgAiAWohFCAEIAFrQQNqIRACQANAIAQtAAAgAUH8zoCAAGotAABHDc0BIAFBA0YNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEH/ACEQDOUCCyAAQQA2AgAgEEEBaiEBQQUhEAzKAQsCQCABIgQgAkcNAEGAASEQDOQCCyAELQAAQdkARw3LASAEQQFqIQFBCCEQDMkBCwJAIAEiBCACRw0AQYEBIRAM4wILAkACQCAELQAAQbJ/ag4DAMwBAcwBCyAEQQFqIQFB6wAhEAzKAgsgBEEBaiEBQewAIRAMyQILAkAgASIEIAJHDQBBggEhEAziAgsCQAJAIAQtAABBuH9qDggAywHLAcsBywHLAcsBAcsBCyAEQQFqIQFB6gAhEAzJAgsgBEEBaiEBQe0AIRAMyAILAkAgASIEIAJHDQBBgwEhEAzhAgsgAiAEayAAKAIAIgFqIRAgBCABa0ECaiEUAkADQCAELQAAIAFBgM+AgABqLQAARw3JASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBA2AgBBgwEhEAzhAgtBACEQIABBADYCACAUQQFqIQEMxgELAkAgASIEIAJHDQBBhAEhEAzgAgsgAiAEayAAKAIAIgFqIRQgBCABa0EEaiEQAkADQCAELQAAIAFBg8+AgABqLQAARw3IASABQQRGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBhAEhEAzgAgsgAEEANgIAIBBBAWohAUEjIRAMxQELAkAgASIEIAJHDQBBhQEhEAzfAgsCQAJAIAQtAABBtH9qDggAyAHIAcgByAHIAcgBAcgBCyAEQQFqIQFB7wAhEAzGAgsgBEEBaiEBQfAAIRAMxQILAkAgASIEIAJHDQBBhgEhEAzeAgsgBC0AAEHFAEcNxQEgBEEBaiEBDIMCCwJAIAEiBCACRw0AQYcBIRAM3QILIAIgBGsgACgCACIBaiEUIAQgAWtBA2ohEAJAA0AgBC0AACABQYjPgIAAai0AAEcNxQEgAUEDRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQYcBIRAM3QILIABBADYCACAQQQFqIQFBLSEQDMIBCwJAIAEiBCACRw0AQYgBIRAM3AILIAIgBGsgACgCACIBaiEUIAQgAWtBCGohEAJAA0AgBC0AACABQdDPgIAAai0AAEcNxAEgAUEIRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQYgBIRAM3AILIABBADYCACAQQQFqIQFBKSEQDMEBCwJAIAEiASACRw0AQYkBIRAM2wILQQEhECABLQAAQd8ARw3AASABQQFqIQEMgQILAkAgASIEIAJHDQBBigEhEAzaAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQA0AgBC0AACABQYzPgIAAai0AAEcNwQEgAUEBRg2vAiABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGKASEQDNkCCwJAIAEiBCACRw0AQYsBIRAM2QILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQY7PgIAAai0AAEcNwQEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQYsBIRAM2QILIABBADYCACAQQQFqIQFBAiEQDL4BCwJAIAEiBCACRw0AQYwBIRAM2AILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQfDPgIAAai0AAEcNwAEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQYwBIRAM2AILIABBADYCACAQQQFqIQFBHyEQDL0BCwJAIAEiBCACRw0AQY0BIRAM1wILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQfLPgIAAai0AAEcNvwEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQY0BIRAM1wILIABBADYCACAQQQFqIQFBCSEQDLwBCwJAIAEiBCACRw0AQY4BIRAM1gILAkACQCAELQAAQbd/ag4HAL8BvwG/Ab8BvwEBvwELIARBAWohAUH4ACEQDL0CCyAEQQFqIQFB+QAhEAy8AgsCQCABIgQgAkcNAEGPASEQDNUCCyACIARrIAAoAgAiAWohFCAEIAFrQQVqIRACQANAIAQtAAAgAUGRz4CAAGotAABHDb0BIAFBBUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGPASEQDNUCCyAAQQA2AgAgEEEBaiEBQRghEAy6AQsCQCABIgQgAkcNAEGQASEQDNQCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUGXz4CAAGotAABHDbwBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGQASEQDNQCCyAAQQA2AgAgEEEBaiEBQRchEAy5AQsCQCABIgQgAkcNAEGRASEQDNMCCyACIARrIAAoAgAiAWohFCAEIAFrQQZqIRACQANAIAQtAAAgAUGaz4CAAGotAABHDbsBIAFBBkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGRASEQDNMCCyAAQQA2AgAgEEEBaiEBQRUhEAy4AQsCQCABIgQgAkcNAEGSASEQDNICCyACIARrIAAoAgAiAWohFCAEIAFrQQVqIRACQANAIAQtAAAgAUGhz4CAAGotAABHDboBIAFBBUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGSASEQDNICCyAAQQA2AgAgEEEBaiEBQR4hEAy3AQsCQCABIgQgAkcNAEGTASEQDNECCyAELQAAQcwARw24ASAEQQFqIQFBCiEQDLYBCwJAIAQgAkcNAEGUASEQDNACCwJAAkAgBC0AAEG/f2oODwC5AbkBuQG5AbkBuQG5AbkBuQG5AbkBuQG5AQG5AQsgBEEBaiEBQf4AIRAMtwILIARBAWohAUH/ACEQDLYCCwJAIAQgAkcNAEGVASEQDM8CCwJAAkAgBC0AAEG/f2oOAwC4AQG4AQsgBEEBaiEBQf0AIRAMtgILIARBAWohBEGAASEQDLUCCwJAIAQgAkcNAEGWASEQDM4CCyACIARrIAAoAgAiAWohFCAEIAFrQQFqIRACQANAIAQtAAAgAUGnz4CAAGotAABHDbYBIAFBAUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGWASEQDM4CCyAAQQA2AgAgEEEBaiEBQQshEAyzAQsCQCAEIAJHDQBBlwEhEAzNAgsCQAJAAkACQCAELQAAQVNqDiMAuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AQG4AbgBuAG4AbgBArgBuAG4AQO4AQsgBEEBaiEBQfsAIRAMtgILIARBAWohAUH8ACEQDLUCCyAEQQFqIQRBgQEhEAy0AgsgBEEBaiEEQYIBIRAMswILAkAgBCACRw0AQZgBIRAMzAILIAIgBGsgACgCACIBaiEUIAQgAWtBBGohEAJAA0AgBC0AACABQanPgIAAai0AAEcNtAEgAUEERg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZgBIRAMzAILIABBADYCACAQQQFqIQFBGSEQDLEBCwJAIAQgAkcNAEGZASEQDMsCCyACIARrIAAoAgAiAWohFCAEIAFrQQVqIRACQANAIAQtAAAgAUGuz4CAAGotAABHDbMBIAFBBUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGZASEQDMsCCyAAQQA2AgAgEEEBaiEBQQYhEAywAQsCQCAEIAJHDQBBmgEhEAzKAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFBtM+AgABqLQAARw2yASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBmgEhEAzKAgsgAEEANgIAIBBBAWohAUEcIRAMrwELAkAgBCACRw0AQZsBIRAMyQILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQbbPgIAAai0AAEcNsQEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZsBIRAMyQILIABBADYCACAQQQFqIQFBJyEQDK4BCwJAIAQgAkcNAEGcASEQDMgCCwJAAkAgBC0AAEGsf2oOAgABsQELIARBAWohBEGGASEQDK8CCyAEQQFqIQRBhwEhEAyuAgsCQCAEIAJHDQBBnQEhEAzHAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFBuM+AgABqLQAARw2vASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBnQEhEAzHAgsgAEEANgIAIBBBAWohAUEmIRAMrAELAkAgBCACRw0AQZ4BIRAMxgILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQbrPgIAAai0AAEcNrgEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZ4BIRAMxgILIABBADYCACAQQQFqIQFBAyEQDKsBCwJAIAQgAkcNAEGfASEQDMUCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHtz4CAAGotAABHDa0BIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGfASEQDMUCCyAAQQA2AgAgEEEBaiEBQQwhEAyqAQsCQCAEIAJHDQBBoAEhEAzEAgsgAiAEayAAKAIAIgFqIRQgBCABa0EDaiEQAkADQCAELQAAIAFBvM+AgABqLQAARw2sASABQQNGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBoAEhEAzEAgsgAEEANgIAIBBBAWohAUENIRAMqQELAkAgBCACRw0AQaEBIRAMwwILAkACQCAELQAAQbp/ag4LAKwBrAGsAawBrAGsAawBrAGsAQGsAQsgBEEBaiEEQYsBIRAMqgILIARBAWohBEGMASEQDKkCCwJAIAQgAkcNAEGiASEQDMICCyAELQAAQdAARw2pASAEQQFqIQQM6QELAkAgBCACRw0AQaMBIRAMwQILAkACQCAELQAAQbd/ag4HAaoBqgGqAaoBqgEAqgELIARBAWohBEGOASEQDKgCCyAEQQFqIQFBIiEQDKYBCwJAIAQgAkcNAEGkASEQDMACCyACIARrIAAoAgAiAWohFCAEIAFrQQFqIRACQANAIAQtAAAgAUHAz4CAAGotAABHDagBIAFBAUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGkASEQDMACCyAAQQA2AgAgEEEBaiEBQR0hEAylAQsCQCAEIAJHDQBBpQEhEAy/AgsCQAJAIAQtAABBrn9qDgMAqAEBqAELIARBAWohBEGQASEQDKYCCyAEQQFqIQFBBCEQDKQBCwJAIAQgAkcNAEGmASEQDL4CCwJAAkACQAJAAkAgBC0AAEG/f2oOFQCqAaoBqgGqAaoBqgGqAaoBqgGqAQGqAaoBAqoBqgEDqgGqAQSqAQsgBEEBaiEEQYgBIRAMqAILIARBAWohBEGJASEQDKcCCyAEQQFqIQRBigEhEAymAgsgBEEBaiEEQY8BIRAMpQILIARBAWohBEGRASEQDKQCCwJAIAQgAkcNAEGnASEQDL0CCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHtz4CAAGotAABHDaUBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGnASEQDL0CCyAAQQA2AgAgEEEBaiEBQREhEAyiAQsCQCAEIAJHDQBBqAEhEAy8AgsgAiAEayAAKAIAIgFqIRQgBCABa0ECaiEQAkADQCAELQAAIAFBws+AgABqLQAARw2kASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBqAEhEAy8AgsgAEEANgIAIBBBAWohAUEsIRAMoQELAkAgBCACRw0AQakBIRAMuwILIAIgBGsgACgCACIBaiEUIAQgAWtBBGohEAJAA0AgBC0AACABQcXPgIAAai0AAEcNowEgAUEERg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQakBIRAMuwILIABBADYCACAQQQFqIQFBKyEQDKABCwJAIAQgAkcNAEGqASEQDLoCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHKz4CAAGotAABHDaIBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGqASEQDLoCCyAAQQA2AgAgEEEBaiEBQRQhEAyfAQsCQCAEIAJHDQBBqwEhEAy5AgsCQAJAAkACQCAELQAAQb5/ag4PAAECpAGkAaQBpAGkAaQBpAGkAaQBpAGkAQOkAQsgBEEBaiEEQZMBIRAMogILIARBAWohBEGUASEQDKECCyAEQQFqIQRBlQEhEAygAgsgBEEBaiEEQZYBIRAMnwILAkAgBCACRw0AQawBIRAMuAILIAQtAABBxQBHDZ8BIARBAWohBAzgAQsCQCAEIAJHDQBBrQEhEAy3AgsgAiAEayAAKAIAIgFqIRQgBCABa0ECaiEQAkADQCAELQAAIAFBzc+AgABqLQAARw2fASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBrQEhEAy3AgsgAEEANgIAIBBBAWohAUEOIRAMnAELAkAgBCACRw0AQa4BIRAMtgILIAQtAABB0ABHDZ0BIARBAWohAUElIRAMmwELAkAgBCACRw0AQa8BIRAMtQILIAIgBGsgACgCACIBaiEUIAQgAWtBCGohEAJAA0AgBC0AACABQdDPgIAAai0AAEcNnQEgAUEIRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQa8BIRAMtQILIABBADYCACAQQQFqIQFBKiEQDJoBCwJAIAQgAkcNAEGwASEQDLQCCwJAAkAgBC0AAEGrf2oOCwCdAZ0BnQGdAZ0BnQGdAZ0BnQEBnQELIARBAWohBEGaASEQDJsCCyAEQQFqIQRBmwEhEAyaAgsCQCAEIAJHDQBBsQEhEAyzAgsCQAJAIAQtAABBv39qDhQAnAGcAZwBnAGcAZwBnAGcAZwBnAGcAZwBnAGcAZwBnAGcAZwBAZwBCyAEQQFqIQRBmQEhEAyaAgsgBEEBaiEEQZwBIRAMmQILAkAgBCACRw0AQbIBIRAMsgILIAIgBGsgACgCACIBaiEUIAQgAWtBA2ohEAJAA0AgBC0AACABQdnPgIAAai0AAEcNmgEgAUEDRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQbIBIRAMsgILIABBADYCACAQQQFqIQFBISEQDJcBCwJAIAQgAkcNAEGzASEQDLECCyACIARrIAAoAgAiAWohFCAEIAFrQQZqIRACQANAIAQtAAAgAUHdz4CAAGotAABHDZkBIAFBBkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGzASEQDLECCyAAQQA2AgAgEEEBaiEBQRohEAyWAQsCQCAEIAJHDQBBtAEhEAywAgsCQAJAAkAgBC0AAEG7f2oOEQCaAZoBmgGaAZoBmgGaAZoBmgEBmgGaAZoBmgGaAQKaAQsgBEEBaiEEQZ0BIRAMmAILIARBAWohBEGeASEQDJcCCyAEQQFqIQRBnwEhEAyWAgsCQCAEIAJHDQBBtQEhEAyvAgsgAiAEayAAKAIAIgFqIRQgBCABa0EFaiEQAkADQCAELQAAIAFB5M+AgABqLQAARw2XASABQQVGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBtQEhEAyvAgsgAEEANgIAIBBBAWohAUEoIRAMlAELAkAgBCACRw0AQbYBIRAMrgILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQerPgIAAai0AAEcNlgEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQbYBIRAMrgILIABBADYCACAQQQFqIQFBByEQDJMBCwJAIAQgAkcNAEG3ASEQDK0CCwJAAkAgBC0AAEG7f2oODgCWAZYBlgGWAZYBlgGWAZYBlgGWAZYBlgEBlgELIARBAWohBEGhASEQDJQCCyAEQQFqIQRBogEhEAyTAgsCQCAEIAJHDQBBuAEhEAysAgsgAiAEayAAKAIAIgFqIRQgBCABa0ECaiEQAkADQCAELQAAIAFB7c+AgABqLQAARw2UASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBuAEhEAysAgsgAEEANgIAIBBBAWohAUESIRAMkQELAkAgBCACRw0AQbkBIRAMqwILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQfDPgIAAai0AAEcNkwEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQbkBIRAMqwILIABBADYCACAQQQFqIQFBICEQDJABCwJAIAQgAkcNAEG6ASEQDKoCCyACIARrIAAoAgAiAWohFCAEIAFrQQFqIRACQANAIAQtAAAgAUHyz4CAAGotAABHDZIBIAFBAUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEG6ASEQDKoCCyAAQQA2AgAgEEEBaiEBQQ8hEAyPAQsCQCAEIAJHDQBBuwEhEAypAgsCQAJAIAQtAABBt39qDgcAkgGSAZIBkgGSAQGSAQsgBEEBaiEEQaUBIRAMkAILIARBAWohBEGmASEQDI8CCwJAIAQgAkcNAEG8ASEQDKgCCyACIARrIAAoAgAiAWohFCAEIAFrQQdqIRACQANAIAQtAAAgAUH0z4CAAGotAABHDZABIAFBB0YNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEG8ASEQDKgCCyAAQQA2AgAgEEEBaiEBQRshEAyNAQsCQCAEIAJHDQBBvQEhEAynAgsCQAJAAkAgBC0AAEG+f2oOEgCRAZEBkQGRAZEBkQGRAZEBkQEBkQGRAZEBkQGRAZEBApEBCyAEQQFqIQRBpAEhEAyPAgsgBEEBaiEEQacBIRAMjgILIARBAWohBEGoASEQDI0CCwJAIAQgAkcNAEG+ASEQDKYCCyAELQAAQc4ARw2NASAEQQFqIQQMzwELAkAgBCACRw0AQb8BIRAMpQILAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgBC0AAEG/f2oOFQABAgOcAQQFBpwBnAGcAQcICQoLnAEMDQ4PnAELIARBAWohAUHoACEQDJoCCyAEQQFqIQFB6QAhEAyZAgsgBEEBaiEBQe4AIRAMmAILIARBAWohAUHyACEQDJcCCyAEQQFqIQFB8wAhEAyWAgsgBEEBaiEBQfYAIRAMlQILIARBAWohAUH3ACEQDJQCCyAEQQFqIQFB+gAhEAyTAgsgBEEBaiEEQYMBIRAMkgILIARBAWohBEGEASEQDJECCyAEQQFqIQRBhQEhEAyQAgsgBEEBaiEEQZIBIRAMjwILIARBAWohBEGYASEQDI4CCyAEQQFqIQRBoAEhEAyNAgsgBEEBaiEEQaMBIRAMjAILIARBAWohBEGqASEQDIsCCwJAIAQgAkYNACAAQZCAgIAANgIIIAAgBDYCBEGrASEQDIsCC0HAASEQDKMCCyAAIAUgAhCqgICAACIBDYsBIAUhAQxcCwJAIAYgAkYNACAGQQFqIQUMjQELQcIBIRAMoQILA0ACQCAQLQAAQXZqDgSMAQAAjwEACyAQQQFqIhAgAkcNAAtBwwEhEAygAgsCQCAHIAJGDQAgAEGRgICAADYCCCAAIAc2AgQgByEBQQEhEAyHAgtBxAEhEAyfAgsCQCAHIAJHDQBBxQEhEAyfAgsCQAJAIActAABBdmoOBAHOAc4BAM4BCyAHQQFqIQYMjQELIAdBAWohBQyJAQsCQCAHIAJHDQBBxgEhEAyeAgsCQAJAIActAABBdmoOFwGPAY8BAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAQCPAQsgB0EBaiEHC0GwASEQDIQCCwJAIAggAkcNAEHIASEQDJ0CCyAILQAAQSBHDY0BIABBADsBMiAIQQFqIQFBswEhEAyDAgsgASEXAkADQCAXIgcgAkYNASAHLQAAQVBqQf8BcSIQQQpPDcwBAkAgAC8BMiIUQZkzSw0AIAAgFEEKbCIUOwEyIBBB//8DcyAUQf7/A3FJDQAgB0EBaiEXIAAgFCAQaiIQOwEyIBBB//8DcUHoB0kNAQsLQQAhECAAQQA2AhwgAEHBiYCAADYCECAAQQ02AgwgACAHQQFqNgIUDJwCC0HHASEQDJsCCyAAIAggAhCugICAACIQRQ3KASAQQRVHDYwBIABByAE2AhwgACAINgIUIABByZeAgAA2AhAgAEEVNgIMQQAhEAyaAgsCQCAJIAJHDQBBzAEhEAyaAgtBACEUQQEhF0EBIRZBACEQAkACQAJAAkACQAJAAkACQAJAIAktAABBUGoOCpYBlQEAAQIDBAUGCJcBC0ECIRAMBgtBAyEQDAULQQQhEAwEC0EFIRAMAwtBBiEQDAILQQchEAwBC0EIIRALQQAhF0EAIRZBACEUDI4BC0EJIRBBASEUQQAhF0EAIRYMjQELAkAgCiACRw0AQc4BIRAMmQILIAotAABBLkcNjgEgCkEBaiEJDMoBCyALIAJHDY4BQdABIRAMlwILAkAgCyACRg0AIABBjoCAgAA2AgggACALNgIEQbcBIRAM/gELQdEBIRAMlgILAkAgBCACRw0AQdIBIRAMlgILIAIgBGsgACgCACIQaiEUIAQgEGtBBGohCwNAIAQtAAAgEEH8z4CAAGotAABHDY4BIBBBBEYN6QEgEEEBaiEQIARBAWoiBCACRw0ACyAAIBQ2AgBB0gEhEAyVAgsgACAMIAIQrICAgAAiAQ2NASAMIQEMuAELAkAgBCACRw0AQdQBIRAMlAILIAIgBGsgACgCACIQaiEUIAQgEGtBAWohDANAIAQtAAAgEEGB0ICAAGotAABHDY8BIBBBAUYNjgEgEEEBaiEQIARBAWoiBCACRw0ACyAAIBQ2AgBB1AEhEAyTAgsCQCAEIAJHDQBB1gEhEAyTAgsgAiAEayAAKAIAIhBqIRQgBCAQa0ECaiELA0AgBC0AACAQQYPQgIAAai0AAEcNjgEgEEECRg2QASAQQQFqIRAgBEEBaiIEIAJHDQALIAAgFDYCAEHWASEQDJICCwJAIAQgAkcNAEHXASEQDJICCwJAAkAgBC0AAEG7f2oOEACPAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BAY8BCyAEQQFqIQRBuwEhEAz5AQsgBEEBaiEEQbwBIRAM+AELAkAgBCACRw0AQdgBIRAMkQILIAQtAABByABHDYwBIARBAWohBAzEAQsCQCAEIAJGDQAgAEGQgICAADYCCCAAIAQ2AgRBvgEhEAz3AQtB2QEhEAyPAgsCQCAEIAJHDQBB2gEhEAyPAgsgBC0AAEHIAEYNwwEgAEEBOgAoDLkBCyAAQQI6AC8gACAEIAIQpoCAgAAiEA2NAUHCASEQDPQBCyAALQAoQX9qDgK3AbkBuAELA0ACQCAELQAAQXZqDgQAjgGOAQCOAQsgBEEBaiIEIAJHDQALQd0BIRAMiwILIABBADoALyAALQAtQQRxRQ2EAgsgAEEAOgAvIABBAToANCABIQEMjAELIBBBFUYN2gEgAEEANgIcIAAgATYCFCAAQaeOgIAANgIQIABBEjYCDEEAIRAMiAILAkAgACAQIAIQtICAgAAiBA0AIBAhAQyBAgsCQCAEQRVHDQAgAEEDNgIcIAAgEDYCFCAAQbCYgIAANgIQIABBFTYCDEEAIRAMiAILIABBADYCHCAAIBA2AhQgAEGnjoCAADYCECAAQRI2AgxBACEQDIcCCyAQQRVGDdYBIABBADYCHCAAIAE2AhQgAEHajYCAADYCECAAQRQ2AgxBACEQDIYCCyAAKAIEIRcgAEEANgIEIBAgEadqIhYhASAAIBcgECAWIBQbIhAQtYCAgAAiFEUNjQEgAEEHNgIcIAAgEDYCFCAAIBQ2AgxBACEQDIUCCyAAIAAvATBBgAFyOwEwIAEhAQtBKiEQDOoBCyAQQRVGDdEBIABBADYCHCAAIAE2AhQgAEGDjICAADYCECAAQRM2AgxBACEQDIICCyAQQRVGDc8BIABBADYCHCAAIAE2AhQgAEGaj4CAADYCECAAQSI2AgxBACEQDIECCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQt4CAgAAiEA0AIAFBAWohAQyNAQsgAEEMNgIcIAAgEDYCDCAAIAFBAWo2AhRBACEQDIACCyAQQRVGDcwBIABBADYCHCAAIAE2AhQgAEGaj4CAADYCECAAQSI2AgxBACEQDP8BCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQt4CAgAAiEA0AIAFBAWohAQyMAQsgAEENNgIcIAAgEDYCDCAAIAFBAWo2AhRBACEQDP4BCyAQQRVGDckBIABBADYCHCAAIAE2AhQgAEHGjICAADYCECAAQSM2AgxBACEQDP0BCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQuYCAgAAiEA0AIAFBAWohAQyLAQsgAEEONgIcIAAgEDYCDCAAIAFBAWo2AhRBACEQDPwBCyAAQQA2AhwgACABNgIUIABBwJWAgAA2AhAgAEECNgIMQQAhEAz7AQsgEEEVRg3FASAAQQA2AhwgACABNgIUIABBxoyAgAA2AhAgAEEjNgIMQQAhEAz6AQsgAEEQNgIcIAAgATYCFCAAIBA2AgxBACEQDPkBCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQuYCAgAAiBA0AIAFBAWohAQzxAQsgAEERNgIcIAAgBDYCDCAAIAFBAWo2AhRBACEQDPgBCyAQQRVGDcEBIABBADYCHCAAIAE2AhQgAEHGjICAADYCECAAQSM2AgxBACEQDPcBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQuYCAgAAiEA0AIAFBAWohAQyIAQsgAEETNgIcIAAgEDYCDCAAIAFBAWo2AhRBACEQDPYBCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQuYCAgAAiBA0AIAFBAWohAQztAQsgAEEUNgIcIAAgBDYCDCAAIAFBAWo2AhRBACEQDPUBCyAQQRVGDb0BIABBADYCHCAAIAE2AhQgAEGaj4CAADYCECAAQSI2AgxBACEQDPQBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQt4CAgAAiEA0AIAFBAWohAQyGAQsgAEEWNgIcIAAgEDYCDCAAIAFBAWo2AhRBACEQDPMBCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQt4CAgAAiBA0AIAFBAWohAQzpAQsgAEEXNgIcIAAgBDYCDCAAIAFBAWo2AhRBACEQDPIBCyAAQQA2AhwgACABNgIUIABBzZOAgAA2AhAgAEEMNgIMQQAhEAzxAQtCASERCyAQQQFqIQECQCAAKQMgIhJC//////////8PVg0AIAAgEkIEhiARhDcDICABIQEMhAELIABBADYCHCAAIAE2AhQgAEGtiYCAADYCECAAQQw2AgxBACEQDO8BCyAAQQA2AhwgACAQNgIUIABBzZOAgAA2AhAgAEEMNgIMQQAhEAzuAQsgACgCBCEXIABBADYCBCAQIBGnaiIWIQEgACAXIBAgFiAUGyIQELWAgIAAIhRFDXMgAEEFNgIcIAAgEDYCFCAAIBQ2AgxBACEQDO0BCyAAQQA2AhwgACAQNgIUIABBqpyAgAA2AhAgAEEPNgIMQQAhEAzsAQsgACAQIAIQtICAgAAiAQ0BIBAhAQtBDiEQDNEBCwJAIAFBFUcNACAAQQI2AhwgACAQNgIUIABBsJiAgAA2AhAgAEEVNgIMQQAhEAzqAQsgAEEANgIcIAAgEDYCFCAAQaeOgIAANgIQIABBEjYCDEEAIRAM6QELIAFBAWohEAJAIAAvATAiAUGAAXFFDQACQCAAIBAgAhC7gICAACIBDQAgECEBDHALIAFBFUcNugEgAEEFNgIcIAAgEDYCFCAAQfmXgIAANgIQIABBFTYCDEEAIRAM6QELAkAgAUGgBHFBoARHDQAgAC0ALUECcQ0AIABBADYCHCAAIBA2AhQgAEGWk4CAADYCECAAQQQ2AgxBACEQDOkBCyAAIBAgAhC9gICAABogECEBAkACQAJAAkACQCAAIBAgAhCzgICAAA4WAgEABAQEBAQEBAQEBAQEBAQEBAQEAwQLIABBAToALgsgACAALwEwQcAAcjsBMCAQIQELQSYhEAzRAQsgAEEjNgIcIAAgEDYCFCAAQaWWgIAANgIQIABBFTYCDEEAIRAM6QELIABBADYCHCAAIBA2AhQgAEHVi4CAADYCECAAQRE2AgxBACEQDOgBCyAALQAtQQFxRQ0BQcMBIRAMzgELAkAgDSACRg0AA0ACQCANLQAAQSBGDQAgDSEBDMQBCyANQQFqIg0gAkcNAAtBJSEQDOcBC0ElIRAM5gELIAAoAgQhBCAAQQA2AgQgACAEIA0Qr4CAgAAiBEUNrQEgAEEmNgIcIAAgBDYCDCAAIA1BAWo2AhRBACEQDOUBCyAQQRVGDasBIABBADYCHCAAIAE2AhQgAEH9jYCAADYCECAAQR02AgxBACEQDOQBCyAAQSc2AhwgACABNgIUIAAgEDYCDEEAIRAM4wELIBAhAUEBIRQCQAJAAkACQAJAAkACQCAALQAsQX5qDgcGBQUDAQIABQsgACAALwEwQQhyOwEwDAMLQQIhFAwBC0EEIRQLIABBAToALCAAIAAvATAgFHI7ATALIBAhAQtBKyEQDMoBCyAAQQA2AhwgACAQNgIUIABBq5KAgAA2AhAgAEELNgIMQQAhEAziAQsgAEEANgIcIAAgATYCFCAAQeGPgIAANgIQIABBCjYCDEEAIRAM4QELIABBADoALCAQIQEMvQELIBAhAUEBIRQCQAJAAkACQAJAIAAtACxBe2oOBAMBAgAFCyAAIAAvATBBCHI7ATAMAwtBAiEUDAELQQQhFAsgAEEBOgAsIAAgAC8BMCAUcjsBMAsgECEBC0EpIRAMxQELIABBADYCHCAAIAE2AhQgAEHwlICAADYCECAAQQM2AgxBACEQDN0BCwJAIA4tAABBDUcNACAAKAIEIQEgAEEANgIEAkAgACABIA4QsYCAgAAiAQ0AIA5BAWohAQx1CyAAQSw2AhwgACABNgIMIAAgDkEBajYCFEEAIRAM3QELIAAtAC1BAXFFDQFBxAEhEAzDAQsCQCAOIAJHDQBBLSEQDNwBCwJAAkADQAJAIA4tAABBdmoOBAIAAAMACyAOQQFqIg4gAkcNAAtBLSEQDN0BCyAAKAIEIQEgAEEANgIEAkAgACABIA4QsYCAgAAiAQ0AIA4hAQx0CyAAQSw2AhwgACAONgIUIAAgATYCDEEAIRAM3AELIAAoAgQhASAAQQA2AgQCQCAAIAEgDhCxgICAACIBDQAgDkEBaiEBDHMLIABBLDYCHCAAIAE2AgwgACAOQQFqNgIUQQAhEAzbAQsgACgCBCEEIABBADYCBCAAIAQgDhCxgICAACIEDaABIA4hAQzOAQsgEEEsRw0BIAFBAWohEEEBIQECQAJAAkACQAJAIAAtACxBe2oOBAMBAgQACyAQIQEMBAtBAiEBDAELQQQhAQsgAEEBOgAsIAAgAC8BMCABcjsBMCAQIQEMAQsgACAALwEwQQhyOwEwIBAhAQtBOSEQDL8BCyAAQQA6ACwgASEBC0E0IRAMvQELIAAgAC8BMEEgcjsBMCABIQEMAgsgACgCBCEEIABBADYCBAJAIAAgBCABELGAgIAAIgQNACABIQEMxwELIABBNzYCHCAAIAE2AhQgACAENgIMQQAhEAzUAQsgAEEIOgAsIAEhAQtBMCEQDLkBCwJAIAAtAChBAUYNACABIQEMBAsgAC0ALUEIcUUNkwEgASEBDAMLIAAtADBBIHENlAFBxQEhEAy3AQsCQCAPIAJGDQACQANAAkAgDy0AAEFQaiIBQf8BcUEKSQ0AIA8hAUE1IRAMugELIAApAyAiEUKZs+bMmbPmzBlWDQEgACARQgp+IhE3AyAgESABrUL/AYMiEkJ/hVYNASAAIBEgEnw3AyAgD0EBaiIPIAJHDQALQTkhEAzRAQsgACgCBCECIABBADYCBCAAIAIgD0EBaiIEELGAgIAAIgINlQEgBCEBDMMBC0E5IRAMzwELAkAgAC8BMCIBQQhxRQ0AIAAtAChBAUcNACAALQAtQQhxRQ2QAQsgACABQff7A3FBgARyOwEwIA8hAQtBNyEQDLQBCyAAIAAvATBBEHI7ATAMqwELIBBBFUYNiwEgAEEANgIcIAAgATYCFCAAQfCOgIAANgIQIABBHDYCDEEAIRAMywELIABBwwA2AhwgACABNgIMIAAgDUEBajYCFEEAIRAMygELAkAgAS0AAEE6Rw0AIAAoAgQhECAAQQA2AgQCQCAAIBAgARCvgICAACIQDQAgAUEBaiEBDGMLIABBwwA2AhwgACAQNgIMIAAgAUEBajYCFEEAIRAMygELIABBADYCHCAAIAE2AhQgAEGxkYCAADYCECAAQQo2AgxBACEQDMkBCyAAQQA2AhwgACABNgIUIABBoJmAgAA2AhAgAEEeNgIMQQAhEAzIAQsgAEEANgIACyAAQYASOwEqIAAgF0EBaiIBIAIQqICAgAAiEA0BIAEhAQtBxwAhEAysAQsgEEEVRw2DASAAQdEANgIcIAAgATYCFCAAQeOXgIAANgIQIABBFTYCDEEAIRAMxAELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDF4LIABB0gA2AhwgACABNgIUIAAgEDYCDEEAIRAMwwELIABBADYCHCAAIBQ2AhQgAEHBqICAADYCECAAQQc2AgwgAEEANgIAQQAhEAzCAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMXQsgAEHTADYCHCAAIAE2AhQgACAQNgIMQQAhEAzBAQtBACEQIABBADYCHCAAIAE2AhQgAEGAkYCAADYCECAAQQk2AgwMwAELIBBBFUYNfSAAQQA2AhwgACABNgIUIABBlI2AgAA2AhAgAEEhNgIMQQAhEAy/AQtBASEWQQAhF0EAIRRBASEQCyAAIBA6ACsgAUEBaiEBAkACQCAALQAtQRBxDQACQAJAAkAgAC0AKg4DAQACBAsgFkUNAwwCCyAUDQEMAgsgF0UNAQsgACgCBCEQIABBADYCBAJAIAAgECABEK2AgIAAIhANACABIQEMXAsgAEHYADYCHCAAIAE2AhQgACAQNgIMQQAhEAy+AQsgACgCBCEEIABBADYCBAJAIAAgBCABEK2AgIAAIgQNACABIQEMrQELIABB2QA2AhwgACABNgIUIAAgBDYCDEEAIRAMvQELIAAoAgQhBCAAQQA2AgQCQCAAIAQgARCtgICAACIEDQAgASEBDKsBCyAAQdoANgIcIAAgATYCFCAAIAQ2AgxBACEQDLwBCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQrYCAgAAiBA0AIAEhAQypAQsgAEHcADYCHCAAIAE2AhQgACAENgIMQQAhEAy7AQsCQCABLQAAQVBqIhBB/wFxQQpPDQAgACAQOgAqIAFBAWohAUHPACEQDKIBCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQrYCAgAAiBA0AIAEhAQynAQsgAEHeADYCHCAAIAE2AhQgACAENgIMQQAhEAy6AQsgAEEANgIAIBdBAWohAQJAIAAtAClBI08NACABIQEMWQsgAEEANgIcIAAgATYCFCAAQdOJgIAANgIQIABBCDYCDEEAIRAMuQELIABBADYCAAtBACEQIABBADYCHCAAIAE2AhQgAEGQs4CAADYCECAAQQg2AgwMtwELIABBADYCACAXQQFqIQECQCAALQApQSFHDQAgASEBDFYLIABBADYCHCAAIAE2AhQgAEGbioCAADYCECAAQQg2AgxBACEQDLYBCyAAQQA2AgAgF0EBaiEBAkAgAC0AKSIQQV1qQQtPDQAgASEBDFULAkAgEEEGSw0AQQEgEHRBygBxRQ0AIAEhAQxVC0EAIRAgAEEANgIcIAAgATYCFCAAQfeJgIAANgIQIABBCDYCDAy1AQsgEEEVRg1xIABBADYCHCAAIAE2AhQgAEG5jYCAADYCECAAQRo2AgxBACEQDLQBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxUCyAAQeUANgIcIAAgATYCFCAAIBA2AgxBACEQDLMBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxNCyAAQdIANgIcIAAgATYCFCAAIBA2AgxBACEQDLIBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxNCyAAQdMANgIcIAAgATYCFCAAIBA2AgxBACEQDLEBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxRCyAAQeUANgIcIAAgATYCFCAAIBA2AgxBACEQDLABCyAAQQA2AhwgACABNgIUIABBxoqAgAA2AhAgAEEHNgIMQQAhEAyvAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMSQsgAEHSADYCHCAAIAE2AhQgACAQNgIMQQAhEAyuAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMSQsgAEHTADYCHCAAIAE2AhQgACAQNgIMQQAhEAytAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMTQsgAEHlADYCHCAAIAE2AhQgACAQNgIMQQAhEAysAQsgAEEANgIcIAAgATYCFCAAQdyIgIAANgIQIABBBzYCDEEAIRAMqwELIBBBP0cNASABQQFqIQELQQUhEAyQAQtBACEQIABBADYCHCAAIAE2AhQgAEH9koCAADYCECAAQQc2AgwMqAELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDEILIABB0gA2AhwgACABNgIUIAAgEDYCDEEAIRAMpwELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDEILIABB0wA2AhwgACABNgIUIAAgEDYCDEEAIRAMpgELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDEYLIABB5QA2AhwgACABNgIUIAAgEDYCDEEAIRAMpQELIAAoAgQhASAAQQA2AgQCQCAAIAEgFBCngICAACIBDQAgFCEBDD8LIABB0gA2AhwgACAUNgIUIAAgATYCDEEAIRAMpAELIAAoAgQhASAAQQA2AgQCQCAAIAEgFBCngICAACIBDQAgFCEBDD8LIABB0wA2AhwgACAUNgIUIAAgATYCDEEAIRAMowELIAAoAgQhASAAQQA2AgQCQCAAIAEgFBCngICAACIBDQAgFCEBDEMLIABB5QA2AhwgACAUNgIUIAAgATYCDEEAIRAMogELIABBADYCHCAAIBQ2AhQgAEHDj4CAADYCECAAQQc2AgxBACEQDKEBCyAAQQA2AhwgACABNgIUIABBw4+AgAA2AhAgAEEHNgIMQQAhEAygAQtBACEQIABBADYCHCAAIBQ2AhQgAEGMnICAADYCECAAQQc2AgwMnwELIABBADYCHCAAIBQ2AhQgAEGMnICAADYCECAAQQc2AgxBACEQDJ4BCyAAQQA2AhwgACAUNgIUIABB/pGAgAA2AhAgAEEHNgIMQQAhEAydAQsgAEEANgIcIAAgATYCFCAAQY6bgIAANgIQIABBBjYCDEEAIRAMnAELIBBBFUYNVyAAQQA2AhwgACABNgIUIABBzI6AgAA2AhAgAEEgNgIMQQAhEAybAQsgAEEANgIAIBBBAWohAUEkIRALIAAgEDoAKSAAKAIEIRAgAEEANgIEIAAgECABEKuAgIAAIhANVCABIQEMPgsgAEEANgIAC0EAIRAgAEEANgIcIAAgBDYCFCAAQfGbgIAANgIQIABBBjYCDAyXAQsgAUEVRg1QIABBADYCHCAAIAU2AhQgAEHwjICAADYCECAAQRs2AgxBACEQDJYBCyAAKAIEIQUgAEEANgIEIAAgBSAQEKmAgIAAIgUNASAQQQFqIQULQa0BIRAMewsgAEHBATYCHCAAIAU2AgwgACAQQQFqNgIUQQAhEAyTAQsgACgCBCEGIABBADYCBCAAIAYgEBCpgICAACIGDQEgEEEBaiEGC0GuASEQDHgLIABBwgE2AhwgACAGNgIMIAAgEEEBajYCFEEAIRAMkAELIABBADYCHCAAIAc2AhQgAEGXi4CAADYCECAAQQ02AgxBACEQDI8BCyAAQQA2AhwgACAINgIUIABB45CAgAA2AhAgAEEJNgIMQQAhEAyOAQsgAEEANgIcIAAgCDYCFCAAQZSNgIAANgIQIABBITYCDEEAIRAMjQELQQEhFkEAIRdBACEUQQEhEAsgACAQOgArIAlBAWohCAJAAkAgAC0ALUEQcQ0AAkACQAJAIAAtACoOAwEAAgQLIBZFDQMMAgsgFA0BDAILIBdFDQELIAAoAgQhECAAQQA2AgQgACAQIAgQrYCAgAAiEEUNPSAAQckBNgIcIAAgCDYCFCAAIBA2AgxBACEQDIwBCyAAKAIEIQQgAEEANgIEIAAgBCAIEK2AgIAAIgRFDXYgAEHKATYCHCAAIAg2AhQgACAENgIMQQAhEAyLAQsgACgCBCEEIABBADYCBCAAIAQgCRCtgICAACIERQ10IABBywE2AhwgACAJNgIUIAAgBDYCDEEAIRAMigELIAAoAgQhBCAAQQA2AgQgACAEIAoQrYCAgAAiBEUNciAAQc0BNgIcIAAgCjYCFCAAIAQ2AgxBACEQDIkBCwJAIAstAABBUGoiEEH/AXFBCk8NACAAIBA6ACogC0EBaiEKQbYBIRAMcAsgACgCBCEEIABBADYCBCAAIAQgCxCtgICAACIERQ1wIABBzwE2AhwgACALNgIUIAAgBDYCDEEAIRAMiAELIABBADYCHCAAIAQ2AhQgAEGQs4CAADYCECAAQQg2AgwgAEEANgIAQQAhEAyHAQsgAUEVRg0/IABBADYCHCAAIAw2AhQgAEHMjoCAADYCECAAQSA2AgxBACEQDIYBCyAAQYEEOwEoIAAoAgQhECAAQgA3AwAgACAQIAxBAWoiDBCrgICAACIQRQ04IABB0wE2AhwgACAMNgIUIAAgEDYCDEEAIRAMhQELIABBADYCAAtBACEQIABBADYCHCAAIAQ2AhQgAEHYm4CAADYCECAAQQg2AgwMgwELIAAoAgQhECAAQgA3AwAgACAQIAtBAWoiCxCrgICAACIQDQFBxgEhEAxpCyAAQQI6ACgMVQsgAEHVATYCHCAAIAs2AhQgACAQNgIMQQAhEAyAAQsgEEEVRg03IABBADYCHCAAIAQ2AhQgAEGkjICAADYCECAAQRA2AgxBACEQDH8LIAAtADRBAUcNNCAAIAQgAhC8gICAACIQRQ00IBBBFUcNNSAAQdwBNgIcIAAgBDYCFCAAQdWWgIAANgIQIABBFTYCDEEAIRAMfgtBACEQIABBADYCHCAAQa+LgIAANgIQIABBAjYCDCAAIBRBAWo2AhQMfQtBACEQDGMLQQIhEAxiC0ENIRAMYQtBDyEQDGALQSUhEAxfC0ETIRAMXgtBFSEQDF0LQRYhEAxcC0EXIRAMWwtBGCEQDFoLQRkhEAxZC0EaIRAMWAtBGyEQDFcLQRwhEAxWC0EdIRAMVQtBHyEQDFQLQSEhEAxTC0EjIRAMUgtBxgAhEAxRC0EuIRAMUAtBLyEQDE8LQTshEAxOC0E9IRAMTQtByAAhEAxMC0HJACEQDEsLQcsAIRAMSgtBzAAhEAxJC0HOACEQDEgLQdEAIRAMRwtB1QAhEAxGC0HYACEQDEULQdkAIRAMRAtB2wAhEAxDC0HkACEQDEILQeUAIRAMQQtB8QAhEAxAC0H0ACEQDD8LQY0BIRAMPgtBlwEhEAw9C0GpASEQDDwLQawBIRAMOwtBwAEhEAw6C0G5ASEQDDkLQa8BIRAMOAtBsQEhEAw3C0GyASEQDDYLQbQBIRAMNQtBtQEhEAw0C0G6ASEQDDMLQb0BIRAMMgtBvwEhEAwxC0HBASEQDDALIABBADYCHCAAIAQ2AhQgAEHpi4CAADYCECAAQR82AgxBACEQDEgLIABB2wE2AhwgACAENgIUIABB+paAgAA2AhAgAEEVNgIMQQAhEAxHCyAAQfgANgIcIAAgDDYCFCAAQcqYgIAANgIQIABBFTYCDEEAIRAMRgsgAEHRADYCHCAAIAU2AhQgAEGwl4CAADYCECAAQRU2AgxBACEQDEULIABB+QA2AhwgACABNgIUIAAgEDYCDEEAIRAMRAsgAEH4ADYCHCAAIAE2AhQgAEHKmICAADYCECAAQRU2AgxBACEQDEMLIABB5AA2AhwgACABNgIUIABB45eAgAA2AhAgAEEVNgIMQQAhEAxCCyAAQdcANgIcIAAgATYCFCAAQcmXgIAANgIQIABBFTYCDEEAIRAMQQsgAEEANgIcIAAgATYCFCAAQbmNgIAANgIQIABBGjYCDEEAIRAMQAsgAEHCADYCHCAAIAE2AhQgAEHjmICAADYCECAAQRU2AgxBACEQDD8LIABBADYCBCAAIA8gDxCxgICAACIERQ0BIABBOjYCHCAAIAQ2AgwgACAPQQFqNgIUQQAhEAw+CyAAKAIEIQQgAEEANgIEAkAgACAEIAEQsYCAgAAiBEUNACAAQTs2AhwgACAENgIMIAAgAUEBajYCFEEAIRAMPgsgAUEBaiEBDC0LIA9BAWohAQwtCyAAQQA2AhwgACAPNgIUIABB5JKAgAA2AhAgAEEENgIMQQAhEAw7CyAAQTY2AhwgACAENgIUIAAgAjYCDEEAIRAMOgsgAEEuNgIcIAAgDjYCFCAAIAQ2AgxBACEQDDkLIABB0AA2AhwgACABNgIUIABBkZiAgAA2AhAgAEEVNgIMQQAhEAw4CyANQQFqIQEMLAsgAEEVNgIcIAAgATYCFCAAQYKZgIAANgIQIABBFTYCDEEAIRAMNgsgAEEbNgIcIAAgATYCFCAAQZGXgIAANgIQIABBFTYCDEEAIRAMNQsgAEEPNgIcIAAgATYCFCAAQZGXgIAANgIQIABBFTYCDEEAIRAMNAsgAEELNgIcIAAgATYCFCAAQZGXgIAANgIQIABBFTYCDEEAIRAMMwsgAEEaNgIcIAAgATYCFCAAQYKZgIAANgIQIABBFTYCDEEAIRAMMgsgAEELNgIcIAAgATYCFCAAQYKZgIAANgIQIABBFTYCDEEAIRAMMQsgAEEKNgIcIAAgATYCFCAAQeSWgIAANgIQIABBFTYCDEEAIRAMMAsgAEEeNgIcIAAgATYCFCAAQfmXgIAANgIQIABBFTYCDEEAIRAMLwsgAEEANgIcIAAgEDYCFCAAQdqNgIAANgIQIABBFDYCDEEAIRAMLgsgAEEENgIcIAAgATYCFCAAQbCYgIAANgIQIABBFTYCDEEAIRAMLQsgAEEANgIAIAtBAWohCwtBuAEhEAwSCyAAQQA2AgAgEEEBaiEBQfUAIRAMEQsgASEBAkAgAC0AKUEFRw0AQeMAIRAMEQtB4gAhEAwQC0EAIRAgAEEANgIcIABB5JGAgAA2AhAgAEEHNgIMIAAgFEEBajYCFAwoCyAAQQA2AgAgF0EBaiEBQcAAIRAMDgtBASEBCyAAIAE6ACwgAEEANgIAIBdBAWohAQtBKCEQDAsLIAEhAQtBOCEQDAkLAkAgASIPIAJGDQADQAJAIA8tAABBgL6AgABqLQAAIgFBAUYNACABQQJHDQMgD0EBaiEBDAQLIA9BAWoiDyACRw0AC0E+IRAMIgtBPiEQDCELIABBADoALCAPIQEMAQtBCyEQDAYLQTohEAwFCyABQQFqIQFBLSEQDAQLIAAgAToALCAAQQA2AgAgFkEBaiEBQQwhEAwDCyAAQQA2AgAgF0EBaiEBQQohEAwCCyAAQQA2AgALIABBADoALCANIQFBCSEQDAALC0EAIRAgAEEANgIcIAAgCzYCFCAAQc2QgIAANgIQIABBCTYCDAwXC0EAIRAgAEEANgIcIAAgCjYCFCAAQemKgIAANgIQIABBCTYCDAwWC0EAIRAgAEEANgIcIAAgCTYCFCAAQbeQgIAANgIQIABBCTYCDAwVC0EAIRAgAEEANgIcIAAgCDYCFCAAQZyRgIAANgIQIABBCTYCDAwUC0EAIRAgAEEANgIcIAAgATYCFCAAQc2QgIAANgIQIABBCTYCDAwTC0EAIRAgAEEANgIcIAAgATYCFCAAQemKgIAANgIQIABBCTYCDAwSC0EAIRAgAEEANgIcIAAgATYCFCAAQbeQgIAANgIQIABBCTYCDAwRC0EAIRAgAEEANgIcIAAgATYCFCAAQZyRgIAANgIQIABBCTYCDAwQC0EAIRAgAEEANgIcIAAgATYCFCAAQZeVgIAANgIQIABBDzYCDAwPC0EAIRAgAEEANgIcIAAgATYCFCAAQZeVgIAANgIQIABBDzYCDAwOC0EAIRAgAEEANgIcIAAgATYCFCAAQcCSgIAANgIQIABBCzYCDAwNC0EAIRAgAEEANgIcIAAgATYCFCAAQZWJgIAANgIQIABBCzYCDAwMC0EAIRAgAEEANgIcIAAgATYCFCAAQeGPgIAANgIQIABBCjYCDAwLC0EAIRAgAEEANgIcIAAgATYCFCAAQfuPgIAANgIQIABBCjYCDAwKC0EAIRAgAEEANgIcIAAgATYCFCAAQfGZgIAANgIQIABBAjYCDAwJC0EAIRAgAEEANgIcIAAgATYCFCAAQcSUgIAANgIQIABBAjYCDAwIC0EAIRAgAEEANgIcIAAgATYCFCAAQfKVgIAANgIQIABBAjYCDAwHCyAAQQI2AhwgACABNgIUIABBnJqAgAA2AhAgAEEWNgIMQQAhEAwGC0EBIRAMBQtB1AAhECABIgQgAkYNBCADQQhqIAAgBCACQdjCgIAAQQoQxYCAgAAgAygCDCEEIAMoAggOAwEEAgALEMqAgIAAAAsgAEEANgIcIABBtZqAgAA2AhAgAEEXNgIMIAAgBEEBajYCFEEAIRAMAgsgAEEANgIcIAAgBDYCFCAAQcqagIAANgIQIABBCTYCDEEAIRAMAQsCQCABIgQgAkcNAEEiIRAMAQsgAEGJgICAADYCCCAAIAQ2AgRBISEQCyADQRBqJICAgIAAIBALrwEBAn8gASgCACEGAkACQCACIANGDQAgBCAGaiEEIAYgA2ogAmshByACIAZBf3MgBWoiBmohBQNAAkAgAi0AACAELQAARg0AQQIhBAwDCwJAIAYNAEEAIQQgBSECDAMLIAZBf2ohBiAEQQFqIQQgAkEBaiICIANHDQALIAchBiADIQILIABBATYCACABIAY2AgAgACACNgIEDwsgAUEANgIAIAAgBDYCACAAIAI2AgQLCgAgABDHgICAAAvyNgELfyOAgICAAEEQayIBJICAgIAAAkBBACgCoNCAgAANAEEAEMuAgIAAQYDUhIAAayICQdkASQ0AQQAhAwJAQQAoAuDTgIAAIgQNAEEAQn83AuzTgIAAQQBCgICEgICAwAA3AuTTgIAAQQAgAUEIakFwcUHYqtWqBXMiBDYC4NOAgABBAEEANgL004CAAEEAQQA2AsTTgIAAC0EAIAI2AszTgIAAQQBBgNSEgAA2AsjTgIAAQQBBgNSEgAA2ApjQgIAAQQAgBDYCrNCAgABBAEF/NgKo0ICAAANAIANBxNCAgABqIANBuNCAgABqIgQ2AgAgBCADQbDQgIAAaiIFNgIAIANBvNCAgABqIAU2AgAgA0HM0ICAAGogA0HA0ICAAGoiBTYCACAFIAQ2AgAgA0HU0ICAAGogA0HI0ICAAGoiBDYCACAEIAU2AgAgA0HQ0ICAAGogBDYCACADQSBqIgNBgAJHDQALQYDUhIAAQXhBgNSEgABrQQ9xQQBBgNSEgABBCGpBD3EbIgNqIgRBBGogAkFIaiIFIANrIgNBAXI2AgBBAEEAKALw04CAADYCpNCAgABBACADNgKU0ICAAEEAIAQ2AqDQgIAAQYDUhIAAIAVqQTg2AgQLAkACQAJAAkACQAJAAkACQAJAAkACQAJAIABB7AFLDQACQEEAKAKI0ICAACIGQRAgAEETakFwcSAAQQtJGyICQQN2IgR2IgNBA3FFDQACQAJAIANBAXEgBHJBAXMiBUEDdCIEQbDQgIAAaiIDIARBuNCAgABqKAIAIgQoAggiAkcNAEEAIAZBfiAFd3E2AojQgIAADAELIAMgAjYCCCACIAM2AgwLIARBCGohAyAEIAVBA3QiBUEDcjYCBCAEIAVqIgQgBCgCBEEBcjYCBAwMCyACQQAoApDQgIAAIgdNDQECQCADRQ0AAkACQCADIAR0QQIgBHQiA0EAIANrcnEiA0EAIANrcUF/aiIDIANBDHZBEHEiA3YiBEEFdkEIcSIFIANyIAQgBXYiA0ECdkEEcSIEciADIAR2IgNBAXZBAnEiBHIgAyAEdiIDQQF2QQFxIgRyIAMgBHZqIgRBA3QiA0Gw0ICAAGoiBSADQbjQgIAAaigCACIDKAIIIgBHDQBBACAGQX4gBHdxIgY2AojQgIAADAELIAUgADYCCCAAIAU2AgwLIAMgAkEDcjYCBCADIARBA3QiBGogBCACayIFNgIAIAMgAmoiACAFQQFyNgIEAkAgB0UNACAHQXhxQbDQgIAAaiECQQAoApzQgIAAIQQCQAJAIAZBASAHQQN2dCIIcQ0AQQAgBiAIcjYCiNCAgAAgAiEIDAELIAIoAgghCAsgCCAENgIMIAIgBDYCCCAEIAI2AgwgBCAINgIICyADQQhqIQNBACAANgKc0ICAAEEAIAU2ApDQgIAADAwLQQAoAozQgIAAIglFDQEgCUEAIAlrcUF/aiIDIANBDHZBEHEiA3YiBEEFdkEIcSIFIANyIAQgBXYiA0ECdkEEcSIEciADIAR2IgNBAXZBAnEiBHIgAyAEdiIDQQF2QQFxIgRyIAMgBHZqQQJ0QbjSgIAAaigCACIAKAIEQXhxIAJrIQQgACEFAkADQAJAIAUoAhAiAw0AIAVBFGooAgAiA0UNAgsgAygCBEF4cSACayIFIAQgBSAESSIFGyEEIAMgACAFGyEAIAMhBQwACwsgACgCGCEKAkAgACgCDCIIIABGDQAgACgCCCIDQQAoApjQgIAASRogCCADNgIIIAMgCDYCDAwLCwJAIABBFGoiBSgCACIDDQAgACgCECIDRQ0DIABBEGohBQsDQCAFIQsgAyIIQRRqIgUoAgAiAw0AIAhBEGohBSAIKAIQIgMNAAsgC0EANgIADAoLQX8hAiAAQb9/Sw0AIABBE2oiA0FwcSECQQAoAozQgIAAIgdFDQBBACELAkAgAkGAAkkNAEEfIQsgAkH///8HSw0AIANBCHYiAyADQYD+P2pBEHZBCHEiA3QiBCAEQYDgH2pBEHZBBHEiBHQiBSAFQYCAD2pBEHZBAnEiBXRBD3YgAyAEciAFcmsiA0EBdCACIANBFWp2QQFxckEcaiELC0EAIAJrIQQCQAJAAkACQCALQQJ0QbjSgIAAaigCACIFDQBBACEDQQAhCAwBC0EAIQMgAkEAQRkgC0EBdmsgC0EfRht0IQBBACEIA0ACQCAFKAIEQXhxIAJrIgYgBE8NACAGIQQgBSEIIAYNAEEAIQQgBSEIIAUhAwwDCyADIAVBFGooAgAiBiAGIAUgAEEddkEEcWpBEGooAgAiBUYbIAMgBhshAyAAQQF0IQAgBQ0ACwsCQCADIAhyDQBBACEIQQIgC3QiA0EAIANrciAHcSIDRQ0DIANBACADa3FBf2oiAyADQQx2QRBxIgN2IgVBBXZBCHEiACADciAFIAB2IgNBAnZBBHEiBXIgAyAFdiIDQQF2QQJxIgVyIAMgBXYiA0EBdkEBcSIFciADIAV2akECdEG40oCAAGooAgAhAwsgA0UNAQsDQCADKAIEQXhxIAJrIgYgBEkhAAJAIAMoAhAiBQ0AIANBFGooAgAhBQsgBiAEIAAbIQQgAyAIIAAbIQggBSEDIAUNAAsLIAhFDQAgBEEAKAKQ0ICAACACa08NACAIKAIYIQsCQCAIKAIMIgAgCEYNACAIKAIIIgNBACgCmNCAgABJGiAAIAM2AgggAyAANgIMDAkLAkAgCEEUaiIFKAIAIgMNACAIKAIQIgNFDQMgCEEQaiEFCwNAIAUhBiADIgBBFGoiBSgCACIDDQAgAEEQaiEFIAAoAhAiAw0ACyAGQQA2AgAMCAsCQEEAKAKQ0ICAACIDIAJJDQBBACgCnNCAgAAhBAJAAkAgAyACayIFQRBJDQAgBCACaiIAIAVBAXI2AgRBACAFNgKQ0ICAAEEAIAA2ApzQgIAAIAQgA2ogBTYCACAEIAJBA3I2AgQMAQsgBCADQQNyNgIEIAQgA2oiAyADKAIEQQFyNgIEQQBBADYCnNCAgABBAEEANgKQ0ICAAAsgBEEIaiEDDAoLAkBBACgClNCAgAAiACACTQ0AQQAoAqDQgIAAIgMgAmoiBCAAIAJrIgVBAXI2AgRBACAFNgKU0ICAAEEAIAQ2AqDQgIAAIAMgAkEDcjYCBCADQQhqIQMMCgsCQAJAQQAoAuDTgIAARQ0AQQAoAujTgIAAIQQMAQtBAEJ/NwLs04CAAEEAQoCAhICAgMAANwLk04CAAEEAIAFBDGpBcHFB2KrVqgVzNgLg04CAAEEAQQA2AvTTgIAAQQBBADYCxNOAgABBgIAEIQQLQQAhAwJAIAQgAkHHAGoiB2oiBkEAIARrIgtxIgggAksNAEEAQTA2AvjTgIAADAoLAkBBACgCwNOAgAAiA0UNAAJAQQAoArjTgIAAIgQgCGoiBSAETQ0AIAUgA00NAQtBACEDQQBBMDYC+NOAgAAMCgtBAC0AxNOAgABBBHENBAJAAkACQEEAKAKg0ICAACIERQ0AQcjTgIAAIQMDQAJAIAMoAgAiBSAESw0AIAUgAygCBGogBEsNAwsgAygCCCIDDQALC0EAEMuAgIAAIgBBf0YNBSAIIQYCQEEAKALk04CAACIDQX9qIgQgAHFFDQAgCCAAayAEIABqQQAgA2txaiEGCyAGIAJNDQUgBkH+////B0sNBQJAQQAoAsDTgIAAIgNFDQBBACgCuNOAgAAiBCAGaiIFIARNDQYgBSADSw0GCyAGEMuAgIAAIgMgAEcNAQwHCyAGIABrIAtxIgZB/v///wdLDQQgBhDLgICAACIAIAMoAgAgAygCBGpGDQMgACEDCwJAIANBf0YNACACQcgAaiAGTQ0AAkAgByAGa0EAKALo04CAACIEakEAIARrcSIEQf7///8HTQ0AIAMhAAwHCwJAIAQQy4CAgABBf0YNACAEIAZqIQYgAyEADAcLQQAgBmsQy4CAgAAaDAQLIAMhACADQX9HDQUMAwtBACEIDAcLQQAhAAwFCyAAQX9HDQILQQBBACgCxNOAgABBBHI2AsTTgIAACyAIQf7///8HSw0BIAgQy4CAgAAhAEEAEMuAgIAAIQMgAEF/Rg0BIANBf0YNASAAIANPDQEgAyAAayIGIAJBOGpNDQELQQBBACgCuNOAgAAgBmoiAzYCuNOAgAACQCADQQAoArzTgIAATQ0AQQAgAzYCvNOAgAALAkACQAJAAkBBACgCoNCAgAAiBEUNAEHI04CAACEDA0AgACADKAIAIgUgAygCBCIIakYNAiADKAIIIgMNAAwDCwsCQAJAQQAoApjQgIAAIgNFDQAgACADTw0BC0EAIAA2ApjQgIAAC0EAIQNBACAGNgLM04CAAEEAIAA2AsjTgIAAQQBBfzYCqNCAgABBAEEAKALg04CAADYCrNCAgABBAEEANgLU04CAAANAIANBxNCAgABqIANBuNCAgABqIgQ2AgAgBCADQbDQgIAAaiIFNgIAIANBvNCAgABqIAU2AgAgA0HM0ICAAGogA0HA0ICAAGoiBTYCACAFIAQ2AgAgA0HU0ICAAGogA0HI0ICAAGoiBDYCACAEIAU2AgAgA0HQ0ICAAGogBDYCACADQSBqIgNBgAJHDQALIABBeCAAa0EPcUEAIABBCGpBD3EbIgNqIgQgBkFIaiIFIANrIgNBAXI2AgRBAEEAKALw04CAADYCpNCAgABBACADNgKU0ICAAEEAIAQ2AqDQgIAAIAAgBWpBODYCBAwCCyADLQAMQQhxDQAgBCAFSQ0AIAQgAE8NACAEQXggBGtBD3FBACAEQQhqQQ9xGyIFaiIAQQAoApTQgIAAIAZqIgsgBWsiBUEBcjYCBCADIAggBmo2AgRBAEEAKALw04CAADYCpNCAgABBACAFNgKU0ICAAEEAIAA2AqDQgIAAIAQgC2pBODYCBAwBCwJAIABBACgCmNCAgAAiCE8NAEEAIAA2ApjQgIAAIAAhCAsgACAGaiEFQcjTgIAAIQMCQAJAAkACQAJAAkACQANAIAMoAgAgBUYNASADKAIIIgMNAAwCCwsgAy0ADEEIcUUNAQtByNOAgAAhAwNAAkAgAygCACIFIARLDQAgBSADKAIEaiIFIARLDQMLIAMoAgghAwwACwsgAyAANgIAIAMgAygCBCAGajYCBCAAQXggAGtBD3FBACAAQQhqQQ9xG2oiCyACQQNyNgIEIAVBeCAFa0EPcUEAIAVBCGpBD3EbaiIGIAsgAmoiAmshAwJAIAYgBEcNAEEAIAI2AqDQgIAAQQBBACgClNCAgAAgA2oiAzYClNCAgAAgAiADQQFyNgIEDAMLAkAgBkEAKAKc0ICAAEcNAEEAIAI2ApzQgIAAQQBBACgCkNCAgAAgA2oiAzYCkNCAgAAgAiADQQFyNgIEIAIgA2ogAzYCAAwDCwJAIAYoAgQiBEEDcUEBRw0AIARBeHEhBwJAAkAgBEH/AUsNACAGKAIIIgUgBEEDdiIIQQN0QbDQgIAAaiIARhoCQCAGKAIMIgQgBUcNAEEAQQAoAojQgIAAQX4gCHdxNgKI0ICAAAwCCyAEIABGGiAEIAU2AgggBSAENgIMDAELIAYoAhghCQJAAkAgBigCDCIAIAZGDQAgBigCCCIEIAhJGiAAIAQ2AgggBCAANgIMDAELAkAgBkEUaiIEKAIAIgUNACAGQRBqIgQoAgAiBQ0AQQAhAAwBCwNAIAQhCCAFIgBBFGoiBCgCACIFDQAgAEEQaiEEIAAoAhAiBQ0ACyAIQQA2AgALIAlFDQACQAJAIAYgBigCHCIFQQJ0QbjSgIAAaiIEKAIARw0AIAQgADYCACAADQFBAEEAKAKM0ICAAEF+IAV3cTYCjNCAgAAMAgsgCUEQQRQgCSgCECAGRhtqIAA2AgAgAEUNAQsgACAJNgIYAkAgBigCECIERQ0AIAAgBDYCECAEIAA2AhgLIAYoAhQiBEUNACAAQRRqIAQ2AgAgBCAANgIYCyAHIANqIQMgBiAHaiIGKAIEIQQLIAYgBEF+cTYCBCACIANqIAM2AgAgAiADQQFyNgIEAkAgA0H/AUsNACADQXhxQbDQgIAAaiEEAkACQEEAKAKI0ICAACIFQQEgA0EDdnQiA3ENAEEAIAUgA3I2AojQgIAAIAQhAwwBCyAEKAIIIQMLIAMgAjYCDCAEIAI2AgggAiAENgIMIAIgAzYCCAwDC0EfIQQCQCADQf///wdLDQAgA0EIdiIEIARBgP4/akEQdkEIcSIEdCIFIAVBgOAfakEQdkEEcSIFdCIAIABBgIAPakEQdkECcSIAdEEPdiAEIAVyIAByayIEQQF0IAMgBEEVanZBAXFyQRxqIQQLIAIgBDYCHCACQgA3AhAgBEECdEG40oCAAGohBQJAQQAoAozQgIAAIgBBASAEdCIIcQ0AIAUgAjYCAEEAIAAgCHI2AozQgIAAIAIgBTYCGCACIAI2AgggAiACNgIMDAMLIANBAEEZIARBAXZrIARBH0YbdCEEIAUoAgAhAANAIAAiBSgCBEF4cSADRg0CIARBHXYhACAEQQF0IQQgBSAAQQRxakEQaiIIKAIAIgANAAsgCCACNgIAIAIgBTYCGCACIAI2AgwgAiACNgIIDAILIABBeCAAa0EPcUEAIABBCGpBD3EbIgNqIgsgBkFIaiIIIANrIgNBAXI2AgQgACAIakE4NgIEIAQgBUE3IAVrQQ9xQQAgBUFJakEPcRtqQUFqIgggCCAEQRBqSRsiCEEjNgIEQQBBACgC8NOAgAA2AqTQgIAAQQAgAzYClNCAgABBACALNgKg0ICAACAIQRBqQQApAtDTgIAANwIAIAhBACkCyNOAgAA3AghBACAIQQhqNgLQ04CAAEEAIAY2AszTgIAAQQAgADYCyNOAgABBAEEANgLU04CAACAIQSRqIQMDQCADQQc2AgAgA0EEaiIDIAVJDQALIAggBEYNAyAIIAgoAgRBfnE2AgQgCCAIIARrIgA2AgAgBCAAQQFyNgIEAkAgAEH/AUsNACAAQXhxQbDQgIAAaiEDAkACQEEAKAKI0ICAACIFQQEgAEEDdnQiAHENAEEAIAUgAHI2AojQgIAAIAMhBQwBCyADKAIIIQULIAUgBDYCDCADIAQ2AgggBCADNgIMIAQgBTYCCAwEC0EfIQMCQCAAQf///wdLDQAgAEEIdiIDIANBgP4/akEQdkEIcSIDdCIFIAVBgOAfakEQdkEEcSIFdCIIIAhBgIAPakEQdkECcSIIdEEPdiADIAVyIAhyayIDQQF0IAAgA0EVanZBAXFyQRxqIQMLIAQgAzYCHCAEQgA3AhAgA0ECdEG40oCAAGohBQJAQQAoAozQgIAAIghBASADdCIGcQ0AIAUgBDYCAEEAIAggBnI2AozQgIAAIAQgBTYCGCAEIAQ2AgggBCAENgIMDAQLIABBAEEZIANBAXZrIANBH0YbdCEDIAUoAgAhCANAIAgiBSgCBEF4cSAARg0DIANBHXYhCCADQQF0IQMgBSAIQQRxakEQaiIGKAIAIggNAAsgBiAENgIAIAQgBTYCGCAEIAQ2AgwgBCAENgIIDAMLIAUoAggiAyACNgIMIAUgAjYCCCACQQA2AhggAiAFNgIMIAIgAzYCCAsgC0EIaiEDDAULIAUoAggiAyAENgIMIAUgBDYCCCAEQQA2AhggBCAFNgIMIAQgAzYCCAtBACgClNCAgAAiAyACTQ0AQQAoAqDQgIAAIgQgAmoiBSADIAJrIgNBAXI2AgRBACADNgKU0ICAAEEAIAU2AqDQgIAAIAQgAkEDcjYCBCAEQQhqIQMMAwtBACEDQQBBMDYC+NOAgAAMAgsCQCALRQ0AAkACQCAIIAgoAhwiBUECdEG40oCAAGoiAygCAEcNACADIAA2AgAgAA0BQQAgB0F+IAV3cSIHNgKM0ICAAAwCCyALQRBBFCALKAIQIAhGG2ogADYCACAARQ0BCyAAIAs2AhgCQCAIKAIQIgNFDQAgACADNgIQIAMgADYCGAsgCEEUaigCACIDRQ0AIABBFGogAzYCACADIAA2AhgLAkACQCAEQQ9LDQAgCCAEIAJqIgNBA3I2AgQgCCADaiIDIAMoAgRBAXI2AgQMAQsgCCACaiIAIARBAXI2AgQgCCACQQNyNgIEIAAgBGogBDYCAAJAIARB/wFLDQAgBEF4cUGw0ICAAGohAwJAAkBBACgCiNCAgAAiBUEBIARBA3Z0IgRxDQBBACAFIARyNgKI0ICAACADIQQMAQsgAygCCCEECyAEIAA2AgwgAyAANgIIIAAgAzYCDCAAIAQ2AggMAQtBHyEDAkAgBEH///8HSw0AIARBCHYiAyADQYD+P2pBEHZBCHEiA3QiBSAFQYDgH2pBEHZBBHEiBXQiAiACQYCAD2pBEHZBAnEiAnRBD3YgAyAFciACcmsiA0EBdCAEIANBFWp2QQFxckEcaiEDCyAAIAM2AhwgAEIANwIQIANBAnRBuNKAgABqIQUCQCAHQQEgA3QiAnENACAFIAA2AgBBACAHIAJyNgKM0ICAACAAIAU2AhggACAANgIIIAAgADYCDAwBCyAEQQBBGSADQQF2ayADQR9GG3QhAyAFKAIAIQICQANAIAIiBSgCBEF4cSAERg0BIANBHXYhAiADQQF0IQMgBSACQQRxakEQaiIGKAIAIgINAAsgBiAANgIAIAAgBTYCGCAAIAA2AgwgACAANgIIDAELIAUoAggiAyAANgIMIAUgADYCCCAAQQA2AhggACAFNgIMIAAgAzYCCAsgCEEIaiEDDAELAkAgCkUNAAJAAkAgACAAKAIcIgVBAnRBuNKAgABqIgMoAgBHDQAgAyAINgIAIAgNAUEAIAlBfiAFd3E2AozQgIAADAILIApBEEEUIAooAhAgAEYbaiAINgIAIAhFDQELIAggCjYCGAJAIAAoAhAiA0UNACAIIAM2AhAgAyAINgIYCyAAQRRqKAIAIgNFDQAgCEEUaiADNgIAIAMgCDYCGAsCQAJAIARBD0sNACAAIAQgAmoiA0EDcjYCBCAAIANqIgMgAygCBEEBcjYCBAwBCyAAIAJqIgUgBEEBcjYCBCAAIAJBA3I2AgQgBSAEaiAENgIAAkAgB0UNACAHQXhxQbDQgIAAaiECQQAoApzQgIAAIQMCQAJAQQEgB0EDdnQiCCAGcQ0AQQAgCCAGcjYCiNCAgAAgAiEIDAELIAIoAgghCAsgCCADNgIMIAIgAzYCCCADIAI2AgwgAyAINgIIC0EAIAU2ApzQgIAAQQAgBDYCkNCAgAALIABBCGohAwsgAUEQaiSAgICAACADCwoAIAAQyYCAgAAL4g0BB38CQCAARQ0AIABBeGoiASAAQXxqKAIAIgJBeHEiAGohAwJAIAJBAXENACACQQNxRQ0BIAEgASgCACICayIBQQAoApjQgIAAIgRJDQEgAiAAaiEAAkAgAUEAKAKc0ICAAEYNAAJAIAJB/wFLDQAgASgCCCIEIAJBA3YiBUEDdEGw0ICAAGoiBkYaAkAgASgCDCICIARHDQBBAEEAKAKI0ICAAEF+IAV3cTYCiNCAgAAMAwsgAiAGRhogAiAENgIIIAQgAjYCDAwCCyABKAIYIQcCQAJAIAEoAgwiBiABRg0AIAEoAggiAiAESRogBiACNgIIIAIgBjYCDAwBCwJAIAFBFGoiAigCACIEDQAgAUEQaiICKAIAIgQNAEEAIQYMAQsDQCACIQUgBCIGQRRqIgIoAgAiBA0AIAZBEGohAiAGKAIQIgQNAAsgBUEANgIACyAHRQ0BAkACQCABIAEoAhwiBEECdEG40oCAAGoiAigCAEcNACACIAY2AgAgBg0BQQBBACgCjNCAgABBfiAEd3E2AozQgIAADAMLIAdBEEEUIAcoAhAgAUYbaiAGNgIAIAZFDQILIAYgBzYCGAJAIAEoAhAiAkUNACAGIAI2AhAgAiAGNgIYCyABKAIUIgJFDQEgBkEUaiACNgIAIAIgBjYCGAwBCyADKAIEIgJBA3FBA0cNACADIAJBfnE2AgRBACAANgKQ0ICAACABIABqIAA2AgAgASAAQQFyNgIEDwsgASADTw0AIAMoAgQiAkEBcUUNAAJAAkAgAkECcQ0AAkAgA0EAKAKg0ICAAEcNAEEAIAE2AqDQgIAAQQBBACgClNCAgAAgAGoiADYClNCAgAAgASAAQQFyNgIEIAFBACgCnNCAgABHDQNBAEEANgKQ0ICAAEEAQQA2ApzQgIAADwsCQCADQQAoApzQgIAARw0AQQAgATYCnNCAgABBAEEAKAKQ0ICAACAAaiIANgKQ0ICAACABIABBAXI2AgQgASAAaiAANgIADwsgAkF4cSAAaiEAAkACQCACQf8BSw0AIAMoAggiBCACQQN2IgVBA3RBsNCAgABqIgZGGgJAIAMoAgwiAiAERw0AQQBBACgCiNCAgABBfiAFd3E2AojQgIAADAILIAIgBkYaIAIgBDYCCCAEIAI2AgwMAQsgAygCGCEHAkACQCADKAIMIgYgA0YNACADKAIIIgJBACgCmNCAgABJGiAGIAI2AgggAiAGNgIMDAELAkAgA0EUaiICKAIAIgQNACADQRBqIgIoAgAiBA0AQQAhBgwBCwNAIAIhBSAEIgZBFGoiAigCACIEDQAgBkEQaiECIAYoAhAiBA0ACyAFQQA2AgALIAdFDQACQAJAIAMgAygCHCIEQQJ0QbjSgIAAaiICKAIARw0AIAIgBjYCACAGDQFBAEEAKAKM0ICAAEF+IAR3cTYCjNCAgAAMAgsgB0EQQRQgBygCECADRhtqIAY2AgAgBkUNAQsgBiAHNgIYAkAgAygCECICRQ0AIAYgAjYCECACIAY2AhgLIAMoAhQiAkUNACAGQRRqIAI2AgAgAiAGNgIYCyABIABqIAA2AgAgASAAQQFyNgIEIAFBACgCnNCAgABHDQFBACAANgKQ0ICAAA8LIAMgAkF+cTYCBCABIABqIAA2AgAgASAAQQFyNgIECwJAIABB/wFLDQAgAEF4cUGw0ICAAGohAgJAAkBBACgCiNCAgAAiBEEBIABBA3Z0IgBxDQBBACAEIAByNgKI0ICAACACIQAMAQsgAigCCCEACyAAIAE2AgwgAiABNgIIIAEgAjYCDCABIAA2AggPC0EfIQICQCAAQf///wdLDQAgAEEIdiICIAJBgP4/akEQdkEIcSICdCIEIARBgOAfakEQdkEEcSIEdCIGIAZBgIAPakEQdkECcSIGdEEPdiACIARyIAZyayICQQF0IAAgAkEVanZBAXFyQRxqIQILIAEgAjYCHCABQgA3AhAgAkECdEG40oCAAGohBAJAAkBBACgCjNCAgAAiBkEBIAJ0IgNxDQAgBCABNgIAQQAgBiADcjYCjNCAgAAgASAENgIYIAEgATYCCCABIAE2AgwMAQsgAEEAQRkgAkEBdmsgAkEfRht0IQIgBCgCACEGAkADQCAGIgQoAgRBeHEgAEYNASACQR12IQYgAkEBdCECIAQgBkEEcWpBEGoiAygCACIGDQALIAMgATYCACABIAQ2AhggASABNgIMIAEgATYCCAwBCyAEKAIIIgAgATYCDCAEIAE2AgggAUEANgIYIAEgBDYCDCABIAA2AggLQQBBACgCqNCAgABBf2oiAUF/IAEbNgKo0ICAAAsLBAAAAAtOAAJAIAANAD8AQRB0DwsCQCAAQf//A3ENACAAQX9MDQACQCAAQRB2QAAiAEF/Rw0AQQBBMDYC+NOAgABBfw8LIABBEHQPCxDKgICAAAAL8gICA38BfgJAIAJFDQAgACABOgAAIAIgAGoiA0F/aiABOgAAIAJBA0kNACAAIAE6AAIgACABOgABIANBfWogAToAACADQX5qIAE6AAAgAkEHSQ0AIAAgAToAAyADQXxqIAE6AAAgAkEJSQ0AIABBACAAa0EDcSIEaiIDIAFB/wFxQYGChAhsIgE2AgAgAyACIARrQXxxIgRqIgJBfGogATYCACAEQQlJDQAgAyABNgIIIAMgATYCBCACQXhqIAE2AgAgAkF0aiABNgIAIARBGUkNACADIAE2AhggAyABNgIUIAMgATYCECADIAE2AgwgAkFwaiABNgIAIAJBbGogATYCACACQWhqIAE2AgAgAkFkaiABNgIAIAQgA0EEcUEYciIFayICQSBJDQAgAa1CgYCAgBB+IQYgAyAFaiEBA0AgASAGNwMYIAEgBjcDECABIAY3AwggASAGNwMAIAFBIGohASACQWBqIgJBH0sNAAsLIAALC45IAQBBgAgLhkgBAAAAAgAAAAMAAAAAAAAAAAAAAAQAAAAFAAAAAAAAAAAAAAAGAAAABwAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEludmFsaWQgY2hhciBpbiB1cmwgcXVlcnkAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9ib2R5AENvbnRlbnQtTGVuZ3RoIG92ZXJmbG93AENodW5rIHNpemUgb3ZlcmZsb3cAUmVzcG9uc2Ugb3ZlcmZsb3cASW52YWxpZCBtZXRob2QgZm9yIEhUVFAveC54IHJlcXVlc3QASW52YWxpZCBtZXRob2QgZm9yIFJUU1AveC54IHJlcXVlc3QARXhwZWN0ZWQgU09VUkNFIG1ldGhvZCBmb3IgSUNFL3gueCByZXF1ZXN0AEludmFsaWQgY2hhciBpbiB1cmwgZnJhZ21lbnQgc3RhcnQARXhwZWN0ZWQgZG90AFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fc3RhdHVzAEludmFsaWQgcmVzcG9uc2Ugc3RhdHVzAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMAVXNlciBjYWxsYmFjayBlcnJvcgBgb25fcmVzZXRgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19oZWFkZXJgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2JlZ2luYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlYCBjYWxsYmFjayBlcnJvcgBgb25fc3RhdHVzX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdmVyc2lvbl9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3VybF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWVzc2FnZV9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX21ldGhvZF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lYCBjYWxsYmFjayBlcnJvcgBVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNlcnZlcgBJbnZhbGlkIGhlYWRlciB2YWx1ZSBjaGFyAEludmFsaWQgaGVhZGVyIGZpZWxkIGNoYXIAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl92ZXJzaW9uAEludmFsaWQgbWlub3IgdmVyc2lvbgBJbnZhbGlkIG1ham9yIHZlcnNpb24ARXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgdmVyc2lvbgBFeHBlY3RlZCBDUkxGIGFmdGVyIHZlcnNpb24ASW52YWxpZCBIVFRQIHZlcnNpb24ASW52YWxpZCBoZWFkZXIgdG9rZW4AU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl91cmwASW52YWxpZCBjaGFyYWN0ZXJzIGluIHVybABVbmV4cGVjdGVkIHN0YXJ0IGNoYXIgaW4gdXJsAERvdWJsZSBAIGluIHVybABFbXB0eSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXJhY3RlciBpbiBDb250ZW50LUxlbmd0aABEdXBsaWNhdGUgQ29udGVudC1MZW5ndGgASW52YWxpZCBjaGFyIGluIHVybCBwYXRoAENvbnRlbnQtTGVuZ3RoIGNhbid0IGJlIHByZXNlbnQgd2l0aCBUcmFuc2Zlci1FbmNvZGluZwBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBzaXplAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25faGVhZGVyX3ZhbHVlAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgdmFsdWUATWlzc2luZyBleHBlY3RlZCBMRiBhZnRlciBoZWFkZXIgdmFsdWUASW52YWxpZCBgVHJhbnNmZXItRW5jb2RpbmdgIGhlYWRlciB2YWx1ZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIHF1b3RlIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAFBhdXNlZCBieSBvbl9oZWFkZXJzX2NvbXBsZXRlAEludmFsaWQgRU9GIHN0YXRlAG9uX3Jlc2V0IHBhdXNlAG9uX2NodW5rX2hlYWRlciBwYXVzZQBvbl9tZXNzYWdlX2JlZ2luIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZSBwYXVzZQBvbl9zdGF0dXNfY29tcGxldGUgcGF1c2UAb25fdmVyc2lvbl9jb21wbGV0ZSBwYXVzZQBvbl91cmxfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlIHBhdXNlAG9uX21lc3NhZ2VfY29tcGxldGUgcGF1c2UAb25fbWV0aG9kX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fbmFtZSBwYXVzZQBVbmV4cGVjdGVkIHNwYWNlIGFmdGVyIHN0YXJ0IGxpbmUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fbmFtZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIG5hbWUAUGF1c2Ugb24gQ09OTkVDVC9VcGdyYWRlAFBhdXNlIG9uIFBSSS9VcGdyYWRlAEV4cGVjdGVkIEhUVFAvMiBDb25uZWN0aW9uIFByZWZhY2UAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9tZXRob2QARXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgbWV0aG9kAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25faGVhZGVyX2ZpZWxkAFBhdXNlZABJbnZhbGlkIHdvcmQgZW5jb3VudGVyZWQASW52YWxpZCBtZXRob2QgZW5jb3VudGVyZWQAVW5leHBlY3RlZCBjaGFyIGluIHVybCBzY2hlbWEAUmVxdWVzdCBoYXMgaW52YWxpZCBgVHJhbnNmZXItRW5jb2RpbmdgAFNXSVRDSF9QUk9YWQBVU0VfUFJPWFkATUtBQ1RJVklUWQBVTlBST0NFU1NBQkxFX0VOVElUWQBDT1BZAE1PVkVEX1BFUk1BTkVOVExZAFRPT19FQVJMWQBOT1RJRlkARkFJTEVEX0RFUEVOREVOQ1kAQkFEX0dBVEVXQVkAUExBWQBQVVQAQ0hFQ0tPVVQAR0FURVdBWV9USU1FT1VUAFJFUVVFU1RfVElNRU9VVABORVRXT1JLX0NPTk5FQ1RfVElNRU9VVABDT05ORUNUSU9OX1RJTUVPVVQATE9HSU5fVElNRU9VVABORVRXT1JLX1JFQURfVElNRU9VVABQT1NUAE1JU0RJUkVDVEVEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfTE9BRF9CQUxBTkNFRF9SRVFVRVNUAEJBRF9SRVFVRVNUAEhUVFBfUkVRVUVTVF9TRU5UX1RPX0hUVFBTX1BPUlQAUkVQT1JUAElNX0FfVEVBUE9UAFJFU0VUX0NPTlRFTlQATk9fQ09OVEVOVABQQVJUSUFMX0NPTlRFTlQASFBFX0lOVkFMSURfQ09OU1RBTlQASFBFX0NCX1JFU0VUAEdFVABIUEVfU1RSSUNUAENPTkZMSUNUAFRFTVBPUkFSWV9SRURJUkVDVABQRVJNQU5FTlRfUkVESVJFQ1QAQ09OTkVDVABNVUxUSV9TVEFUVVMASFBFX0lOVkFMSURfU1RBVFVTAFRPT19NQU5ZX1JFUVVFU1RTAEVBUkxZX0hJTlRTAFVOQVZBSUxBQkxFX0ZPUl9MRUdBTF9SRUFTT05TAE9QVElPTlMAU1dJVENISU5HX1BST1RPQ09MUwBWQVJJQU5UX0FMU09fTkVHT1RJQVRFUwBNVUxUSVBMRV9DSE9JQ0VTAElOVEVSTkFMX1NFUlZFUl9FUlJPUgBXRUJfU0VSVkVSX1VOS05PV05fRVJST1IAUkFJTEdVTl9FUlJPUgBJREVOVElUWV9QUk9WSURFUl9BVVRIRU5USUNBVElPTl9FUlJPUgBTU0xfQ0VSVElGSUNBVEVfRVJST1IASU5WQUxJRF9YX0ZPUldBUkRFRF9GT1IAU0VUX1BBUkFNRVRFUgBHRVRfUEFSQU1FVEVSAEhQRV9VU0VSAFNFRV9PVEhFUgBIUEVfQ0JfQ0hVTktfSEVBREVSAE1LQ0FMRU5EQVIAU0VUVVAAV0VCX1NFUlZFUl9JU19ET1dOAFRFQVJET1dOAEhQRV9DTE9TRURfQ09OTkVDVElPTgBIRVVSSVNUSUNfRVhQSVJBVElPTgBESVNDT05ORUNURURfT1BFUkFUSU9OAE5PTl9BVVRIT1JJVEFUSVZFX0lORk9STUFUSU9OAEhQRV9JTlZBTElEX1ZFUlNJT04ASFBFX0NCX01FU1NBR0VfQkVHSU4AU0lURV9JU19GUk9aRU4ASFBFX0lOVkFMSURfSEVBREVSX1RPS0VOAElOVkFMSURfVE9LRU4ARk9SQklEREVOAEVOSEFOQ0VfWU9VUl9DQUxNAEhQRV9JTlZBTElEX1VSTABCTE9DS0VEX0JZX1BBUkVOVEFMX0NPTlRST0wATUtDT0wAQUNMAEhQRV9JTlRFUk5BTABSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFX1VOT0ZGSUNJQUwASFBFX09LAFVOTElOSwBVTkxPQ0sAUFJJAFJFVFJZX1dJVEgASFBFX0lOVkFMSURfQ09OVEVOVF9MRU5HVEgASFBFX1VORVhQRUNURURfQ09OVEVOVF9MRU5HVEgARkxVU0gAUFJPUFBBVENIAE0tU0VBUkNIAFVSSV9UT09fTE9ORwBQUk9DRVNTSU5HAE1JU0NFTExBTkVPVVNfUEVSU0lTVEVOVF9XQVJOSU5HAE1JU0NFTExBTkVPVVNfV0FSTklORwBIUEVfSU5WQUxJRF9UUkFOU0ZFUl9FTkNPRElORwBFeHBlY3RlZCBDUkxGAEhQRV9JTlZBTElEX0NIVU5LX1NJWkUATU9WRQBDT05USU5VRQBIUEVfQ0JfU1RBVFVTX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJTX0NPTVBMRVRFAEhQRV9DQl9WRVJTSU9OX0NPTVBMRVRFAEhQRV9DQl9VUkxfQ09NUExFVEUASFBFX0NCX0NIVU5LX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJfVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9WQUxVRV9DT01QTEVURQBIUEVfQ0JfQ0hVTktfRVhURU5TSU9OX05BTUVfQ09NUExFVEUASFBFX0NCX01FU1NBR0VfQ09NUExFVEUASFBFX0NCX01FVEhPRF9DT01QTEVURQBIUEVfQ0JfSEVBREVSX0ZJRUxEX0NPTVBMRVRFAERFTEVURQBIUEVfSU5WQUxJRF9FT0ZfU1RBVEUASU5WQUxJRF9TU0xfQ0VSVElGSUNBVEUAUEFVU0UATk9fUkVTUE9OU0UAVU5TVVBQT1JURURfTUVESUFfVFlQRQBHT05FAE5PVF9BQ0NFUFRBQkxFAFNFUlZJQ0VfVU5BVkFJTEFCTEUAUkFOR0VfTk9UX1NBVElTRklBQkxFAE9SSUdJTl9JU19VTlJFQUNIQUJMRQBSRVNQT05TRV9JU19TVEFMRQBQVVJHRQBNRVJHRQBSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFAFJFUVVFU1RfSEVBREVSX1RPT19MQVJHRQBQQVlMT0FEX1RPT19MQVJHRQBJTlNVRkZJQ0lFTlRfU1RPUkFHRQBIUEVfUEFVU0VEX1VQR1JBREUASFBFX1BBVVNFRF9IMl9VUEdSQURFAFNPVVJDRQBBTk5PVU5DRQBUUkFDRQBIUEVfVU5FWFBFQ1RFRF9TUEFDRQBERVNDUklCRQBVTlNVQlNDUklCRQBSRUNPUkQASFBFX0lOVkFMSURfTUVUSE9EAE5PVF9GT1VORABQUk9QRklORABVTkJJTkQAUkVCSU5EAFVOQVVUSE9SSVpFRABNRVRIT0RfTk9UX0FMTE9XRUQASFRUUF9WRVJTSU9OX05PVF9TVVBQT1JURUQAQUxSRUFEWV9SRVBPUlRFRABBQ0NFUFRFRABOT1RfSU1QTEVNRU5URUQATE9PUF9ERVRFQ1RFRABIUEVfQ1JfRVhQRUNURUQASFBFX0xGX0VYUEVDVEVEAENSRUFURUQASU1fVVNFRABIUEVfUEFVU0VEAFRJTUVPVVRfT0NDVVJFRABQQVlNRU5UX1JFUVVJUkVEAFBSRUNPTkRJVElPTl9SRVFVSVJFRABQUk9YWV9BVVRIRU5USUNBVElPTl9SRVFVSVJFRABORVRXT1JLX0FVVEhFTlRJQ0FUSU9OX1JFUVVJUkVEAExFTkdUSF9SRVFVSVJFRABTU0xfQ0VSVElGSUNBVEVfUkVRVUlSRUQAVVBHUkFERV9SRVFVSVJFRABQQUdFX0VYUElSRUQAUFJFQ09ORElUSU9OX0ZBSUxFRABFWFBFQ1RBVElPTl9GQUlMRUQAUkVWQUxJREFUSU9OX0ZBSUxFRABTU0xfSEFORFNIQUtFX0ZBSUxFRABMT0NLRUQAVFJBTlNGT1JNQVRJT05fQVBQTElFRABOT1RfTU9ESUZJRUQATk9UX0VYVEVOREVEAEJBTkRXSURUSF9MSU1JVF9FWENFRURFRABTSVRFX0lTX09WRVJMT0FERUQASEVBRABFeHBlY3RlZCBIVFRQLwAAXhMAACYTAAAwEAAA8BcAAJ0TAAAVEgAAORcAAPASAAAKEAAAdRIAAK0SAACCEwAATxQAAH8QAACgFQAAIxQAAIkSAACLFAAATRUAANQRAADPFAAAEBgAAMkWAADcFgAAwREAAOAXAAC7FAAAdBQAAHwVAADlFAAACBcAAB8QAABlFQAAoxQAACgVAAACFQAAmRUAACwQAACLGQAATw8AANQOAABqEAAAzhAAAAIXAACJDgAAbhMAABwTAABmFAAAVhcAAMETAADNEwAAbBMAAGgXAABmFwAAXxcAACITAADODwAAaQ4AANgOAABjFgAAyxMAAKoOAAAoFwAAJhcAAMUTAABdFgAA6BEAAGcTAABlEwAA8hYAAHMTAAAdFwAA+RYAAPMRAADPDgAAzhUAAAwSAACzEQAApREAAGEQAAAyFwAAuxMAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIDAgICAgIAAAICAAICAAICAgICAgICAgIABAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgIAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgICAgACAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAICAgICAAACAgACAgACAgICAgICAgICAAMABAAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbG9zZWVlcC1hbGl2ZQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBY2h1bmtlZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAQEBAQEAAAEBAAEBAAEBAQEBAQEBAQEAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABlY3Rpb25lbnQtbGVuZ3Rob25yb3h5LWNvbm5lY3Rpb24AAAAAAAAAAAAAAAAAAAByYW5zZmVyLWVuY29kaW5ncGdyYWRlDQoNCg0KU00NCg0KVFRQL0NFL1RTUC8AAAAAAAAAAAAAAAABAgABAwAAAAAAAAAAAAAAAAAAAAAAAAQBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAQIAAQMAAAAAAAAAAAAAAAAAAAAAAAAEAQEFAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAQAAAgAAAAAAAAAAAAAAAAAAAAAAAAMEAAAEBAQEBAQEBAQEBAUEBAQEBAQEBAQEBAQABAAGBwQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAIAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABOT1VOQ0VFQ0tPVVRORUNURVRFQ1JJQkVMVVNIRVRFQURTRUFSQ0hSR0VDVElWSVRZTEVOREFSVkVPVElGWVBUSU9OU0NIU0VBWVNUQVRDSEdFT1JESVJFQ1RPUlRSQ0hQQVJBTUVURVJVUkNFQlNDUklCRUFSRE9XTkFDRUlORE5LQ0tVQlNDUklCRUhUVFAvQURUUC8=' + + +/***/ }), + +/***/ 3006: +/***/ ((module) => { + +module.exports = 'AGFzbQEAAAABMAhgAX8Bf2ADf39/AX9gBH9/f38Bf2AAAGADf39/AGABfwBgAn9/AGAGf39/f39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQACA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAA0ZFAwMEAAAFAAAAAAAABQEFAAUFBQAABgAAAAAGBgYGAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAABAQcAAAUFAwABBAUBcAESEgUDAQACBggBfwFBgNQECwfRBSIGbWVtb3J5AgALX2luaXRpYWxpemUACRlfX2luZGlyZWN0X2Z1bmN0aW9uX3RhYmxlAQALbGxodHRwX2luaXQAChhsbGh0dHBfc2hvdWxkX2tlZXBfYWxpdmUAQQxsbGh0dHBfYWxsb2MADAZtYWxsb2MARgtsbGh0dHBfZnJlZQANBGZyZWUASA9sbGh0dHBfZ2V0X3R5cGUADhVsbGh0dHBfZ2V0X2h0dHBfbWFqb3IADxVsbGh0dHBfZ2V0X2h0dHBfbWlub3IAEBFsbGh0dHBfZ2V0X21ldGhvZAARFmxsaHR0cF9nZXRfc3RhdHVzX2NvZGUAEhJsbGh0dHBfZ2V0X3VwZ3JhZGUAEwxsbGh0dHBfcmVzZXQAFA5sbGh0dHBfZXhlY3V0ZQAVFGxsaHR0cF9zZXR0aW5nc19pbml0ABYNbGxodHRwX2ZpbmlzaAAXDGxsaHR0cF9wYXVzZQAYDWxsaHR0cF9yZXN1bWUAGRtsbGh0dHBfcmVzdW1lX2FmdGVyX3VwZ3JhZGUAGhBsbGh0dHBfZ2V0X2Vycm5vABsXbGxodHRwX2dldF9lcnJvcl9yZWFzb24AHBdsbGh0dHBfc2V0X2Vycm9yX3JlYXNvbgAdFGxsaHR0cF9nZXRfZXJyb3JfcG9zAB4RbGxodHRwX2Vycm5vX25hbWUAHxJsbGh0dHBfbWV0aG9kX25hbWUAIBJsbGh0dHBfc3RhdHVzX25hbWUAIRpsbGh0dHBfc2V0X2xlbmllbnRfaGVhZGVycwAiIWxsaHR0cF9zZXRfbGVuaWVudF9jaHVua2VkX2xlbmd0aAAjHWxsaHR0cF9zZXRfbGVuaWVudF9rZWVwX2FsaXZlACQkbGxodHRwX3NldF9sZW5pZW50X3RyYW5zZmVyX2VuY29kaW5nACUYbGxodHRwX21lc3NhZ2VfbmVlZHNfZW9mAD8JFwEAQQELEQECAwQFCwYHNTk3MS8tJyspCrLgAkUCAAsIABCIgICAAAsZACAAEMKAgIAAGiAAIAI2AjggACABOgAoCxwAIAAgAC8BMiAALQAuIAAQwYCAgAAQgICAgAALKgEBf0HAABDGgICAACIBEMKAgIAAGiABQYCIgIAANgI4IAEgADoAKCABCwoAIAAQyICAgAALBwAgAC0AKAsHACAALQAqCwcAIAAtACsLBwAgAC0AKQsHACAALwEyCwcAIAAtAC4LRQEEfyAAKAIYIQEgAC0ALSECIAAtACghAyAAKAI4IQQgABDCgICAABogACAENgI4IAAgAzoAKCAAIAI6AC0gACABNgIYCxEAIAAgASABIAJqEMOAgIAACxAAIABBAEHcABDMgICAABoLZwEBf0EAIQECQCAAKAIMDQACQAJAAkACQCAALQAvDgMBAAMCCyAAKAI4IgFFDQAgASgCLCIBRQ0AIAAgARGAgICAAAAiAQ0DC0EADwsQyoCAgAAACyAAQcOWgIAANgIQQQ4hAQsgAQseAAJAIAAoAgwNACAAQdGbgIAANgIQIABBFTYCDAsLFgACQCAAKAIMQRVHDQAgAEEANgIMCwsWAAJAIAAoAgxBFkcNACAAQQA2AgwLCwcAIAAoAgwLBwAgACgCEAsJACAAIAE2AhALBwAgACgCFAsiAAJAIABBJEkNABDKgICAAAALIABBAnRBoLOAgABqKAIACyIAAkAgAEEuSQ0AEMqAgIAAAAsgAEECdEGwtICAAGooAgAL7gsBAX9B66iAgAAhAQJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIABBnH9qDvQDY2IAAWFhYWFhYQIDBAVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhBgcICQoLDA0OD2FhYWFhEGFhYWFhYWFhYWFhEWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYRITFBUWFxgZGhthYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2YTc4OTphYWFhYWFhYTthYWE8YWFhYT0+P2FhYWFhYWFhQGFhQWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYUJDREVGR0hJSktMTU5PUFFSU2FhYWFhYWFhVFVWV1hZWlthXF1hYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFeYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhX2BhC0Hhp4CAAA8LQaShgIAADwtBy6yAgAAPC0H+sYCAAA8LQcCkgIAADwtBq6SAgAAPC0GNqICAAA8LQeKmgIAADwtBgLCAgAAPC0G5r4CAAA8LQdekgIAADwtB75+AgAAPC0Hhn4CAAA8LQfqfgIAADwtB8qCAgAAPC0Gor4CAAA8LQa6ygIAADwtBiLCAgAAPC0Hsp4CAAA8LQYKigIAADwtBjp2AgAAPC0HQroCAAA8LQcqjgIAADwtBxbKAgAAPC0HfnICAAA8LQdKcgIAADwtBxKCAgAAPC0HXoICAAA8LQaKfgIAADwtB7a6AgAAPC0GrsICAAA8LQdSlgIAADwtBzK6AgAAPC0H6roCAAA8LQfyrgIAADwtB0rCAgAAPC0HxnYCAAA8LQbuggIAADwtB96uAgAAPC0GQsYCAAA8LQdexgIAADwtBoq2AgAAPC0HUp4CAAA8LQeCrgIAADwtBn6yAgAAPC0HrsYCAAA8LQdWfgIAADwtByrGAgAAPC0HepYCAAA8LQdSegIAADwtB9JyAgAAPC0GnsoCAAA8LQbGdgIAADwtBoJ2AgAAPC0G5sYCAAA8LQbywgIAADwtBkqGAgAAPC0GzpoCAAA8LQemsgIAADwtBrJ6AgAAPC0HUq4CAAA8LQfemgIAADwtBgKaAgAAPC0GwoYCAAA8LQf6egIAADwtBjaOAgAAPC0GJrYCAAA8LQfeigIAADwtBoLGAgAAPC0Gun4CAAA8LQcalgIAADwtB6J6AgAAPC0GTooCAAA8LQcKvgIAADwtBw52AgAAPC0GLrICAAA8LQeGdgIAADwtBja+AgAAPC0HqoYCAAA8LQbStgIAADwtB0q+AgAAPC0HfsoCAAA8LQdKygIAADwtB8LCAgAAPC0GpooCAAA8LQfmjgIAADwtBmZ6AgAAPC0G1rICAAA8LQZuwgIAADwtBkrKAgAAPC0G2q4CAAA8LQcKigIAADwtB+LKAgAAPC0GepYCAAA8LQdCigIAADwtBup6AgAAPC0GBnoCAAA8LEMqAgIAAAAtB1qGAgAAhAQsgAQsWACAAIAAtAC1B/gFxIAFBAEdyOgAtCxkAIAAgAC0ALUH9AXEgAUEAR0EBdHI6AC0LGQAgACAALQAtQfsBcSABQQBHQQJ0cjoALQsZACAAIAAtAC1B9wFxIAFBAEdBA3RyOgAtCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAgAiBEUNACAAIAQRgICAgAAAIQMLIAMLSQECf0EAIQMCQCAAKAI4IgRFDQAgBCgCBCIERQ0AIAAgASACIAFrIAQRgYCAgAAAIgNBf0cNACAAQcaRgIAANgIQQRghAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIwIgRFDQAgACAEEYCAgIAAACEDCyADC0kBAn9BACEDAkAgACgCOCIERQ0AIAQoAggiBEUNACAAIAEgAiABayAEEYGAgIAAACIDQX9HDQAgAEH2ioCAADYCEEEYIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCNCIERQ0AIAAgBBGAgICAAAAhAwsgAwtJAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIMIgRFDQAgACABIAIgAWsgBBGBgICAAAAiA0F/Rw0AIABB7ZqAgAA2AhBBGCEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAjgiBEUNACAAIAQRgICAgAAAIQMLIAMLSQECf0EAIQMCQCAAKAI4IgRFDQAgBCgCECIERQ0AIAAgASACIAFrIAQRgYCAgAAAIgNBf0cNACAAQZWQgIAANgIQQRghAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAI8IgRFDQAgACAEEYCAgIAAACEDCyADC0kBAn9BACEDAkAgACgCOCIERQ0AIAQoAhQiBEUNACAAIAEgAiABayAEEYGAgIAAACIDQX9HDQAgAEGqm4CAADYCEEEYIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCQCIERQ0AIAAgBBGAgICAAAAhAwsgAwtJAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIYIgRFDQAgACABIAIgAWsgBBGBgICAAAAiA0F/Rw0AIABB7ZOAgAA2AhBBGCEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAkQiBEUNACAAIAQRgICAgAAAIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCJCIERQ0AIAAgBBGAgICAAAAhAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIsIgRFDQAgACAEEYCAgIAAACEDCyADC0kBAn9BACEDAkAgACgCOCIERQ0AIAQoAigiBEUNACAAIAEgAiABayAEEYGAgIAAACIDQX9HDQAgAEH2iICAADYCEEEYIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCUCIERQ0AIAAgBBGAgICAAAAhAwsgAwtJAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAIcIgRFDQAgACABIAIgAWsgBBGBgICAAAAiA0F/Rw0AIABBwpmAgAA2AhBBGCEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAkgiBEUNACAAIAQRgICAgAAAIQMLIAMLSQECf0EAIQMCQCAAKAI4IgRFDQAgBCgCICIERQ0AIAAgASACIAFrIAQRgYCAgAAAIgNBf0cNACAAQZSUgIAANgIQQRghAwsgAwsuAQJ/QQAhAwJAIAAoAjgiBEUNACAEKAJMIgRFDQAgACAEEYCAgIAAACEDCyADCy4BAn9BACEDAkAgACgCOCIERQ0AIAQoAlQiBEUNACAAIAQRgICAgAAAIQMLIAMLLgECf0EAIQMCQCAAKAI4IgRFDQAgBCgCWCIERQ0AIAAgBBGAgICAAAAhAwsgAwtFAQF/AkACQCAALwEwQRRxQRRHDQBBASEDIAAtAChBAUYNASAALwEyQeUARiEDDAELIAAtAClBBUYhAwsgACADOgAuQQAL/gEBA39BASEDAkAgAC8BMCIEQQhxDQAgACkDIEIAUiEDCwJAAkAgAC0ALkUNAEEBIQUgAC0AKUEFRg0BQQEhBSAEQcAAcUUgA3FBAUcNAQtBACEFIARBwABxDQBBAiEFIARB//8DcSIDQQhxDQACQCADQYAEcUUNAAJAIAAtAChBAUcNACAALQAtQQpxDQBBBQ8LQQQPCwJAIANBIHENAAJAIAAtAChBAUYNACAALwEyQf//A3EiAEGcf2pB5ABJDQAgAEHMAUYNACAAQbACRg0AQQQhBSAEQShxRQ0CIANBiARxQYAERg0CC0EADwtBAEEDIAApAyBQGyEFCyAFC2IBAn9BACEBAkAgAC0AKEEBRg0AIAAvATJB//8DcSICQZx/akHkAEkNACACQcwBRg0AIAJBsAJGDQAgAC8BMCIAQcAAcQ0AQQEhASAAQYgEcUGABEYNACAAQShxRSEBCyABC6cBAQN/AkACQAJAIAAtACpFDQAgAC0AK0UNAEEAIQMgAC8BMCIEQQJxRQ0BDAILQQAhAyAALwEwIgRBAXFFDQELQQEhAyAALQAoQQFGDQAgAC8BMkH//wNxIgVBnH9qQeQASQ0AIAVBzAFGDQAgBUGwAkYNACAEQcAAcQ0AQQAhAyAEQYgEcUGABEYNACAEQShxQQBHIQMLIABBADsBMCAAQQA6AC8gAwuZAQECfwJAAkACQCAALQAqRQ0AIAAtACtFDQBBACEBIAAvATAiAkECcUUNAQwCC0EAIQEgAC8BMCICQQFxRQ0BC0EBIQEgAC0AKEEBRg0AIAAvATJB//8DcSIAQZx/akHkAEkNACAAQcwBRg0AIABBsAJGDQAgAkHAAHENAEEAIQEgAkGIBHFBgARGDQAgAkEocUEARyEBCyABC0kBAXsgAEEQav0MAAAAAAAAAAAAAAAAAAAAACIB/QsDACAAIAH9CwMAIABBMGogAf0LAwAgAEEgaiAB/QsDACAAQd0BNgIcQQALewEBfwJAIAAoAgwiAw0AAkAgACgCBEUNACAAIAE2AgQLAkAgACABIAIQxICAgAAiAw0AIAAoAgwPCyAAIAM2AhxBACEDIAAoAgQiAUUNACAAIAEgAiAAKAIIEYGAgIAAACIBRQ0AIAAgAjYCFCAAIAE2AgwgASEDCyADC+TzAQMOfwN+BH8jgICAgABBEGsiAySAgICAACABIQQgASEFIAEhBiABIQcgASEIIAEhCSABIQogASELIAEhDCABIQ0gASEOIAEhDwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAAKAIcIhBBf2oO3QHaAQHZAQIDBAUGBwgJCgsMDQ7YAQ8Q1wEREtYBExQVFhcYGRob4AHfARwdHtUBHyAhIiMkJdQBJicoKSorLNMB0gEtLtEB0AEvMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUbbAUdISUrPAc4BS80BTMwBTU5PUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ent8fX5/gAGBAYIBgwGEAYUBhgGHAYgBiQGKAYsBjAGNAY4BjwGQAZEBkgGTAZQBlQGWAZcBmAGZAZoBmwGcAZ0BngGfAaABoQGiAaMBpAGlAaYBpwGoAakBqgGrAawBrQGuAa8BsAGxAbIBswG0AbUBtgG3AcsBygG4AckBuQHIAboBuwG8Ab0BvgG/AcABwQHCAcMBxAHFAcYBANwBC0EAIRAMxgELQQ4hEAzFAQtBDSEQDMQBC0EPIRAMwwELQRAhEAzCAQtBEyEQDMEBC0EUIRAMwAELQRUhEAy/AQtBFiEQDL4BC0EXIRAMvQELQRghEAy8AQtBGSEQDLsBC0EaIRAMugELQRshEAy5AQtBHCEQDLgBC0EIIRAMtwELQR0hEAy2AQtBICEQDLUBC0EfIRAMtAELQQchEAyzAQtBISEQDLIBC0EiIRAMsQELQR4hEAywAQtBIyEQDK8BC0ESIRAMrgELQREhEAytAQtBJCEQDKwBC0ElIRAMqwELQSYhEAyqAQtBJyEQDKkBC0HDASEQDKgBC0EpIRAMpwELQSshEAymAQtBLCEQDKUBC0EtIRAMpAELQS4hEAyjAQtBLyEQDKIBC0HEASEQDKEBC0EwIRAMoAELQTQhEAyfAQtBDCEQDJ4BC0ExIRAMnQELQTIhEAycAQtBMyEQDJsBC0E5IRAMmgELQTUhEAyZAQtBxQEhEAyYAQtBCyEQDJcBC0E6IRAMlgELQTYhEAyVAQtBCiEQDJQBC0E3IRAMkwELQTghEAySAQtBPCEQDJEBC0E7IRAMkAELQT0hEAyPAQtBCSEQDI4BC0EoIRAMjQELQT4hEAyMAQtBPyEQDIsBC0HAACEQDIoBC0HBACEQDIkBC0HCACEQDIgBC0HDACEQDIcBC0HEACEQDIYBC0HFACEQDIUBC0HGACEQDIQBC0EqIRAMgwELQccAIRAMggELQcgAIRAMgQELQckAIRAMgAELQcoAIRAMfwtBywAhEAx+C0HNACEQDH0LQcwAIRAMfAtBzgAhEAx7C0HPACEQDHoLQdAAIRAMeQtB0QAhEAx4C0HSACEQDHcLQdMAIRAMdgtB1AAhEAx1C0HWACEQDHQLQdUAIRAMcwtBBiEQDHILQdcAIRAMcQtBBSEQDHALQdgAIRAMbwtBBCEQDG4LQdkAIRAMbQtB2gAhEAxsC0HbACEQDGsLQdwAIRAMagtBAyEQDGkLQd0AIRAMaAtB3gAhEAxnC0HfACEQDGYLQeEAIRAMZQtB4AAhEAxkC0HiACEQDGMLQeMAIRAMYgtBAiEQDGELQeQAIRAMYAtB5QAhEAxfC0HmACEQDF4LQecAIRAMXQtB6AAhEAxcC0HpACEQDFsLQeoAIRAMWgtB6wAhEAxZC0HsACEQDFgLQe0AIRAMVwtB7gAhEAxWC0HvACEQDFULQfAAIRAMVAtB8QAhEAxTC0HyACEQDFILQfMAIRAMUQtB9AAhEAxQC0H1ACEQDE8LQfYAIRAMTgtB9wAhEAxNC0H4ACEQDEwLQfkAIRAMSwtB+gAhEAxKC0H7ACEQDEkLQfwAIRAMSAtB/QAhEAxHC0H+ACEQDEYLQf8AIRAMRQtBgAEhEAxEC0GBASEQDEMLQYIBIRAMQgtBgwEhEAxBC0GEASEQDEALQYUBIRAMPwtBhgEhEAw+C0GHASEQDD0LQYgBIRAMPAtBiQEhEAw7C0GKASEQDDoLQYsBIRAMOQtBjAEhEAw4C0GNASEQDDcLQY4BIRAMNgtBjwEhEAw1C0GQASEQDDQLQZEBIRAMMwtBkgEhEAwyC0GTASEQDDELQZQBIRAMMAtBlQEhEAwvC0GWASEQDC4LQZcBIRAMLQtBmAEhEAwsC0GZASEQDCsLQZoBIRAMKgtBmwEhEAwpC0GcASEQDCgLQZ0BIRAMJwtBngEhEAwmC0GfASEQDCULQaABIRAMJAtBoQEhEAwjC0GiASEQDCILQaMBIRAMIQtBpAEhEAwgC0GlASEQDB8LQaYBIRAMHgtBpwEhEAwdC0GoASEQDBwLQakBIRAMGwtBqgEhEAwaC0GrASEQDBkLQawBIRAMGAtBrQEhEAwXC0GuASEQDBYLQQEhEAwVC0GvASEQDBQLQbABIRAMEwtBsQEhEAwSC0GzASEQDBELQbIBIRAMEAtBtAEhEAwPC0G1ASEQDA4LQbYBIRAMDQtBtwEhEAwMC0G4ASEQDAsLQbkBIRAMCgtBugEhEAwJC0G7ASEQDAgLQcYBIRAMBwtBvAEhEAwGC0G9ASEQDAULQb4BIRAMBAtBvwEhEAwDC0HAASEQDAILQcIBIRAMAQtBwQEhEAsDQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIBAOxwEAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB4fICEjJSg/QEFERUZHSElKS0xNT1BRUlPeA1dZW1xdYGJlZmdoaWprbG1vcHFyc3R1dnd4eXp7fH1+gAGCAYUBhgGHAYkBiwGMAY0BjgGPAZABkQGUAZUBlgGXAZgBmQGaAZsBnAGdAZ4BnwGgAaEBogGjAaQBpQGmAacBqAGpAaoBqwGsAa0BrgGvAbABsQGyAbMBtAG1AbYBtwG4AbkBugG7AbwBvQG+Ab8BwAHBAcIBwwHEAcUBxgHHAcgByQHKAcsBzAHNAc4BzwHQAdEB0gHTAdQB1QHWAdcB2AHZAdoB2wHcAd0B3gHgAeEB4gHjAeQB5QHmAecB6AHpAeoB6wHsAe0B7gHvAfAB8QHyAfMBmQKkArAC/gL+AgsgASIEIAJHDfMBQd0BIRAM/wMLIAEiECACRw3dAUHDASEQDP4DCyABIgEgAkcNkAFB9wAhEAz9AwsgASIBIAJHDYYBQe8AIRAM/AMLIAEiASACRw1/QeoAIRAM+wMLIAEiASACRw17QegAIRAM+gMLIAEiASACRw14QeYAIRAM+QMLIAEiASACRw0aQRghEAz4AwsgASIBIAJHDRRBEiEQDPcDCyABIgEgAkcNWUHFACEQDPYDCyABIgEgAkcNSkE/IRAM9QMLIAEiASACRw1IQTwhEAz0AwsgASIBIAJHDUFBMSEQDPMDCyAALQAuQQFGDesDDIcCCyAAIAEiASACEMCAgIAAQQFHDeYBIABCADcDIAznAQsgACABIgEgAhC0gICAACIQDecBIAEhAQz1AgsCQCABIgEgAkcNAEEGIRAM8AMLIAAgAUEBaiIBIAIQu4CAgAAiEA3oASABIQEMMQsgAEIANwMgQRIhEAzVAwsgASIQIAJHDStBHSEQDO0DCwJAIAEiASACRg0AIAFBAWohAUEQIRAM1AMLQQchEAzsAwsgAEIAIAApAyAiESACIAEiEGutIhJ9IhMgEyARVhs3AyAgESASViIURQ3lAUEIIRAM6wMLAkAgASIBIAJGDQAgAEGJgICAADYCCCAAIAE2AgQgASEBQRQhEAzSAwtBCSEQDOoDCyABIQEgACkDIFAN5AEgASEBDPICCwJAIAEiASACRw0AQQshEAzpAwsgACABQQFqIgEgAhC2gICAACIQDeUBIAEhAQzyAgsgACABIgEgAhC4gICAACIQDeUBIAEhAQzyAgsgACABIgEgAhC4gICAACIQDeYBIAEhAQwNCyAAIAEiASACELqAgIAAIhAN5wEgASEBDPACCwJAIAEiASACRw0AQQ8hEAzlAwsgAS0AACIQQTtGDQggEEENRw3oASABQQFqIQEM7wILIAAgASIBIAIQuoCAgAAiEA3oASABIQEM8gILA0ACQCABLQAAQfC1gIAAai0AACIQQQFGDQAgEEECRw3rASAAKAIEIRAgAEEANgIEIAAgECABQQFqIgEQuYCAgAAiEA3qASABIQEM9AILIAFBAWoiASACRw0AC0ESIRAM4gMLIAAgASIBIAIQuoCAgAAiEA3pASABIQEMCgsgASIBIAJHDQZBGyEQDOADCwJAIAEiASACRw0AQRYhEAzgAwsgAEGKgICAADYCCCAAIAE2AgQgACABIAIQuICAgAAiEA3qASABIQFBICEQDMYDCwJAIAEiASACRg0AA0ACQCABLQAAQfC3gIAAai0AACIQQQJGDQACQCAQQX9qDgTlAewBAOsB7AELIAFBAWohAUEIIRAMyAMLIAFBAWoiASACRw0AC0EVIRAM3wMLQRUhEAzeAwsDQAJAIAEtAABB8LmAgABqLQAAIhBBAkYNACAQQX9qDgTeAewB4AHrAewBCyABQQFqIgEgAkcNAAtBGCEQDN0DCwJAIAEiASACRg0AIABBi4CAgAA2AgggACABNgIEIAEhAUEHIRAMxAMLQRkhEAzcAwsgAUEBaiEBDAILAkAgASIUIAJHDQBBGiEQDNsDCyAUIQECQCAULQAAQXNqDhTdAu4C7gLuAu4C7gLuAu4C7gLuAu4C7gLuAu4C7gLuAu4C7gLuAgDuAgtBACEQIABBADYCHCAAQa+LgIAANgIQIABBAjYCDCAAIBRBAWo2AhQM2gMLAkAgAS0AACIQQTtGDQAgEEENRw3oASABQQFqIQEM5QILIAFBAWohAQtBIiEQDL8DCwJAIAEiECACRw0AQRwhEAzYAwtCACERIBAhASAQLQAAQVBqDjfnAeYBAQIDBAUGBwgAAAAAAAAACQoLDA0OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPEBESExQAC0EeIRAMvQMLQgIhEQzlAQtCAyERDOQBC0IEIREM4wELQgUhEQziAQtCBiERDOEBC0IHIREM4AELQgghEQzfAQtCCSERDN4BC0IKIREM3QELQgshEQzcAQtCDCERDNsBC0INIREM2gELQg4hEQzZAQtCDyERDNgBC0IKIREM1wELQgshEQzWAQtCDCERDNUBC0INIREM1AELQg4hEQzTAQtCDyERDNIBC0IAIRECQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIBAtAABBUGoON+UB5AEAAQIDBAUGB+YB5gHmAeYB5gHmAeYBCAkKCwwN5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAQ4PEBESE+YBC0ICIREM5AELQgMhEQzjAQtCBCERDOIBC0IFIREM4QELQgYhEQzgAQtCByERDN8BC0IIIREM3gELQgkhEQzdAQtCCiERDNwBC0ILIREM2wELQgwhEQzaAQtCDSERDNkBC0IOIREM2AELQg8hEQzXAQtCCiERDNYBC0ILIREM1QELQgwhEQzUAQtCDSERDNMBC0IOIREM0gELQg8hEQzRAQsgAEIAIAApAyAiESACIAEiEGutIhJ9IhMgEyARVhs3AyAgESASViIURQ3SAUEfIRAMwAMLAkAgASIBIAJGDQAgAEGJgICAADYCCCAAIAE2AgQgASEBQSQhEAynAwtBICEQDL8DCyAAIAEiECACEL6AgIAAQX9qDgW2AQDFAgHRAdIBC0ERIRAMpAMLIABBAToALyAQIQEMuwMLIAEiASACRw3SAUEkIRAMuwMLIAEiDSACRw0eQcYAIRAMugMLIAAgASIBIAIQsoCAgAAiEA3UASABIQEMtQELIAEiECACRw0mQdAAIRAMuAMLAkAgASIBIAJHDQBBKCEQDLgDCyAAQQA2AgQgAEGMgICAADYCCCAAIAEgARCxgICAACIQDdMBIAEhAQzYAQsCQCABIhAgAkcNAEEpIRAMtwMLIBAtAAAiAUEgRg0UIAFBCUcN0wEgEEEBaiEBDBULAkAgASIBIAJGDQAgAUEBaiEBDBcLQSohEAy1AwsCQCABIhAgAkcNAEErIRAMtQMLAkAgEC0AACIBQQlGDQAgAUEgRw3VAQsgAC0ALEEIRg3TASAQIQEMkQMLAkAgASIBIAJHDQBBLCEQDLQDCyABLQAAQQpHDdUBIAFBAWohAQzJAgsgASIOIAJHDdUBQS8hEAyyAwsDQAJAIAEtAAAiEEEgRg0AAkAgEEF2ag4EANwB3AEA2gELIAEhAQzgAQsgAUEBaiIBIAJHDQALQTEhEAyxAwtBMiEQIAEiFCACRg2wAyACIBRrIAAoAgAiAWohFSAUIAFrQQNqIRYCQANAIBQtAAAiF0EgciAXIBdBv39qQf8BcUEaSRtB/wFxIAFB8LuAgABqLQAARw0BAkAgAUEDRw0AQQYhAQyWAwsgAUEBaiEBIBRBAWoiFCACRw0ACyAAIBU2AgAMsQMLIABBADYCACAUIQEM2QELQTMhECABIhQgAkYNrwMgAiAUayAAKAIAIgFqIRUgFCABa0EIaiEWAkADQCAULQAAIhdBIHIgFyAXQb9/akH/AXFBGkkbQf8BcSABQfS7gIAAai0AAEcNAQJAIAFBCEcNAEEFIQEMlQMLIAFBAWohASAUQQFqIhQgAkcNAAsgACAVNgIADLADCyAAQQA2AgAgFCEBDNgBC0E0IRAgASIUIAJGDa4DIAIgFGsgACgCACIBaiEVIBQgAWtBBWohFgJAA0AgFC0AACIXQSByIBcgF0G/f2pB/wFxQRpJG0H/AXEgAUHQwoCAAGotAABHDQECQCABQQVHDQBBByEBDJQDCyABQQFqIQEgFEEBaiIUIAJHDQALIAAgFTYCAAyvAwsgAEEANgIAIBQhAQzXAQsCQCABIgEgAkYNAANAAkAgAS0AAEGAvoCAAGotAAAiEEEBRg0AIBBBAkYNCiABIQEM3QELIAFBAWoiASACRw0AC0EwIRAMrgMLQTAhEAytAwsCQCABIgEgAkYNAANAAkAgAS0AACIQQSBGDQAgEEF2ag4E2QHaAdoB2QHaAQsgAUEBaiIBIAJHDQALQTghEAytAwtBOCEQDKwDCwNAAkAgAS0AACIQQSBGDQAgEEEJRw0DCyABQQFqIgEgAkcNAAtBPCEQDKsDCwNAAkAgAS0AACIQQSBGDQACQAJAIBBBdmoOBNoBAQHaAQALIBBBLEYN2wELIAEhAQwECyABQQFqIgEgAkcNAAtBPyEQDKoDCyABIQEM2wELQcAAIRAgASIUIAJGDagDIAIgFGsgACgCACIBaiEWIBQgAWtBBmohFwJAA0AgFC0AAEEgciABQYDAgIAAai0AAEcNASABQQZGDY4DIAFBAWohASAUQQFqIhQgAkcNAAsgACAWNgIADKkDCyAAQQA2AgAgFCEBC0E2IRAMjgMLAkAgASIPIAJHDQBBwQAhEAynAwsgAEGMgICAADYCCCAAIA82AgQgDyEBIAAtACxBf2oOBM0B1QHXAdkBhwMLIAFBAWohAQzMAQsCQCABIgEgAkYNAANAAkAgAS0AACIQQSByIBAgEEG/f2pB/wFxQRpJG0H/AXEiEEEJRg0AIBBBIEYNAAJAAkACQAJAIBBBnX9qDhMAAwMDAwMDAwEDAwMDAwMDAwMCAwsgAUEBaiEBQTEhEAyRAwsgAUEBaiEBQTIhEAyQAwsgAUEBaiEBQTMhEAyPAwsgASEBDNABCyABQQFqIgEgAkcNAAtBNSEQDKUDC0E1IRAMpAMLAkAgASIBIAJGDQADQAJAIAEtAABBgLyAgABqLQAAQQFGDQAgASEBDNMBCyABQQFqIgEgAkcNAAtBPSEQDKQDC0E9IRAMowMLIAAgASIBIAIQsICAgAAiEA3WASABIQEMAQsgEEEBaiEBC0E8IRAMhwMLAkAgASIBIAJHDQBBwgAhEAygAwsCQANAAkAgAS0AAEF3ag4YAAL+Av4ChAP+Av4C/gL+Av4C/gL+Av4C/gL+Av4C/gL+Av4C/gL+Av4C/gIA/gILIAFBAWoiASACRw0AC0HCACEQDKADCyABQQFqIQEgAC0ALUEBcUUNvQEgASEBC0EsIRAMhQMLIAEiASACRw3TAUHEACEQDJ0DCwNAAkAgAS0AAEGQwICAAGotAABBAUYNACABIQEMtwILIAFBAWoiASACRw0AC0HFACEQDJwDCyANLQAAIhBBIEYNswEgEEE6Rw2BAyAAKAIEIQEgAEEANgIEIAAgASANEK+AgIAAIgEN0AEgDUEBaiEBDLMCC0HHACEQIAEiDSACRg2aAyACIA1rIAAoAgAiAWohFiANIAFrQQVqIRcDQCANLQAAIhRBIHIgFCAUQb9/akH/AXFBGkkbQf8BcSABQZDCgIAAai0AAEcNgAMgAUEFRg30AiABQQFqIQEgDUEBaiINIAJHDQALIAAgFjYCAAyaAwtByAAhECABIg0gAkYNmQMgAiANayAAKAIAIgFqIRYgDSABa0EJaiEXA0AgDS0AACIUQSByIBQgFEG/f2pB/wFxQRpJG0H/AXEgAUGWwoCAAGotAABHDf8CAkAgAUEJRw0AQQIhAQz1AgsgAUEBaiEBIA1BAWoiDSACRw0ACyAAIBY2AgAMmQMLAkAgASINIAJHDQBByQAhEAyZAwsCQAJAIA0tAAAiAUEgciABIAFBv39qQf8BcUEaSRtB/wFxQZJ/ag4HAIADgAOAA4ADgAMBgAMLIA1BAWohAUE+IRAMgAMLIA1BAWohAUE/IRAM/wILQcoAIRAgASINIAJGDZcDIAIgDWsgACgCACIBaiEWIA0gAWtBAWohFwNAIA0tAAAiFEEgciAUIBRBv39qQf8BcUEaSRtB/wFxIAFBoMKAgABqLQAARw39AiABQQFGDfACIAFBAWohASANQQFqIg0gAkcNAAsgACAWNgIADJcDC0HLACEQIAEiDSACRg2WAyACIA1rIAAoAgAiAWohFiANIAFrQQ5qIRcDQCANLQAAIhRBIHIgFCAUQb9/akH/AXFBGkkbQf8BcSABQaLCgIAAai0AAEcN/AIgAUEORg3wAiABQQFqIQEgDUEBaiINIAJHDQALIAAgFjYCAAyWAwtBzAAhECABIg0gAkYNlQMgAiANayAAKAIAIgFqIRYgDSABa0EPaiEXA0AgDS0AACIUQSByIBQgFEG/f2pB/wFxQRpJG0H/AXEgAUHAwoCAAGotAABHDfsCAkAgAUEPRw0AQQMhAQzxAgsgAUEBaiEBIA1BAWoiDSACRw0ACyAAIBY2AgAMlQMLQc0AIRAgASINIAJGDZQDIAIgDWsgACgCACIBaiEWIA0gAWtBBWohFwNAIA0tAAAiFEEgciAUIBRBv39qQf8BcUEaSRtB/wFxIAFB0MKAgABqLQAARw36AgJAIAFBBUcNAEEEIQEM8AILIAFBAWohASANQQFqIg0gAkcNAAsgACAWNgIADJQDCwJAIAEiDSACRw0AQc4AIRAMlAMLAkACQAJAAkAgDS0AACIBQSByIAEgAUG/f2pB/wFxQRpJG0H/AXFBnX9qDhMA/QL9Av0C/QL9Av0C/QL9Av0C/QL9Av0CAf0C/QL9AgID/QILIA1BAWohAUHBACEQDP0CCyANQQFqIQFBwgAhEAz8AgsgDUEBaiEBQcMAIRAM+wILIA1BAWohAUHEACEQDPoCCwJAIAEiASACRg0AIABBjYCAgAA2AgggACABNgIEIAEhAUHFACEQDPoCC0HPACEQDJIDCyAQIQECQAJAIBAtAABBdmoOBAGoAqgCAKgCCyAQQQFqIQELQSchEAz4AgsCQCABIgEgAkcNAEHRACEQDJEDCwJAIAEtAABBIEYNACABIQEMjQELIAFBAWohASAALQAtQQFxRQ3HASABIQEMjAELIAEiFyACRw3IAUHSACEQDI8DC0HTACEQIAEiFCACRg2OAyACIBRrIAAoAgAiAWohFiAUIAFrQQFqIRcDQCAULQAAIAFB1sKAgABqLQAARw3MASABQQFGDccBIAFBAWohASAUQQFqIhQgAkcNAAsgACAWNgIADI4DCwJAIAEiASACRw0AQdUAIRAMjgMLIAEtAABBCkcNzAEgAUEBaiEBDMcBCwJAIAEiASACRw0AQdYAIRAMjQMLAkACQCABLQAAQXZqDgQAzQHNAQHNAQsgAUEBaiEBDMcBCyABQQFqIQFBygAhEAzzAgsgACABIgEgAhCugICAACIQDcsBIAEhAUHNACEQDPICCyAALQApQSJGDYUDDKYCCwJAIAEiASACRw0AQdsAIRAMigMLQQAhFEEBIRdBASEWQQAhEAJAAkACQAJAAkACQAJAAkACQCABLQAAQVBqDgrUAdMBAAECAwQFBgjVAQtBAiEQDAYLQQMhEAwFC0EEIRAMBAtBBSEQDAMLQQYhEAwCC0EHIRAMAQtBCCEQC0EAIRdBACEWQQAhFAzMAQtBCSEQQQEhFEEAIRdBACEWDMsBCwJAIAEiASACRw0AQd0AIRAMiQMLIAEtAABBLkcNzAEgAUEBaiEBDKYCCyABIgEgAkcNzAFB3wAhEAyHAwsCQCABIgEgAkYNACAAQY6AgIAANgIIIAAgATYCBCABIQFB0AAhEAzuAgtB4AAhEAyGAwtB4QAhECABIgEgAkYNhQMgAiABayAAKAIAIhRqIRYgASAUa0EDaiEXA0AgAS0AACAUQeLCgIAAai0AAEcNzQEgFEEDRg3MASAUQQFqIRQgAUEBaiIBIAJHDQALIAAgFjYCAAyFAwtB4gAhECABIgEgAkYNhAMgAiABayAAKAIAIhRqIRYgASAUa0ECaiEXA0AgAS0AACAUQebCgIAAai0AAEcNzAEgFEECRg3OASAUQQFqIRQgAUEBaiIBIAJHDQALIAAgFjYCAAyEAwtB4wAhECABIgEgAkYNgwMgAiABayAAKAIAIhRqIRYgASAUa0EDaiEXA0AgAS0AACAUQenCgIAAai0AAEcNywEgFEEDRg3OASAUQQFqIRQgAUEBaiIBIAJHDQALIAAgFjYCAAyDAwsCQCABIgEgAkcNAEHlACEQDIMDCyAAIAFBAWoiASACEKiAgIAAIhANzQEgASEBQdYAIRAM6QILAkAgASIBIAJGDQADQAJAIAEtAAAiEEEgRg0AAkACQAJAIBBBuH9qDgsAAc8BzwHPAc8BzwHPAc8BzwECzwELIAFBAWohAUHSACEQDO0CCyABQQFqIQFB0wAhEAzsAgsgAUEBaiEBQdQAIRAM6wILIAFBAWoiASACRw0AC0HkACEQDIIDC0HkACEQDIEDCwNAAkAgAS0AAEHwwoCAAGotAAAiEEEBRg0AIBBBfmoOA88B0AHRAdIBCyABQQFqIgEgAkcNAAtB5gAhEAyAAwsCQCABIgEgAkYNACABQQFqIQEMAwtB5wAhEAz/AgsDQAJAIAEtAABB8MSAgABqLQAAIhBBAUYNAAJAIBBBfmoOBNIB0wHUAQDVAQsgASEBQdcAIRAM5wILIAFBAWoiASACRw0AC0HoACEQDP4CCwJAIAEiASACRw0AQekAIRAM/gILAkAgAS0AACIQQXZqDhq6AdUB1QG8AdUB1QHVAdUB1QHVAdUB1QHVAdUB1QHVAdUB1QHVAdUB1QHVAcoB1QHVAQDTAQsgAUEBaiEBC0EGIRAM4wILA0ACQCABLQAAQfDGgIAAai0AAEEBRg0AIAEhAQyeAgsgAUEBaiIBIAJHDQALQeoAIRAM+wILAkAgASIBIAJGDQAgAUEBaiEBDAMLQesAIRAM+gILAkAgASIBIAJHDQBB7AAhEAz6AgsgAUEBaiEBDAELAkAgASIBIAJHDQBB7QAhEAz5AgsgAUEBaiEBC0EEIRAM3gILAkAgASIUIAJHDQBB7gAhEAz3AgsgFCEBAkACQAJAIBQtAABB8MiAgABqLQAAQX9qDgfUAdUB1gEAnAIBAtcBCyAUQQFqIQEMCgsgFEEBaiEBDM0BC0EAIRAgAEEANgIcIABBm5KAgAA2AhAgAEEHNgIMIAAgFEEBajYCFAz2AgsCQANAAkAgAS0AAEHwyICAAGotAAAiEEEERg0AAkACQCAQQX9qDgfSAdMB1AHZAQAEAdkBCyABIQFB2gAhEAzgAgsgAUEBaiEBQdwAIRAM3wILIAFBAWoiASACRw0AC0HvACEQDPYCCyABQQFqIQEMywELAkAgASIUIAJHDQBB8AAhEAz1AgsgFC0AAEEvRw3UASAUQQFqIQEMBgsCQCABIhQgAkcNAEHxACEQDPQCCwJAIBQtAAAiAUEvRw0AIBRBAWohAUHdACEQDNsCCyABQXZqIgRBFksN0wFBASAEdEGJgIACcUUN0wEMygILAkAgASIBIAJGDQAgAUEBaiEBQd4AIRAM2gILQfIAIRAM8gILAkAgASIUIAJHDQBB9AAhEAzyAgsgFCEBAkAgFC0AAEHwzICAAGotAABBf2oOA8kClAIA1AELQeEAIRAM2AILAkAgASIUIAJGDQADQAJAIBQtAABB8MqAgABqLQAAIgFBA0YNAAJAIAFBf2oOAssCANUBCyAUIQFB3wAhEAzaAgsgFEEBaiIUIAJHDQALQfMAIRAM8QILQfMAIRAM8AILAkAgASIBIAJGDQAgAEGPgICAADYCCCAAIAE2AgQgASEBQeAAIRAM1wILQfUAIRAM7wILAkAgASIBIAJHDQBB9gAhEAzvAgsgAEGPgICAADYCCCAAIAE2AgQgASEBC0EDIRAM1AILA0AgAS0AAEEgRw3DAiABQQFqIgEgAkcNAAtB9wAhEAzsAgsCQCABIgEgAkcNAEH4ACEQDOwCCyABLQAAQSBHDc4BIAFBAWohAQzvAQsgACABIgEgAhCsgICAACIQDc4BIAEhAQyOAgsCQCABIgQgAkcNAEH6ACEQDOoCCyAELQAAQcwARw3RASAEQQFqIQFBEyEQDM8BCwJAIAEiBCACRw0AQfsAIRAM6QILIAIgBGsgACgCACIBaiEUIAQgAWtBBWohEANAIAQtAAAgAUHwzoCAAGotAABHDdABIAFBBUYNzgEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBB+wAhEAzoAgsCQCABIgQgAkcNAEH8ACEQDOgCCwJAAkAgBC0AAEG9f2oODADRAdEB0QHRAdEB0QHRAdEB0QHRAQHRAQsgBEEBaiEBQeYAIRAMzwILIARBAWohAUHnACEQDM4CCwJAIAEiBCACRw0AQf0AIRAM5wILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQe3PgIAAai0AAEcNzwEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQf0AIRAM5wILIABBADYCACAQQQFqIQFBECEQDMwBCwJAIAEiBCACRw0AQf4AIRAM5gILIAIgBGsgACgCACIBaiEUIAQgAWtBBWohEAJAA0AgBC0AACABQfbOgIAAai0AAEcNzgEgAUEFRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQf4AIRAM5gILIABBADYCACAQQQFqIQFBFiEQDMsBCwJAIAEiBCACRw0AQf8AIRAM5QILIAIgBGsgACgCACIBaiEUIAQgAWtBA2ohEAJAA0AgBC0AACABQfzOgIAAai0AAEcNzQEgAUEDRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQf8AIRAM5QILIABBADYCACAQQQFqIQFBBSEQDMoBCwJAIAEiBCACRw0AQYABIRAM5AILIAQtAABB2QBHDcsBIARBAWohAUEIIRAMyQELAkAgASIEIAJHDQBBgQEhEAzjAgsCQAJAIAQtAABBsn9qDgMAzAEBzAELIARBAWohAUHrACEQDMoCCyAEQQFqIQFB7AAhEAzJAgsCQCABIgQgAkcNAEGCASEQDOICCwJAAkAgBC0AAEG4f2oOCADLAcsBywHLAcsBywEBywELIARBAWohAUHqACEQDMkCCyAEQQFqIQFB7QAhEAzIAgsCQCABIgQgAkcNAEGDASEQDOECCyACIARrIAAoAgAiAWohECAEIAFrQQJqIRQCQANAIAQtAAAgAUGAz4CAAGotAABHDckBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgEDYCAEGDASEQDOECC0EAIRAgAEEANgIAIBRBAWohAQzGAQsCQCABIgQgAkcNAEGEASEQDOACCyACIARrIAAoAgAiAWohFCAEIAFrQQRqIRACQANAIAQtAAAgAUGDz4CAAGotAABHDcgBIAFBBEYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGEASEQDOACCyAAQQA2AgAgEEEBaiEBQSMhEAzFAQsCQCABIgQgAkcNAEGFASEQDN8CCwJAAkAgBC0AAEG0f2oOCADIAcgByAHIAcgByAEByAELIARBAWohAUHvACEQDMYCCyAEQQFqIQFB8AAhEAzFAgsCQCABIgQgAkcNAEGGASEQDN4CCyAELQAAQcUARw3FASAEQQFqIQEMgwILAkAgASIEIAJHDQBBhwEhEAzdAgsgAiAEayAAKAIAIgFqIRQgBCABa0EDaiEQAkADQCAELQAAIAFBiM+AgABqLQAARw3FASABQQNGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBhwEhEAzdAgsgAEEANgIAIBBBAWohAUEtIRAMwgELAkAgASIEIAJHDQBBiAEhEAzcAgsgAiAEayAAKAIAIgFqIRQgBCABa0EIaiEQAkADQCAELQAAIAFB0M+AgABqLQAARw3EASABQQhGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBiAEhEAzcAgsgAEEANgIAIBBBAWohAUEpIRAMwQELAkAgASIBIAJHDQBBiQEhEAzbAgtBASEQIAEtAABB3wBHDcABIAFBAWohAQyBAgsCQCABIgQgAkcNAEGKASEQDNoCCyACIARrIAAoAgAiAWohFCAEIAFrQQFqIRADQCAELQAAIAFBjM+AgABqLQAARw3BASABQQFGDa8CIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQYoBIRAM2QILAkAgASIEIAJHDQBBiwEhEAzZAgsgAiAEayAAKAIAIgFqIRQgBCABa0ECaiEQAkADQCAELQAAIAFBjs+AgABqLQAARw3BASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBiwEhEAzZAgsgAEEANgIAIBBBAWohAUECIRAMvgELAkAgASIEIAJHDQBBjAEhEAzYAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFB8M+AgABqLQAARw3AASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBjAEhEAzYAgsgAEEANgIAIBBBAWohAUEfIRAMvQELAkAgASIEIAJHDQBBjQEhEAzXAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFB8s+AgABqLQAARw2/ASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBjQEhEAzXAgsgAEEANgIAIBBBAWohAUEJIRAMvAELAkAgASIEIAJHDQBBjgEhEAzWAgsCQAJAIAQtAABBt39qDgcAvwG/Ab8BvwG/AQG/AQsgBEEBaiEBQfgAIRAMvQILIARBAWohAUH5ACEQDLwCCwJAIAEiBCACRw0AQY8BIRAM1QILIAIgBGsgACgCACIBaiEUIAQgAWtBBWohEAJAA0AgBC0AACABQZHPgIAAai0AAEcNvQEgAUEFRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQY8BIRAM1QILIABBADYCACAQQQFqIQFBGCEQDLoBCwJAIAEiBCACRw0AQZABIRAM1AILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQZfPgIAAai0AAEcNvAEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZABIRAM1AILIABBADYCACAQQQFqIQFBFyEQDLkBCwJAIAEiBCACRw0AQZEBIRAM0wILIAIgBGsgACgCACIBaiEUIAQgAWtBBmohEAJAA0AgBC0AACABQZrPgIAAai0AAEcNuwEgAUEGRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZEBIRAM0wILIABBADYCACAQQQFqIQFBFSEQDLgBCwJAIAEiBCACRw0AQZIBIRAM0gILIAIgBGsgACgCACIBaiEUIAQgAWtBBWohEAJAA0AgBC0AACABQaHPgIAAai0AAEcNugEgAUEFRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZIBIRAM0gILIABBADYCACAQQQFqIQFBHiEQDLcBCwJAIAEiBCACRw0AQZMBIRAM0QILIAQtAABBzABHDbgBIARBAWohAUEKIRAMtgELAkAgBCACRw0AQZQBIRAM0AILAkACQCAELQAAQb9/ag4PALkBuQG5AbkBuQG5AbkBuQG5AbkBuQG5AbkBAbkBCyAEQQFqIQFB/gAhEAy3AgsgBEEBaiEBQf8AIRAMtgILAkAgBCACRw0AQZUBIRAMzwILAkACQCAELQAAQb9/ag4DALgBAbgBCyAEQQFqIQFB/QAhEAy2AgsgBEEBaiEEQYABIRAMtQILAkAgBCACRw0AQZYBIRAMzgILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQafPgIAAai0AAEcNtgEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZYBIRAMzgILIABBADYCACAQQQFqIQFBCyEQDLMBCwJAIAQgAkcNAEGXASEQDM0CCwJAAkACQAJAIAQtAABBU2oOIwC4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBuAG4AbgBAbgBuAG4AbgBuAECuAG4AbgBA7gBCyAEQQFqIQFB+wAhEAy2AgsgBEEBaiEBQfwAIRAMtQILIARBAWohBEGBASEQDLQCCyAEQQFqIQRBggEhEAyzAgsCQCAEIAJHDQBBmAEhEAzMAgsgAiAEayAAKAIAIgFqIRQgBCABa0EEaiEQAkADQCAELQAAIAFBqc+AgABqLQAARw20ASABQQRGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBmAEhEAzMAgsgAEEANgIAIBBBAWohAUEZIRAMsQELAkAgBCACRw0AQZkBIRAMywILIAIgBGsgACgCACIBaiEUIAQgAWtBBWohEAJAA0AgBC0AACABQa7PgIAAai0AAEcNswEgAUEFRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZkBIRAMywILIABBADYCACAQQQFqIQFBBiEQDLABCwJAIAQgAkcNAEGaASEQDMoCCyACIARrIAAoAgAiAWohFCAEIAFrQQFqIRACQANAIAQtAAAgAUG0z4CAAGotAABHDbIBIAFBAUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGaASEQDMoCCyAAQQA2AgAgEEEBaiEBQRwhEAyvAQsCQCAEIAJHDQBBmwEhEAzJAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFBts+AgABqLQAARw2xASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBmwEhEAzJAgsgAEEANgIAIBBBAWohAUEnIRAMrgELAkAgBCACRw0AQZwBIRAMyAILAkACQCAELQAAQax/ag4CAAGxAQsgBEEBaiEEQYYBIRAMrwILIARBAWohBEGHASEQDK4CCwJAIAQgAkcNAEGdASEQDMcCCyACIARrIAAoAgAiAWohFCAEIAFrQQFqIRACQANAIAQtAAAgAUG4z4CAAGotAABHDa8BIAFBAUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGdASEQDMcCCyAAQQA2AgAgEEEBaiEBQSYhEAysAQsCQCAEIAJHDQBBngEhEAzGAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFBus+AgABqLQAARw2uASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBngEhEAzGAgsgAEEANgIAIBBBAWohAUEDIRAMqwELAkAgBCACRw0AQZ8BIRAMxQILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQe3PgIAAai0AAEcNrQEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQZ8BIRAMxQILIABBADYCACAQQQFqIQFBDCEQDKoBCwJAIAQgAkcNAEGgASEQDMQCCyACIARrIAAoAgAiAWohFCAEIAFrQQNqIRACQANAIAQtAAAgAUG8z4CAAGotAABHDawBIAFBA0YNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGgASEQDMQCCyAAQQA2AgAgEEEBaiEBQQ0hEAypAQsCQCAEIAJHDQBBoQEhEAzDAgsCQAJAIAQtAABBun9qDgsArAGsAawBrAGsAawBrAGsAawBAawBCyAEQQFqIQRBiwEhEAyqAgsgBEEBaiEEQYwBIRAMqQILAkAgBCACRw0AQaIBIRAMwgILIAQtAABB0ABHDakBIARBAWohBAzpAQsCQCAEIAJHDQBBowEhEAzBAgsCQAJAIAQtAABBt39qDgcBqgGqAaoBqgGqAQCqAQsgBEEBaiEEQY4BIRAMqAILIARBAWohAUEiIRAMpgELAkAgBCACRw0AQaQBIRAMwAILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQcDPgIAAai0AAEcNqAEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQaQBIRAMwAILIABBADYCACAQQQFqIQFBHSEQDKUBCwJAIAQgAkcNAEGlASEQDL8CCwJAAkAgBC0AAEGuf2oOAwCoAQGoAQsgBEEBaiEEQZABIRAMpgILIARBAWohAUEEIRAMpAELAkAgBCACRw0AQaYBIRAMvgILAkACQAJAAkACQCAELQAAQb9/ag4VAKoBqgGqAaoBqgGqAaoBqgGqAaoBAaoBqgECqgGqAQOqAaoBBKoBCyAEQQFqIQRBiAEhEAyoAgsgBEEBaiEEQYkBIRAMpwILIARBAWohBEGKASEQDKYCCyAEQQFqIQRBjwEhEAylAgsgBEEBaiEEQZEBIRAMpAILAkAgBCACRw0AQacBIRAMvQILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQe3PgIAAai0AAEcNpQEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQacBIRAMvQILIABBADYCACAQQQFqIQFBESEQDKIBCwJAIAQgAkcNAEGoASEQDLwCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHCz4CAAGotAABHDaQBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGoASEQDLwCCyAAQQA2AgAgEEEBaiEBQSwhEAyhAQsCQCAEIAJHDQBBqQEhEAy7AgsgAiAEayAAKAIAIgFqIRQgBCABa0EEaiEQAkADQCAELQAAIAFBxc+AgABqLQAARw2jASABQQRGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBqQEhEAy7AgsgAEEANgIAIBBBAWohAUErIRAMoAELAkAgBCACRw0AQaoBIRAMugILIAIgBGsgACgCACIBaiEUIAQgAWtBAmohEAJAA0AgBC0AACABQcrPgIAAai0AAEcNogEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQaoBIRAMugILIABBADYCACAQQQFqIQFBFCEQDJ8BCwJAIAQgAkcNAEGrASEQDLkCCwJAAkACQAJAIAQtAABBvn9qDg8AAQKkAaQBpAGkAaQBpAGkAaQBpAGkAaQBA6QBCyAEQQFqIQRBkwEhEAyiAgsgBEEBaiEEQZQBIRAMoQILIARBAWohBEGVASEQDKACCyAEQQFqIQRBlgEhEAyfAgsCQCAEIAJHDQBBrAEhEAy4AgsgBC0AAEHFAEcNnwEgBEEBaiEEDOABCwJAIAQgAkcNAEGtASEQDLcCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHNz4CAAGotAABHDZ8BIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEGtASEQDLcCCyAAQQA2AgAgEEEBaiEBQQ4hEAycAQsCQCAEIAJHDQBBrgEhEAy2AgsgBC0AAEHQAEcNnQEgBEEBaiEBQSUhEAybAQsCQCAEIAJHDQBBrwEhEAy1AgsgAiAEayAAKAIAIgFqIRQgBCABa0EIaiEQAkADQCAELQAAIAFB0M+AgABqLQAARw2dASABQQhGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBrwEhEAy1AgsgAEEANgIAIBBBAWohAUEqIRAMmgELAkAgBCACRw0AQbABIRAMtAILAkACQCAELQAAQat/ag4LAJ0BnQGdAZ0BnQGdAZ0BnQGdAQGdAQsgBEEBaiEEQZoBIRAMmwILIARBAWohBEGbASEQDJoCCwJAIAQgAkcNAEGxASEQDLMCCwJAAkAgBC0AAEG/f2oOFACcAZwBnAGcAZwBnAGcAZwBnAGcAZwBnAGcAZwBnAGcAZwBnAEBnAELIARBAWohBEGZASEQDJoCCyAEQQFqIQRBnAEhEAyZAgsCQCAEIAJHDQBBsgEhEAyyAgsgAiAEayAAKAIAIgFqIRQgBCABa0EDaiEQAkADQCAELQAAIAFB2c+AgABqLQAARw2aASABQQNGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBsgEhEAyyAgsgAEEANgIAIBBBAWohAUEhIRAMlwELAkAgBCACRw0AQbMBIRAMsQILIAIgBGsgACgCACIBaiEUIAQgAWtBBmohEAJAA0AgBC0AACABQd3PgIAAai0AAEcNmQEgAUEGRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQbMBIRAMsQILIABBADYCACAQQQFqIQFBGiEQDJYBCwJAIAQgAkcNAEG0ASEQDLACCwJAAkACQCAELQAAQbt/ag4RAJoBmgGaAZoBmgGaAZoBmgGaAQGaAZoBmgGaAZoBApoBCyAEQQFqIQRBnQEhEAyYAgsgBEEBaiEEQZ4BIRAMlwILIARBAWohBEGfASEQDJYCCwJAIAQgAkcNAEG1ASEQDK8CCyACIARrIAAoAgAiAWohFCAEIAFrQQVqIRACQANAIAQtAAAgAUHkz4CAAGotAABHDZcBIAFBBUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEG1ASEQDK8CCyAAQQA2AgAgEEEBaiEBQSghEAyUAQsCQCAEIAJHDQBBtgEhEAyuAgsgAiAEayAAKAIAIgFqIRQgBCABa0ECaiEQAkADQCAELQAAIAFB6s+AgABqLQAARw2WASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBtgEhEAyuAgsgAEEANgIAIBBBAWohAUEHIRAMkwELAkAgBCACRw0AQbcBIRAMrQILAkACQCAELQAAQbt/ag4OAJYBlgGWAZYBlgGWAZYBlgGWAZYBlgGWAQGWAQsgBEEBaiEEQaEBIRAMlAILIARBAWohBEGiASEQDJMCCwJAIAQgAkcNAEG4ASEQDKwCCyACIARrIAAoAgAiAWohFCAEIAFrQQJqIRACQANAIAQtAAAgAUHtz4CAAGotAABHDZQBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgFDYCAEG4ASEQDKwCCyAAQQA2AgAgEEEBaiEBQRIhEAyRAQsCQCAEIAJHDQBBuQEhEAyrAgsgAiAEayAAKAIAIgFqIRQgBCABa0EBaiEQAkADQCAELQAAIAFB8M+AgABqLQAARw2TASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIBQ2AgBBuQEhEAyrAgsgAEEANgIAIBBBAWohAUEgIRAMkAELAkAgBCACRw0AQboBIRAMqgILIAIgBGsgACgCACIBaiEUIAQgAWtBAWohEAJAA0AgBC0AACABQfLPgIAAai0AAEcNkgEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQboBIRAMqgILIABBADYCACAQQQFqIQFBDyEQDI8BCwJAIAQgAkcNAEG7ASEQDKkCCwJAAkAgBC0AAEG3f2oOBwCSAZIBkgGSAZIBAZIBCyAEQQFqIQRBpQEhEAyQAgsgBEEBaiEEQaYBIRAMjwILAkAgBCACRw0AQbwBIRAMqAILIAIgBGsgACgCACIBaiEUIAQgAWtBB2ohEAJAA0AgBC0AACABQfTPgIAAai0AAEcNkAEgAUEHRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAUNgIAQbwBIRAMqAILIABBADYCACAQQQFqIQFBGyEQDI0BCwJAIAQgAkcNAEG9ASEQDKcCCwJAAkACQCAELQAAQb5/ag4SAJEBkQGRAZEBkQGRAZEBkQGRAQGRAZEBkQGRAZEBkQECkQELIARBAWohBEGkASEQDI8CCyAEQQFqIQRBpwEhEAyOAgsgBEEBaiEEQagBIRAMjQILAkAgBCACRw0AQb4BIRAMpgILIAQtAABBzgBHDY0BIARBAWohBAzPAQsCQCAEIAJHDQBBvwEhEAylAgsCQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAELQAAQb9/ag4VAAECA5wBBAUGnAGcAZwBBwgJCgucAQwNDg+cAQsgBEEBaiEBQegAIRAMmgILIARBAWohAUHpACEQDJkCCyAEQQFqIQFB7gAhEAyYAgsgBEEBaiEBQfIAIRAMlwILIARBAWohAUHzACEQDJYCCyAEQQFqIQFB9gAhEAyVAgsgBEEBaiEBQfcAIRAMlAILIARBAWohAUH6ACEQDJMCCyAEQQFqIQRBgwEhEAySAgsgBEEBaiEEQYQBIRAMkQILIARBAWohBEGFASEQDJACCyAEQQFqIQRBkgEhEAyPAgsgBEEBaiEEQZgBIRAMjgILIARBAWohBEGgASEQDI0CCyAEQQFqIQRBowEhEAyMAgsgBEEBaiEEQaoBIRAMiwILAkAgBCACRg0AIABBkICAgAA2AgggACAENgIEQasBIRAMiwILQcABIRAMowILIAAgBSACEKqAgIAAIgENiwEgBSEBDFwLAkAgBiACRg0AIAZBAWohBQyNAQtBwgEhEAyhAgsDQAJAIBAtAABBdmoOBIwBAACPAQALIBBBAWoiECACRw0AC0HDASEQDKACCwJAIAcgAkYNACAAQZGAgIAANgIIIAAgBzYCBCAHIQFBASEQDIcCC0HEASEQDJ8CCwJAIAcgAkcNAEHFASEQDJ8CCwJAAkAgBy0AAEF2ag4EAc4BzgEAzgELIAdBAWohBgyNAQsgB0EBaiEFDIkBCwJAIAcgAkcNAEHGASEQDJ4CCwJAAkAgBy0AAEF2ag4XAY8BjwEBjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BAI8BCyAHQQFqIQcLQbABIRAMhAILAkAgCCACRw0AQcgBIRAMnQILIAgtAABBIEcNjQEgAEEAOwEyIAhBAWohAUGzASEQDIMCCyABIRcCQANAIBciByACRg0BIActAABBUGpB/wFxIhBBCk8NzAECQCAALwEyIhRBmTNLDQAgACAUQQpsIhQ7ATIgEEH//wNzIBRB/v8DcUkNACAHQQFqIRcgACAUIBBqIhA7ATIgEEH//wNxQegHSQ0BCwtBACEQIABBADYCHCAAQcGJgIAANgIQIABBDTYCDCAAIAdBAWo2AhQMnAILQccBIRAMmwILIAAgCCACEK6AgIAAIhBFDcoBIBBBFUcNjAEgAEHIATYCHCAAIAg2AhQgAEHJl4CAADYCECAAQRU2AgxBACEQDJoCCwJAIAkgAkcNAEHMASEQDJoCC0EAIRRBASEXQQEhFkEAIRACQAJAAkACQAJAAkACQAJAAkAgCS0AAEFQag4KlgGVAQABAgMEBQYIlwELQQIhEAwGC0EDIRAMBQtBBCEQDAQLQQUhEAwDC0EGIRAMAgtBByEQDAELQQghEAtBACEXQQAhFkEAIRQMjgELQQkhEEEBIRRBACEXQQAhFgyNAQsCQCAKIAJHDQBBzgEhEAyZAgsgCi0AAEEuRw2OASAKQQFqIQkMygELIAsgAkcNjgFB0AEhEAyXAgsCQCALIAJGDQAgAEGOgICAADYCCCAAIAs2AgRBtwEhEAz+AQtB0QEhEAyWAgsCQCAEIAJHDQBB0gEhEAyWAgsgAiAEayAAKAIAIhBqIRQgBCAQa0EEaiELA0AgBC0AACAQQfzPgIAAai0AAEcNjgEgEEEERg3pASAQQQFqIRAgBEEBaiIEIAJHDQALIAAgFDYCAEHSASEQDJUCCyAAIAwgAhCsgICAACIBDY0BIAwhAQy4AQsCQCAEIAJHDQBB1AEhEAyUAgsgAiAEayAAKAIAIhBqIRQgBCAQa0EBaiEMA0AgBC0AACAQQYHQgIAAai0AAEcNjwEgEEEBRg2OASAQQQFqIRAgBEEBaiIEIAJHDQALIAAgFDYCAEHUASEQDJMCCwJAIAQgAkcNAEHWASEQDJMCCyACIARrIAAoAgAiEGohFCAEIBBrQQJqIQsDQCAELQAAIBBBg9CAgABqLQAARw2OASAQQQJGDZABIBBBAWohECAEQQFqIgQgAkcNAAsgACAUNgIAQdYBIRAMkgILAkAgBCACRw0AQdcBIRAMkgILAkACQCAELQAAQbt/ag4QAI8BjwGPAY8BjwGPAY8BjwGPAY8BjwGPAY8BjwEBjwELIARBAWohBEG7ASEQDPkBCyAEQQFqIQRBvAEhEAz4AQsCQCAEIAJHDQBB2AEhEAyRAgsgBC0AAEHIAEcNjAEgBEEBaiEEDMQBCwJAIAQgAkYNACAAQZCAgIAANgIIIAAgBDYCBEG+ASEQDPcBC0HZASEQDI8CCwJAIAQgAkcNAEHaASEQDI8CCyAELQAAQcgARg3DASAAQQE6ACgMuQELIABBAjoALyAAIAQgAhCmgICAACIQDY0BQcIBIRAM9AELIAAtAChBf2oOArcBuQG4AQsDQAJAIAQtAABBdmoOBACOAY4BAI4BCyAEQQFqIgQgAkcNAAtB3QEhEAyLAgsgAEEAOgAvIAAtAC1BBHFFDYQCCyAAQQA6AC8gAEEBOgA0IAEhAQyMAQsgEEEVRg3aASAAQQA2AhwgACABNgIUIABBp46AgAA2AhAgAEESNgIMQQAhEAyIAgsCQCAAIBAgAhC0gICAACIEDQAgECEBDIECCwJAIARBFUcNACAAQQM2AhwgACAQNgIUIABBsJiAgAA2AhAgAEEVNgIMQQAhEAyIAgsgAEEANgIcIAAgEDYCFCAAQaeOgIAANgIQIABBEjYCDEEAIRAMhwILIBBBFUYN1gEgAEEANgIcIAAgATYCFCAAQdqNgIAANgIQIABBFDYCDEEAIRAMhgILIAAoAgQhFyAAQQA2AgQgECARp2oiFiEBIAAgFyAQIBYgFBsiEBC1gICAACIURQ2NASAAQQc2AhwgACAQNgIUIAAgFDYCDEEAIRAMhQILIAAgAC8BMEGAAXI7ATAgASEBC0EqIRAM6gELIBBBFUYN0QEgAEEANgIcIAAgATYCFCAAQYOMgIAANgIQIABBEzYCDEEAIRAMggILIBBBFUYNzwEgAEEANgIcIAAgATYCFCAAQZqPgIAANgIQIABBIjYCDEEAIRAMgQILIAAoAgQhECAAQQA2AgQCQCAAIBAgARC3gICAACIQDQAgAUEBaiEBDI0BCyAAQQw2AhwgACAQNgIMIAAgAUEBajYCFEEAIRAMgAILIBBBFUYNzAEgAEEANgIcIAAgATYCFCAAQZqPgIAANgIQIABBIjYCDEEAIRAM/wELIAAoAgQhECAAQQA2AgQCQCAAIBAgARC3gICAACIQDQAgAUEBaiEBDIwBCyAAQQ02AhwgACAQNgIMIAAgAUEBajYCFEEAIRAM/gELIBBBFUYNyQEgAEEANgIcIAAgATYCFCAAQcaMgIAANgIQIABBIzYCDEEAIRAM/QELIAAoAgQhECAAQQA2AgQCQCAAIBAgARC5gICAACIQDQAgAUEBaiEBDIsBCyAAQQ42AhwgACAQNgIMIAAgAUEBajYCFEEAIRAM/AELIABBADYCHCAAIAE2AhQgAEHAlYCAADYCECAAQQI2AgxBACEQDPsBCyAQQRVGDcUBIABBADYCHCAAIAE2AhQgAEHGjICAADYCECAAQSM2AgxBACEQDPoBCyAAQRA2AhwgACABNgIUIAAgEDYCDEEAIRAM+QELIAAoAgQhBCAAQQA2AgQCQCAAIAQgARC5gICAACIEDQAgAUEBaiEBDPEBCyAAQRE2AhwgACAENgIMIAAgAUEBajYCFEEAIRAM+AELIBBBFUYNwQEgAEEANgIcIAAgATYCFCAAQcaMgIAANgIQIABBIzYCDEEAIRAM9wELIAAoAgQhECAAQQA2AgQCQCAAIBAgARC5gICAACIQDQAgAUEBaiEBDIgBCyAAQRM2AhwgACAQNgIMIAAgAUEBajYCFEEAIRAM9gELIAAoAgQhBCAAQQA2AgQCQCAAIAQgARC5gICAACIEDQAgAUEBaiEBDO0BCyAAQRQ2AhwgACAENgIMIAAgAUEBajYCFEEAIRAM9QELIBBBFUYNvQEgAEEANgIcIAAgATYCFCAAQZqPgIAANgIQIABBIjYCDEEAIRAM9AELIAAoAgQhECAAQQA2AgQCQCAAIBAgARC3gICAACIQDQAgAUEBaiEBDIYBCyAAQRY2AhwgACAQNgIMIAAgAUEBajYCFEEAIRAM8wELIAAoAgQhBCAAQQA2AgQCQCAAIAQgARC3gICAACIEDQAgAUEBaiEBDOkBCyAAQRc2AhwgACAENgIMIAAgAUEBajYCFEEAIRAM8gELIABBADYCHCAAIAE2AhQgAEHNk4CAADYCECAAQQw2AgxBACEQDPEBC0IBIRELIBBBAWohAQJAIAApAyAiEkL//////////w9WDQAgACASQgSGIBGENwMgIAEhAQyEAQsgAEEANgIcIAAgATYCFCAAQa2JgIAANgIQIABBDDYCDEEAIRAM7wELIABBADYCHCAAIBA2AhQgAEHNk4CAADYCECAAQQw2AgxBACEQDO4BCyAAKAIEIRcgAEEANgIEIBAgEadqIhYhASAAIBcgECAWIBQbIhAQtYCAgAAiFEUNcyAAQQU2AhwgACAQNgIUIAAgFDYCDEEAIRAM7QELIABBADYCHCAAIBA2AhQgAEGqnICAADYCECAAQQ82AgxBACEQDOwBCyAAIBAgAhC0gICAACIBDQEgECEBC0EOIRAM0QELAkAgAUEVRw0AIABBAjYCHCAAIBA2AhQgAEGwmICAADYCECAAQRU2AgxBACEQDOoBCyAAQQA2AhwgACAQNgIUIABBp46AgAA2AhAgAEESNgIMQQAhEAzpAQsgAUEBaiEQAkAgAC8BMCIBQYABcUUNAAJAIAAgECACELuAgIAAIgENACAQIQEMcAsgAUEVRw26ASAAQQU2AhwgACAQNgIUIABB+ZeAgAA2AhAgAEEVNgIMQQAhEAzpAQsCQCABQaAEcUGgBEcNACAALQAtQQJxDQAgAEEANgIcIAAgEDYCFCAAQZaTgIAANgIQIABBBDYCDEEAIRAM6QELIAAgECACEL2AgIAAGiAQIQECQAJAAkACQAJAIAAgECACELOAgIAADhYCAQAEBAQEBAQEBAQEBAQEBAQEBAQDBAsgAEEBOgAuCyAAIAAvATBBwAByOwEwIBAhAQtBJiEQDNEBCyAAQSM2AhwgACAQNgIUIABBpZaAgAA2AhAgAEEVNgIMQQAhEAzpAQsgAEEANgIcIAAgEDYCFCAAQdWLgIAANgIQIABBETYCDEEAIRAM6AELIAAtAC1BAXFFDQFBwwEhEAzOAQsCQCANIAJGDQADQAJAIA0tAABBIEYNACANIQEMxAELIA1BAWoiDSACRw0AC0ElIRAM5wELQSUhEAzmAQsgACgCBCEEIABBADYCBCAAIAQgDRCvgICAACIERQ2tASAAQSY2AhwgACAENgIMIAAgDUEBajYCFEEAIRAM5QELIBBBFUYNqwEgAEEANgIcIAAgATYCFCAAQf2NgIAANgIQIABBHTYCDEEAIRAM5AELIABBJzYCHCAAIAE2AhQgACAQNgIMQQAhEAzjAQsgECEBQQEhFAJAAkACQAJAAkACQAJAIAAtACxBfmoOBwYFBQMBAgAFCyAAIAAvATBBCHI7ATAMAwtBAiEUDAELQQQhFAsgAEEBOgAsIAAgAC8BMCAUcjsBMAsgECEBC0ErIRAMygELIABBADYCHCAAIBA2AhQgAEGrkoCAADYCECAAQQs2AgxBACEQDOIBCyAAQQA2AhwgACABNgIUIABB4Y+AgAA2AhAgAEEKNgIMQQAhEAzhAQsgAEEAOgAsIBAhAQy9AQsgECEBQQEhFAJAAkACQAJAAkAgAC0ALEF7ag4EAwECAAULIAAgAC8BMEEIcjsBMAwDC0ECIRQMAQtBBCEUCyAAQQE6ACwgACAALwEwIBRyOwEwCyAQIQELQSkhEAzFAQsgAEEANgIcIAAgATYCFCAAQfCUgIAANgIQIABBAzYCDEEAIRAM3QELAkAgDi0AAEENRw0AIAAoAgQhASAAQQA2AgQCQCAAIAEgDhCxgICAACIBDQAgDkEBaiEBDHULIABBLDYCHCAAIAE2AgwgACAOQQFqNgIUQQAhEAzdAQsgAC0ALUEBcUUNAUHEASEQDMMBCwJAIA4gAkcNAEEtIRAM3AELAkACQANAAkAgDi0AAEF2ag4EAgAAAwALIA5BAWoiDiACRw0AC0EtIRAM3QELIAAoAgQhASAAQQA2AgQCQCAAIAEgDhCxgICAACIBDQAgDiEBDHQLIABBLDYCHCAAIA42AhQgACABNgIMQQAhEAzcAQsgACgCBCEBIABBADYCBAJAIAAgASAOELGAgIAAIgENACAOQQFqIQEMcwsgAEEsNgIcIAAgATYCDCAAIA5BAWo2AhRBACEQDNsBCyAAKAIEIQQgAEEANgIEIAAgBCAOELGAgIAAIgQNoAEgDiEBDM4BCyAQQSxHDQEgAUEBaiEQQQEhAQJAAkACQAJAAkAgAC0ALEF7ag4EAwECBAALIBAhAQwEC0ECIQEMAQtBBCEBCyAAQQE6ACwgACAALwEwIAFyOwEwIBAhAQwBCyAAIAAvATBBCHI7ATAgECEBC0E5IRAMvwELIABBADoALCABIQELQTQhEAy9AQsgACAALwEwQSByOwEwIAEhAQwCCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQsYCAgAAiBA0AIAEhAQzHAQsgAEE3NgIcIAAgATYCFCAAIAQ2AgxBACEQDNQBCyAAQQg6ACwgASEBC0EwIRAMuQELAkAgAC0AKEEBRg0AIAEhAQwECyAALQAtQQhxRQ2TASABIQEMAwsgAC0AMEEgcQ2UAUHFASEQDLcBCwJAIA8gAkYNAAJAA0ACQCAPLQAAQVBqIgFB/wFxQQpJDQAgDyEBQTUhEAy6AQsgACkDICIRQpmz5syZs+bMGVYNASAAIBFCCn4iETcDICARIAGtQv8BgyISQn+FVg0BIAAgESASfDcDICAPQQFqIg8gAkcNAAtBOSEQDNEBCyAAKAIEIQIgAEEANgIEIAAgAiAPQQFqIgQQsYCAgAAiAg2VASAEIQEMwwELQTkhEAzPAQsCQCAALwEwIgFBCHFFDQAgAC0AKEEBRw0AIAAtAC1BCHFFDZABCyAAIAFB9/sDcUGABHI7ATAgDyEBC0E3IRAMtAELIAAgAC8BMEEQcjsBMAyrAQsgEEEVRg2LASAAQQA2AhwgACABNgIUIABB8I6AgAA2AhAgAEEcNgIMQQAhEAzLAQsgAEHDADYCHCAAIAE2AgwgACANQQFqNgIUQQAhEAzKAQsCQCABLQAAQTpHDQAgACgCBCEQIABBADYCBAJAIAAgECABEK+AgIAAIhANACABQQFqIQEMYwsgAEHDADYCHCAAIBA2AgwgACABQQFqNgIUQQAhEAzKAQsgAEEANgIcIAAgATYCFCAAQbGRgIAANgIQIABBCjYCDEEAIRAMyQELIABBADYCHCAAIAE2AhQgAEGgmYCAADYCECAAQR42AgxBACEQDMgBCyAAQQA2AgALIABBgBI7ASogACAXQQFqIgEgAhCogICAACIQDQEgASEBC0HHACEQDKwBCyAQQRVHDYMBIABB0QA2AhwgACABNgIUIABB45eAgAA2AhAgAEEVNgIMQQAhEAzEAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMXgsgAEHSADYCHCAAIAE2AhQgACAQNgIMQQAhEAzDAQsgAEEANgIcIAAgFDYCFCAAQcGogIAANgIQIABBBzYCDCAAQQA2AgBBACEQDMIBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxdCyAAQdMANgIcIAAgATYCFCAAIBA2AgxBACEQDMEBC0EAIRAgAEEANgIcIAAgATYCFCAAQYCRgIAANgIQIABBCTYCDAzAAQsgEEEVRg19IABBADYCHCAAIAE2AhQgAEGUjYCAADYCECAAQSE2AgxBACEQDL8BC0EBIRZBACEXQQAhFEEBIRALIAAgEDoAKyABQQFqIQECQAJAIAAtAC1BEHENAAJAAkACQCAALQAqDgMBAAIECyAWRQ0DDAILIBQNAQwCCyAXRQ0BCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQrYCAgAAiEA0AIAEhAQxcCyAAQdgANgIcIAAgATYCFCAAIBA2AgxBACEQDL4BCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQrYCAgAAiBA0AIAEhAQytAQsgAEHZADYCHCAAIAE2AhQgACAENgIMQQAhEAy9AQsgACgCBCEEIABBADYCBAJAIAAgBCABEK2AgIAAIgQNACABIQEMqwELIABB2gA2AhwgACABNgIUIAAgBDYCDEEAIRAMvAELIAAoAgQhBCAAQQA2AgQCQCAAIAQgARCtgICAACIEDQAgASEBDKkBCyAAQdwANgIcIAAgATYCFCAAIAQ2AgxBACEQDLsBCwJAIAEtAABBUGoiEEH/AXFBCk8NACAAIBA6ACogAUEBaiEBQc8AIRAMogELIAAoAgQhBCAAQQA2AgQCQCAAIAQgARCtgICAACIEDQAgASEBDKcBCyAAQd4ANgIcIAAgATYCFCAAIAQ2AgxBACEQDLoBCyAAQQA2AgAgF0EBaiEBAkAgAC0AKUEjTw0AIAEhAQxZCyAAQQA2AhwgACABNgIUIABB04mAgAA2AhAgAEEINgIMQQAhEAy5AQsgAEEANgIAC0EAIRAgAEEANgIcIAAgATYCFCAAQZCzgIAANgIQIABBCDYCDAy3AQsgAEEANgIAIBdBAWohAQJAIAAtAClBIUcNACABIQEMVgsgAEEANgIcIAAgATYCFCAAQZuKgIAANgIQIABBCDYCDEEAIRAMtgELIABBADYCACAXQQFqIQECQCAALQApIhBBXWpBC08NACABIQEMVQsCQCAQQQZLDQBBASAQdEHKAHFFDQAgASEBDFULQQAhECAAQQA2AhwgACABNgIUIABB94mAgAA2AhAgAEEINgIMDLUBCyAQQRVGDXEgAEEANgIcIAAgATYCFCAAQbmNgIAANgIQIABBGjYCDEEAIRAMtAELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDFQLIABB5QA2AhwgACABNgIUIAAgEDYCDEEAIRAMswELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDE0LIABB0gA2AhwgACABNgIUIAAgEDYCDEEAIRAMsgELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDE0LIABB0wA2AhwgACABNgIUIAAgEDYCDEEAIRAMsQELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDFELIABB5QA2AhwgACABNgIUIAAgEDYCDEEAIRAMsAELIABBADYCHCAAIAE2AhQgAEHGioCAADYCECAAQQc2AgxBACEQDK8BCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxJCyAAQdIANgIcIAAgATYCFCAAIBA2AgxBACEQDK4BCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxJCyAAQdMANgIcIAAgATYCFCAAIBA2AgxBACEQDK0BCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxNCyAAQeUANgIcIAAgATYCFCAAIBA2AgxBACEQDKwBCyAAQQA2AhwgACABNgIUIABB3IiAgAA2AhAgAEEHNgIMQQAhEAyrAQsgEEE/Rw0BIAFBAWohAQtBBSEQDJABC0EAIRAgAEEANgIcIAAgATYCFCAAQf2SgIAANgIQIABBBzYCDAyoAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMQgsgAEHSADYCHCAAIAE2AhQgACAQNgIMQQAhEAynAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMQgsgAEHTADYCHCAAIAE2AhQgACAQNgIMQQAhEAymAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMRgsgAEHlADYCHCAAIAE2AhQgACAQNgIMQQAhEAylAQsgACgCBCEBIABBADYCBAJAIAAgASAUEKeAgIAAIgENACAUIQEMPwsgAEHSADYCHCAAIBQ2AhQgACABNgIMQQAhEAykAQsgACgCBCEBIABBADYCBAJAIAAgASAUEKeAgIAAIgENACAUIQEMPwsgAEHTADYCHCAAIBQ2AhQgACABNgIMQQAhEAyjAQsgACgCBCEBIABBADYCBAJAIAAgASAUEKeAgIAAIgENACAUIQEMQwsgAEHlADYCHCAAIBQ2AhQgACABNgIMQQAhEAyiAQsgAEEANgIcIAAgFDYCFCAAQcOPgIAANgIQIABBBzYCDEEAIRAMoQELIABBADYCHCAAIAE2AhQgAEHDj4CAADYCECAAQQc2AgxBACEQDKABC0EAIRAgAEEANgIcIAAgFDYCFCAAQYycgIAANgIQIABBBzYCDAyfAQsgAEEANgIcIAAgFDYCFCAAQYycgIAANgIQIABBBzYCDEEAIRAMngELIABBADYCHCAAIBQ2AhQgAEH+kYCAADYCECAAQQc2AgxBACEQDJ0BCyAAQQA2AhwgACABNgIUIABBjpuAgAA2AhAgAEEGNgIMQQAhEAycAQsgEEEVRg1XIABBADYCHCAAIAE2AhQgAEHMjoCAADYCECAAQSA2AgxBACEQDJsBCyAAQQA2AgAgEEEBaiEBQSQhEAsgACAQOgApIAAoAgQhECAAQQA2AgQgACAQIAEQq4CAgAAiEA1UIAEhAQw+CyAAQQA2AgALQQAhECAAQQA2AhwgACAENgIUIABB8ZuAgAA2AhAgAEEGNgIMDJcBCyABQRVGDVAgAEEANgIcIAAgBTYCFCAAQfCMgIAANgIQIABBGzYCDEEAIRAMlgELIAAoAgQhBSAAQQA2AgQgACAFIBAQqYCAgAAiBQ0BIBBBAWohBQtBrQEhEAx7CyAAQcEBNgIcIAAgBTYCDCAAIBBBAWo2AhRBACEQDJMBCyAAKAIEIQYgAEEANgIEIAAgBiAQEKmAgIAAIgYNASAQQQFqIQYLQa4BIRAMeAsgAEHCATYCHCAAIAY2AgwgACAQQQFqNgIUQQAhEAyQAQsgAEEANgIcIAAgBzYCFCAAQZeLgIAANgIQIABBDTYCDEEAIRAMjwELIABBADYCHCAAIAg2AhQgAEHjkICAADYCECAAQQk2AgxBACEQDI4BCyAAQQA2AhwgACAINgIUIABBlI2AgAA2AhAgAEEhNgIMQQAhEAyNAQtBASEWQQAhF0EAIRRBASEQCyAAIBA6ACsgCUEBaiEIAkACQCAALQAtQRBxDQACQAJAAkAgAC0AKg4DAQACBAsgFkUNAwwCCyAUDQEMAgsgF0UNAQsgACgCBCEQIABBADYCBCAAIBAgCBCtgICAACIQRQ09IABByQE2AhwgACAINgIUIAAgEDYCDEEAIRAMjAELIAAoAgQhBCAAQQA2AgQgACAEIAgQrYCAgAAiBEUNdiAAQcoBNgIcIAAgCDYCFCAAIAQ2AgxBACEQDIsBCyAAKAIEIQQgAEEANgIEIAAgBCAJEK2AgIAAIgRFDXQgAEHLATYCHCAAIAk2AhQgACAENgIMQQAhEAyKAQsgACgCBCEEIABBADYCBCAAIAQgChCtgICAACIERQ1yIABBzQE2AhwgACAKNgIUIAAgBDYCDEEAIRAMiQELAkAgCy0AAEFQaiIQQf8BcUEKTw0AIAAgEDoAKiALQQFqIQpBtgEhEAxwCyAAKAIEIQQgAEEANgIEIAAgBCALEK2AgIAAIgRFDXAgAEHPATYCHCAAIAs2AhQgACAENgIMQQAhEAyIAQsgAEEANgIcIAAgBDYCFCAAQZCzgIAANgIQIABBCDYCDCAAQQA2AgBBACEQDIcBCyABQRVGDT8gAEEANgIcIAAgDDYCFCAAQcyOgIAANgIQIABBIDYCDEEAIRAMhgELIABBgQQ7ASggACgCBCEQIABCADcDACAAIBAgDEEBaiIMEKuAgIAAIhBFDTggAEHTATYCHCAAIAw2AhQgACAQNgIMQQAhEAyFAQsgAEEANgIAC0EAIRAgAEEANgIcIAAgBDYCFCAAQdibgIAANgIQIABBCDYCDAyDAQsgACgCBCEQIABCADcDACAAIBAgC0EBaiILEKuAgIAAIhANAUHGASEQDGkLIABBAjoAKAxVCyAAQdUBNgIcIAAgCzYCFCAAIBA2AgxBACEQDIABCyAQQRVGDTcgAEEANgIcIAAgBDYCFCAAQaSMgIAANgIQIABBEDYCDEEAIRAMfwsgAC0ANEEBRw00IAAgBCACELyAgIAAIhBFDTQgEEEVRw01IABB3AE2AhwgACAENgIUIABB1ZaAgAA2AhAgAEEVNgIMQQAhEAx+C0EAIRAgAEEANgIcIABBr4uAgAA2AhAgAEECNgIMIAAgFEEBajYCFAx9C0EAIRAMYwtBAiEQDGILQQ0hEAxhC0EPIRAMYAtBJSEQDF8LQRMhEAxeC0EVIRAMXQtBFiEQDFwLQRchEAxbC0EYIRAMWgtBGSEQDFkLQRohEAxYC0EbIRAMVwtBHCEQDFYLQR0hEAxVC0EfIRAMVAtBISEQDFMLQSMhEAxSC0HGACEQDFELQS4hEAxQC0EvIRAMTwtBOyEQDE4LQT0hEAxNC0HIACEQDEwLQckAIRAMSwtBywAhEAxKC0HMACEQDEkLQc4AIRAMSAtB0QAhEAxHC0HVACEQDEYLQdgAIRAMRQtB2QAhEAxEC0HbACEQDEMLQeQAIRAMQgtB5QAhEAxBC0HxACEQDEALQfQAIRAMPwtBjQEhEAw+C0GXASEQDD0LQakBIRAMPAtBrAEhEAw7C0HAASEQDDoLQbkBIRAMOQtBrwEhEAw4C0GxASEQDDcLQbIBIRAMNgtBtAEhEAw1C0G1ASEQDDQLQboBIRAMMwtBvQEhEAwyC0G/ASEQDDELQcEBIRAMMAsgAEEANgIcIAAgBDYCFCAAQemLgIAANgIQIABBHzYCDEEAIRAMSAsgAEHbATYCHCAAIAQ2AhQgAEH6loCAADYCECAAQRU2AgxBACEQDEcLIABB+AA2AhwgACAMNgIUIABBypiAgAA2AhAgAEEVNgIMQQAhEAxGCyAAQdEANgIcIAAgBTYCFCAAQbCXgIAANgIQIABBFTYCDEEAIRAMRQsgAEH5ADYCHCAAIAE2AhQgACAQNgIMQQAhEAxECyAAQfgANgIcIAAgATYCFCAAQcqYgIAANgIQIABBFTYCDEEAIRAMQwsgAEHkADYCHCAAIAE2AhQgAEHjl4CAADYCECAAQRU2AgxBACEQDEILIABB1wA2AhwgACABNgIUIABByZeAgAA2AhAgAEEVNgIMQQAhEAxBCyAAQQA2AhwgACABNgIUIABBuY2AgAA2AhAgAEEaNgIMQQAhEAxACyAAQcIANgIcIAAgATYCFCAAQeOYgIAANgIQIABBFTYCDEEAIRAMPwsgAEEANgIEIAAgDyAPELGAgIAAIgRFDQEgAEE6NgIcIAAgBDYCDCAAIA9BAWo2AhRBACEQDD4LIAAoAgQhBCAAQQA2AgQCQCAAIAQgARCxgICAACIERQ0AIABBOzYCHCAAIAQ2AgwgACABQQFqNgIUQQAhEAw+CyABQQFqIQEMLQsgD0EBaiEBDC0LIABBADYCHCAAIA82AhQgAEHkkoCAADYCECAAQQQ2AgxBACEQDDsLIABBNjYCHCAAIAQ2AhQgACACNgIMQQAhEAw6CyAAQS42AhwgACAONgIUIAAgBDYCDEEAIRAMOQsgAEHQADYCHCAAIAE2AhQgAEGRmICAADYCECAAQRU2AgxBACEQDDgLIA1BAWohAQwsCyAAQRU2AhwgACABNgIUIABBgpmAgAA2AhAgAEEVNgIMQQAhEAw2CyAAQRs2AhwgACABNgIUIABBkZeAgAA2AhAgAEEVNgIMQQAhEAw1CyAAQQ82AhwgACABNgIUIABBkZeAgAA2AhAgAEEVNgIMQQAhEAw0CyAAQQs2AhwgACABNgIUIABBkZeAgAA2AhAgAEEVNgIMQQAhEAwzCyAAQRo2AhwgACABNgIUIABBgpmAgAA2AhAgAEEVNgIMQQAhEAwyCyAAQQs2AhwgACABNgIUIABBgpmAgAA2AhAgAEEVNgIMQQAhEAwxCyAAQQo2AhwgACABNgIUIABB5JaAgAA2AhAgAEEVNgIMQQAhEAwwCyAAQR42AhwgACABNgIUIABB+ZeAgAA2AhAgAEEVNgIMQQAhEAwvCyAAQQA2AhwgACAQNgIUIABB2o2AgAA2AhAgAEEUNgIMQQAhEAwuCyAAQQQ2AhwgACABNgIUIABBsJiAgAA2AhAgAEEVNgIMQQAhEAwtCyAAQQA2AgAgC0EBaiELC0G4ASEQDBILIABBADYCACAQQQFqIQFB9QAhEAwRCyABIQECQCAALQApQQVHDQBB4wAhEAwRC0HiACEQDBALQQAhECAAQQA2AhwgAEHkkYCAADYCECAAQQc2AgwgACAUQQFqNgIUDCgLIABBADYCACAXQQFqIQFBwAAhEAwOC0EBIQELIAAgAToALCAAQQA2AgAgF0EBaiEBC0EoIRAMCwsgASEBC0E4IRAMCQsCQCABIg8gAkYNAANAAkAgDy0AAEGAvoCAAGotAAAiAUEBRg0AIAFBAkcNAyAPQQFqIQEMBAsgD0EBaiIPIAJHDQALQT4hEAwiC0E+IRAMIQsgAEEAOgAsIA8hAQwBC0ELIRAMBgtBOiEQDAULIAFBAWohAUEtIRAMBAsgACABOgAsIABBADYCACAWQQFqIQFBDCEQDAMLIABBADYCACAXQQFqIQFBCiEQDAILIABBADYCAAsgAEEAOgAsIA0hAUEJIRAMAAsLQQAhECAAQQA2AhwgACALNgIUIABBzZCAgAA2AhAgAEEJNgIMDBcLQQAhECAAQQA2AhwgACAKNgIUIABB6YqAgAA2AhAgAEEJNgIMDBYLQQAhECAAQQA2AhwgACAJNgIUIABBt5CAgAA2AhAgAEEJNgIMDBULQQAhECAAQQA2AhwgACAINgIUIABBnJGAgAA2AhAgAEEJNgIMDBQLQQAhECAAQQA2AhwgACABNgIUIABBzZCAgAA2AhAgAEEJNgIMDBMLQQAhECAAQQA2AhwgACABNgIUIABB6YqAgAA2AhAgAEEJNgIMDBILQQAhECAAQQA2AhwgACABNgIUIABBt5CAgAA2AhAgAEEJNgIMDBELQQAhECAAQQA2AhwgACABNgIUIABBnJGAgAA2AhAgAEEJNgIMDBALQQAhECAAQQA2AhwgACABNgIUIABBl5WAgAA2AhAgAEEPNgIMDA8LQQAhECAAQQA2AhwgACABNgIUIABBl5WAgAA2AhAgAEEPNgIMDA4LQQAhECAAQQA2AhwgACABNgIUIABBwJKAgAA2AhAgAEELNgIMDA0LQQAhECAAQQA2AhwgACABNgIUIABBlYmAgAA2AhAgAEELNgIMDAwLQQAhECAAQQA2AhwgACABNgIUIABB4Y+AgAA2AhAgAEEKNgIMDAsLQQAhECAAQQA2AhwgACABNgIUIABB+4+AgAA2AhAgAEEKNgIMDAoLQQAhECAAQQA2AhwgACABNgIUIABB8ZmAgAA2AhAgAEECNgIMDAkLQQAhECAAQQA2AhwgACABNgIUIABBxJSAgAA2AhAgAEECNgIMDAgLQQAhECAAQQA2AhwgACABNgIUIABB8pWAgAA2AhAgAEECNgIMDAcLIABBAjYCHCAAIAE2AhQgAEGcmoCAADYCECAAQRY2AgxBACEQDAYLQQEhEAwFC0HUACEQIAEiBCACRg0EIANBCGogACAEIAJB2MKAgABBChDFgICAACADKAIMIQQgAygCCA4DAQQCAAsQyoCAgAAACyAAQQA2AhwgAEG1moCAADYCECAAQRc2AgwgACAEQQFqNgIUQQAhEAwCCyAAQQA2AhwgACAENgIUIABBypqAgAA2AhAgAEEJNgIMQQAhEAwBCwJAIAEiBCACRw0AQSIhEAwBCyAAQYmAgIAANgIIIAAgBDYCBEEhIRALIANBEGokgICAgAAgEAuvAQECfyABKAIAIQYCQAJAIAIgA0YNACAEIAZqIQQgBiADaiACayEHIAIgBkF/cyAFaiIGaiEFA0ACQCACLQAAIAQtAABGDQBBAiEEDAMLAkAgBg0AQQAhBCAFIQIMAwsgBkF/aiEGIARBAWohBCACQQFqIgIgA0cNAAsgByEGIAMhAgsgAEEBNgIAIAEgBjYCACAAIAI2AgQPCyABQQA2AgAgACAENgIAIAAgAjYCBAsKACAAEMeAgIAAC/I2AQt/I4CAgIAAQRBrIgEkgICAgAACQEEAKAKg0ICAAA0AQQAQy4CAgABBgNSEgABrIgJB2QBJDQBBACEDAkBBACgC4NOAgAAiBA0AQQBCfzcC7NOAgABBAEKAgISAgIDAADcC5NOAgABBACABQQhqQXBxQdiq1aoFcyIENgLg04CAAEEAQQA2AvTTgIAAQQBBADYCxNOAgAALQQAgAjYCzNOAgABBAEGA1ISAADYCyNOAgABBAEGA1ISAADYCmNCAgABBACAENgKs0ICAAEEAQX82AqjQgIAAA0AgA0HE0ICAAGogA0G40ICAAGoiBDYCACAEIANBsNCAgABqIgU2AgAgA0G80ICAAGogBTYCACADQczQgIAAaiADQcDQgIAAaiIFNgIAIAUgBDYCACADQdTQgIAAaiADQcjQgIAAaiIENgIAIAQgBTYCACADQdDQgIAAaiAENgIAIANBIGoiA0GAAkcNAAtBgNSEgABBeEGA1ISAAGtBD3FBAEGA1ISAAEEIakEPcRsiA2oiBEEEaiACQUhqIgUgA2siA0EBcjYCAEEAQQAoAvDTgIAANgKk0ICAAEEAIAM2ApTQgIAAQQAgBDYCoNCAgABBgNSEgAAgBWpBODYCBAsCQAJAAkACQAJAAkACQAJAAkACQAJAAkAgAEHsAUsNAAJAQQAoAojQgIAAIgZBECAAQRNqQXBxIABBC0kbIgJBA3YiBHYiA0EDcUUNAAJAAkAgA0EBcSAEckEBcyIFQQN0IgRBsNCAgABqIgMgBEG40ICAAGooAgAiBCgCCCICRw0AQQAgBkF+IAV3cTYCiNCAgAAMAQsgAyACNgIIIAIgAzYCDAsgBEEIaiEDIAQgBUEDdCIFQQNyNgIEIAQgBWoiBCAEKAIEQQFyNgIEDAwLIAJBACgCkNCAgAAiB00NAQJAIANFDQACQAJAIAMgBHRBAiAEdCIDQQAgA2tycSIDQQAgA2txQX9qIgMgA0EMdkEQcSIDdiIEQQV2QQhxIgUgA3IgBCAFdiIDQQJ2QQRxIgRyIAMgBHYiA0EBdkECcSIEciADIAR2IgNBAXZBAXEiBHIgAyAEdmoiBEEDdCIDQbDQgIAAaiIFIANBuNCAgABqKAIAIgMoAggiAEcNAEEAIAZBfiAEd3EiBjYCiNCAgAAMAQsgBSAANgIIIAAgBTYCDAsgAyACQQNyNgIEIAMgBEEDdCIEaiAEIAJrIgU2AgAgAyACaiIAIAVBAXI2AgQCQCAHRQ0AIAdBeHFBsNCAgABqIQJBACgCnNCAgAAhBAJAAkAgBkEBIAdBA3Z0IghxDQBBACAGIAhyNgKI0ICAACACIQgMAQsgAigCCCEICyAIIAQ2AgwgAiAENgIIIAQgAjYCDCAEIAg2AggLIANBCGohA0EAIAA2ApzQgIAAQQAgBTYCkNCAgAAMDAtBACgCjNCAgAAiCUUNASAJQQAgCWtxQX9qIgMgA0EMdkEQcSIDdiIEQQV2QQhxIgUgA3IgBCAFdiIDQQJ2QQRxIgRyIAMgBHYiA0EBdkECcSIEciADIAR2IgNBAXZBAXEiBHIgAyAEdmpBAnRBuNKAgABqKAIAIgAoAgRBeHEgAmshBCAAIQUCQANAAkAgBSgCECIDDQAgBUEUaigCACIDRQ0CCyADKAIEQXhxIAJrIgUgBCAFIARJIgUbIQQgAyAAIAUbIQAgAyEFDAALCyAAKAIYIQoCQCAAKAIMIgggAEYNACAAKAIIIgNBACgCmNCAgABJGiAIIAM2AgggAyAINgIMDAsLAkAgAEEUaiIFKAIAIgMNACAAKAIQIgNFDQMgAEEQaiEFCwNAIAUhCyADIghBFGoiBSgCACIDDQAgCEEQaiEFIAgoAhAiAw0ACyALQQA2AgAMCgtBfyECIABBv39LDQAgAEETaiIDQXBxIQJBACgCjNCAgAAiB0UNAEEAIQsCQCACQYACSQ0AQR8hCyACQf///wdLDQAgA0EIdiIDIANBgP4/akEQdkEIcSIDdCIEIARBgOAfakEQdkEEcSIEdCIFIAVBgIAPakEQdkECcSIFdEEPdiADIARyIAVyayIDQQF0IAIgA0EVanZBAXFyQRxqIQsLQQAgAmshBAJAAkACQAJAIAtBAnRBuNKAgABqKAIAIgUNAEEAIQNBACEIDAELQQAhAyACQQBBGSALQQF2ayALQR9GG3QhAEEAIQgDQAJAIAUoAgRBeHEgAmsiBiAETw0AIAYhBCAFIQggBg0AQQAhBCAFIQggBSEDDAMLIAMgBUEUaigCACIGIAYgBSAAQR12QQRxakEQaigCACIFRhsgAyAGGyEDIABBAXQhACAFDQALCwJAIAMgCHINAEEAIQhBAiALdCIDQQAgA2tyIAdxIgNFDQMgA0EAIANrcUF/aiIDIANBDHZBEHEiA3YiBUEFdkEIcSIAIANyIAUgAHYiA0ECdkEEcSIFciADIAV2IgNBAXZBAnEiBXIgAyAFdiIDQQF2QQFxIgVyIAMgBXZqQQJ0QbjSgIAAaigCACEDCyADRQ0BCwNAIAMoAgRBeHEgAmsiBiAESSEAAkAgAygCECIFDQAgA0EUaigCACEFCyAGIAQgABshBCADIAggABshCCAFIQMgBQ0ACwsgCEUNACAEQQAoApDQgIAAIAJrTw0AIAgoAhghCwJAIAgoAgwiACAIRg0AIAgoAggiA0EAKAKY0ICAAEkaIAAgAzYCCCADIAA2AgwMCQsCQCAIQRRqIgUoAgAiAw0AIAgoAhAiA0UNAyAIQRBqIQULA0AgBSEGIAMiAEEUaiIFKAIAIgMNACAAQRBqIQUgACgCECIDDQALIAZBADYCAAwICwJAQQAoApDQgIAAIgMgAkkNAEEAKAKc0ICAACEEAkACQCADIAJrIgVBEEkNACAEIAJqIgAgBUEBcjYCBEEAIAU2ApDQgIAAQQAgADYCnNCAgAAgBCADaiAFNgIAIAQgAkEDcjYCBAwBCyAEIANBA3I2AgQgBCADaiIDIAMoAgRBAXI2AgRBAEEANgKc0ICAAEEAQQA2ApDQgIAACyAEQQhqIQMMCgsCQEEAKAKU0ICAACIAIAJNDQBBACgCoNCAgAAiAyACaiIEIAAgAmsiBUEBcjYCBEEAIAU2ApTQgIAAQQAgBDYCoNCAgAAgAyACQQNyNgIEIANBCGohAwwKCwJAAkBBACgC4NOAgABFDQBBACgC6NOAgAAhBAwBC0EAQn83AuzTgIAAQQBCgICEgICAwAA3AuTTgIAAQQAgAUEMakFwcUHYqtWqBXM2AuDTgIAAQQBBADYC9NOAgABBAEEANgLE04CAAEGAgAQhBAtBACEDAkAgBCACQccAaiIHaiIGQQAgBGsiC3EiCCACSw0AQQBBMDYC+NOAgAAMCgsCQEEAKALA04CAACIDRQ0AAkBBACgCuNOAgAAiBCAIaiIFIARNDQAgBSADTQ0BC0EAIQNBAEEwNgL404CAAAwKC0EALQDE04CAAEEEcQ0EAkACQAJAQQAoAqDQgIAAIgRFDQBByNOAgAAhAwNAAkAgAygCACIFIARLDQAgBSADKAIEaiAESw0DCyADKAIIIgMNAAsLQQAQy4CAgAAiAEF/Rg0FIAghBgJAQQAoAuTTgIAAIgNBf2oiBCAAcUUNACAIIABrIAQgAGpBACADa3FqIQYLIAYgAk0NBSAGQf7///8HSw0FAkBBACgCwNOAgAAiA0UNAEEAKAK404CAACIEIAZqIgUgBE0NBiAFIANLDQYLIAYQy4CAgAAiAyAARw0BDAcLIAYgAGsgC3EiBkH+////B0sNBCAGEMuAgIAAIgAgAygCACADKAIEakYNAyAAIQMLAkAgA0F/Rg0AIAJByABqIAZNDQACQCAHIAZrQQAoAujTgIAAIgRqQQAgBGtxIgRB/v///wdNDQAgAyEADAcLAkAgBBDLgICAAEF/Rg0AIAQgBmohBiADIQAMBwtBACAGaxDLgICAABoMBAsgAyEAIANBf0cNBQwDC0EAIQgMBwtBACEADAULIABBf0cNAgtBAEEAKALE04CAAEEEcjYCxNOAgAALIAhB/v///wdLDQEgCBDLgICAACEAQQAQy4CAgAAhAyAAQX9GDQEgA0F/Rg0BIAAgA08NASADIABrIgYgAkE4ak0NAQtBAEEAKAK404CAACAGaiIDNgK404CAAAJAIANBACgCvNOAgABNDQBBACADNgK804CAAAsCQAJAAkACQEEAKAKg0ICAACIERQ0AQcjTgIAAIQMDQCAAIAMoAgAiBSADKAIEIghqRg0CIAMoAggiAw0ADAMLCwJAAkBBACgCmNCAgAAiA0UNACAAIANPDQELQQAgADYCmNCAgAALQQAhA0EAIAY2AszTgIAAQQAgADYCyNOAgABBAEF/NgKo0ICAAEEAQQAoAuDTgIAANgKs0ICAAEEAQQA2AtTTgIAAA0AgA0HE0ICAAGogA0G40ICAAGoiBDYCACAEIANBsNCAgABqIgU2AgAgA0G80ICAAGogBTYCACADQczQgIAAaiADQcDQgIAAaiIFNgIAIAUgBDYCACADQdTQgIAAaiADQcjQgIAAaiIENgIAIAQgBTYCACADQdDQgIAAaiAENgIAIANBIGoiA0GAAkcNAAsgAEF4IABrQQ9xQQAgAEEIakEPcRsiA2oiBCAGQUhqIgUgA2siA0EBcjYCBEEAQQAoAvDTgIAANgKk0ICAAEEAIAM2ApTQgIAAQQAgBDYCoNCAgAAgACAFakE4NgIEDAILIAMtAAxBCHENACAEIAVJDQAgBCAATw0AIARBeCAEa0EPcUEAIARBCGpBD3EbIgVqIgBBACgClNCAgAAgBmoiCyAFayIFQQFyNgIEIAMgCCAGajYCBEEAQQAoAvDTgIAANgKk0ICAAEEAIAU2ApTQgIAAQQAgADYCoNCAgAAgBCALakE4NgIEDAELAkAgAEEAKAKY0ICAACIITw0AQQAgADYCmNCAgAAgACEICyAAIAZqIQVByNOAgAAhAwJAAkACQAJAAkACQAJAA0AgAygCACAFRg0BIAMoAggiAw0ADAILCyADLQAMQQhxRQ0BC0HI04CAACEDA0ACQCADKAIAIgUgBEsNACAFIAMoAgRqIgUgBEsNAwsgAygCCCEDDAALCyADIAA2AgAgAyADKAIEIAZqNgIEIABBeCAAa0EPcUEAIABBCGpBD3EbaiILIAJBA3I2AgQgBUF4IAVrQQ9xQQAgBUEIakEPcRtqIgYgCyACaiICayEDAkAgBiAERw0AQQAgAjYCoNCAgABBAEEAKAKU0ICAACADaiIDNgKU0ICAACACIANBAXI2AgQMAwsCQCAGQQAoApzQgIAARw0AQQAgAjYCnNCAgABBAEEAKAKQ0ICAACADaiIDNgKQ0ICAACACIANBAXI2AgQgAiADaiADNgIADAMLAkAgBigCBCIEQQNxQQFHDQAgBEF4cSEHAkACQCAEQf8BSw0AIAYoAggiBSAEQQN2IghBA3RBsNCAgABqIgBGGgJAIAYoAgwiBCAFRw0AQQBBACgCiNCAgABBfiAId3E2AojQgIAADAILIAQgAEYaIAQgBTYCCCAFIAQ2AgwMAQsgBigCGCEJAkACQCAGKAIMIgAgBkYNACAGKAIIIgQgCEkaIAAgBDYCCCAEIAA2AgwMAQsCQCAGQRRqIgQoAgAiBQ0AIAZBEGoiBCgCACIFDQBBACEADAELA0AgBCEIIAUiAEEUaiIEKAIAIgUNACAAQRBqIQQgACgCECIFDQALIAhBADYCAAsgCUUNAAJAAkAgBiAGKAIcIgVBAnRBuNKAgABqIgQoAgBHDQAgBCAANgIAIAANAUEAQQAoAozQgIAAQX4gBXdxNgKM0ICAAAwCCyAJQRBBFCAJKAIQIAZGG2ogADYCACAARQ0BCyAAIAk2AhgCQCAGKAIQIgRFDQAgACAENgIQIAQgADYCGAsgBigCFCIERQ0AIABBFGogBDYCACAEIAA2AhgLIAcgA2ohAyAGIAdqIgYoAgQhBAsgBiAEQX5xNgIEIAIgA2ogAzYCACACIANBAXI2AgQCQCADQf8BSw0AIANBeHFBsNCAgABqIQQCQAJAQQAoAojQgIAAIgVBASADQQN2dCIDcQ0AQQAgBSADcjYCiNCAgAAgBCEDDAELIAQoAgghAwsgAyACNgIMIAQgAjYCCCACIAQ2AgwgAiADNgIIDAMLQR8hBAJAIANB////B0sNACADQQh2IgQgBEGA/j9qQRB2QQhxIgR0IgUgBUGA4B9qQRB2QQRxIgV0IgAgAEGAgA9qQRB2QQJxIgB0QQ92IAQgBXIgAHJrIgRBAXQgAyAEQRVqdkEBcXJBHGohBAsgAiAENgIcIAJCADcCECAEQQJ0QbjSgIAAaiEFAkBBACgCjNCAgAAiAEEBIAR0IghxDQAgBSACNgIAQQAgACAIcjYCjNCAgAAgAiAFNgIYIAIgAjYCCCACIAI2AgwMAwsgA0EAQRkgBEEBdmsgBEEfRht0IQQgBSgCACEAA0AgACIFKAIEQXhxIANGDQIgBEEddiEAIARBAXQhBCAFIABBBHFqQRBqIggoAgAiAA0ACyAIIAI2AgAgAiAFNgIYIAIgAjYCDCACIAI2AggMAgsgAEF4IABrQQ9xQQAgAEEIakEPcRsiA2oiCyAGQUhqIgggA2siA0EBcjYCBCAAIAhqQTg2AgQgBCAFQTcgBWtBD3FBACAFQUlqQQ9xG2pBQWoiCCAIIARBEGpJGyIIQSM2AgRBAEEAKALw04CAADYCpNCAgABBACADNgKU0ICAAEEAIAs2AqDQgIAAIAhBEGpBACkC0NOAgAA3AgAgCEEAKQLI04CAADcCCEEAIAhBCGo2AtDTgIAAQQAgBjYCzNOAgABBACAANgLI04CAAEEAQQA2AtTTgIAAIAhBJGohAwNAIANBBzYCACADQQRqIgMgBUkNAAsgCCAERg0DIAggCCgCBEF+cTYCBCAIIAggBGsiADYCACAEIABBAXI2AgQCQCAAQf8BSw0AIABBeHFBsNCAgABqIQMCQAJAQQAoAojQgIAAIgVBASAAQQN2dCIAcQ0AQQAgBSAAcjYCiNCAgAAgAyEFDAELIAMoAgghBQsgBSAENgIMIAMgBDYCCCAEIAM2AgwgBCAFNgIIDAQLQR8hAwJAIABB////B0sNACAAQQh2IgMgA0GA/j9qQRB2QQhxIgN0IgUgBUGA4B9qQRB2QQRxIgV0IgggCEGAgA9qQRB2QQJxIgh0QQ92IAMgBXIgCHJrIgNBAXQgACADQRVqdkEBcXJBHGohAwsgBCADNgIcIARCADcCECADQQJ0QbjSgIAAaiEFAkBBACgCjNCAgAAiCEEBIAN0IgZxDQAgBSAENgIAQQAgCCAGcjYCjNCAgAAgBCAFNgIYIAQgBDYCCCAEIAQ2AgwMBAsgAEEAQRkgA0EBdmsgA0EfRht0IQMgBSgCACEIA0AgCCIFKAIEQXhxIABGDQMgA0EddiEIIANBAXQhAyAFIAhBBHFqQRBqIgYoAgAiCA0ACyAGIAQ2AgAgBCAFNgIYIAQgBDYCDCAEIAQ2AggMAwsgBSgCCCIDIAI2AgwgBSACNgIIIAJBADYCGCACIAU2AgwgAiADNgIICyALQQhqIQMMBQsgBSgCCCIDIAQ2AgwgBSAENgIIIARBADYCGCAEIAU2AgwgBCADNgIIC0EAKAKU0ICAACIDIAJNDQBBACgCoNCAgAAiBCACaiIFIAMgAmsiA0EBcjYCBEEAIAM2ApTQgIAAQQAgBTYCoNCAgAAgBCACQQNyNgIEIARBCGohAwwDC0EAIQNBAEEwNgL404CAAAwCCwJAIAtFDQACQAJAIAggCCgCHCIFQQJ0QbjSgIAAaiIDKAIARw0AIAMgADYCACAADQFBACAHQX4gBXdxIgc2AozQgIAADAILIAtBEEEUIAsoAhAgCEYbaiAANgIAIABFDQELIAAgCzYCGAJAIAgoAhAiA0UNACAAIAM2AhAgAyAANgIYCyAIQRRqKAIAIgNFDQAgAEEUaiADNgIAIAMgADYCGAsCQAJAIARBD0sNACAIIAQgAmoiA0EDcjYCBCAIIANqIgMgAygCBEEBcjYCBAwBCyAIIAJqIgAgBEEBcjYCBCAIIAJBA3I2AgQgACAEaiAENgIAAkAgBEH/AUsNACAEQXhxQbDQgIAAaiEDAkACQEEAKAKI0ICAACIFQQEgBEEDdnQiBHENAEEAIAUgBHI2AojQgIAAIAMhBAwBCyADKAIIIQQLIAQgADYCDCADIAA2AgggACADNgIMIAAgBDYCCAwBC0EfIQMCQCAEQf///wdLDQAgBEEIdiIDIANBgP4/akEQdkEIcSIDdCIFIAVBgOAfakEQdkEEcSIFdCICIAJBgIAPakEQdkECcSICdEEPdiADIAVyIAJyayIDQQF0IAQgA0EVanZBAXFyQRxqIQMLIAAgAzYCHCAAQgA3AhAgA0ECdEG40oCAAGohBQJAIAdBASADdCICcQ0AIAUgADYCAEEAIAcgAnI2AozQgIAAIAAgBTYCGCAAIAA2AgggACAANgIMDAELIARBAEEZIANBAXZrIANBH0YbdCEDIAUoAgAhAgJAA0AgAiIFKAIEQXhxIARGDQEgA0EddiECIANBAXQhAyAFIAJBBHFqQRBqIgYoAgAiAg0ACyAGIAA2AgAgACAFNgIYIAAgADYCDCAAIAA2AggMAQsgBSgCCCIDIAA2AgwgBSAANgIIIABBADYCGCAAIAU2AgwgACADNgIICyAIQQhqIQMMAQsCQCAKRQ0AAkACQCAAIAAoAhwiBUECdEG40oCAAGoiAygCAEcNACADIAg2AgAgCA0BQQAgCUF+IAV3cTYCjNCAgAAMAgsgCkEQQRQgCigCECAARhtqIAg2AgAgCEUNAQsgCCAKNgIYAkAgACgCECIDRQ0AIAggAzYCECADIAg2AhgLIABBFGooAgAiA0UNACAIQRRqIAM2AgAgAyAINgIYCwJAAkAgBEEPSw0AIAAgBCACaiIDQQNyNgIEIAAgA2oiAyADKAIEQQFyNgIEDAELIAAgAmoiBSAEQQFyNgIEIAAgAkEDcjYCBCAFIARqIAQ2AgACQCAHRQ0AIAdBeHFBsNCAgABqIQJBACgCnNCAgAAhAwJAAkBBASAHQQN2dCIIIAZxDQBBACAIIAZyNgKI0ICAACACIQgMAQsgAigCCCEICyAIIAM2AgwgAiADNgIIIAMgAjYCDCADIAg2AggLQQAgBTYCnNCAgABBACAENgKQ0ICAAAsgAEEIaiEDCyABQRBqJICAgIAAIAMLCgAgABDJgICAAAviDQEHfwJAIABFDQAgAEF4aiIBIABBfGooAgAiAkF4cSIAaiEDAkAgAkEBcQ0AIAJBA3FFDQEgASABKAIAIgJrIgFBACgCmNCAgAAiBEkNASACIABqIQACQCABQQAoApzQgIAARg0AAkAgAkH/AUsNACABKAIIIgQgAkEDdiIFQQN0QbDQgIAAaiIGRhoCQCABKAIMIgIgBEcNAEEAQQAoAojQgIAAQX4gBXdxNgKI0ICAAAwDCyACIAZGGiACIAQ2AgggBCACNgIMDAILIAEoAhghBwJAAkAgASgCDCIGIAFGDQAgASgCCCICIARJGiAGIAI2AgggAiAGNgIMDAELAkAgAUEUaiICKAIAIgQNACABQRBqIgIoAgAiBA0AQQAhBgwBCwNAIAIhBSAEIgZBFGoiAigCACIEDQAgBkEQaiECIAYoAhAiBA0ACyAFQQA2AgALIAdFDQECQAJAIAEgASgCHCIEQQJ0QbjSgIAAaiICKAIARw0AIAIgBjYCACAGDQFBAEEAKAKM0ICAAEF+IAR3cTYCjNCAgAAMAwsgB0EQQRQgBygCECABRhtqIAY2AgAgBkUNAgsgBiAHNgIYAkAgASgCECICRQ0AIAYgAjYCECACIAY2AhgLIAEoAhQiAkUNASAGQRRqIAI2AgAgAiAGNgIYDAELIAMoAgQiAkEDcUEDRw0AIAMgAkF+cTYCBEEAIAA2ApDQgIAAIAEgAGogADYCACABIABBAXI2AgQPCyABIANPDQAgAygCBCICQQFxRQ0AAkACQCACQQJxDQACQCADQQAoAqDQgIAARw0AQQAgATYCoNCAgABBAEEAKAKU0ICAACAAaiIANgKU0ICAACABIABBAXI2AgQgAUEAKAKc0ICAAEcNA0EAQQA2ApDQgIAAQQBBADYCnNCAgAAPCwJAIANBACgCnNCAgABHDQBBACABNgKc0ICAAEEAQQAoApDQgIAAIABqIgA2ApDQgIAAIAEgAEEBcjYCBCABIABqIAA2AgAPCyACQXhxIABqIQACQAJAIAJB/wFLDQAgAygCCCIEIAJBA3YiBUEDdEGw0ICAAGoiBkYaAkAgAygCDCICIARHDQBBAEEAKAKI0ICAAEF+IAV3cTYCiNCAgAAMAgsgAiAGRhogAiAENgIIIAQgAjYCDAwBCyADKAIYIQcCQAJAIAMoAgwiBiADRg0AIAMoAggiAkEAKAKY0ICAAEkaIAYgAjYCCCACIAY2AgwMAQsCQCADQRRqIgIoAgAiBA0AIANBEGoiAigCACIEDQBBACEGDAELA0AgAiEFIAQiBkEUaiICKAIAIgQNACAGQRBqIQIgBigCECIEDQALIAVBADYCAAsgB0UNAAJAAkAgAyADKAIcIgRBAnRBuNKAgABqIgIoAgBHDQAgAiAGNgIAIAYNAUEAQQAoAozQgIAAQX4gBHdxNgKM0ICAAAwCCyAHQRBBFCAHKAIQIANGG2ogBjYCACAGRQ0BCyAGIAc2AhgCQCADKAIQIgJFDQAgBiACNgIQIAIgBjYCGAsgAygCFCICRQ0AIAZBFGogAjYCACACIAY2AhgLIAEgAGogADYCACABIABBAXI2AgQgAUEAKAKc0ICAAEcNAUEAIAA2ApDQgIAADwsgAyACQX5xNgIEIAEgAGogADYCACABIABBAXI2AgQLAkAgAEH/AUsNACAAQXhxQbDQgIAAaiECAkACQEEAKAKI0ICAACIEQQEgAEEDdnQiAHENAEEAIAQgAHI2AojQgIAAIAIhAAwBCyACKAIIIQALIAAgATYCDCACIAE2AgggASACNgIMIAEgADYCCA8LQR8hAgJAIABB////B0sNACAAQQh2IgIgAkGA/j9qQRB2QQhxIgJ0IgQgBEGA4B9qQRB2QQRxIgR0IgYgBkGAgA9qQRB2QQJxIgZ0QQ92IAIgBHIgBnJrIgJBAXQgACACQRVqdkEBcXJBHGohAgsgASACNgIcIAFCADcCECACQQJ0QbjSgIAAaiEEAkACQEEAKAKM0ICAACIGQQEgAnQiA3ENACAEIAE2AgBBACAGIANyNgKM0ICAACABIAQ2AhggASABNgIIIAEgATYCDAwBCyAAQQBBGSACQQF2ayACQR9GG3QhAiAEKAIAIQYCQANAIAYiBCgCBEF4cSAARg0BIAJBHXYhBiACQQF0IQIgBCAGQQRxakEQaiIDKAIAIgYNAAsgAyABNgIAIAEgBDYCGCABIAE2AgwgASABNgIIDAELIAQoAggiACABNgIMIAQgATYCCCABQQA2AhggASAENgIMIAEgADYCCAtBAEEAKAKo0ICAAEF/aiIBQX8gARs2AqjQgIAACwsEAAAAC04AAkAgAA0APwBBEHQPCwJAIABB//8DcQ0AIABBf0wNAAJAIABBEHZAACIAQX9HDQBBAEEwNgL404CAAEF/DwsgAEEQdA8LEMqAgIAAAAvyAgIDfwF+AkAgAkUNACAAIAE6AAAgAiAAaiIDQX9qIAE6AAAgAkEDSQ0AIAAgAToAAiAAIAE6AAEgA0F9aiABOgAAIANBfmogAToAACACQQdJDQAgACABOgADIANBfGogAToAACACQQlJDQAgAEEAIABrQQNxIgRqIgMgAUH/AXFBgYKECGwiATYCACADIAIgBGtBfHEiBGoiAkF8aiABNgIAIARBCUkNACADIAE2AgggAyABNgIEIAJBeGogATYCACACQXRqIAE2AgAgBEEZSQ0AIAMgATYCGCADIAE2AhQgAyABNgIQIAMgATYCDCACQXBqIAE2AgAgAkFsaiABNgIAIAJBaGogATYCACACQWRqIAE2AgAgBCADQQRxQRhyIgVrIgJBIEkNACABrUKBgICAEH4hBiADIAVqIQEDQCABIAY3AxggASAGNwMQIAEgBjcDCCABIAY3AwAgAUEgaiEBIAJBYGoiAkEfSw0ACwsgAAsLjkgBAEGACAuGSAEAAAACAAAAAwAAAAAAAAAAAAAABAAAAAUAAAAAAAAAAAAAAAYAAAAHAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASW52YWxpZCBjaGFyIGluIHVybCBxdWVyeQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2JvZHkAQ29udGVudC1MZW5ndGggb3ZlcmZsb3cAQ2h1bmsgc2l6ZSBvdmVyZmxvdwBSZXNwb25zZSBvdmVyZmxvdwBJbnZhbGlkIG1ldGhvZCBmb3IgSFRUUC94LnggcmVxdWVzdABJbnZhbGlkIG1ldGhvZCBmb3IgUlRTUC94LnggcmVxdWVzdABFeHBlY3RlZCBTT1VSQ0UgbWV0aG9kIGZvciBJQ0UveC54IHJlcXVlc3QASW52YWxpZCBjaGFyIGluIHVybCBmcmFnbWVudCBzdGFydABFeHBlY3RlZCBkb3QAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9zdGF0dXMASW52YWxpZCByZXNwb25zZSBzdGF0dXMASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucwBVc2VyIGNhbGxiYWNrIGVycm9yAGBvbl9yZXNldGAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2hlYWRlcmAgY2FsbGJhY2sgZXJyb3IAYG9uX21lc3NhZ2VfYmVnaW5gIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19leHRlbnNpb25fdmFsdWVgIGNhbGxiYWNrIGVycm9yAGBvbl9zdGF0dXNfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl92ZXJzaW9uX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdXJsX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9oZWFkZXJfdmFsdWVfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWV0aG9kX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX2ZpZWxkX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX25hbWVgIGNhbGxiYWNrIGVycm9yAFVuZXhwZWN0ZWQgY2hhciBpbiB1cmwgc2VydmVyAEludmFsaWQgaGVhZGVyIHZhbHVlIGNoYXIASW52YWxpZCBoZWFkZXIgZmllbGQgY2hhcgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3ZlcnNpb24ASW52YWxpZCBtaW5vciB2ZXJzaW9uAEludmFsaWQgbWFqb3IgdmVyc2lvbgBFeHBlY3RlZCBzcGFjZSBhZnRlciB2ZXJzaW9uAEV4cGVjdGVkIENSTEYgYWZ0ZXIgdmVyc2lvbgBJbnZhbGlkIEhUVFAgdmVyc2lvbgBJbnZhbGlkIGhlYWRlciB0b2tlbgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3VybABJbnZhbGlkIGNoYXJhY3RlcnMgaW4gdXJsAFVuZXhwZWN0ZWQgc3RhcnQgY2hhciBpbiB1cmwARG91YmxlIEAgaW4gdXJsAEVtcHR5IENvbnRlbnQtTGVuZ3RoAEludmFsaWQgY2hhcmFjdGVyIGluIENvbnRlbnQtTGVuZ3RoAER1cGxpY2F0ZSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXIgaW4gdXJsIHBhdGgAQ29udGVudC1MZW5ndGggY2FuJ3QgYmUgcHJlc2VudCB3aXRoIFRyYW5zZmVyLUVuY29kaW5nAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIHNpemUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfdmFsdWUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyB2YWx1ZQBNaXNzaW5nIGV4cGVjdGVkIExGIGFmdGVyIGhlYWRlciB2YWx1ZQBJbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AgaGVhZGVyIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGUgdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBxdW90ZWQgdmFsdWUAUGF1c2VkIGJ5IG9uX2hlYWRlcnNfY29tcGxldGUASW52YWxpZCBFT0Ygc3RhdGUAb25fcmVzZXQgcGF1c2UAb25fY2h1bmtfaGVhZGVyIHBhdXNlAG9uX21lc3NhZ2VfYmVnaW4gcGF1c2UAb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlIHBhdXNlAG9uX3N0YXR1c19jb21wbGV0ZSBwYXVzZQBvbl92ZXJzaW9uX2NvbXBsZXRlIHBhdXNlAG9uX3VybF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19jb21wbGV0ZSBwYXVzZQBvbl9oZWFkZXJfdmFsdWVfY29tcGxldGUgcGF1c2UAb25fbWVzc2FnZV9jb21wbGV0ZSBwYXVzZQBvbl9tZXRob2RfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX2ZpZWxkX2NvbXBsZXRlIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lIHBhdXNlAFVuZXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgc3RhcnQgbGluZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgbmFtZQBQYXVzZSBvbiBDT05ORUNUL1VwZ3JhZGUAUGF1c2Ugb24gUFJJL1VwZ3JhZGUARXhwZWN0ZWQgSFRUUC8yIENvbm5lY3Rpb24gUHJlZmFjZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX21ldGhvZABFeHBlY3RlZCBzcGFjZSBhZnRlciBtZXRob2QAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfZmllbGQAUGF1c2VkAEludmFsaWQgd29yZCBlbmNvdW50ZXJlZABJbnZhbGlkIG1ldGhvZCBlbmNvdW50ZXJlZABVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNjaGVtYQBSZXF1ZXN0IGhhcyBpbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AAU1dJVENIX1BST1hZAFVTRV9QUk9YWQBNS0FDVElWSVRZAFVOUFJPQ0VTU0FCTEVfRU5USVRZAENPUFkATU9WRURfUEVSTUFORU5UTFkAVE9PX0VBUkxZAE5PVElGWQBGQUlMRURfREVQRU5ERU5DWQBCQURfR0FURVdBWQBQTEFZAFBVVABDSEVDS09VVABHQVRFV0FZX1RJTUVPVVQAUkVRVUVTVF9USU1FT1VUAE5FVFdPUktfQ09OTkVDVF9USU1FT1VUAENPTk5FQ1RJT05fVElNRU9VVABMT0dJTl9USU1FT1VUAE5FVFdPUktfUkVBRF9USU1FT1VUAFBPU1QATUlTRElSRUNURURfUkVRVUVTVABDTElFTlRfQ0xPU0VEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9MT0FEX0JBTEFOQ0VEX1JFUVVFU1QAQkFEX1JFUVVFU1QASFRUUF9SRVFVRVNUX1NFTlRfVE9fSFRUUFNfUE9SVABSRVBPUlQASU1fQV9URUFQT1QAUkVTRVRfQ09OVEVOVABOT19DT05URU5UAFBBUlRJQUxfQ09OVEVOVABIUEVfSU5WQUxJRF9DT05TVEFOVABIUEVfQ0JfUkVTRVQAR0VUAEhQRV9TVFJJQ1QAQ09ORkxJQ1QAVEVNUE9SQVJZX1JFRElSRUNUAFBFUk1BTkVOVF9SRURJUkVDVABDT05ORUNUAE1VTFRJX1NUQVRVUwBIUEVfSU5WQUxJRF9TVEFUVVMAVE9PX01BTllfUkVRVUVTVFMARUFSTFlfSElOVFMAVU5BVkFJTEFCTEVfRk9SX0xFR0FMX1JFQVNPTlMAT1BUSU9OUwBTV0lUQ0hJTkdfUFJPVE9DT0xTAFZBUklBTlRfQUxTT19ORUdPVElBVEVTAE1VTFRJUExFX0NIT0lDRVMASU5URVJOQUxfU0VSVkVSX0VSUk9SAFdFQl9TRVJWRVJfVU5LTk9XTl9FUlJPUgBSQUlMR1VOX0VSUk9SAElERU5USVRZX1BST1ZJREVSX0FVVEhFTlRJQ0FUSU9OX0VSUk9SAFNTTF9DRVJUSUZJQ0FURV9FUlJPUgBJTlZBTElEX1hfRk9SV0FSREVEX0ZPUgBTRVRfUEFSQU1FVEVSAEdFVF9QQVJBTUVURVIASFBFX1VTRVIAU0VFX09USEVSAEhQRV9DQl9DSFVOS19IRUFERVIATUtDQUxFTkRBUgBTRVRVUABXRUJfU0VSVkVSX0lTX0RPV04AVEVBUkRPV04ASFBFX0NMT1NFRF9DT05ORUNUSU9OAEhFVVJJU1RJQ19FWFBJUkFUSU9OAERJU0NPTk5FQ1RFRF9PUEVSQVRJT04ATk9OX0FVVEhPUklUQVRJVkVfSU5GT1JNQVRJT04ASFBFX0lOVkFMSURfVkVSU0lPTgBIUEVfQ0JfTUVTU0FHRV9CRUdJTgBTSVRFX0lTX0ZST1pFTgBIUEVfSU5WQUxJRF9IRUFERVJfVE9LRU4ASU5WQUxJRF9UT0tFTgBGT1JCSURERU4ARU5IQU5DRV9ZT1VSX0NBTE0ASFBFX0lOVkFMSURfVVJMAEJMT0NLRURfQllfUEFSRU5UQUxfQ09OVFJPTABNS0NPTABBQ0wASFBFX0lOVEVSTkFMAFJFUVVFU1RfSEVBREVSX0ZJRUxEU19UT09fTEFSR0VfVU5PRkZJQ0lBTABIUEVfT0sAVU5MSU5LAFVOTE9DSwBQUkkAUkVUUllfV0lUSABIUEVfSU5WQUxJRF9DT05URU5UX0xFTkdUSABIUEVfVU5FWFBFQ1RFRF9DT05URU5UX0xFTkdUSABGTFVTSABQUk9QUEFUQ0gATS1TRUFSQ0gAVVJJX1RPT19MT05HAFBST0NFU1NJTkcATUlTQ0VMTEFORU9VU19QRVJTSVNURU5UX1dBUk5JTkcATUlTQ0VMTEFORU9VU19XQVJOSU5HAEhQRV9JTlZBTElEX1RSQU5TRkVSX0VOQ09ESU5HAEV4cGVjdGVkIENSTEYASFBFX0lOVkFMSURfQ0hVTktfU0laRQBNT1ZFAENPTlRJTlVFAEhQRV9DQl9TVEFUVVNfQ09NUExFVEUASFBFX0NCX0hFQURFUlNfQ09NUExFVEUASFBFX0NCX1ZFUlNJT05fQ09NUExFVEUASFBFX0NCX1VSTF9DT01QTEVURQBIUEVfQ0JfQ0hVTktfQ09NUExFVEUASFBFX0NCX0hFQURFUl9WQUxVRV9DT01QTEVURQBIUEVfQ0JfQ0hVTktfRVhURU5TSU9OX1ZBTFVFX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19FWFRFTlNJT05fTkFNRV9DT01QTEVURQBIUEVfQ0JfTUVTU0FHRV9DT01QTEVURQBIUEVfQ0JfTUVUSE9EX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJfRklFTERfQ09NUExFVEUAREVMRVRFAEhQRV9JTlZBTElEX0VPRl9TVEFURQBJTlZBTElEX1NTTF9DRVJUSUZJQ0FURQBQQVVTRQBOT19SRVNQT05TRQBVTlNVUFBPUlRFRF9NRURJQV9UWVBFAEdPTkUATk9UX0FDQ0VQVEFCTEUAU0VSVklDRV9VTkFWQUlMQUJMRQBSQU5HRV9OT1RfU0FUSVNGSUFCTEUAT1JJR0lOX0lTX1VOUkVBQ0hBQkxFAFJFU1BPTlNFX0lTX1NUQUxFAFBVUkdFAE1FUkdFAFJFUVVFU1RfSEVBREVSX0ZJRUxEU19UT09fTEFSR0UAUkVRVUVTVF9IRUFERVJfVE9PX0xBUkdFAFBBWUxPQURfVE9PX0xBUkdFAElOU1VGRklDSUVOVF9TVE9SQUdFAEhQRV9QQVVTRURfVVBHUkFERQBIUEVfUEFVU0VEX0gyX1VQR1JBREUAU09VUkNFAEFOTk9VTkNFAFRSQUNFAEhQRV9VTkVYUEVDVEVEX1NQQUNFAERFU0NSSUJFAFVOU1VCU0NSSUJFAFJFQ09SRABIUEVfSU5WQUxJRF9NRVRIT0QATk9UX0ZPVU5EAFBST1BGSU5EAFVOQklORABSRUJJTkQAVU5BVVRIT1JJWkVEAE1FVEhPRF9OT1RfQUxMT1dFRABIVFRQX1ZFUlNJT05fTk9UX1NVUFBPUlRFRABBTFJFQURZX1JFUE9SVEVEAEFDQ0VQVEVEAE5PVF9JTVBMRU1FTlRFRABMT09QX0RFVEVDVEVEAEhQRV9DUl9FWFBFQ1RFRABIUEVfTEZfRVhQRUNURUQAQ1JFQVRFRABJTV9VU0VEAEhQRV9QQVVTRUQAVElNRU9VVF9PQ0NVUkVEAFBBWU1FTlRfUkVRVUlSRUQAUFJFQ09ORElUSU9OX1JFUVVJUkVEAFBST1hZX0FVVEhFTlRJQ0FUSU9OX1JFUVVJUkVEAE5FVFdPUktfQVVUSEVOVElDQVRJT05fUkVRVUlSRUQATEVOR1RIX1JFUVVJUkVEAFNTTF9DRVJUSUZJQ0FURV9SRVFVSVJFRABVUEdSQURFX1JFUVVJUkVEAFBBR0VfRVhQSVJFRABQUkVDT05ESVRJT05fRkFJTEVEAEVYUEVDVEFUSU9OX0ZBSUxFRABSRVZBTElEQVRJT05fRkFJTEVEAFNTTF9IQU5EU0hBS0VfRkFJTEVEAExPQ0tFRABUUkFOU0ZPUk1BVElPTl9BUFBMSUVEAE5PVF9NT0RJRklFRABOT1RfRVhURU5ERUQAQkFORFdJRFRIX0xJTUlUX0VYQ0VFREVEAFNJVEVfSVNfT1ZFUkxPQURFRABIRUFEAEV4cGVjdGVkIEhUVFAvAABeEwAAJhMAADAQAADwFwAAnRMAABUSAAA5FwAA8BIAAAoQAAB1EgAArRIAAIITAABPFAAAfxAAAKAVAAAjFAAAiRIAAIsUAABNFQAA1BEAAM8UAAAQGAAAyRYAANwWAADBEQAA4BcAALsUAAB0FAAAfBUAAOUUAAAIFwAAHxAAAGUVAACjFAAAKBUAAAIVAACZFQAALBAAAIsZAABPDwAA1A4AAGoQAADOEAAAAhcAAIkOAABuEwAAHBMAAGYUAABWFwAAwRMAAM0TAABsEwAAaBcAAGYXAABfFwAAIhMAAM4PAABpDgAA2A4AAGMWAADLEwAAqg4AACgXAAAmFwAAxRMAAF0WAADoEQAAZxMAAGUTAADyFgAAcxMAAB0XAAD5FgAA8xEAAM8OAADOFQAADBIAALMRAAClEQAAYRAAADIXAAC7EwAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAgEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgMCAgICAgAAAgIAAgIAAgICAgICAgICAgAEAAAAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAgICAAIAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAgICAgIAAAICAAICAAICAgICAgICAgIAAwAEAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgIAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgICAgACAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABsb3NlZWVwLWFsaXZlAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAgEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQFjaHVua2VkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQABAQEBAQAAAQEAAQEAAQEBAQEBAQEBAQAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGVjdGlvbmVudC1sZW5ndGhvbnJveHktY29ubmVjdGlvbgAAAAAAAAAAAAAAAAAAAHJhbnNmZXItZW5jb2RpbmdwZ3JhZGUNCg0KDQpTTQ0KDQpUVFAvQ0UvVFNQLwAAAAAAAAAAAAAAAAECAAEDAAAAAAAAAAAAAAAAAAAAAAAABAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAABAgABAwAAAAAAAAAAAAAAAAAAAAAAAAQBAQUBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAQAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAABAAACAAAAAAAAAAAAAAAAAAAAAAAAAwQAAAQEBAQEBAQEBAQEBQQEBAQEBAQEBAQEBAAEAAYHBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQABAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAgAAAAACAAAAAAAAAAAAAAAAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE5PVU5DRUVDS09VVE5FQ1RFVEVDUklCRUxVU0hFVEVBRFNFQVJDSFJHRUNUSVZJVFlMRU5EQVJWRU9USUZZUFRJT05TQ0hTRUFZU1RBVENIR0VPUkRJUkVDVE9SVFJDSFBBUkFNRVRFUlVSQ0VCU0NSSUJFQVJET1dOQUNFSU5ETktDS1VCU0NSSUJFSFRUUC9BRFRQLw==' + + +/***/ }), + +/***/ 432: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.enumToMap = void 0; +function enumToMap(obj) { + const res = {}; + Object.keys(obj).forEach((key) => { + const value = obj[key]; + if (typeof value === 'number') { + res[key] = value; + } + }); + return res; +} +exports.enumToMap = enumToMap; +//# sourceMappingURL=utils.js.map + +/***/ }), + +/***/ 4169: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kClients } = __nccwpck_require__(9583) +const Agent = __nccwpck_require__(2841) +const { + kAgent, + kMockAgentSet, + kMockAgentGet, + kDispatches, + kIsMockActive, + kNetConnect, + kGetNetConnect, + kOptions, + kFactory +} = __nccwpck_require__(5217) +const MockClient = __nccwpck_require__(441) +const MockPool = __nccwpck_require__(9216) +const { matchValue, buildMockOptions } = __nccwpck_require__(8214) +const { InvalidArgumentError, UndiciError } = __nccwpck_require__(1975) +const Dispatcher = __nccwpck_require__(5903) +const Pluralizer = __nccwpck_require__(4301) +const PendingInterceptorsFormatter = __nccwpck_require__(3546) + +class FakeWeakRef { + constructor (value) { + this.value = value + } + + deref () { + return this.value + } +} + +class MockAgent extends Dispatcher { + constructor (opts) { + super(opts) + + this[kNetConnect] = true + this[kIsMockActive] = true + + // Instantiate Agent and encapsulate + if ((opts && opts.agent && typeof opts.agent.dispatch !== 'function')) { + throw new InvalidArgumentError('Argument opts.agent must implement Agent') + } + const agent = opts && opts.agent ? opts.agent : new Agent(opts) + this[kAgent] = agent + + this[kClients] = agent[kClients] + this[kOptions] = buildMockOptions(opts) + } + + get (origin) { + let dispatcher = this[kMockAgentGet](origin) + + if (!dispatcher) { + dispatcher = this[kFactory](origin) + this[kMockAgentSet](origin, dispatcher) + } + return dispatcher + } + + dispatch (opts, handler) { + // Call MockAgent.get to perform additional setup before dispatching as normal + this.get(opts.origin) + return this[kAgent].dispatch(opts, handler) + } + + async close () { + await this[kAgent].close() + this[kClients].clear() + } + + deactivate () { + this[kIsMockActive] = false + } + + activate () { + this[kIsMockActive] = true + } + + enableNetConnect (matcher) { + if (typeof matcher === 'string' || typeof matcher === 'function' || matcher instanceof RegExp) { + if (Array.isArray(this[kNetConnect])) { + this[kNetConnect].push(matcher) + } else { + this[kNetConnect] = [matcher] + } + } else if (typeof matcher === 'undefined') { + this[kNetConnect] = true + } else { + throw new InvalidArgumentError('Unsupported matcher. Must be one of String|Function|RegExp.') + } + } + + disableNetConnect () { + this[kNetConnect] = false + } + + // This is required to bypass issues caused by using global symbols - see: + // https://github.com/nodejs/undici/issues/1447 + get isMockActive () { + return this[kIsMockActive] + } + + [kMockAgentSet] (origin, dispatcher) { + this[kClients].set(origin, new FakeWeakRef(dispatcher)) + } + + [kFactory] (origin) { + const mockOptions = Object.assign({ agent: this }, this[kOptions]) + return this[kOptions] && this[kOptions].connections === 1 + ? new MockClient(origin, mockOptions) + : new MockPool(origin, mockOptions) + } + + [kMockAgentGet] (origin) { + // First check if we can immediately find it + const ref = this[kClients].get(origin) + if (ref) { + return ref.deref() + } + + // If the origin is not a string create a dummy parent pool and return to user + if (typeof origin !== 'string') { + const dispatcher = this[kFactory]('http://localhost:9999') + this[kMockAgentSet](origin, dispatcher) + return dispatcher + } + + // If we match, create a pool and assign the same dispatches + for (const [keyMatcher, nonExplicitRef] of Array.from(this[kClients])) { + const nonExplicitDispatcher = nonExplicitRef.deref() + if (nonExplicitDispatcher && typeof keyMatcher !== 'string' && matchValue(keyMatcher, origin)) { + const dispatcher = this[kFactory](origin) + this[kMockAgentSet](origin, dispatcher) + dispatcher[kDispatches] = nonExplicitDispatcher[kDispatches] + return dispatcher + } + } + } + + [kGetNetConnect] () { + return this[kNetConnect] + } + + pendingInterceptors () { + const mockAgentClients = this[kClients] + + return Array.from(mockAgentClients.entries()) + .flatMap(([origin, scope]) => scope.deref()[kDispatches].map(dispatch => ({ ...dispatch, origin }))) + .filter(({ pending }) => pending) + } + + assertNoPendingInterceptors ({ pendingInterceptorsFormatter = new PendingInterceptorsFormatter() } = {}) { + const pending = this.pendingInterceptors() + + if (pending.length === 0) { + return + } + + const pluralizer = new Pluralizer('interceptor', 'interceptors').pluralize(pending.length) + + throw new UndiciError(` +${pluralizer.count} ${pluralizer.noun} ${pluralizer.is} pending: + +${pendingInterceptorsFormatter.format(pending)} +`.trim()) + } +} + +module.exports = MockAgent + + +/***/ }), + +/***/ 441: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { promisify } = __nccwpck_require__(9023) +const Client = __nccwpck_require__(5833) +const { buildMockDispatch } = __nccwpck_require__(8214) +const { + kDispatches, + kMockAgent, + kClose, + kOriginalClose, + kOrigin, + kOriginalDispatch, + kConnected +} = __nccwpck_require__(5217) +const { MockInterceptor } = __nccwpck_require__(9251) +const Symbols = __nccwpck_require__(9583) +const { InvalidArgumentError } = __nccwpck_require__(1975) + +/** + * MockClient provides an API that extends the Client to influence the mockDispatches. + */ +class MockClient extends Client { + constructor (origin, opts) { + super(origin, opts) + + if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') { + throw new InvalidArgumentError('Argument opts.agent must implement Agent') + } + + this[kMockAgent] = opts.agent + this[kOrigin] = origin + this[kDispatches] = [] + this[kConnected] = 1 + this[kOriginalDispatch] = this.dispatch + this[kOriginalClose] = this.close.bind(this) + + this.dispatch = buildMockDispatch.call(this) + this.close = this[kClose] + } + + get [Symbols.kConnected] () { + return this[kConnected] + } + + /** + * Sets up the base interceptor for mocking replies from undici. + */ + intercept (opts) { + return new MockInterceptor(opts, this[kDispatches]) + } + + async [kClose] () { + await promisify(this[kOriginalClose])() + this[kConnected] = 0 + this[kMockAgent][Symbols.kClients].delete(this[kOrigin]) + } +} + +module.exports = MockClient + + +/***/ }), + +/***/ 4257: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { UndiciError } = __nccwpck_require__(1975) + +class MockNotMatchedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, MockNotMatchedError) + this.name = 'MockNotMatchedError' + this.message = message || 'The request does not match any registered mock dispatches' + this.code = 'UND_MOCK_ERR_MOCK_NOT_MATCHED' + } +} + +module.exports = { + MockNotMatchedError +} + + +/***/ }), + +/***/ 9251: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { getResponseData, buildKey, addMockDispatch } = __nccwpck_require__(8214) +const { + kDispatches, + kDispatchKey, + kDefaultHeaders, + kDefaultTrailers, + kContentLength, + kMockDispatch +} = __nccwpck_require__(5217) +const { InvalidArgumentError } = __nccwpck_require__(1975) +const { buildURL } = __nccwpck_require__(1436) + +/** + * Defines the scope API for an interceptor reply + */ +class MockScope { + constructor (mockDispatch) { + this[kMockDispatch] = mockDispatch + } + + /** + * Delay a reply by a set amount in ms. + */ + delay (waitInMs) { + if (typeof waitInMs !== 'number' || !Number.isInteger(waitInMs) || waitInMs <= 0) { + throw new InvalidArgumentError('waitInMs must be a valid integer > 0') + } + + this[kMockDispatch].delay = waitInMs + return this + } + + /** + * For a defined reply, never mark as consumed. + */ + persist () { + this[kMockDispatch].persist = true + return this + } + + /** + * Allow one to define a reply for a set amount of matching requests. + */ + times (repeatTimes) { + if (typeof repeatTimes !== 'number' || !Number.isInteger(repeatTimes) || repeatTimes <= 0) { + throw new InvalidArgumentError('repeatTimes must be a valid integer > 0') + } + + this[kMockDispatch].times = repeatTimes + return this + } +} + +/** + * Defines an interceptor for a Mock + */ +class MockInterceptor { + constructor (opts, mockDispatches) { + if (typeof opts !== 'object') { + throw new InvalidArgumentError('opts must be an object') + } + if (typeof opts.path === 'undefined') { + throw new InvalidArgumentError('opts.path must be defined') + } + if (typeof opts.method === 'undefined') { + opts.method = 'GET' + } + // See https://github.com/nodejs/undici/issues/1245 + // As per RFC 3986, clients are not supposed to send URI + // fragments to servers when they retrieve a document, + if (typeof opts.path === 'string') { + if (opts.query) { + opts.path = buildURL(opts.path, opts.query) + } else { + // Matches https://github.com/nodejs/undici/blob/main/lib/fetch/index.js#L1811 + const parsedURL = new URL(opts.path, 'data://') + opts.path = parsedURL.pathname + parsedURL.search + } + } + if (typeof opts.method === 'string') { + opts.method = opts.method.toUpperCase() + } + + this[kDispatchKey] = buildKey(opts) + this[kDispatches] = mockDispatches + this[kDefaultHeaders] = {} + this[kDefaultTrailers] = {} + this[kContentLength] = false + } + + createMockScopeDispatchData (statusCode, data, responseOptions = {}) { + const responseData = getResponseData(data) + const contentLength = this[kContentLength] ? { 'content-length': responseData.length } : {} + const headers = { ...this[kDefaultHeaders], ...contentLength, ...responseOptions.headers } + const trailers = { ...this[kDefaultTrailers], ...responseOptions.trailers } + + return { statusCode, data, headers, trailers } + } + + validateReplyParameters (statusCode, data, responseOptions) { + if (typeof statusCode === 'undefined') { + throw new InvalidArgumentError('statusCode must be defined') + } + if (typeof data === 'undefined') { + throw new InvalidArgumentError('data must be defined') + } + if (typeof responseOptions !== 'object') { + throw new InvalidArgumentError('responseOptions must be an object') + } + } + + /** + * Mock an undici request with a defined reply. + */ + reply (replyData) { + // Values of reply aren't available right now as they + // can only be available when the reply callback is invoked. + if (typeof replyData === 'function') { + // We'll first wrap the provided callback in another function, + // this function will properly resolve the data from the callback + // when invoked. + const wrappedDefaultsCallback = (opts) => { + // Our reply options callback contains the parameter for statusCode, data and options. + const resolvedData = replyData(opts) + + // Check if it is in the right format + if (typeof resolvedData !== 'object') { + throw new InvalidArgumentError('reply options callback must return an object') + } + + const { statusCode, data = '', responseOptions = {} } = resolvedData + this.validateReplyParameters(statusCode, data, responseOptions) + // Since the values can be obtained immediately we return them + // from this higher order function that will be resolved later. + return { + ...this.createMockScopeDispatchData(statusCode, data, responseOptions) + } + } + + // Add usual dispatch data, but this time set the data parameter to function that will eventually provide data. + const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], wrappedDefaultsCallback) + return new MockScope(newMockDispatch) + } + + // We can have either one or three parameters, if we get here, + // we should have 1-3 parameters. So we spread the arguments of + // this function to obtain the parameters, since replyData will always + // just be the statusCode. + const [statusCode, data = '', responseOptions = {}] = [...arguments] + this.validateReplyParameters(statusCode, data, responseOptions) + + // Send in-already provided data like usual + const dispatchData = this.createMockScopeDispatchData(statusCode, data, responseOptions) + const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], dispatchData) + return new MockScope(newMockDispatch) + } + + /** + * Mock an undici request with a defined error. + */ + replyWithError (error) { + if (typeof error === 'undefined') { + throw new InvalidArgumentError('error must be defined') + } + + const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], { error }) + return new MockScope(newMockDispatch) + } + + /** + * Set default reply headers on the interceptor for subsequent replies + */ + defaultReplyHeaders (headers) { + if (typeof headers === 'undefined') { + throw new InvalidArgumentError('headers must be defined') + } + + this[kDefaultHeaders] = headers + return this + } + + /** + * Set default reply trailers on the interceptor for subsequent replies + */ + defaultReplyTrailers (trailers) { + if (typeof trailers === 'undefined') { + throw new InvalidArgumentError('trailers must be defined') + } + + this[kDefaultTrailers] = trailers + return this + } + + /** + * Set reply content length header for replies on the interceptor + */ + replyContentLength () { + this[kContentLength] = true + return this + } +} + +module.exports.MockInterceptor = MockInterceptor +module.exports.MockScope = MockScope + + +/***/ }), + +/***/ 9216: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { promisify } = __nccwpck_require__(9023) +const Pool = __nccwpck_require__(6976) +const { buildMockDispatch } = __nccwpck_require__(8214) +const { + kDispatches, + kMockAgent, + kClose, + kOriginalClose, + kOrigin, + kOriginalDispatch, + kConnected +} = __nccwpck_require__(5217) +const { MockInterceptor } = __nccwpck_require__(9251) +const Symbols = __nccwpck_require__(9583) +const { InvalidArgumentError } = __nccwpck_require__(1975) + +/** + * MockPool provides an API that extends the Pool to influence the mockDispatches. + */ +class MockPool extends Pool { + constructor (origin, opts) { + super(origin, opts) + + if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') { + throw new InvalidArgumentError('Argument opts.agent must implement Agent') + } + + this[kMockAgent] = opts.agent + this[kOrigin] = origin + this[kDispatches] = [] + this[kConnected] = 1 + this[kOriginalDispatch] = this.dispatch + this[kOriginalClose] = this.close.bind(this) + + this.dispatch = buildMockDispatch.call(this) + this.close = this[kClose] + } + + get [Symbols.kConnected] () { + return this[kConnected] + } + + /** + * Sets up the base interceptor for mocking replies from undici. + */ + intercept (opts) { + return new MockInterceptor(opts, this[kDispatches]) + } + + async [kClose] () { + await promisify(this[kOriginalClose])() + this[kConnected] = 0 + this[kMockAgent][Symbols.kClients].delete(this[kOrigin]) + } +} + +module.exports = MockPool + + +/***/ }), + +/***/ 5217: +/***/ ((module) => { + +"use strict"; + + +module.exports = { + kAgent: Symbol('agent'), + kOptions: Symbol('options'), + kFactory: Symbol('factory'), + kDispatches: Symbol('dispatches'), + kDispatchKey: Symbol('dispatch key'), + kDefaultHeaders: Symbol('default headers'), + kDefaultTrailers: Symbol('default trailers'), + kContentLength: Symbol('content length'), + kMockAgent: Symbol('mock agent'), + kMockAgentSet: Symbol('mock agent set'), + kMockAgentGet: Symbol('mock agent get'), + kMockDispatch: Symbol('mock dispatch'), + kClose: Symbol('close'), + kOriginalClose: Symbol('original agent close'), + kOrigin: Symbol('origin'), + kIsMockActive: Symbol('is mock active'), + kNetConnect: Symbol('net connect'), + kGetNetConnect: Symbol('get net connect'), + kConnected: Symbol('connected') +} + + +/***/ }), + +/***/ 8214: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { MockNotMatchedError } = __nccwpck_require__(4257) +const { + kDispatches, + kMockAgent, + kOriginalDispatch, + kOrigin, + kGetNetConnect +} = __nccwpck_require__(5217) +const { buildURL, nop } = __nccwpck_require__(1436) +const { STATUS_CODES } = __nccwpck_require__(8611) +const { + types: { + isPromise + } +} = __nccwpck_require__(9023) + +function matchValue (match, value) { + if (typeof match === 'string') { + return match === value + } + if (match instanceof RegExp) { + return match.test(value) + } + if (typeof match === 'function') { + return match(value) === true + } + return false +} + +function lowerCaseEntries (headers) { + return Object.fromEntries( + Object.entries(headers).map(([headerName, headerValue]) => { + return [headerName.toLocaleLowerCase(), headerValue] + }) + ) +} + +/** + * @param {import('../../index').Headers|string[]|Record} headers + * @param {string} key + */ +function getHeaderByName (headers, key) { + if (Array.isArray(headers)) { + for (let i = 0; i < headers.length; i += 2) { + if (headers[i].toLocaleLowerCase() === key.toLocaleLowerCase()) { + return headers[i + 1] + } + } + + return undefined + } else if (typeof headers.get === 'function') { + return headers.get(key) + } else { + return lowerCaseEntries(headers)[key.toLocaleLowerCase()] + } +} + +/** @param {string[]} headers */ +function buildHeadersFromArray (headers) { // fetch HeadersList + const clone = headers.slice() + const entries = [] + for (let index = 0; index < clone.length; index += 2) { + entries.push([clone[index], clone[index + 1]]) + } + return Object.fromEntries(entries) +} + +function matchHeaders (mockDispatch, headers) { + if (typeof mockDispatch.headers === 'function') { + if (Array.isArray(headers)) { // fetch HeadersList + headers = buildHeadersFromArray(headers) + } + return mockDispatch.headers(headers ? lowerCaseEntries(headers) : {}) + } + if (typeof mockDispatch.headers === 'undefined') { + return true + } + if (typeof headers !== 'object' || typeof mockDispatch.headers !== 'object') { + return false + } + + for (const [matchHeaderName, matchHeaderValue] of Object.entries(mockDispatch.headers)) { + const headerValue = getHeaderByName(headers, matchHeaderName) + + if (!matchValue(matchHeaderValue, headerValue)) { + return false + } + } + return true +} + +function safeUrl (path) { + if (typeof path !== 'string') { + return path + } + + const pathSegments = path.split('?') + + if (pathSegments.length !== 2) { + return path + } + + const qp = new URLSearchParams(pathSegments.pop()) + qp.sort() + return [...pathSegments, qp.toString()].join('?') +} + +function matchKey (mockDispatch, { path, method, body, headers }) { + const pathMatch = matchValue(mockDispatch.path, path) + const methodMatch = matchValue(mockDispatch.method, method) + const bodyMatch = typeof mockDispatch.body !== 'undefined' ? matchValue(mockDispatch.body, body) : true + const headersMatch = matchHeaders(mockDispatch, headers) + return pathMatch && methodMatch && bodyMatch && headersMatch +} + +function getResponseData (data) { + if (Buffer.isBuffer(data)) { + return data + } else if (typeof data === 'object') { + return JSON.stringify(data) + } else { + return data.toString() + } +} + +function getMockDispatch (mockDispatches, key) { + const basePath = key.query ? buildURL(key.path, key.query) : key.path + const resolvedPath = typeof basePath === 'string' ? safeUrl(basePath) : basePath + + // Match path + let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path }) => matchValue(safeUrl(path), resolvedPath)) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for path '${resolvedPath}'`) + } + + // Match method + matchedMockDispatches = matchedMockDispatches.filter(({ method }) => matchValue(method, key.method)) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for method '${key.method}'`) + } + + // Match body + matchedMockDispatches = matchedMockDispatches.filter(({ body }) => typeof body !== 'undefined' ? matchValue(body, key.body) : true) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for body '${key.body}'`) + } + + // Match headers + matchedMockDispatches = matchedMockDispatches.filter((mockDispatch) => matchHeaders(mockDispatch, key.headers)) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for headers '${typeof key.headers === 'object' ? JSON.stringify(key.headers) : key.headers}'`) + } + + return matchedMockDispatches[0] +} + +function addMockDispatch (mockDispatches, key, data) { + const baseData = { timesInvoked: 0, times: 1, persist: false, consumed: false } + const replyData = typeof data === 'function' ? { callback: data } : { ...data } + const newMockDispatch = { ...baseData, ...key, pending: true, data: { error: null, ...replyData } } + mockDispatches.push(newMockDispatch) + return newMockDispatch +} + +function deleteMockDispatch (mockDispatches, key) { + const index = mockDispatches.findIndex(dispatch => { + if (!dispatch.consumed) { + return false + } + return matchKey(dispatch, key) + }) + if (index !== -1) { + mockDispatches.splice(index, 1) + } +} + +function buildKey (opts) { + const { path, method, body, headers, query } = opts + return { + path, + method, + body, + headers, + query + } +} + +function generateKeyValues (data) { + return Object.entries(data).reduce((keyValuePairs, [key, value]) => [ + ...keyValuePairs, + Buffer.from(`${key}`), + Array.isArray(value) ? value.map(x => Buffer.from(`${x}`)) : Buffer.from(`${value}`) + ], []) +} + +/** + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status + * @param {number} statusCode + */ +function getStatusText (statusCode) { + return STATUS_CODES[statusCode] || 'unknown' +} + +async function getResponse (body) { + const buffers = [] + for await (const data of body) { + buffers.push(data) + } + return Buffer.concat(buffers).toString('utf8') +} + +/** + * Mock dispatch function used to simulate undici dispatches + */ +function mockDispatch (opts, handler) { + // Get mock dispatch from built key + const key = buildKey(opts) + const mockDispatch = getMockDispatch(this[kDispatches], key) + + mockDispatch.timesInvoked++ + + // Here's where we resolve a callback if a callback is present for the dispatch data. + if (mockDispatch.data.callback) { + mockDispatch.data = { ...mockDispatch.data, ...mockDispatch.data.callback(opts) } + } + + // Parse mockDispatch data + const { data: { statusCode, data, headers, trailers, error }, delay, persist } = mockDispatch + const { timesInvoked, times } = mockDispatch + + // If it's used up and not persistent, mark as consumed + mockDispatch.consumed = !persist && timesInvoked >= times + mockDispatch.pending = timesInvoked < times + + // If specified, trigger dispatch error + if (error !== null) { + deleteMockDispatch(this[kDispatches], key) + handler.onError(error) + return true + } + + // Handle the request with a delay if necessary + if (typeof delay === 'number' && delay > 0) { + setTimeout(() => { + handleReply(this[kDispatches]) + }, delay) + } else { + handleReply(this[kDispatches]) + } + + function handleReply (mockDispatches, _data = data) { + // fetch's HeadersList is a 1D string array + const optsHeaders = Array.isArray(opts.headers) + ? buildHeadersFromArray(opts.headers) + : opts.headers + const body = typeof _data === 'function' + ? _data({ ...opts, headers: optsHeaders }) + : _data + + // util.types.isPromise is likely needed for jest. + if (isPromise(body)) { + // If handleReply is asynchronous, throwing an error + // in the callback will reject the promise, rather than + // synchronously throw the error, which breaks some tests. + // Rather, we wait for the callback to resolve if it is a + // promise, and then re-run handleReply with the new body. + body.then((newData) => handleReply(mockDispatches, newData)) + return + } + + const responseData = getResponseData(body) + const responseHeaders = generateKeyValues(headers) + const responseTrailers = generateKeyValues(trailers) + + handler.abort = nop + handler.onHeaders(statusCode, responseHeaders, resume, getStatusText(statusCode)) + handler.onData(Buffer.from(responseData)) + handler.onComplete(responseTrailers) + deleteMockDispatch(mockDispatches, key) + } + + function resume () {} + + return true +} + +function buildMockDispatch () { + const agent = this[kMockAgent] + const origin = this[kOrigin] + const originalDispatch = this[kOriginalDispatch] + + return function dispatch (opts, handler) { + if (agent.isMockActive) { + try { + mockDispatch.call(this, opts, handler) + } catch (error) { + if (error instanceof MockNotMatchedError) { + const netConnect = agent[kGetNetConnect]() + if (netConnect === false) { + throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`) + } + if (checkNetConnect(netConnect, origin)) { + originalDispatch.call(this, opts, handler) + } else { + throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)`) + } + } else { + throw error + } + } + } else { + originalDispatch.call(this, opts, handler) + } + } +} + +function checkNetConnect (netConnect, origin) { + const url = new URL(origin) + if (netConnect === true) { + return true + } else if (Array.isArray(netConnect) && netConnect.some((matcher) => matchValue(matcher, url.host))) { + return true + } + return false +} + +function buildMockOptions (opts) { + if (opts) { + const { agent, ...mockOptions } = opts + return mockOptions + } +} + +module.exports = { + getResponseData, + getMockDispatch, + addMockDispatch, + deleteMockDispatch, + buildKey, + generateKeyValues, + matchValue, + getResponse, + getStatusText, + mockDispatch, + buildMockDispatch, + checkNetConnect, + buildMockOptions, + getHeaderByName +} + + +/***/ }), + +/***/ 3546: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { Transform } = __nccwpck_require__(2203) +const { Console } = __nccwpck_require__(4236) + +/** + * Gets the output of `console.table(…)` as a string. + */ +module.exports = class PendingInterceptorsFormatter { + constructor ({ disableColors } = {}) { + this.transform = new Transform({ + transform (chunk, _enc, cb) { + cb(null, chunk) + } + }) + + this.logger = new Console({ + stdout: this.transform, + inspectOptions: { + colors: !disableColors && !process.env.CI + } + }) + } + + format (pendingInterceptors) { + const withPrettyHeaders = pendingInterceptors.map( + ({ method, path, data: { statusCode }, persist, times, timesInvoked, origin }) => ({ + Method: method, + Origin: origin, + Path: path, + 'Status code': statusCode, + Persistent: persist ? '✅' : '❌', + Invocations: timesInvoked, + Remaining: persist ? Infinity : times - timesInvoked + })) + + this.logger.table(withPrettyHeaders) + return this.transform.read().toString() + } +} + + +/***/ }), + +/***/ 4301: +/***/ ((module) => { + +"use strict"; + + +const singulars = { + pronoun: 'it', + is: 'is', + was: 'was', + this: 'this' +} + +const plurals = { + pronoun: 'they', + is: 'are', + was: 'were', + this: 'these' +} + +module.exports = class Pluralizer { + constructor (singular, plural) { + this.singular = singular + this.plural = plural + } + + pluralize (count) { + const one = count === 1 + const keys = one ? singulars : plurals + const noun = one ? this.singular : this.plural + return { ...keys, count, noun } + } +} + + +/***/ }), + +/***/ 7825: +/***/ ((module) => { + +"use strict"; +/* eslint-disable */ + + + +// Extracted from node/lib/internal/fixed_queue.js + +// Currently optimal queue size, tested on V8 6.0 - 6.6. Must be power of two. +const kSize = 2048; +const kMask = kSize - 1; + +// The FixedQueue is implemented as a singly-linked list of fixed-size +// circular buffers. It looks something like this: +// +// head tail +// | | +// v v +// +-----------+ <-----\ +-----------+ <------\ +-----------+ +// | [null] | \----- | next | \------- | next | +// +-----------+ +-----------+ +-----------+ +// | item | <-- bottom | item | <-- bottom | [empty] | +// | item | | item | | [empty] | +// | item | | item | | [empty] | +// | item | | item | | [empty] | +// | item | | item | bottom --> | item | +// | item | | item | | item | +// | ... | | ... | | ... | +// | item | | item | | item | +// | item | | item | | item | +// | [empty] | <-- top | item | | item | +// | [empty] | | item | | item | +// | [empty] | | [empty] | <-- top top --> | [empty] | +// +-----------+ +-----------+ +-----------+ +// +// Or, if there is only one circular buffer, it looks something +// like either of these: +// +// head tail head tail +// | | | | +// v v v v +// +-----------+ +-----------+ +// | [null] | | [null] | +// +-----------+ +-----------+ +// | [empty] | | item | +// | [empty] | | item | +// | item | <-- bottom top --> | [empty] | +// | item | | [empty] | +// | [empty] | <-- top bottom --> | item | +// | [empty] | | item | +// +-----------+ +-----------+ +// +// Adding a value means moving `top` forward by one, removing means +// moving `bottom` forward by one. After reaching the end, the queue +// wraps around. +// +// When `top === bottom` the current queue is empty and when +// `top + 1 === bottom` it's full. This wastes a single space of storage +// but allows much quicker checks. + +class FixedCircularBuffer { + constructor() { + this.bottom = 0; + this.top = 0; + this.list = new Array(kSize); + this.next = null; + } + + isEmpty() { + return this.top === this.bottom; + } + + isFull() { + return ((this.top + 1) & kMask) === this.bottom; + } + + push(data) { + this.list[this.top] = data; + this.top = (this.top + 1) & kMask; + } + + shift() { + const nextItem = this.list[this.bottom]; + if (nextItem === undefined) + return null; + this.list[this.bottom] = undefined; + this.bottom = (this.bottom + 1) & kMask; + return nextItem; + } +} + +module.exports = class FixedQueue { + constructor() { + this.head = this.tail = new FixedCircularBuffer(); + } + + isEmpty() { + return this.head.isEmpty(); + } + + push(data) { + if (this.head.isFull()) { + // Head is full: Creates a new queue, sets the old queue's `.next` to it, + // and sets it as the new main queue. + this.head = this.head.next = new FixedCircularBuffer(); + } + this.head.push(data); + } + + shift() { + const tail = this.tail; + const next = tail.shift(); + if (tail.isEmpty() && tail.next !== null) { + // If there is another queue, it forms the new tail. + this.tail = tail.next; + } + return next; + } +}; + + +/***/ }), + +/***/ 7556: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const DispatcherBase = __nccwpck_require__(3301) +const FixedQueue = __nccwpck_require__(7825) +const { kConnected, kSize, kRunning, kPending, kQueued, kBusy, kFree, kUrl, kClose, kDestroy, kDispatch } = __nccwpck_require__(9583) +const PoolStats = __nccwpck_require__(3154) + +const kClients = Symbol('clients') +const kNeedDrain = Symbol('needDrain') +const kQueue = Symbol('queue') +const kClosedResolve = Symbol('closed resolve') +const kOnDrain = Symbol('onDrain') +const kOnConnect = Symbol('onConnect') +const kOnDisconnect = Symbol('onDisconnect') +const kOnConnectionError = Symbol('onConnectionError') +const kGetDispatcher = Symbol('get dispatcher') +const kAddClient = Symbol('add client') +const kRemoveClient = Symbol('remove client') +const kStats = Symbol('stats') + +class PoolBase extends DispatcherBase { + constructor () { + super() + + this[kQueue] = new FixedQueue() + this[kClients] = [] + this[kQueued] = 0 + + const pool = this + + this[kOnDrain] = function onDrain (origin, targets) { + const queue = pool[kQueue] + + let needDrain = false + + while (!needDrain) { + const item = queue.shift() + if (!item) { + break + } + pool[kQueued]-- + needDrain = !this.dispatch(item.opts, item.handler) + } + + this[kNeedDrain] = needDrain + + if (!this[kNeedDrain] && pool[kNeedDrain]) { + pool[kNeedDrain] = false + pool.emit('drain', origin, [pool, ...targets]) + } + + if (pool[kClosedResolve] && queue.isEmpty()) { + Promise + .all(pool[kClients].map(c => c.close())) + .then(pool[kClosedResolve]) + } + } + + this[kOnConnect] = (origin, targets) => { + pool.emit('connect', origin, [pool, ...targets]) + } + + this[kOnDisconnect] = (origin, targets, err) => { + pool.emit('disconnect', origin, [pool, ...targets], err) + } + + this[kOnConnectionError] = (origin, targets, err) => { + pool.emit('connectionError', origin, [pool, ...targets], err) + } + + this[kStats] = new PoolStats(this) + } + + get [kBusy] () { + return this[kNeedDrain] + } + + get [kConnected] () { + return this[kClients].filter(client => client[kConnected]).length + } + + get [kFree] () { + return this[kClients].filter(client => client[kConnected] && !client[kNeedDrain]).length + } + + get [kPending] () { + let ret = this[kQueued] + for (const { [kPending]: pending } of this[kClients]) { + ret += pending + } + return ret + } + + get [kRunning] () { + let ret = 0 + for (const { [kRunning]: running } of this[kClients]) { + ret += running + } + return ret + } + + get [kSize] () { + let ret = this[kQueued] + for (const { [kSize]: size } of this[kClients]) { + ret += size + } + return ret + } + + get stats () { + return this[kStats] + } + + async [kClose] () { + if (this[kQueue].isEmpty()) { + return Promise.all(this[kClients].map(c => c.close())) + } else { + return new Promise((resolve) => { + this[kClosedResolve] = resolve + }) + } + } + + async [kDestroy] (err) { + while (true) { + const item = this[kQueue].shift() + if (!item) { + break + } + item.handler.onError(err) + } + + return Promise.all(this[kClients].map(c => c.destroy(err))) + } + + [kDispatch] (opts, handler) { + const dispatcher = this[kGetDispatcher]() + + if (!dispatcher) { + this[kNeedDrain] = true + this[kQueue].push({ opts, handler }) + this[kQueued]++ + } else if (!dispatcher.dispatch(opts, handler)) { + dispatcher[kNeedDrain] = true + this[kNeedDrain] = !this[kGetDispatcher]() + } + + return !this[kNeedDrain] + } + + [kAddClient] (client) { + client + .on('drain', this[kOnDrain]) + .on('connect', this[kOnConnect]) + .on('disconnect', this[kOnDisconnect]) + .on('connectionError', this[kOnConnectionError]) + + this[kClients].push(client) + + if (this[kNeedDrain]) { + process.nextTick(() => { + if (this[kNeedDrain]) { + this[kOnDrain](client[kUrl], [this, client]) + } + }) + } + + return this + } + + [kRemoveClient] (client) { + client.close(() => { + const idx = this[kClients].indexOf(client) + if (idx !== -1) { + this[kClients].splice(idx, 1) + } + }) + + this[kNeedDrain] = this[kClients].some(dispatcher => ( + !dispatcher[kNeedDrain] && + dispatcher.closed !== true && + dispatcher.destroyed !== true + )) + } +} + +module.exports = { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kRemoveClient, + kGetDispatcher +} + + +/***/ }), + +/***/ 3154: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const { kFree, kConnected, kPending, kQueued, kRunning, kSize } = __nccwpck_require__(9583) +const kPool = Symbol('pool') + +class PoolStats { + constructor (pool) { + this[kPool] = pool + } + + get connected () { + return this[kPool][kConnected] + } + + get free () { + return this[kPool][kFree] + } + + get pending () { + return this[kPool][kPending] + } + + get queued () { + return this[kPool][kQueued] + } + + get running () { + return this[kPool][kRunning] + } + + get size () { + return this[kPool][kSize] + } +} + +module.exports = PoolStats + + +/***/ }), + +/***/ 6976: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kGetDispatcher +} = __nccwpck_require__(7556) +const Client = __nccwpck_require__(5833) +const { + InvalidArgumentError +} = __nccwpck_require__(1975) +const util = __nccwpck_require__(1436) +const { kUrl, kInterceptors } = __nccwpck_require__(9583) +const buildConnector = __nccwpck_require__(3940) + +const kOptions = Symbol('options') +const kConnections = Symbol('connections') +const kFactory = Symbol('factory') + +function defaultFactory (origin, opts) { + return new Client(origin, opts) +} + +class Pool extends PoolBase { + constructor (origin, { + connections, + factory = defaultFactory, + connect, + connectTimeout, + tls, + maxCachedSessions, + socketPath, + autoSelectFamily, + autoSelectFamilyAttemptTimeout, + allowH2, + ...options + } = {}) { + super() + + if (connections != null && (!Number.isFinite(connections) || connections < 0)) { + throw new InvalidArgumentError('invalid connections') + } + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { + throw new InvalidArgumentError('connect must be a function or an object') + } + + if (typeof connect !== 'function') { + connect = buildConnector({ + ...tls, + maxCachedSessions, + allowH2, + socketPath, + timeout: connectTimeout, + ...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), + ...connect + }) + } + + this[kInterceptors] = options.interceptors && options.interceptors.Pool && Array.isArray(options.interceptors.Pool) + ? options.interceptors.Pool + : [] + this[kConnections] = connections || null + this[kUrl] = util.parseOrigin(origin) + this[kOptions] = { ...util.deepClone(options), connect, allowH2 } + this[kOptions].interceptors = options.interceptors + ? { ...options.interceptors } + : undefined + this[kFactory] = factory + } + + [kGetDispatcher] () { + let dispatcher = this[kClients].find(dispatcher => !dispatcher[kNeedDrain]) + + if (dispatcher) { + return dispatcher + } + + if (!this[kConnections] || this[kClients].length < this[kConnections]) { + dispatcher = this[kFactory](this[kUrl], this[kOptions]) + this[kAddClient](dispatcher) + } + + return dispatcher + } +} + +module.exports = Pool + + +/***/ }), + +/***/ 3644: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kProxy, kClose, kDestroy, kInterceptors } = __nccwpck_require__(9583) +const { URL } = __nccwpck_require__(7016) +const Agent = __nccwpck_require__(2841) +const Pool = __nccwpck_require__(6976) +const DispatcherBase = __nccwpck_require__(3301) +const { InvalidArgumentError, RequestAbortedError } = __nccwpck_require__(1975) +const buildConnector = __nccwpck_require__(3940) + +const kAgent = Symbol('proxy agent') +const kClient = Symbol('proxy client') +const kProxyHeaders = Symbol('proxy headers') +const kRequestTls = Symbol('request tls settings') +const kProxyTls = Symbol('proxy tls settings') +const kConnectEndpoint = Symbol('connect endpoint function') + +function defaultProtocolPort (protocol) { + return protocol === 'https:' ? 443 : 80 +} + +function buildProxyOptions (opts) { + if (typeof opts === 'string') { + opts = { uri: opts } + } + + if (!opts || !opts.uri) { + throw new InvalidArgumentError('Proxy opts.uri is mandatory') + } + + return { + uri: opts.uri, + protocol: opts.protocol || 'https' + } +} + +function defaultFactory (origin, opts) { + return new Pool(origin, opts) +} + +class ProxyAgent extends DispatcherBase { + constructor (opts) { + super(opts) + this[kProxy] = buildProxyOptions(opts) + this[kAgent] = new Agent(opts) + this[kInterceptors] = opts.interceptors && opts.interceptors.ProxyAgent && Array.isArray(opts.interceptors.ProxyAgent) + ? opts.interceptors.ProxyAgent + : [] + + if (typeof opts === 'string') { + opts = { uri: opts } + } + + if (!opts || !opts.uri) { + throw new InvalidArgumentError('Proxy opts.uri is mandatory') + } + + const { clientFactory = defaultFactory } = opts + + if (typeof clientFactory !== 'function') { + throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.') + } + + this[kRequestTls] = opts.requestTls + this[kProxyTls] = opts.proxyTls + this[kProxyHeaders] = opts.headers || {} + + const resolvedUrl = new URL(opts.uri) + const { origin, port, host, username, password } = resolvedUrl + + if (opts.auth && opts.token) { + throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token') + } else if (opts.auth) { + /* @deprecated in favour of opts.token */ + this[kProxyHeaders]['proxy-authorization'] = `Basic ${opts.auth}` + } else if (opts.token) { + this[kProxyHeaders]['proxy-authorization'] = opts.token + } else if (username && password) { + this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}` + } + + const connect = buildConnector({ ...opts.proxyTls }) + this[kConnectEndpoint] = buildConnector({ ...opts.requestTls }) + this[kClient] = clientFactory(resolvedUrl, { connect }) + this[kAgent] = new Agent({ + ...opts, + connect: async (opts, callback) => { + let requestedHost = opts.host + if (!opts.port) { + requestedHost += `:${defaultProtocolPort(opts.protocol)}` + } + try { + const { socket, statusCode } = await this[kClient].connect({ + origin, + port, + path: requestedHost, + signal: opts.signal, + headers: { + ...this[kProxyHeaders], + host + } + }) + if (statusCode !== 200) { + socket.on('error', () => {}).destroy() + callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`)) + } + if (opts.protocol !== 'https:') { + callback(null, socket) + return + } + let servername + if (this[kRequestTls]) { + servername = this[kRequestTls].servername + } else { + servername = opts.servername + } + this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback) + } catch (err) { + callback(err) + } + } + }) + } + + dispatch (opts, handler) { + const { host } = new URL(opts.origin) + const headers = buildHeaders(opts.headers) + throwIfProxyAuthIsSent(headers) + return this[kAgent].dispatch( + { + ...opts, + headers: { + ...headers, + host + } + }, + handler + ) + } + + async [kClose] () { + await this[kAgent].close() + await this[kClient].close() + } + + async [kDestroy] () { + await this[kAgent].destroy() + await this[kClient].destroy() + } +} + +/** + * @param {string[] | Record} headers + * @returns {Record} + */ +function buildHeaders (headers) { + // When using undici.fetch, the headers list is stored + // as an array. + if (Array.isArray(headers)) { + /** @type {Record} */ + const headersPair = {} + + for (let i = 0; i < headers.length; i += 2) { + headersPair[headers[i]] = headers[i + 1] + } + + return headersPair + } + + return headers +} + +/** + * @param {Record} headers + * + * Previous versions of ProxyAgent suggests the Proxy-Authorization in request headers + * Nevertheless, it was changed and to avoid a security vulnerability by end users + * this check was created. + * It should be removed in the next major version for performance reasons + */ +function throwIfProxyAuthIsSent (headers) { + const existProxyAuth = headers && Object.keys(headers) + .find((key) => key.toLowerCase() === 'proxy-authorization') + if (existProxyAuth) { + throw new InvalidArgumentError('Proxy-Authorization should be sent in ProxyAgent constructor') + } +} + +module.exports = ProxyAgent + + +/***/ }), + +/***/ 2688: +/***/ ((module) => { + +"use strict"; + + +let fastNow = Date.now() +let fastNowTimeout + +const fastTimers = [] + +function onTimeout () { + fastNow = Date.now() + + let len = fastTimers.length + let idx = 0 + while (idx < len) { + const timer = fastTimers[idx] + + if (timer.state === 0) { + timer.state = fastNow + timer.delay + } else if (timer.state > 0 && fastNow >= timer.state) { + timer.state = -1 + timer.callback(timer.opaque) + } + + if (timer.state === -1) { + timer.state = -2 + if (idx !== len - 1) { + fastTimers[idx] = fastTimers.pop() + } else { + fastTimers.pop() + } + len -= 1 + } else { + idx += 1 + } + } + + if (fastTimers.length > 0) { + refreshTimeout() + } +} + +function refreshTimeout () { + if (fastNowTimeout && fastNowTimeout.refresh) { + fastNowTimeout.refresh() + } else { + clearTimeout(fastNowTimeout) + fastNowTimeout = setTimeout(onTimeout, 1e3) + if (fastNowTimeout.unref) { + fastNowTimeout.unref() + } + } +} + +class Timeout { + constructor (callback, delay, opaque) { + this.callback = callback + this.delay = delay + this.opaque = opaque + + // -2 not in timer list + // -1 in timer list but inactive + // 0 in timer list waiting for time + // > 0 in timer list waiting for time to expire + this.state = -2 + + this.refresh() + } + + refresh () { + if (this.state === -2) { + fastTimers.push(this) + if (!fastNowTimeout || fastTimers.length === 1) { + refreshTimeout() + } + } + + this.state = 0 + } + + clear () { + this.state = -1 + } +} + +module.exports = { + setTimeout (callback, delay, opaque) { + return delay < 1e3 + ? setTimeout(callback, delay, opaque) + : new Timeout(callback, delay, opaque) + }, + clearTimeout (timeout) { + if (timeout instanceof Timeout) { + timeout.clear() + } else { + clearTimeout(timeout) + } + } +} + + +/***/ }), + +/***/ 8530: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const diagnosticsChannel = __nccwpck_require__(1637) +const { uid, states } = __nccwpck_require__(3341) +const { + kReadyState, + kSentClose, + kByteParser, + kReceivedClose +} = __nccwpck_require__(5865) +const { fireEvent, failWebsocketConnection } = __nccwpck_require__(5026) +const { CloseEvent } = __nccwpck_require__(7739) +const { makeRequest } = __nccwpck_require__(1558) +const { fetching } = __nccwpck_require__(1279) +const { Headers } = __nccwpck_require__(161) +const { getGlobalDispatcher } = __nccwpck_require__(8377) +const { kHeadersList } = __nccwpck_require__(9583) + +const channels = {} +channels.open = diagnosticsChannel.channel('undici:websocket:open') +channels.close = diagnosticsChannel.channel('undici:websocket:close') +channels.socketError = diagnosticsChannel.channel('undici:websocket:socket_error') + +/** @type {import('crypto')} */ +let crypto +try { + crypto = __nccwpck_require__(6982) +} catch { + +} + +/** + * @see https://websockets.spec.whatwg.org/#concept-websocket-establish + * @param {URL} url + * @param {string|string[]} protocols + * @param {import('./websocket').WebSocket} ws + * @param {(response: any) => void} onEstablish + * @param {Partial} options + */ +function establishWebSocketConnection (url, protocols, ws, onEstablish, options) { + // 1. Let requestURL be a copy of url, with its scheme set to "http", if url’s + // scheme is "ws", and to "https" otherwise. + const requestURL = url + + requestURL.protocol = url.protocol === 'ws:' ? 'http:' : 'https:' + + // 2. Let request be a new request, whose URL is requestURL, client is client, + // service-workers mode is "none", referrer is "no-referrer", mode is + // "websocket", credentials mode is "include", cache mode is "no-store" , + // and redirect mode is "error". + const request = makeRequest({ + urlList: [requestURL], + serviceWorkers: 'none', + referrer: 'no-referrer', + mode: 'websocket', + credentials: 'include', + cache: 'no-store', + redirect: 'error' + }) + + // Note: undici extension, allow setting custom headers. + if (options.headers) { + const headersList = new Headers(options.headers)[kHeadersList] + + request.headersList = headersList + } + + // 3. Append (`Upgrade`, `websocket`) to request’s header list. + // 4. Append (`Connection`, `Upgrade`) to request’s header list. + // Note: both of these are handled by undici currently. + // https://github.com/nodejs/undici/blob/68c269c4144c446f3f1220951338daef4a6b5ec4/lib/client.js#L1397 + + // 5. Let keyValue be a nonce consisting of a randomly selected + // 16-byte value that has been forgiving-base64-encoded and + // isomorphic encoded. + const keyValue = crypto.randomBytes(16).toString('base64') + + // 6. Append (`Sec-WebSocket-Key`, keyValue) to request’s + // header list. + request.headersList.append('sec-websocket-key', keyValue) + + // 7. Append (`Sec-WebSocket-Version`, `13`) to request’s + // header list. + request.headersList.append('sec-websocket-version', '13') + + // 8. For each protocol in protocols, combine + // (`Sec-WebSocket-Protocol`, protocol) in request’s header + // list. + for (const protocol of protocols) { + request.headersList.append('sec-websocket-protocol', protocol) + } + + // 9. Let permessageDeflate be a user-agent defined + // "permessage-deflate" extension header value. + // https://github.com/mozilla/gecko-dev/blob/ce78234f5e653a5d3916813ff990f053510227bc/netwerk/protocol/websocket/WebSocketChannel.cpp#L2673 + // TODO: enable once permessage-deflate is supported + const permessageDeflate = '' // 'permessage-deflate; 15' + + // 10. Append (`Sec-WebSocket-Extensions`, permessageDeflate) to + // request’s header list. + // request.headersList.append('sec-websocket-extensions', permessageDeflate) + + // 11. Fetch request with useParallelQueue set to true, and + // processResponse given response being these steps: + const controller = fetching({ + request, + useParallelQueue: true, + dispatcher: options.dispatcher ?? getGlobalDispatcher(), + processResponse (response) { + // 1. If response is a network error or its status is not 101, + // fail the WebSocket connection. + if (response.type === 'error' || response.status !== 101) { + failWebsocketConnection(ws, 'Received network error or non-101 status code.') + return + } + + // 2. If protocols is not the empty list and extracting header + // list values given `Sec-WebSocket-Protocol` and response’s + // header list results in null, failure, or the empty byte + // sequence, then fail the WebSocket connection. + if (protocols.length !== 0 && !response.headersList.get('Sec-WebSocket-Protocol')) { + failWebsocketConnection(ws, 'Server did not respond with sent protocols.') + return + } + + // 3. Follow the requirements stated step 2 to step 6, inclusive, + // of the last set of steps in section 4.1 of The WebSocket + // Protocol to validate response. This either results in fail + // the WebSocket connection or the WebSocket connection is + // established. + + // 2. If the response lacks an |Upgrade| header field or the |Upgrade| + // header field contains a value that is not an ASCII case- + // insensitive match for the value "websocket", the client MUST + // _Fail the WebSocket Connection_. + if (response.headersList.get('Upgrade')?.toLowerCase() !== 'websocket') { + failWebsocketConnection(ws, 'Server did not set Upgrade header to "websocket".') + return + } + + // 3. If the response lacks a |Connection| header field or the + // |Connection| header field doesn't contain a token that is an + // ASCII case-insensitive match for the value "Upgrade", the client + // MUST _Fail the WebSocket Connection_. + if (response.headersList.get('Connection')?.toLowerCase() !== 'upgrade') { + failWebsocketConnection(ws, 'Server did not set Connection header to "upgrade".') + return + } + + // 4. If the response lacks a |Sec-WebSocket-Accept| header field or + // the |Sec-WebSocket-Accept| contains a value other than the + // base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket- + // Key| (as a string, not base64-decoded) with the string "258EAFA5- + // E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and + // trailing whitespace, the client MUST _Fail the WebSocket + // Connection_. + const secWSAccept = response.headersList.get('Sec-WebSocket-Accept') + const digest = crypto.createHash('sha1').update(keyValue + uid).digest('base64') + if (secWSAccept !== digest) { + failWebsocketConnection(ws, 'Incorrect hash received in Sec-WebSocket-Accept header.') + return + } + + // 5. If the response includes a |Sec-WebSocket-Extensions| header + // field and this header field indicates the use of an extension + // that was not present in the client's handshake (the server has + // indicated an extension not requested by the client), the client + // MUST _Fail the WebSocket Connection_. (The parsing of this + // header field to determine which extensions are requested is + // discussed in Section 9.1.) + const secExtension = response.headersList.get('Sec-WebSocket-Extensions') + + if (secExtension !== null && secExtension !== permessageDeflate) { + failWebsocketConnection(ws, 'Received different permessage-deflate than the one set.') + return + } + + // 6. If the response includes a |Sec-WebSocket-Protocol| header field + // and this header field indicates the use of a subprotocol that was + // not present in the client's handshake (the server has indicated a + // subprotocol not requested by the client), the client MUST _Fail + // the WebSocket Connection_. + const secProtocol = response.headersList.get('Sec-WebSocket-Protocol') + + if (secProtocol !== null && secProtocol !== request.headersList.get('Sec-WebSocket-Protocol')) { + failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.') + return + } + + response.socket.on('data', onSocketData) + response.socket.on('close', onSocketClose) + response.socket.on('error', onSocketError) + + if (channels.open.hasSubscribers) { + channels.open.publish({ + address: response.socket.address(), + protocol: secProtocol, + extensions: secExtension + }) + } + + onEstablish(response) + } + }) + + return controller +} + +/** + * @param {Buffer} chunk + */ +function onSocketData (chunk) { + if (!this.ws[kByteParser].write(chunk)) { + this.pause() + } +} + +/** + * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol + * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.4 + */ +function onSocketClose () { + const { ws } = this + + // If the TCP connection was closed after the + // WebSocket closing handshake was completed, the WebSocket connection + // is said to have been closed _cleanly_. + const wasClean = ws[kSentClose] && ws[kReceivedClose] + + let code = 1005 + let reason = '' + + const result = ws[kByteParser].closingInfo + + if (result) { + code = result.code ?? 1005 + reason = result.reason + } else if (!ws[kSentClose]) { + // If _The WebSocket + // Connection is Closed_ and no Close control frame was received by the + // endpoint (such as could occur if the underlying transport connection + // is lost), _The WebSocket Connection Close Code_ is considered to be + // 1006. + code = 1006 + } + + // 1. Change the ready state to CLOSED (3). + ws[kReadyState] = states.CLOSED + + // 2. If the user agent was required to fail the WebSocket + // connection, or if the WebSocket connection was closed + // after being flagged as full, fire an event named error + // at the WebSocket object. + // TODO + + // 3. Fire an event named close at the WebSocket object, + // using CloseEvent, with the wasClean attribute + // initialized to true if the connection closed cleanly + // and false otherwise, the code attribute initialized to + // the WebSocket connection close code, and the reason + // attribute initialized to the result of applying UTF-8 + // decode without BOM to the WebSocket connection close + // reason. + fireEvent('close', ws, CloseEvent, { + wasClean, code, reason + }) + + if (channels.close.hasSubscribers) { + channels.close.publish({ + websocket: ws, + code, + reason + }) + } +} + +function onSocketError (error) { + const { ws } = this + + ws[kReadyState] = states.CLOSING + + if (channels.socketError.hasSubscribers) { + channels.socketError.publish(error) + } + + this.destroy() +} + +module.exports = { + establishWebSocketConnection +} + + +/***/ }), + +/***/ 3341: +/***/ ((module) => { + +"use strict"; + + +// This is a Globally Unique Identifier unique used +// to validate that the endpoint accepts websocket +// connections. +// See https://www.rfc-editor.org/rfc/rfc6455.html#section-1.3 +const uid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' + +/** @type {PropertyDescriptor} */ +const staticPropertyDescriptors = { + enumerable: true, + writable: false, + configurable: false +} + +const states = { + CONNECTING: 0, + OPEN: 1, + CLOSING: 2, + CLOSED: 3 +} + +const opcodes = { + CONTINUATION: 0x0, + TEXT: 0x1, + BINARY: 0x2, + CLOSE: 0x8, + PING: 0x9, + PONG: 0xA +} + +const maxUnsigned16Bit = 2 ** 16 - 1 // 65535 + +const parserStates = { + INFO: 0, + PAYLOADLENGTH_16: 2, + PAYLOADLENGTH_64: 3, + READ_DATA: 4 +} + +const emptyBuffer = Buffer.allocUnsafe(0) + +module.exports = { + uid, + staticPropertyDescriptors, + states, + opcodes, + maxUnsigned16Bit, + parserStates, + emptyBuffer +} + + +/***/ }), + +/***/ 7739: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { webidl } = __nccwpck_require__(274) +const { kEnumerableProperty } = __nccwpck_require__(1436) +const { MessagePort } = __nccwpck_require__(8167) + +/** + * @see https://html.spec.whatwg.org/multipage/comms.html#messageevent + */ +class MessageEvent extends Event { + #eventInit + + constructor (type, eventInitDict = {}) { + webidl.argumentLengthCheck(arguments, 1, { header: 'MessageEvent constructor' }) + + type = webidl.converters.DOMString(type) + eventInitDict = webidl.converters.MessageEventInit(eventInitDict) + + super(type, eventInitDict) + + this.#eventInit = eventInitDict + } + + get data () { + webidl.brandCheck(this, MessageEvent) + + return this.#eventInit.data + } + + get origin () { + webidl.brandCheck(this, MessageEvent) + + return this.#eventInit.origin + } + + get lastEventId () { + webidl.brandCheck(this, MessageEvent) + + return this.#eventInit.lastEventId + } + + get source () { + webidl.brandCheck(this, MessageEvent) + + return this.#eventInit.source + } + + get ports () { + webidl.brandCheck(this, MessageEvent) + + if (!Object.isFrozen(this.#eventInit.ports)) { + Object.freeze(this.#eventInit.ports) + } + + return this.#eventInit.ports + } + + initMessageEvent ( + type, + bubbles = false, + cancelable = false, + data = null, + origin = '', + lastEventId = '', + source = null, + ports = [] + ) { + webidl.brandCheck(this, MessageEvent) + + webidl.argumentLengthCheck(arguments, 1, { header: 'MessageEvent.initMessageEvent' }) + + return new MessageEvent(type, { + bubbles, cancelable, data, origin, lastEventId, source, ports + }) + } +} + +/** + * @see https://websockets.spec.whatwg.org/#the-closeevent-interface + */ +class CloseEvent extends Event { + #eventInit + + constructor (type, eventInitDict = {}) { + webidl.argumentLengthCheck(arguments, 1, { header: 'CloseEvent constructor' }) + + type = webidl.converters.DOMString(type) + eventInitDict = webidl.converters.CloseEventInit(eventInitDict) + + super(type, eventInitDict) + + this.#eventInit = eventInitDict + } + + get wasClean () { + webidl.brandCheck(this, CloseEvent) + + return this.#eventInit.wasClean + } + + get code () { + webidl.brandCheck(this, CloseEvent) + + return this.#eventInit.code + } + + get reason () { + webidl.brandCheck(this, CloseEvent) + + return this.#eventInit.reason + } +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#the-errorevent-interface +class ErrorEvent extends Event { + #eventInit + + constructor (type, eventInitDict) { + webidl.argumentLengthCheck(arguments, 1, { header: 'ErrorEvent constructor' }) + + super(type, eventInitDict) + + type = webidl.converters.DOMString(type) + eventInitDict = webidl.converters.ErrorEventInit(eventInitDict ?? {}) + + this.#eventInit = eventInitDict + } + + get message () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.message + } + + get filename () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.filename + } + + get lineno () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.lineno + } + + get colno () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.colno + } + + get error () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.error + } +} + +Object.defineProperties(MessageEvent.prototype, { + [Symbol.toStringTag]: { + value: 'MessageEvent', + configurable: true + }, + data: kEnumerableProperty, + origin: kEnumerableProperty, + lastEventId: kEnumerableProperty, + source: kEnumerableProperty, + ports: kEnumerableProperty, + initMessageEvent: kEnumerableProperty +}) + +Object.defineProperties(CloseEvent.prototype, { + [Symbol.toStringTag]: { + value: 'CloseEvent', + configurable: true + }, + reason: kEnumerableProperty, + code: kEnumerableProperty, + wasClean: kEnumerableProperty +}) + +Object.defineProperties(ErrorEvent.prototype, { + [Symbol.toStringTag]: { + value: 'ErrorEvent', + configurable: true + }, + message: kEnumerableProperty, + filename: kEnumerableProperty, + lineno: kEnumerableProperty, + colno: kEnumerableProperty, + error: kEnumerableProperty +}) + +webidl.converters.MessagePort = webidl.interfaceConverter(MessagePort) + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.MessagePort +) + +const eventInit = [ + { + key: 'bubbles', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'cancelable', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'composed', + converter: webidl.converters.boolean, + defaultValue: false + } +] + +webidl.converters.MessageEventInit = webidl.dictionaryConverter([ + ...eventInit, + { + key: 'data', + converter: webidl.converters.any, + defaultValue: null + }, + { + key: 'origin', + converter: webidl.converters.USVString, + defaultValue: '' + }, + { + key: 'lastEventId', + converter: webidl.converters.DOMString, + defaultValue: '' + }, + { + key: 'source', + // Node doesn't implement WindowProxy or ServiceWorker, so the only + // valid value for source is a MessagePort. + converter: webidl.nullableConverter(webidl.converters.MessagePort), + defaultValue: null + }, + { + key: 'ports', + converter: webidl.converters['sequence'], + get defaultValue () { + return [] + } + } +]) + +webidl.converters.CloseEventInit = webidl.dictionaryConverter([ + ...eventInit, + { + key: 'wasClean', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'code', + converter: webidl.converters['unsigned short'], + defaultValue: 0 + }, + { + key: 'reason', + converter: webidl.converters.USVString, + defaultValue: '' + } +]) + +webidl.converters.ErrorEventInit = webidl.dictionaryConverter([ + ...eventInit, + { + key: 'message', + converter: webidl.converters.DOMString, + defaultValue: '' + }, + { + key: 'filename', + converter: webidl.converters.USVString, + defaultValue: '' + }, + { + key: 'lineno', + converter: webidl.converters['unsigned long'], + defaultValue: 0 + }, + { + key: 'colno', + converter: webidl.converters['unsigned long'], + defaultValue: 0 + }, + { + key: 'error', + converter: webidl.converters.any + } +]) + +module.exports = { + MessageEvent, + CloseEvent, + ErrorEvent +} + + +/***/ }), + +/***/ 5001: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { maxUnsigned16Bit } = __nccwpck_require__(3341) + +/** @type {import('crypto')} */ +let crypto +try { + crypto = __nccwpck_require__(6982) +} catch { + +} + +class WebsocketFrameSend { + /** + * @param {Buffer|undefined} data + */ + constructor (data) { + this.frameData = data + this.maskKey = crypto.randomBytes(4) + } + + createFrame (opcode) { + const bodyLength = this.frameData?.byteLength ?? 0 + + /** @type {number} */ + let payloadLength = bodyLength // 0-125 + let offset = 6 + + if (bodyLength > maxUnsigned16Bit) { + offset += 8 // payload length is next 8 bytes + payloadLength = 127 + } else if (bodyLength > 125) { + offset += 2 // payload length is next 2 bytes + payloadLength = 126 + } + + const buffer = Buffer.allocUnsafe(bodyLength + offset) + + // Clear first 2 bytes, everything else is overwritten + buffer[0] = buffer[1] = 0 + buffer[0] |= 0x80 // FIN + buffer[0] = (buffer[0] & 0xF0) + opcode // opcode + + /*! ws. MIT License. Einar Otto Stangvik */ + buffer[offset - 4] = this.maskKey[0] + buffer[offset - 3] = this.maskKey[1] + buffer[offset - 2] = this.maskKey[2] + buffer[offset - 1] = this.maskKey[3] + + buffer[1] = payloadLength + + if (payloadLength === 126) { + buffer.writeUInt16BE(bodyLength, 2) + } else if (payloadLength === 127) { + // Clear extended payload length + buffer[2] = buffer[3] = 0 + buffer.writeUIntBE(bodyLength, 4, 6) + } + + buffer[1] |= 0x80 // MASK + + // mask body + for (let i = 0; i < bodyLength; i++) { + buffer[offset + i] = this.frameData[i] ^ this.maskKey[i % 4] + } + + return buffer + } +} + +module.exports = { + WebsocketFrameSend +} + + +/***/ }), + +/***/ 7879: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { Writable } = __nccwpck_require__(2203) +const diagnosticsChannel = __nccwpck_require__(1637) +const { parserStates, opcodes, states, emptyBuffer } = __nccwpck_require__(3341) +const { kReadyState, kSentClose, kResponse, kReceivedClose } = __nccwpck_require__(5865) +const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived } = __nccwpck_require__(5026) +const { WebsocketFrameSend } = __nccwpck_require__(5001) + +// This code was influenced by ws released under the MIT license. +// Copyright (c) 2011 Einar Otto Stangvik +// Copyright (c) 2013 Arnout Kazemier and contributors +// Copyright (c) 2016 Luigi Pinca and contributors + +const channels = {} +channels.ping = diagnosticsChannel.channel('undici:websocket:ping') +channels.pong = diagnosticsChannel.channel('undici:websocket:pong') + +class ByteParser extends Writable { + #buffers = [] + #byteOffset = 0 + + #state = parserStates.INFO + + #info = {} + #fragments = [] + + constructor (ws) { + super() + + this.ws = ws + } + + /** + * @param {Buffer} chunk + * @param {() => void} callback + */ + _write (chunk, _, callback) { + this.#buffers.push(chunk) + this.#byteOffset += chunk.length + + this.run(callback) + } + + /** + * Runs whenever a new chunk is received. + * Callback is called whenever there are no more chunks buffering, + * or not enough bytes are buffered to parse. + */ + run (callback) { + while (true) { + if (this.#state === parserStates.INFO) { + // If there aren't enough bytes to parse the payload length, etc. + if (this.#byteOffset < 2) { + return callback() + } + + const buffer = this.consume(2) + + this.#info.fin = (buffer[0] & 0x80) !== 0 + this.#info.opcode = buffer[0] & 0x0F + + // If we receive a fragmented message, we use the type of the first + // frame to parse the full message as binary/text, when it's terminated + this.#info.originalOpcode ??= this.#info.opcode + + this.#info.fragmented = !this.#info.fin && this.#info.opcode !== opcodes.CONTINUATION + + if (this.#info.fragmented && this.#info.opcode !== opcodes.BINARY && this.#info.opcode !== opcodes.TEXT) { + // Only text and binary frames can be fragmented + failWebsocketConnection(this.ws, 'Invalid frame type was fragmented.') + return + } + + const payloadLength = buffer[1] & 0x7F + + if (payloadLength <= 125) { + this.#info.payloadLength = payloadLength + this.#state = parserStates.READ_DATA + } else if (payloadLength === 126) { + this.#state = parserStates.PAYLOADLENGTH_16 + } else if (payloadLength === 127) { + this.#state = parserStates.PAYLOADLENGTH_64 + } + + if (this.#info.fragmented && payloadLength > 125) { + // A fragmented frame can't be fragmented itself + failWebsocketConnection(this.ws, 'Fragmented frame exceeded 125 bytes.') + return + } else if ( + (this.#info.opcode === opcodes.PING || + this.#info.opcode === opcodes.PONG || + this.#info.opcode === opcodes.CLOSE) && + payloadLength > 125 + ) { + // Control frames can have a payload length of 125 bytes MAX + failWebsocketConnection(this.ws, 'Payload length for control frame exceeded 125 bytes.') + return + } else if (this.#info.opcode === opcodes.CLOSE) { + if (payloadLength === 1) { + failWebsocketConnection(this.ws, 'Received close frame with a 1-byte body.') + return + } + + const body = this.consume(payloadLength) + + this.#info.closeInfo = this.parseCloseBody(false, body) + + if (!this.ws[kSentClose]) { + // If an endpoint receives a Close frame and did not previously send a + // Close frame, the endpoint MUST send a Close frame in response. (When + // sending a Close frame in response, the endpoint typically echos the + // status code it received.) + const body = Buffer.allocUnsafe(2) + body.writeUInt16BE(this.#info.closeInfo.code, 0) + const closeFrame = new WebsocketFrameSend(body) + + this.ws[kResponse].socket.write( + closeFrame.createFrame(opcodes.CLOSE), + (err) => { + if (!err) { + this.ws[kSentClose] = true + } + } + ) + } + + // Upon either sending or receiving a Close control frame, it is said + // that _The WebSocket Closing Handshake is Started_ and that the + // WebSocket connection is in the CLOSING state. + this.ws[kReadyState] = states.CLOSING + this.ws[kReceivedClose] = true + + this.end() + + return + } else if (this.#info.opcode === opcodes.PING) { + // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in + // response, unless it already received a Close frame. + // A Pong frame sent in response to a Ping frame must have identical + // "Application data" + + const body = this.consume(payloadLength) + + if (!this.ws[kReceivedClose]) { + const frame = new WebsocketFrameSend(body) + + this.ws[kResponse].socket.write(frame.createFrame(opcodes.PONG)) + + if (channels.ping.hasSubscribers) { + channels.ping.publish({ + payload: body + }) + } + } + + this.#state = parserStates.INFO + + if (this.#byteOffset > 0) { + continue + } else { + callback() + return + } + } else if (this.#info.opcode === opcodes.PONG) { + // A Pong frame MAY be sent unsolicited. This serves as a + // unidirectional heartbeat. A response to an unsolicited Pong frame is + // not expected. + + const body = this.consume(payloadLength) + + if (channels.pong.hasSubscribers) { + channels.pong.publish({ + payload: body + }) + } + + if (this.#byteOffset > 0) { + continue + } else { + callback() + return + } + } + } else if (this.#state === parserStates.PAYLOADLENGTH_16) { + if (this.#byteOffset < 2) { + return callback() + } + + const buffer = this.consume(2) + + this.#info.payloadLength = buffer.readUInt16BE(0) + this.#state = parserStates.READ_DATA + } else if (this.#state === parserStates.PAYLOADLENGTH_64) { + if (this.#byteOffset < 8) { + return callback() + } + + const buffer = this.consume(8) + const upper = buffer.readUInt32BE(0) + + // 2^31 is the maxinimum bytes an arraybuffer can contain + // on 32-bit systems. Although, on 64-bit systems, this is + // 2^53-1 bytes. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length + // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275 + // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e + if (upper > 2 ** 31 - 1) { + failWebsocketConnection(this.ws, 'Received payload length > 2^31 bytes.') + return + } + + const lower = buffer.readUInt32BE(4) + + this.#info.payloadLength = (upper << 8) + lower + this.#state = parserStates.READ_DATA + } else if (this.#state === parserStates.READ_DATA) { + if (this.#byteOffset < this.#info.payloadLength) { + // If there is still more data in this chunk that needs to be read + return callback() + } else if (this.#byteOffset >= this.#info.payloadLength) { + // If the server sent multiple frames in a single chunk + + const body = this.consume(this.#info.payloadLength) + + this.#fragments.push(body) + + // If the frame is unfragmented, or a fragmented frame was terminated, + // a message was received + if (!this.#info.fragmented || (this.#info.fin && this.#info.opcode === opcodes.CONTINUATION)) { + const fullMessage = Buffer.concat(this.#fragments) + + websocketMessageReceived(this.ws, this.#info.originalOpcode, fullMessage) + + this.#info = {} + this.#fragments.length = 0 + } + + this.#state = parserStates.INFO + } + } + + if (this.#byteOffset > 0) { + continue + } else { + callback() + break + } + } + } + + /** + * Take n bytes from the buffered Buffers + * @param {number} n + * @returns {Buffer|null} + */ + consume (n) { + if (n > this.#byteOffset) { + return null + } else if (n === 0) { + return emptyBuffer + } + + if (this.#buffers[0].length === n) { + this.#byteOffset -= this.#buffers[0].length + return this.#buffers.shift() + } + + const buffer = Buffer.allocUnsafe(n) + let offset = 0 + + while (offset !== n) { + const next = this.#buffers[0] + const { length } = next + + if (length + offset === n) { + buffer.set(this.#buffers.shift(), offset) + break + } else if (length + offset > n) { + buffer.set(next.subarray(0, n - offset), offset) + this.#buffers[0] = next.subarray(n - offset) + break + } else { + buffer.set(this.#buffers.shift(), offset) + offset += next.length + } + } + + this.#byteOffset -= n + + return buffer + } + + parseCloseBody (onlyCode, data) { + // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5 + /** @type {number|undefined} */ + let code + + if (data.length >= 2) { + // _The WebSocket Connection Close Code_ is + // defined as the status code (Section 7.4) contained in the first Close + // control frame received by the application + code = data.readUInt16BE(0) + } + + if (onlyCode) { + if (!isValidStatusCode(code)) { + return null + } + + return { code } + } + + // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6 + /** @type {Buffer} */ + let reason = data.subarray(2) + + // Remove BOM + if (reason[0] === 0xEF && reason[1] === 0xBB && reason[2] === 0xBF) { + reason = reason.subarray(3) + } + + if (code !== undefined && !isValidStatusCode(code)) { + return null + } + + try { + // TODO: optimize this + reason = new TextDecoder('utf-8', { fatal: true }).decode(reason) + } catch { + return null + } + + return { code, reason } + } + + get closingInfo () { + return this.#info.closeInfo + } +} + +module.exports = { + ByteParser +} + + +/***/ }), + +/***/ 5865: +/***/ ((module) => { + +"use strict"; + + +module.exports = { + kWebSocketURL: Symbol('url'), + kReadyState: Symbol('ready state'), + kController: Symbol('controller'), + kResponse: Symbol('response'), + kBinaryType: Symbol('binary type'), + kSentClose: Symbol('sent close'), + kReceivedClose: Symbol('received close'), + kByteParser: Symbol('byte parser') +} + + +/***/ }), + +/***/ 5026: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kReadyState, kController, kResponse, kBinaryType, kWebSocketURL } = __nccwpck_require__(5865) +const { states, opcodes } = __nccwpck_require__(3341) +const { MessageEvent, ErrorEvent } = __nccwpck_require__(7739) + +/* globals Blob */ + +/** + * @param {import('./websocket').WebSocket} ws + */ +function isEstablished (ws) { + // If the server's response is validated as provided for above, it is + // said that _The WebSocket Connection is Established_ and that the + // WebSocket Connection is in the OPEN state. + return ws[kReadyState] === states.OPEN +} + +/** + * @param {import('./websocket').WebSocket} ws + */ +function isClosing (ws) { + // Upon either sending or receiving a Close control frame, it is said + // that _The WebSocket Closing Handshake is Started_ and that the + // WebSocket connection is in the CLOSING state. + return ws[kReadyState] === states.CLOSING +} + +/** + * @param {import('./websocket').WebSocket} ws + */ +function isClosed (ws) { + return ws[kReadyState] === states.CLOSED +} + +/** + * @see https://dom.spec.whatwg.org/#concept-event-fire + * @param {string} e + * @param {EventTarget} target + * @param {EventInit | undefined} eventInitDict + */ +function fireEvent (e, target, eventConstructor = Event, eventInitDict) { + // 1. If eventConstructor is not given, then let eventConstructor be Event. + + // 2. Let event be the result of creating an event given eventConstructor, + // in the relevant realm of target. + // 3. Initialize event’s type attribute to e. + const event = new eventConstructor(e, eventInitDict) // eslint-disable-line new-cap + + // 4. Initialize any other IDL attributes of event as described in the + // invocation of this algorithm. + + // 5. Return the result of dispatching event at target, with legacy target + // override flag set if set. + target.dispatchEvent(event) +} + +/** + * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol + * @param {import('./websocket').WebSocket} ws + * @param {number} type Opcode + * @param {Buffer} data application data + */ +function websocketMessageReceived (ws, type, data) { + // 1. If ready state is not OPEN (1), then return. + if (ws[kReadyState] !== states.OPEN) { + return + } + + // 2. Let dataForEvent be determined by switching on type and binary type: + let dataForEvent + + if (type === opcodes.TEXT) { + // -> type indicates that the data is Text + // a new DOMString containing data + try { + dataForEvent = new TextDecoder('utf-8', { fatal: true }).decode(data) + } catch { + failWebsocketConnection(ws, 'Received invalid UTF-8 in text frame.') + return + } + } else if (type === opcodes.BINARY) { + if (ws[kBinaryType] === 'blob') { + // -> type indicates that the data is Binary and binary type is "blob" + // a new Blob object, created in the relevant Realm of the WebSocket + // object, that represents data as its raw data + dataForEvent = new Blob([data]) + } else { + // -> type indicates that the data is Binary and binary type is "arraybuffer" + // a new ArrayBuffer object, created in the relevant Realm of the + // WebSocket object, whose contents are data + dataForEvent = new Uint8Array(data).buffer + } + } + + // 3. Fire an event named message at the WebSocket object, using MessageEvent, + // with the origin attribute initialized to the serialization of the WebSocket + // object’s url's origin, and the data attribute initialized to dataForEvent. + fireEvent('message', ws, MessageEvent, { + origin: ws[kWebSocketURL].origin, + data: dataForEvent + }) +} + +/** + * @see https://datatracker.ietf.org/doc/html/rfc6455 + * @see https://datatracker.ietf.org/doc/html/rfc2616 + * @see https://bugs.chromium.org/p/chromium/issues/detail?id=398407 + * @param {string} protocol + */ +function isValidSubprotocol (protocol) { + // If present, this value indicates one + // or more comma-separated subprotocol the client wishes to speak, + // ordered by preference. The elements that comprise this value + // MUST be non-empty strings with characters in the range U+0021 to + // U+007E not including separator characters as defined in + // [RFC2616] and MUST all be unique strings. + if (protocol.length === 0) { + return false + } + + for (const char of protocol) { + const code = char.charCodeAt(0) + + if ( + code < 0x21 || + code > 0x7E || + char === '(' || + char === ')' || + char === '<' || + char === '>' || + char === '@' || + char === ',' || + char === ';' || + char === ':' || + char === '\\' || + char === '"' || + char === '/' || + char === '[' || + char === ']' || + char === '?' || + char === '=' || + char === '{' || + char === '}' || + code === 32 || // SP + code === 9 // HT + ) { + return false + } + } + + return true +} + +/** + * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7-4 + * @param {number} code + */ +function isValidStatusCode (code) { + if (code >= 1000 && code < 1015) { + return ( + code !== 1004 && // reserved + code !== 1005 && // "MUST NOT be set as a status code" + code !== 1006 // "MUST NOT be set as a status code" + ) + } + + return code >= 3000 && code <= 4999 +} + +/** + * @param {import('./websocket').WebSocket} ws + * @param {string|undefined} reason + */ +function failWebsocketConnection (ws, reason) { + const { [kController]: controller, [kResponse]: response } = ws + + controller.abort() + + if (response?.socket && !response.socket.destroyed) { + response.socket.destroy() + } + + if (reason) { + fireEvent('error', ws, ErrorEvent, { + error: new Error(reason) + }) + } +} + +module.exports = { + isEstablished, + isClosing, + isClosed, + fireEvent, + isValidSubprotocol, + isValidStatusCode, + failWebsocketConnection, + websocketMessageReceived +} + + +/***/ }), + +/***/ 599: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { webidl } = __nccwpck_require__(274) +const { DOMException } = __nccwpck_require__(450) +const { URLSerializer } = __nccwpck_require__(5294) +const { getGlobalOrigin } = __nccwpck_require__(960) +const { staticPropertyDescriptors, states, opcodes, emptyBuffer } = __nccwpck_require__(3341) +const { + kWebSocketURL, + kReadyState, + kController, + kBinaryType, + kResponse, + kSentClose, + kByteParser +} = __nccwpck_require__(5865) +const { isEstablished, isClosing, isValidSubprotocol, failWebsocketConnection, fireEvent } = __nccwpck_require__(5026) +const { establishWebSocketConnection } = __nccwpck_require__(8530) +const { WebsocketFrameSend } = __nccwpck_require__(5001) +const { ByteParser } = __nccwpck_require__(7879) +const { kEnumerableProperty, isBlobLike } = __nccwpck_require__(1436) +const { getGlobalDispatcher } = __nccwpck_require__(8377) +const { types } = __nccwpck_require__(9023) + +let experimentalWarned = false + +// https://websockets.spec.whatwg.org/#interface-definition +class WebSocket extends EventTarget { + #events = { + open: null, + error: null, + close: null, + message: null + } + + #bufferedAmount = 0 + #protocol = '' + #extensions = '' + + /** + * @param {string} url + * @param {string|string[]} protocols + */ + constructor (url, protocols = []) { + super() + + webidl.argumentLengthCheck(arguments, 1, { header: 'WebSocket constructor' }) + + if (!experimentalWarned) { + experimentalWarned = true + process.emitWarning('WebSockets are experimental, expect them to change at any time.', { + code: 'UNDICI-WS' + }) + } + + const options = webidl.converters['DOMString or sequence or WebSocketInit'](protocols) + + url = webidl.converters.USVString(url) + protocols = options.protocols + + // 1. Let baseURL be this's relevant settings object's API base URL. + const baseURL = getGlobalOrigin() + + // 1. Let urlRecord be the result of applying the URL parser to url with baseURL. + let urlRecord + + try { + urlRecord = new URL(url, baseURL) + } catch (e) { + // 3. If urlRecord is failure, then throw a "SyntaxError" DOMException. + throw new DOMException(e, 'SyntaxError') + } + + // 4. If urlRecord’s scheme is "http", then set urlRecord’s scheme to "ws". + if (urlRecord.protocol === 'http:') { + urlRecord.protocol = 'ws:' + } else if (urlRecord.protocol === 'https:') { + // 5. Otherwise, if urlRecord’s scheme is "https", set urlRecord’s scheme to "wss". + urlRecord.protocol = 'wss:' + } + + // 6. If urlRecord’s scheme is not "ws" or "wss", then throw a "SyntaxError" DOMException. + if (urlRecord.protocol !== 'ws:' && urlRecord.protocol !== 'wss:') { + throw new DOMException( + `Expected a ws: or wss: protocol, got ${urlRecord.protocol}`, + 'SyntaxError' + ) + } + + // 7. If urlRecord’s fragment is non-null, then throw a "SyntaxError" + // DOMException. + if (urlRecord.hash || urlRecord.href.endsWith('#')) { + throw new DOMException('Got fragment', 'SyntaxError') + } + + // 8. If protocols is a string, set protocols to a sequence consisting + // of just that string. + if (typeof protocols === 'string') { + protocols = [protocols] + } + + // 9. If any of the values in protocols occur more than once or otherwise + // fail to match the requirements for elements that comprise the value + // of `Sec-WebSocket-Protocol` fields as defined by The WebSocket + // protocol, then throw a "SyntaxError" DOMException. + if (protocols.length !== new Set(protocols.map(p => p.toLowerCase())).size) { + throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError') + } + + if (protocols.length > 0 && !protocols.every(p => isValidSubprotocol(p))) { + throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError') + } + + // 10. Set this's url to urlRecord. + this[kWebSocketURL] = new URL(urlRecord.href) + + // 11. Let client be this's relevant settings object. + + // 12. Run this step in parallel: + + // 1. Establish a WebSocket connection given urlRecord, protocols, + // and client. + this[kController] = establishWebSocketConnection( + urlRecord, + protocols, + this, + (response) => this.#onConnectionEstablished(response), + options + ) + + // Each WebSocket object has an associated ready state, which is a + // number representing the state of the connection. Initially it must + // be CONNECTING (0). + this[kReadyState] = WebSocket.CONNECTING + + // The extensions attribute must initially return the empty string. + + // The protocol attribute must initially return the empty string. + + // Each WebSocket object has an associated binary type, which is a + // BinaryType. Initially it must be "blob". + this[kBinaryType] = 'blob' + } + + /** + * @see https://websockets.spec.whatwg.org/#dom-websocket-close + * @param {number|undefined} code + * @param {string|undefined} reason + */ + close (code = undefined, reason = undefined) { + webidl.brandCheck(this, WebSocket) + + if (code !== undefined) { + code = webidl.converters['unsigned short'](code, { clamp: true }) + } + + if (reason !== undefined) { + reason = webidl.converters.USVString(reason) + } + + // 1. If code is present, but is neither an integer equal to 1000 nor an + // integer in the range 3000 to 4999, inclusive, throw an + // "InvalidAccessError" DOMException. + if (code !== undefined) { + if (code !== 1000 && (code < 3000 || code > 4999)) { + throw new DOMException('invalid code', 'InvalidAccessError') + } + } + + let reasonByteLength = 0 + + // 2. If reason is present, then run these substeps: + if (reason !== undefined) { + // 1. Let reasonBytes be the result of encoding reason. + // 2. If reasonBytes is longer than 123 bytes, then throw a + // "SyntaxError" DOMException. + reasonByteLength = Buffer.byteLength(reason) + + if (reasonByteLength > 123) { + throw new DOMException( + `Reason must be less than 123 bytes; received ${reasonByteLength}`, + 'SyntaxError' + ) + } + } + + // 3. Run the first matching steps from the following list: + if (this[kReadyState] === WebSocket.CLOSING || this[kReadyState] === WebSocket.CLOSED) { + // If this's ready state is CLOSING (2) or CLOSED (3) + // Do nothing. + } else if (!isEstablished(this)) { + // If the WebSocket connection is not yet established + // Fail the WebSocket connection and set this's ready state + // to CLOSING (2). + failWebsocketConnection(this, 'Connection was closed before it was established.') + this[kReadyState] = WebSocket.CLOSING + } else if (!isClosing(this)) { + // If the WebSocket closing handshake has not yet been started + // Start the WebSocket closing handshake and set this's ready + // state to CLOSING (2). + // - If neither code nor reason is present, the WebSocket Close + // message must not have a body. + // - If code is present, then the status code to use in the + // WebSocket Close message must be the integer given by code. + // - If reason is also present, then reasonBytes must be + // provided in the Close message after the status code. + + const frame = new WebsocketFrameSend() + + // If neither code nor reason is present, the WebSocket Close + // message must not have a body. + + // If code is present, then the status code to use in the + // WebSocket Close message must be the integer given by code. + if (code !== undefined && reason === undefined) { + frame.frameData = Buffer.allocUnsafe(2) + frame.frameData.writeUInt16BE(code, 0) + } else if (code !== undefined && reason !== undefined) { + // If reason is also present, then reasonBytes must be + // provided in the Close message after the status code. + frame.frameData = Buffer.allocUnsafe(2 + reasonByteLength) + frame.frameData.writeUInt16BE(code, 0) + // the body MAY contain UTF-8-encoded data with value /reason/ + frame.frameData.write(reason, 2, 'utf-8') + } else { + frame.frameData = emptyBuffer + } + + /** @type {import('stream').Duplex} */ + const socket = this[kResponse].socket + + socket.write(frame.createFrame(opcodes.CLOSE), (err) => { + if (!err) { + this[kSentClose] = true + } + }) + + // Upon either sending or receiving a Close control frame, it is said + // that _The WebSocket Closing Handshake is Started_ and that the + // WebSocket connection is in the CLOSING state. + this[kReadyState] = states.CLOSING + } else { + // Otherwise + // Set this's ready state to CLOSING (2). + this[kReadyState] = WebSocket.CLOSING + } + } + + /** + * @see https://websockets.spec.whatwg.org/#dom-websocket-send + * @param {NodeJS.TypedArray|ArrayBuffer|Blob|string} data + */ + send (data) { + webidl.brandCheck(this, WebSocket) + + webidl.argumentLengthCheck(arguments, 1, { header: 'WebSocket.send' }) + + data = webidl.converters.WebSocketSendData(data) + + // 1. If this's ready state is CONNECTING, then throw an + // "InvalidStateError" DOMException. + if (this[kReadyState] === WebSocket.CONNECTING) { + throw new DOMException('Sent before connected.', 'InvalidStateError') + } + + // 2. Run the appropriate set of steps from the following list: + // https://datatracker.ietf.org/doc/html/rfc6455#section-6.1 + // https://datatracker.ietf.org/doc/html/rfc6455#section-5.2 + + if (!isEstablished(this) || isClosing(this)) { + return + } + + /** @type {import('stream').Duplex} */ + const socket = this[kResponse].socket + + // If data is a string + if (typeof data === 'string') { + // If the WebSocket connection is established and the WebSocket + // closing handshake has not yet started, then the user agent + // must send a WebSocket Message comprised of the data argument + // using a text frame opcode; if the data cannot be sent, e.g. + // because it would need to be buffered but the buffer is full, + // the user agent must flag the WebSocket as full and then close + // the WebSocket connection. Any invocation of this method with a + // string argument that does not throw an exception must increase + // the bufferedAmount attribute by the number of bytes needed to + // express the argument as UTF-8. + + const value = Buffer.from(data) + const frame = new WebsocketFrameSend(value) + const buffer = frame.createFrame(opcodes.TEXT) + + this.#bufferedAmount += value.byteLength + socket.write(buffer, () => { + this.#bufferedAmount -= value.byteLength + }) + } else if (types.isArrayBuffer(data)) { + // If the WebSocket connection is established, and the WebSocket + // closing handshake has not yet started, then the user agent must + // send a WebSocket Message comprised of data using a binary frame + // opcode; if the data cannot be sent, e.g. because it would need + // to be buffered but the buffer is full, the user agent must flag + // the WebSocket as full and then close the WebSocket connection. + // The data to be sent is the data stored in the buffer described + // by the ArrayBuffer object. Any invocation of this method with an + // ArrayBuffer argument that does not throw an exception must + // increase the bufferedAmount attribute by the length of the + // ArrayBuffer in bytes. + + const value = Buffer.from(data) + const frame = new WebsocketFrameSend(value) + const buffer = frame.createFrame(opcodes.BINARY) + + this.#bufferedAmount += value.byteLength + socket.write(buffer, () => { + this.#bufferedAmount -= value.byteLength + }) + } else if (ArrayBuffer.isView(data)) { + // If the WebSocket connection is established, and the WebSocket + // closing handshake has not yet started, then the user agent must + // send a WebSocket Message comprised of data using a binary frame + // opcode; if the data cannot be sent, e.g. because it would need to + // be buffered but the buffer is full, the user agent must flag the + // WebSocket as full and then close the WebSocket connection. The + // data to be sent is the data stored in the section of the buffer + // described by the ArrayBuffer object that data references. Any + // invocation of this method with this kind of argument that does + // not throw an exception must increase the bufferedAmount attribute + // by the length of data’s buffer in bytes. + + const ab = Buffer.from(data, data.byteOffset, data.byteLength) + + const frame = new WebsocketFrameSend(ab) + const buffer = frame.createFrame(opcodes.BINARY) + + this.#bufferedAmount += ab.byteLength + socket.write(buffer, () => { + this.#bufferedAmount -= ab.byteLength + }) + } else if (isBlobLike(data)) { + // If the WebSocket connection is established, and the WebSocket + // closing handshake has not yet started, then the user agent must + // send a WebSocket Message comprised of data using a binary frame + // opcode; if the data cannot be sent, e.g. because it would need to + // be buffered but the buffer is full, the user agent must flag the + // WebSocket as full and then close the WebSocket connection. The data + // to be sent is the raw data represented by the Blob object. Any + // invocation of this method with a Blob argument that does not throw + // an exception must increase the bufferedAmount attribute by the size + // of the Blob object’s raw data, in bytes. + + const frame = new WebsocketFrameSend() + + data.arrayBuffer().then((ab) => { + const value = Buffer.from(ab) + frame.frameData = value + const buffer = frame.createFrame(opcodes.BINARY) + + this.#bufferedAmount += value.byteLength + socket.write(buffer, () => { + this.#bufferedAmount -= value.byteLength + }) + }) + } + } + + get readyState () { + webidl.brandCheck(this, WebSocket) + + // The readyState getter steps are to return this's ready state. + return this[kReadyState] + } + + get bufferedAmount () { + webidl.brandCheck(this, WebSocket) + + return this.#bufferedAmount + } + + get url () { + webidl.brandCheck(this, WebSocket) + + // The url getter steps are to return this's url, serialized. + return URLSerializer(this[kWebSocketURL]) + } + + get extensions () { + webidl.brandCheck(this, WebSocket) + + return this.#extensions + } + + get protocol () { + webidl.brandCheck(this, WebSocket) + + return this.#protocol + } + + get onopen () { + webidl.brandCheck(this, WebSocket) + + return this.#events.open + } + + set onopen (fn) { + webidl.brandCheck(this, WebSocket) + + if (this.#events.open) { + this.removeEventListener('open', this.#events.open) + } + + if (typeof fn === 'function') { + this.#events.open = fn + this.addEventListener('open', fn) + } else { + this.#events.open = null + } + } + + get onerror () { + webidl.brandCheck(this, WebSocket) + + return this.#events.error + } + + set onerror (fn) { + webidl.brandCheck(this, WebSocket) + + if (this.#events.error) { + this.removeEventListener('error', this.#events.error) + } + + if (typeof fn === 'function') { + this.#events.error = fn + this.addEventListener('error', fn) + } else { + this.#events.error = null + } + } + + get onclose () { + webidl.brandCheck(this, WebSocket) + + return this.#events.close + } + + set onclose (fn) { + webidl.brandCheck(this, WebSocket) + + if (this.#events.close) { + this.removeEventListener('close', this.#events.close) + } + + if (typeof fn === 'function') { + this.#events.close = fn + this.addEventListener('close', fn) + } else { + this.#events.close = null + } + } + + get onmessage () { + webidl.brandCheck(this, WebSocket) + + return this.#events.message + } + + set onmessage (fn) { + webidl.brandCheck(this, WebSocket) + + if (this.#events.message) { + this.removeEventListener('message', this.#events.message) + } + + if (typeof fn === 'function') { + this.#events.message = fn + this.addEventListener('message', fn) + } else { + this.#events.message = null + } + } + + get binaryType () { + webidl.brandCheck(this, WebSocket) + + return this[kBinaryType] + } + + set binaryType (type) { + webidl.brandCheck(this, WebSocket) + + if (type !== 'blob' && type !== 'arraybuffer') { + this[kBinaryType] = 'blob' + } else { + this[kBinaryType] = type + } + } + + /** + * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol + */ + #onConnectionEstablished (response) { + // processResponse is called when the "response’s header list has been received and initialized." + // once this happens, the connection is open + this[kResponse] = response + + const parser = new ByteParser(this) + parser.on('drain', function onParserDrain () { + this.ws[kResponse].socket.resume() + }) + + response.socket.ws = this + this[kByteParser] = parser + + // 1. Change the ready state to OPEN (1). + this[kReadyState] = states.OPEN + + // 2. Change the extensions attribute’s value to the extensions in use, if + // it is not the null value. + // https://datatracker.ietf.org/doc/html/rfc6455#section-9.1 + const extensions = response.headersList.get('sec-websocket-extensions') + + if (extensions !== null) { + this.#extensions = extensions + } + + // 3. Change the protocol attribute’s value to the subprotocol in use, if + // it is not the null value. + // https://datatracker.ietf.org/doc/html/rfc6455#section-1.9 + const protocol = response.headersList.get('sec-websocket-protocol') + + if (protocol !== null) { + this.#protocol = protocol + } + + // 4. Fire an event named open at the WebSocket object. + fireEvent('open', this) + } +} + +// https://websockets.spec.whatwg.org/#dom-websocket-connecting +WebSocket.CONNECTING = WebSocket.prototype.CONNECTING = states.CONNECTING +// https://websockets.spec.whatwg.org/#dom-websocket-open +WebSocket.OPEN = WebSocket.prototype.OPEN = states.OPEN +// https://websockets.spec.whatwg.org/#dom-websocket-closing +WebSocket.CLOSING = WebSocket.prototype.CLOSING = states.CLOSING +// https://websockets.spec.whatwg.org/#dom-websocket-closed +WebSocket.CLOSED = WebSocket.prototype.CLOSED = states.CLOSED + +Object.defineProperties(WebSocket.prototype, { + CONNECTING: staticPropertyDescriptors, + OPEN: staticPropertyDescriptors, + CLOSING: staticPropertyDescriptors, + CLOSED: staticPropertyDescriptors, + url: kEnumerableProperty, + readyState: kEnumerableProperty, + bufferedAmount: kEnumerableProperty, + onopen: kEnumerableProperty, + onerror: kEnumerableProperty, + onclose: kEnumerableProperty, + close: kEnumerableProperty, + onmessage: kEnumerableProperty, + binaryType: kEnumerableProperty, + send: kEnumerableProperty, + extensions: kEnumerableProperty, + protocol: kEnumerableProperty, + [Symbol.toStringTag]: { + value: 'WebSocket', + writable: false, + enumerable: false, + configurable: true + } +}) + +Object.defineProperties(WebSocket, { + CONNECTING: staticPropertyDescriptors, + OPEN: staticPropertyDescriptors, + CLOSING: staticPropertyDescriptors, + CLOSED: staticPropertyDescriptors +}) + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.DOMString +) + +webidl.converters['DOMString or sequence'] = function (V) { + if (webidl.util.Type(V) === 'Object' && Symbol.iterator in V) { + return webidl.converters['sequence'](V) + } + + return webidl.converters.DOMString(V) +} + +// This implements the propsal made in https://github.com/whatwg/websockets/issues/42 +webidl.converters.WebSocketInit = webidl.dictionaryConverter([ + { + key: 'protocols', + converter: webidl.converters['DOMString or sequence'], + get defaultValue () { + return [] + } + }, + { + key: 'dispatcher', + converter: (V) => V, + get defaultValue () { + return getGlobalDispatcher() + } + }, + { + key: 'headers', + converter: webidl.nullableConverter(webidl.converters.HeadersInit) + } +]) + +webidl.converters['DOMString or sequence or WebSocketInit'] = function (V) { + if (webidl.util.Type(V) === 'Object' && !(Symbol.iterator in V)) { + return webidl.converters.WebSocketInit(V) + } + + return { protocols: webidl.converters['DOMString or sequence'](V) } +} + +webidl.converters.WebSocketSendData = function (V) { + if (webidl.util.Type(V) === 'Object') { + if (isBlobLike(V)) { + return webidl.converters.Blob(V, { strict: false }) + } + + if (ArrayBuffer.isView(V) || types.isAnyArrayBuffer(V)) { + return webidl.converters.BufferSource(V) + } + } + + return webidl.converters.USVString(V) +} + +module.exports = { + WebSocket +} + + +/***/ }), + +/***/ 8465: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", ({ value: true })); +const steps_1 = __nccwpck_require__(5558); +const inputs_1 = __nccwpck_require__(7432); +const state_1 = __nccwpck_require__(3328); +const core = __importStar(__nccwpck_require__(8185)); +const types_1 = __nccwpck_require__(6468); +/** + * Runs the action + */ +async function run() { + const inputs = await (0, inputs_1.getInputs)(); + for (const step of inputs.steps) { + core.info(`Running step: ${step}`); + switch (step) { + case types_1.ActionStep.BUILD: { + await (0, steps_1.buildStep)(); + break; + } + case types_1.ActionStep.PUSH: { + await (0, steps_1.pushStep)(); + break; + } + case types_1.ActionStep.DEPLOY: { + await (0, steps_1.deployStep)(); + break; + } + default: + core.error(`Unknown action step: ${step}!`); + throw new Error(`Unknown action step: ${step}!`); + } + } +} +/** + * Runs the post action cleanup step + */ +async function post() { } +setImmediate(async () => { + if (!state_1.IS_POST) { + await run(); + } + else { + await post(); + } +}); + + +/***/ }), + +/***/ 7432: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getInputs = void 0; +const core = __importStar(__nccwpck_require__(8185)); +const types_1 = __nccwpck_require__(6468); +const utils_1 = __nccwpck_require__(9640); +const getBuildInputs = () => { + const tag = core.getInput('tag'); + return { + tag, + images: [], + }; +}; +const getPushInputs = () => { + const username = process.env.ORACLE_DOCKER_USERNAME; + if (!username) { + core.error('No Oracle Docker username found in ORACLE_DOCKER_USERNAME environment variable!'); + throw new Error('No Oracle Docker username found in ORACLE_DOCKER_USERNAME environment variable!'); + } + const password = process.env.ORACLE_DOCKER_PASSWORD; + if (!password) { + core.error('No Oracle Docker password found in ORACLE_DOCKER_PASSWORD environment variable!'); + throw new Error('No Oracle Docker password found in ORACLE_DOCKER_PASSWORD environment variable!'); + } + return { + dockerUsername: username, + dockerPassword: password, + }; +}; +const getDeployIUputs = () => { + const services = getInputList('services'); + const cloudEnvironment = process.env['CLOUD_ENV']; + if (!cloudEnvironment) { + core.error('No CLOUD_ENV environment variable found!'); + throw new Error('No CLOUD_ENV environment variable found!'); + } + let aws; + let oracle; + if (cloudEnvironment === types_1.CloudEnvironment.LF_ORACLE_PRODUCTION || + cloudEnvironment === types_1.CloudEnvironment.LF_ORACLE_STAGING) { + const user = process.env.ORACLE_USER; + if (!user) { + core.error('No ORACLE_USER environment variable found!'); + throw new Error('No ORACLE_USER environment variable found!'); + } + const tenant = process.env.ORACLE_TENANT; + if (!tenant) { + core.error('No ORACLE_TENANT environment variable found!'); + throw new Error('No ORACLE_TENANT environment variable found!'); + } + const region = process.env.ORACLE_REGION; + if (!region) { + core.error('No ORACLE_REGION environment variable found!'); + throw new Error('No ORACLE_REGION environment variable found!'); + } + const fingerprint = process.env.ORACLE_FINGERPRINT; + if (!fingerprint) { + core.error('No ORACLE_FINGERPRINT environment variable found!'); + throw new Error('No ORACLE_FINGERPRINT environment variable found!'); + } + const key = process.env.ORACLE_KEY; + if (!key) { + core.error('No ORACLE_KEY environment variable found!'); + throw new Error('No ORACLE_KEY environment variable found!'); + } + const cluster = process.env.ORACLE_CLUSTER; + if (!cluster) { + core.error('No ORACLE_CLUSTER environment variable found!'); + throw new Error('No ORACLE_CLUSTER environment variable found!'); + } + oracle = { + user, + tenant, + region, + fingerprint, + key, + cluster, + }; + } + else { + const eksClusterName = process.env.CROWD_CLUSTER; + if (!eksClusterName) { + core.error('No CROWD_CLUSTER environment variable found!'); + throw new Error('No CROWD_CLUSTER environment variable found!'); + } + const awsRoleArn = process.env.CROWD_ROLE_ARN; + if (!awsRoleArn) { + core.error('No CROWD_ROLE_ARN environment variable found!'); + throw new Error('No CROWD_ROLE_ARN environment variable found!'); + } + const awsAccessKeyId = process.env.AWS_ACCESS_KEY_ID; + if (!awsAccessKeyId) { + core.error('No AWS_ACCESS_KEY_ID environment variable found!'); + throw new Error('No AWS_ACCESS_KEY_ID environment variable found!'); + } + const awsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; + if (!awsSecretAccessKey) { + core.error('No AWS_SECRET_ACCESS_KEY environment variable found!'); + throw new Error('No AWS_SECRET_ACCESS_KEY environment variable found!'); + } + const awsRegion = process.env.AWS_REGION; + if (!awsRegion) { + core.error('No AWS_REGION environment variable found!'); + throw new Error('No AWS_REGION environment variable found!'); + } + aws = { + eksClusterName, + awsRoleArn, + awsAccessKeyId, + awsSecretAccessKey, + awsRegion, + }; + } + return { + services, + cloudEnvironment, + aws, + oracle, + }; +}; +let inputs; +const getInputs = async () => { + if (inputs !== undefined) { + return inputs; + } + const actionSteps = getInputList('steps'); + if (actionSteps.length === 0) { + core.error('No action steps provided!'); + throw new Error('No action steps provided!'); + } + const results = { + steps: actionSteps, + }; + for (const step of actionSteps) { + switch (step) { + case types_1.ActionStep.BUILD: + results[types_1.ActionStep.BUILD] = getBuildInputs(); + break; + case types_1.ActionStep.PUSH: + results[types_1.ActionStep.PUSH] = getPushInputs(); + break; + case types_1.ActionStep.DEPLOY: + results[types_1.ActionStep.DEPLOY] = getDeployIUputs(); + break; + default: + core.error(`Unknown action step: ${step}!`); + throw new Error(`Unknown action step: ${step}!`); + } + } + if (results[types_1.ActionStep.BUILD] !== undefined) { + if (results[types_1.ActionStep.BUILD].images.length === 0 && results[types_1.ActionStep.DEPLOY] !== undefined) { + // calculate images from services + const buildDefinitions = await (0, utils_1.getBuilderDefinitions)(); + const images = []; + for (const service of results[types_1.ActionStep.DEPLOY].services) { + const definition = buildDefinitions.find((d) => d.services.includes(service)); + if (definition === undefined) { + core.error(`No builder definition found for service: ${service}!`); + throw new Error(`No builder definition found for service: ${service}!`); + } + if (!images.includes(definition.imageName)) { + images.push(definition.imageName); + } + } + results[types_1.ActionStep.BUILD].images = images; + } + } + if (results[types_1.ActionStep.PUSH] !== undefined && results[types_1.ActionStep.BUILD] === undefined) { + core.error('Push step provided without build step!'); + throw new Error('Push step provided without build step!'); + } + if (results[types_1.ActionStep.DEPLOY] !== undefined && results[types_1.ActionStep.PUSH] === undefined) { + core.error('Deploy step provided without push step!'); + throw new Error('Deploy step provided without push step!'); + } + inputs = results; + return results; +}; +exports.getInputs = getInputs; +const getInputList = (name) => { + const items = core.getInput(name); + return items + .split(' ') + .map((s) => s.trim()) + .filter((s) => s.length > 0); +}; + + +/***/ }), + +/***/ 3328: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.IS_POST = void 0; +const core = __importStar(__nccwpck_require__(8185)); +exports.IS_POST = !!process.env['STATE_isPost']; +if (!exports.IS_POST) { + core.saveState('isPost', 'true'); +} + + +/***/ }), + +/***/ 5558: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.deployStep = exports.pushStep = exports.buildStep = void 0; +const inputs_1 = __nccwpck_require__(7432); +const types_1 = __nccwpck_require__(6468); +const core = __importStar(__nccwpck_require__(8185)); +const exec = __importStar(__nccwpck_require__(9046)); +const utils_1 = __nccwpck_require__(9640); +const fs_1 = __importDefault(__nccwpck_require__(9896)); +const os_1 = __importDefault(__nccwpck_require__(857)); +const path_1 = __importDefault(__nccwpck_require__(6928)); +const imageTagMap = new Map(); +const buildStep = async () => { + const inputs = await (0, inputs_1.getInputs)(); + if (inputs[types_1.ActionStep.BUILD] === undefined) { + core.error('No build inputs provided!'); + throw new Error('No build inputs provided!'); + } + const { images, tag } = inputs[types_1.ActionStep.BUILD]; + if (images.length === 0) { + core.error('No images provided!'); + throw new Error('No images provided!'); + } + if (!tag) { + core.error('No tag provided!'); + throw new Error('No tag provided!'); + } + const timestamp = Math.floor(Date.now() / 1000); + const actualTag = `${tag}.${timestamp}`; + const alreadyBuilt = []; + for (const image of images) { + if (alreadyBuilt.includes(image)) { + core.info(`Skipping already built image: ${image}:${actualTag}`); + continue; + } + core.info(`Building image: ${image}:${actualTag}`); + const exitCode = await exec.exec('bash', ['cli', 'build', image, actualTag], { + cwd: './scripts', + }); + if (exitCode !== 0) { + core.error(`Failed to build image: ${image}:${actualTag}`); + } + else { + alreadyBuilt.push(image); + imageTagMap.set(image, actualTag); + } + } +}; +exports.buildStep = buildStep; +const pushStep = async () => { + var _a, _b; + const inputs = await (0, inputs_1.getInputs)(); + const images = (_b = (_a = inputs[types_1.ActionStep.BUILD]) === null || _a === void 0 ? void 0 : _a.images) !== null && _b !== void 0 ? _b : []; + const pushInput = inputs[types_1.ActionStep.PUSH]; + if (!pushInput) { + core.error('No push inputs provided!'); + throw new Error('No push inputs provided!'); + } + if (images.length === 0) { + core.error('No images provided!'); + throw new Error('No images provided!'); + } + // do a docker login + const exitCode = await exec.exec('docker', [ + 'login', + 'sjc.ocir.io', + '--username', + pushInput.dockerUsername, + '--password', + pushInput.dockerPassword, + ]); + if (exitCode !== 0) { + core.error('Failed to login to docker!'); + throw new Error('Failed to login to docker!'); + } + // now push the images + const alreadyPushed = []; + for (const image of images) { + if (alreadyPushed.includes(image)) { + core.info(`Skipping already pushed image: ${image}`); + continue; + } + if (!imageTagMap.has(image)) { + core.warning(`No tag found for image: ${image} - image wasn't built successfully!`); + continue; + } + const tag = imageTagMap.get(image); + core.info(`Pushing image: ${image}:${tag}!`); + const exitCode = await exec.exec('bash', ['cli', 'push', image, tag], { + cwd: './scripts', + }); + if (exitCode !== 0) { + core.error(`Failed to push image: ${image}:${tag}`); + imageTagMap.delete(image); + } + else { + alreadyPushed.push(image); + } + } +}; +exports.pushStep = pushStep; +const deployStep = async () => { + const inputs = await (0, inputs_1.getInputs)(); + const deployInput = inputs[types_1.ActionStep.DEPLOY]; + if (!deployInput) { + core.error('No deploy inputs provided!'); + throw new Error('No deploy inputs provided!'); + } + if (deployInput.services.length === 0) { + core.warning('No services specified for deploy!'); + return; + } + // check if any images failed to build + const builderDefinitions = await (0, utils_1.getBuilderDefinitions)(); + const servicesToDeploy = []; + for (const service of deployInput.services) { + const builderDef = builderDefinitions.find((b) => b.services.includes(service)); + if (!builderDef) { + core.error(`No builder definition found for service: ${service}`); + throw new Error(`No builder definition found for service: ${service}`); + } + if (!imageTagMap.has(builderDef.imageName)) { + core.error(`No tag found for image: ${builderDef.imageName} - image wasn't built successfully!`); + throw new Error(`No tag found for image: ${builderDef.imageName} - image wasn't built successfully!`); + } + const tag = imageTagMap.get(builderDef.imageName); + servicesToDeploy.push({ + service, + tag, + builderDef, + }); + } + let exitCode; + if (deployInput.aws) { + const env = { + AWS_ACCESS_KEY_ID: deployInput.aws.awsAccessKeyId, + AWS_SECRET_ACCESS_KEY: deployInput.aws.awsSecretAccessKey, + AWS_REGION: deployInput.aws.awsRegion, + }; + exitCode = await exec.exec('aws', [ + 'eks', + 'update-kubeconfig', + '--name', + deployInput.aws.eksClusterName, + '--role-arn', + deployInput.aws.awsRoleArn, + ], { + env, + }); + if (exitCode !== 0) { + core.error('Failed to update kubeconfig!'); + throw new Error('Failed to update kubeconfig!'); + } + } + else if (deployInput.oracle) { + const homeDir = os_1.default.homedir(); + const kubeDir = path_1.default.join(homeDir, '.kube'); + const ociDir = path_1.default.join(homeDir, '.oci'); + const configPath = path_1.default.join(ociDir, 'config'); + const keyPath = path_1.default.join(ociDir, 'oci_api_key.pem'); + // prepare oracle config + let config = ` +[DEFAULT] +user=${deployInput.oracle.user} +fingerprint=${deployInput.oracle.fingerprint} +key_file=${keyPath} +tenancy=${deployInput.oracle.tenant} +region=${deployInput.oracle.region} +`; + // create the ~/.oci folder if it doesn't exists + await fs_1.default.mkdirSync(ociDir, { recursive: true }); + // write config to ~/.oci/config + await fs_1.default.writeFileSync(configPath, config, 'utf8'); + // write private key to ~/.oci/oci_api_key.pem + await fs_1.default.writeFileSync(keyPath, deployInput.oracle.key, 'utf8'); + // chmod 600 to key and config + await fs_1.default.chmodSync(configPath, 0o600); + await fs_1.default.chmodSync(keyPath, 0o600); + // get kubernetes context + await fs_1.default.mkdirSync(kubeDir, { recursive: true }); + exitCode = await exec.exec('oci', [ + 'ce', + 'cluster', + 'create-kubeconfig', + '--cluster-id', + deployInput.oracle.cluster, + '--file', + '~/.kube/config', + '--region', + deployInput.oracle.region, + '--token-version', + '2.0.0', + '--kube-endpoint', + 'PUBLIC_ENDPOINT', + '--config-file', + configPath, + ]); + if (exitCode !== 0) { + core.error('Failed to create kubeconfig!'); + throw new Error('Failed to create kubeconfig'); + } + } + else { + core.error('No cloud provider specified!'); + throw new Error('No cloud provider specified!'); + } + let failed = []; + for (const serviceDef of servicesToDeploy) { + const tag = serviceDef.tag; + const service = serviceDef.service; + const prioritized = serviceDef.builderDef.prioritizedServices.includes(service); + const servicesToUpdate = []; + if (prioritized) { + switch (deployInput.cloudEnvironment) { + case types_1.CloudEnvironment.PRODUCTION: { + servicesToUpdate.push(...[`${service}-system`, `${service}-normal`, `${service}-high`, `${service}-urgent`]); + break; + } + case types_1.CloudEnvironment.LF_ORACLE_PRODUCTION: + case types_1.CloudEnvironment.LF_PRODUCTION: { + servicesToUpdate.push(...[`${service}-system`, `${service}-normal`, `${service}-high`]); + break; + } + case types_1.CloudEnvironment.LF_ORACLE_STAGING: + case types_1.CloudEnvironment.LF_STAGING: + case types_1.CloudEnvironment.STAGING: { + servicesToUpdate.push(`${service}-normal`); + break; + } + default: + core.error(`Unknown cloud environment: ${deployInput.cloudEnvironment}`); + throw new Error(`Unknown cloud environment: ${deployInput.cloudEnvironment}`); + } + } + else { + servicesToUpdate.push(service); + } + core.info(`Deploying service: ${service} with image: ${serviceDef.builderDef.dockerRepository}:${tag} to deployments: ${servicesToUpdate.join(', ')}`); + for (const toDeploy of servicesToUpdate) { + exitCode = await exec.exec('kubectl', [ + 'set', + 'image', + `deployments/${toDeploy}-dpl`, + `${toDeploy}=${serviceDef.builderDef.dockerRepository}:${tag}`, + ]); + if (exitCode !== 0) { + core.error(`Failed to deploy service: ${service} to deployment: ${toDeploy}`); + if (!failed.includes(service)) { + failed.push(service); + } + } + } + } + if (failed.length > 0) { + core.error(`Failed to deploy services: ${failed.join(', ')}`); + throw new Error(`Failed to deploy services: ${failed.join(', ')}`); + } +}; +exports.deployStep = deployStep; + + +/***/ }), + +/***/ 6468: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.CloudEnvironment = exports.ActionStep = void 0; +var ActionStep; +(function (ActionStep) { + ActionStep["BUILD"] = "build"; + ActionStep["PUSH"] = "push"; + ActionStep["DEPLOY"] = "deploy"; +})(ActionStep || (exports.ActionStep = ActionStep = {})); +var CloudEnvironment; +(function (CloudEnvironment) { + CloudEnvironment["PRODUCTION"] = "production"; + CloudEnvironment["STAGING"] = "staging"; + CloudEnvironment["LF_PRODUCTION"] = "lf-production"; + CloudEnvironment["LF_ORACLE_PRODUCTION"] = "lf-oracle-production"; + CloudEnvironment["LF_ORACLE_STAGING"] = "lf-oracle-staging"; + CloudEnvironment["LF_STAGING"] = "lf-staging"; +})(CloudEnvironment || (exports.CloudEnvironment = CloudEnvironment = {})); + + +/***/ }), + +/***/ 9640: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getBuilderDefinitions = void 0; +const dotenv_1 = __importDefault(__nccwpck_require__(3085)); +const fs_1 = __importDefault(__nccwpck_require__(9896)); +let definitions; +const getBuilderDefinitions = async () => { + if (definitions !== undefined) { + return definitions; + } + const results = []; + const files = fs_1.default.readdirSync('./scripts/builders'); + for (const result of files) { + if (result.endsWith('.env')) { + const content = fs_1.default.readFileSync(`./scripts/builders/${result}`, 'utf-8'); + const parsed = dotenv_1.default.parse(content); + const imageName = result.split('.env')[0]; + const dockerRepository = parsed.REPO; + let services = []; + if (parsed.SERVICES !== undefined) { + services = parsed.SERVICES.split(' ') + .map((s) => s.trim()) + .filter((s) => s.length > 0); + } + let prioritizedServices = []; + if (parsed.PRIORITIZED !== undefined) { + prioritizedServices = parsed.PRIORITIZED.split(' ') + .map((s) => s.trim()) + .filter((s) => s.length > 0); + } + results.push({ + imageName, + dockerRepository, + services, + prioritizedServices, + }); + } + } + definitions = results; + return results; +}; +exports.getBuilderDefinitions = getBuilderDefinitions; + + +/***/ }), + +/***/ 2613: +/***/ ((module) => { + +"use strict"; +module.exports = require("assert"); + +/***/ }), + +/***/ 290: +/***/ ((module) => { + +"use strict"; +module.exports = require("async_hooks"); + +/***/ }), + +/***/ 181: +/***/ ((module) => { + +"use strict"; +module.exports = require("buffer"); + +/***/ }), + +/***/ 5317: +/***/ ((module) => { + +"use strict"; +module.exports = require("child_process"); + +/***/ }), + +/***/ 4236: +/***/ ((module) => { + +"use strict"; +module.exports = require("console"); + +/***/ }), + +/***/ 6982: +/***/ ((module) => { + +"use strict"; +module.exports = require("crypto"); + +/***/ }), + +/***/ 1637: +/***/ ((module) => { + +"use strict"; +module.exports = require("diagnostics_channel"); + +/***/ }), + +/***/ 4434: +/***/ ((module) => { + +"use strict"; +module.exports = require("events"); + +/***/ }), + +/***/ 9896: +/***/ ((module) => { + +"use strict"; +module.exports = require("fs"); + +/***/ }), + +/***/ 8611: +/***/ ((module) => { + +"use strict"; +module.exports = require("http"); + +/***/ }), + +/***/ 5675: +/***/ ((module) => { + +"use strict"; +module.exports = require("http2"); + +/***/ }), + +/***/ 5692: +/***/ ((module) => { + +"use strict"; +module.exports = require("https"); + +/***/ }), + +/***/ 9278: +/***/ ((module) => { + +"use strict"; +module.exports = require("net"); + +/***/ }), + +/***/ 7598: +/***/ ((module) => { + +"use strict"; +module.exports = require("node:crypto"); + +/***/ }), + +/***/ 8474: +/***/ ((module) => { + +"use strict"; +module.exports = require("node:events"); + +/***/ }), + +/***/ 7075: +/***/ ((module) => { + +"use strict"; +module.exports = require("node:stream"); + +/***/ }), + +/***/ 7975: +/***/ ((module) => { + +"use strict"; +module.exports = require("node:util"); + +/***/ }), + +/***/ 857: +/***/ ((module) => { + +"use strict"; +module.exports = require("os"); + +/***/ }), + +/***/ 6928: +/***/ ((module) => { + +"use strict"; +module.exports = require("path"); + +/***/ }), + +/***/ 2987: +/***/ ((module) => { + +"use strict"; +module.exports = require("perf_hooks"); + +/***/ }), + +/***/ 3480: +/***/ ((module) => { + +"use strict"; +module.exports = require("querystring"); + +/***/ }), + +/***/ 2203: +/***/ ((module) => { + +"use strict"; +module.exports = require("stream"); + +/***/ }), + +/***/ 3774: +/***/ ((module) => { + +"use strict"; +module.exports = require("stream/web"); + +/***/ }), + +/***/ 3193: +/***/ ((module) => { + +"use strict"; +module.exports = require("string_decoder"); + +/***/ }), + +/***/ 3557: +/***/ ((module) => { + +"use strict"; +module.exports = require("timers"); + +/***/ }), + +/***/ 4756: +/***/ ((module) => { + +"use strict"; +module.exports = require("tls"); + +/***/ }), + +/***/ 7016: +/***/ ((module) => { + +"use strict"; +module.exports = require("url"); + +/***/ }), + +/***/ 9023: +/***/ ((module) => { + +"use strict"; +module.exports = require("util"); + +/***/ }), + +/***/ 8253: +/***/ ((module) => { + +"use strict"; +module.exports = require("util/types"); + +/***/ }), + +/***/ 8167: +/***/ ((module) => { + +"use strict"; +module.exports = require("worker_threads"); + +/***/ }), + +/***/ 3106: +/***/ ((module) => { + +"use strict"; +module.exports = require("zlib"); + +/***/ }), + +/***/ 8683: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const WritableStream = (__nccwpck_require__(7075).Writable) +const inherits = (__nccwpck_require__(7975).inherits) + +const StreamSearch = __nccwpck_require__(6761) + +const PartStream = __nccwpck_require__(883) +const HeaderParser = __nccwpck_require__(2788) + +const DASH = 45 +const B_ONEDASH = Buffer.from('-') +const B_CRLF = Buffer.from('\r\n') +const EMPTY_FN = function () {} + +function Dicer (cfg) { + if (!(this instanceof Dicer)) { return new Dicer(cfg) } + WritableStream.call(this, cfg) + + if (!cfg || (!cfg.headerFirst && typeof cfg.boundary !== 'string')) { throw new TypeError('Boundary required') } + + if (typeof cfg.boundary === 'string') { this.setBoundary(cfg.boundary) } else { this._bparser = undefined } + + this._headerFirst = cfg.headerFirst + + this._dashes = 0 + this._parts = 0 + this._finished = false + this._realFinish = false + this._isPreamble = true + this._justMatched = false + this._firstWrite = true + this._inHeader = true + this._part = undefined + this._cb = undefined + this._ignoreData = false + this._partOpts = { highWaterMark: cfg.partHwm } + this._pause = false + + const self = this + this._hparser = new HeaderParser(cfg) + this._hparser.on('header', function (header) { + self._inHeader = false + self._part.emit('header', header) + }) +} +inherits(Dicer, WritableStream) + +Dicer.prototype.emit = function (ev) { + if (ev === 'finish' && !this._realFinish) { + if (!this._finished) { + const self = this + process.nextTick(function () { + self.emit('error', new Error('Unexpected end of multipart data')) + if (self._part && !self._ignoreData) { + const type = (self._isPreamble ? 'Preamble' : 'Part') + self._part.emit('error', new Error(type + ' terminated early due to unexpected end of multipart data')) + self._part.push(null) + process.nextTick(function () { + self._realFinish = true + self.emit('finish') + self._realFinish = false + }) + return + } + self._realFinish = true + self.emit('finish') + self._realFinish = false + }) + } + } else { WritableStream.prototype.emit.apply(this, arguments) } +} + +Dicer.prototype._write = function (data, encoding, cb) { + // ignore unexpected data (e.g. extra trailer data after finished) + if (!this._hparser && !this._bparser) { return cb() } + + if (this._headerFirst && this._isPreamble) { + if (!this._part) { + this._part = new PartStream(this._partOpts) + if (this.listenerCount('preamble') !== 0) { this.emit('preamble', this._part) } else { this._ignore() } + } + const r = this._hparser.push(data) + if (!this._inHeader && r !== undefined && r < data.length) { data = data.slice(r) } else { return cb() } + } + + // allows for "easier" testing + if (this._firstWrite) { + this._bparser.push(B_CRLF) + this._firstWrite = false + } + + this._bparser.push(data) + + if (this._pause) { this._cb = cb } else { cb() } +} + +Dicer.prototype.reset = function () { + this._part = undefined + this._bparser = undefined + this._hparser = undefined +} + +Dicer.prototype.setBoundary = function (boundary) { + const self = this + this._bparser = new StreamSearch('\r\n--' + boundary) + this._bparser.on('info', function (isMatch, data, start, end) { + self._oninfo(isMatch, data, start, end) + }) +} + +Dicer.prototype._ignore = function () { + if (this._part && !this._ignoreData) { + this._ignoreData = true + this._part.on('error', EMPTY_FN) + // we must perform some kind of read on the stream even though we are + // ignoring the data, otherwise node's Readable stream will not emit 'end' + // after pushing null to the stream + this._part.resume() + } +} + +Dicer.prototype._oninfo = function (isMatch, data, start, end) { + let buf; const self = this; let i = 0; let r; let shouldWriteMore = true + + if (!this._part && this._justMatched && data) { + while (this._dashes < 2 && (start + i) < end) { + if (data[start + i] === DASH) { + ++i + ++this._dashes + } else { + if (this._dashes) { buf = B_ONEDASH } + this._dashes = 0 + break + } + } + if (this._dashes === 2) { + if ((start + i) < end && this.listenerCount('trailer') !== 0) { this.emit('trailer', data.slice(start + i, end)) } + this.reset() + this._finished = true + // no more parts will be added + if (self._parts === 0) { + self._realFinish = true + self.emit('finish') + self._realFinish = false + } + } + if (this._dashes) { return } + } + if (this._justMatched) { this._justMatched = false } + if (!this._part) { + this._part = new PartStream(this._partOpts) + this._part._read = function (n) { + self._unpause() + } + if (this._isPreamble && this.listenerCount('preamble') !== 0) { + this.emit('preamble', this._part) + } else if (this._isPreamble !== true && this.listenerCount('part') !== 0) { + this.emit('part', this._part) + } else { + this._ignore() + } + if (!this._isPreamble) { this._inHeader = true } + } + if (data && start < end && !this._ignoreData) { + if (this._isPreamble || !this._inHeader) { + if (buf) { shouldWriteMore = this._part.push(buf) } + shouldWriteMore = this._part.push(data.slice(start, end)) + if (!shouldWriteMore) { this._pause = true } + } else if (!this._isPreamble && this._inHeader) { + if (buf) { this._hparser.push(buf) } + r = this._hparser.push(data.slice(start, end)) + if (!this._inHeader && r !== undefined && r < end) { this._oninfo(false, data, start + r, end) } + } + } + if (isMatch) { + this._hparser.reset() + if (this._isPreamble) { this._isPreamble = false } else { + if (start !== end) { + ++this._parts + this._part.on('end', function () { + if (--self._parts === 0) { + if (self._finished) { + self._realFinish = true + self.emit('finish') + self._realFinish = false + } else { + self._unpause() + } + } + }) + } + } + this._part.push(null) + this._part = undefined + this._ignoreData = false + this._justMatched = true + this._dashes = 0 + } +} + +Dicer.prototype._unpause = function () { + if (!this._pause) { return } + + this._pause = false + if (this._cb) { + const cb = this._cb + this._cb = undefined + cb() + } +} + +module.exports = Dicer + + +/***/ }), + +/***/ 2788: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const EventEmitter = (__nccwpck_require__(8474).EventEmitter) +const inherits = (__nccwpck_require__(7975).inherits) +const getLimit = __nccwpck_require__(8352) + +const StreamSearch = __nccwpck_require__(6761) + +const B_DCRLF = Buffer.from('\r\n\r\n') +const RE_CRLF = /\r\n/g +const RE_HDR = /^([^:]+):[ \t]?([\x00-\xFF]+)?$/ // eslint-disable-line no-control-regex + +function HeaderParser (cfg) { + EventEmitter.call(this) + + cfg = cfg || {} + const self = this + this.nread = 0 + this.maxed = false + this.npairs = 0 + this.maxHeaderPairs = getLimit(cfg, 'maxHeaderPairs', 2000) + this.maxHeaderSize = getLimit(cfg, 'maxHeaderSize', 80 * 1024) + this.buffer = '' + this.header = {} + this.finished = false + this.ss = new StreamSearch(B_DCRLF) + this.ss.on('info', function (isMatch, data, start, end) { + if (data && !self.maxed) { + if (self.nread + end - start >= self.maxHeaderSize) { + end = self.maxHeaderSize - self.nread + start + self.nread = self.maxHeaderSize + self.maxed = true + } else { self.nread += (end - start) } + + self.buffer += data.toString('binary', start, end) + } + if (isMatch) { self._finish() } + }) +} +inherits(HeaderParser, EventEmitter) + +HeaderParser.prototype.push = function (data) { + const r = this.ss.push(data) + if (this.finished) { return r } +} + +HeaderParser.prototype.reset = function () { + this.finished = false + this.buffer = '' + this.header = {} + this.ss.reset() +} + +HeaderParser.prototype._finish = function () { + if (this.buffer) { this._parseHeader() } + this.ss.matches = this.ss.maxMatches + const header = this.header + this.header = {} + this.buffer = '' + this.finished = true + this.nread = this.npairs = 0 + this.maxed = false + this.emit('header', header) +} + +HeaderParser.prototype._parseHeader = function () { + if (this.npairs === this.maxHeaderPairs) { return } + + const lines = this.buffer.split(RE_CRLF) + const len = lines.length + let m, h + + for (var i = 0; i < len; ++i) { // eslint-disable-line no-var + if (lines[i].length === 0) { continue } + if (lines[i][0] === '\t' || lines[i][0] === ' ') { + // folded header content + // RFC2822 says to just remove the CRLF and not the whitespace following + // it, so we follow the RFC and include the leading whitespace ... + if (h) { + this.header[h][this.header[h].length - 1] += lines[i] + continue + } + } + + const posColon = lines[i].indexOf(':') + if ( + posColon === -1 || + posColon === 0 + ) { + return + } + m = RE_HDR.exec(lines[i]) + h = m[1].toLowerCase() + this.header[h] = this.header[h] || [] + this.header[h].push((m[2] || '')) + if (++this.npairs === this.maxHeaderPairs) { break } + } +} + +module.exports = HeaderParser + + +/***/ }), + +/***/ 883: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const inherits = (__nccwpck_require__(7975).inherits) +const ReadableStream = (__nccwpck_require__(7075).Readable) + +function PartStream (opts) { + ReadableStream.call(this, opts) +} +inherits(PartStream, ReadableStream) + +PartStream.prototype._read = function (n) {} + +module.exports = PartStream + + +/***/ }), + +/***/ 6761: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +/** + * Copyright Brian White. All rights reserved. + * + * @see https://github.com/mscdex/streamsearch + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + * Based heavily on the Streaming Boyer-Moore-Horspool C++ implementation + * by Hongli Lai at: https://github.com/FooBarWidget/boyer-moore-horspool + */ +const EventEmitter = (__nccwpck_require__(8474).EventEmitter) +const inherits = (__nccwpck_require__(7975).inherits) + +function SBMH (needle) { + if (typeof needle === 'string') { + needle = Buffer.from(needle) + } + + if (!Buffer.isBuffer(needle)) { + throw new TypeError('The needle has to be a String or a Buffer.') + } + + const needleLength = needle.length + + if (needleLength === 0) { + throw new Error('The needle cannot be an empty String/Buffer.') + } + + if (needleLength > 256) { + throw new Error('The needle cannot have a length bigger than 256.') + } + + this.maxMatches = Infinity + this.matches = 0 + + this._occ = new Array(256) + .fill(needleLength) // Initialize occurrence table. + this._lookbehind_size = 0 + this._needle = needle + this._bufpos = 0 + + this._lookbehind = Buffer.alloc(needleLength) + + // Populate occurrence table with analysis of the needle, + // ignoring last letter. + for (var i = 0; i < needleLength - 1; ++i) { // eslint-disable-line no-var + this._occ[needle[i]] = needleLength - 1 - i + } +} +inherits(SBMH, EventEmitter) + +SBMH.prototype.reset = function () { + this._lookbehind_size = 0 + this.matches = 0 + this._bufpos = 0 +} + +SBMH.prototype.push = function (chunk, pos) { + if (!Buffer.isBuffer(chunk)) { + chunk = Buffer.from(chunk, 'binary') + } + const chlen = chunk.length + this._bufpos = pos || 0 + let r + while (r !== chlen && this.matches < this.maxMatches) { r = this._sbmh_feed(chunk) } + return r +} + +SBMH.prototype._sbmh_feed = function (data) { + const len = data.length + const needle = this._needle + const needleLength = needle.length + const lastNeedleChar = needle[needleLength - 1] + + // Positive: points to a position in `data` + // pos == 3 points to data[3] + // Negative: points to a position in the lookbehind buffer + // pos == -2 points to lookbehind[lookbehind_size - 2] + let pos = -this._lookbehind_size + let ch + + if (pos < 0) { + // Lookbehind buffer is not empty. Perform Boyer-Moore-Horspool + // search with character lookup code that considers both the + // lookbehind buffer and the current round's haystack data. + // + // Loop until + // there is a match. + // or until + // we've moved past the position that requires the + // lookbehind buffer. In this case we switch to the + // optimized loop. + // or until + // the character to look at lies outside the haystack. + while (pos < 0 && pos <= len - needleLength) { + ch = this._sbmh_lookup_char(data, pos + needleLength - 1) + + if ( + ch === lastNeedleChar && + this._sbmh_memcmp(data, pos, needleLength - 1) + ) { + this._lookbehind_size = 0 + ++this.matches + this.emit('info', true) + + return (this._bufpos = pos + needleLength) + } + pos += this._occ[ch] + } + + // No match. + + if (pos < 0) { + // There's too few data for Boyer-Moore-Horspool to run, + // so let's use a different algorithm to skip as much as + // we can. + // Forward pos until + // the trailing part of lookbehind + data + // looks like the beginning of the needle + // or until + // pos == 0 + while (pos < 0 && !this._sbmh_memcmp(data, pos, len - pos)) { ++pos } + } + + if (pos >= 0) { + // Discard lookbehind buffer. + this.emit('info', false, this._lookbehind, 0, this._lookbehind_size) + this._lookbehind_size = 0 + } else { + // Cut off part of the lookbehind buffer that has + // been processed and append the entire haystack + // into it. + const bytesToCutOff = this._lookbehind_size + pos + if (bytesToCutOff > 0) { + // The cut off data is guaranteed not to contain the needle. + this.emit('info', false, this._lookbehind, 0, bytesToCutOff) + } + + this._lookbehind.copy(this._lookbehind, 0, bytesToCutOff, + this._lookbehind_size - bytesToCutOff) + this._lookbehind_size -= bytesToCutOff + + data.copy(this._lookbehind, this._lookbehind_size) + this._lookbehind_size += len + + this._bufpos = len + return len + } + } + + pos += (pos >= 0) * this._bufpos + + // Lookbehind buffer is now empty. We only need to check if the + // needle is in the haystack. + if (data.indexOf(needle, pos) !== -1) { + pos = data.indexOf(needle, pos) + ++this.matches + if (pos > 0) { this.emit('info', true, data, this._bufpos, pos) } else { this.emit('info', true) } + + return (this._bufpos = pos + needleLength) + } else { + pos = len - needleLength + } + + // There was no match. If there's trailing haystack data that we cannot + // match yet using the Boyer-Moore-Horspool algorithm (because the trailing + // data is less than the needle size) then match using a modified + // algorithm that starts matching from the beginning instead of the end. + // Whatever trailing data is left after running this algorithm is added to + // the lookbehind buffer. + while ( + pos < len && + ( + data[pos] !== needle[0] || + ( + (Buffer.compare( + data.subarray(pos, pos + len - pos), + needle.subarray(0, len - pos) + ) !== 0) + ) + ) + ) { + ++pos + } + if (pos < len) { + data.copy(this._lookbehind, 0, pos, pos + (len - pos)) + this._lookbehind_size = len - pos + } + + // Everything until pos is guaranteed not to contain needle data. + if (pos > 0) { this.emit('info', false, data, this._bufpos, pos < len ? pos : len) } + + this._bufpos = len + return len +} + +SBMH.prototype._sbmh_lookup_char = function (data, pos) { + return (pos < 0) + ? this._lookbehind[this._lookbehind_size + pos] + : data[pos] +} + +SBMH.prototype._sbmh_memcmp = function (data, pos, len) { + for (var i = 0; i < len; ++i) { // eslint-disable-line no-var + if (this._sbmh_lookup_char(data, pos + i) !== this._needle[i]) { return false } + } + return true +} + +module.exports = SBMH + + +/***/ }), + +/***/ 9780: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const WritableStream = (__nccwpck_require__(7075).Writable) +const { inherits } = __nccwpck_require__(7975) +const Dicer = __nccwpck_require__(8683) + +const MultipartParser = __nccwpck_require__(5035) +const UrlencodedParser = __nccwpck_require__(8718) +const parseParams = __nccwpck_require__(8898) + +function Busboy (opts) { + if (!(this instanceof Busboy)) { return new Busboy(opts) } + + if (typeof opts !== 'object') { + throw new TypeError('Busboy expected an options-Object.') + } + if (typeof opts.headers !== 'object') { + throw new TypeError('Busboy expected an options-Object with headers-attribute.') + } + if (typeof opts.headers['content-type'] !== 'string') { + throw new TypeError('Missing Content-Type-header.') + } + + const { + headers, + ...streamOptions + } = opts + + this.opts = { + autoDestroy: false, + ...streamOptions + } + WritableStream.call(this, this.opts) + + this._done = false + this._parser = this.getParserByHeaders(headers) + this._finished = false +} +inherits(Busboy, WritableStream) + +Busboy.prototype.emit = function (ev) { + if (ev === 'finish') { + if (!this._done) { + this._parser?.end() + return + } else if (this._finished) { + return + } + this._finished = true + } + WritableStream.prototype.emit.apply(this, arguments) +} + +Busboy.prototype.getParserByHeaders = function (headers) { + const parsed = parseParams(headers['content-type']) + + const cfg = { + defCharset: this.opts.defCharset, + fileHwm: this.opts.fileHwm, + headers, + highWaterMark: this.opts.highWaterMark, + isPartAFile: this.opts.isPartAFile, + limits: this.opts.limits, + parsedConType: parsed, + preservePath: this.opts.preservePath + } + + if (MultipartParser.detect.test(parsed[0])) { + return new MultipartParser(this, cfg) + } + if (UrlencodedParser.detect.test(parsed[0])) { + return new UrlencodedParser(this, cfg) + } + throw new Error('Unsupported Content-Type.') +} + +Busboy.prototype._write = function (chunk, encoding, cb) { + this._parser.write(chunk, cb) +} + +module.exports = Busboy +module.exports["default"] = Busboy +module.exports.Busboy = Busboy + +module.exports.Dicer = Dicer + + +/***/ }), + +/***/ 5035: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +// TODO: +// * support 1 nested multipart level +// (see second multipart example here: +// http://www.w3.org/TR/html401/interact/forms.html#didx-multipartform-data) +// * support limits.fieldNameSize +// -- this will require modifications to utils.parseParams + +const { Readable } = __nccwpck_require__(7075) +const { inherits } = __nccwpck_require__(7975) + +const Dicer = __nccwpck_require__(8683) + +const parseParams = __nccwpck_require__(8898) +const decodeText = __nccwpck_require__(1350) +const basename = __nccwpck_require__(7461) +const getLimit = __nccwpck_require__(8352) + +const RE_BOUNDARY = /^boundary$/i +const RE_FIELD = /^form-data$/i +const RE_CHARSET = /^charset$/i +const RE_FILENAME = /^filename$/i +const RE_NAME = /^name$/i + +Multipart.detect = /^multipart\/form-data/i +function Multipart (boy, cfg) { + let i + let len + const self = this + let boundary + const limits = cfg.limits + const isPartAFile = cfg.isPartAFile || ((fieldName, contentType, fileName) => (contentType === 'application/octet-stream' || fileName !== undefined)) + const parsedConType = cfg.parsedConType || [] + const defCharset = cfg.defCharset || 'utf8' + const preservePath = cfg.preservePath + const fileOpts = { highWaterMark: cfg.fileHwm } + + for (i = 0, len = parsedConType.length; i < len; ++i) { + if (Array.isArray(parsedConType[i]) && + RE_BOUNDARY.test(parsedConType[i][0])) { + boundary = parsedConType[i][1] + break + } + } + + function checkFinished () { + if (nends === 0 && finished && !boy._done) { + finished = false + self.end() + } + } + + if (typeof boundary !== 'string') { throw new Error('Multipart: Boundary not found') } + + const fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024) + const fileSizeLimit = getLimit(limits, 'fileSize', Infinity) + const filesLimit = getLimit(limits, 'files', Infinity) + const fieldsLimit = getLimit(limits, 'fields', Infinity) + const partsLimit = getLimit(limits, 'parts', Infinity) + const headerPairsLimit = getLimit(limits, 'headerPairs', 2000) + const headerSizeLimit = getLimit(limits, 'headerSize', 80 * 1024) + + let nfiles = 0 + let nfields = 0 + let nends = 0 + let curFile + let curField + let finished = false + + this._needDrain = false + this._pause = false + this._cb = undefined + this._nparts = 0 + this._boy = boy + + const parserCfg = { + boundary, + maxHeaderPairs: headerPairsLimit, + maxHeaderSize: headerSizeLimit, + partHwm: fileOpts.highWaterMark, + highWaterMark: cfg.highWaterMark + } + + this.parser = new Dicer(parserCfg) + this.parser.on('drain', function () { + self._needDrain = false + if (self._cb && !self._pause) { + const cb = self._cb + self._cb = undefined + cb() + } + }).on('part', function onPart (part) { + if (++self._nparts > partsLimit) { + self.parser.removeListener('part', onPart) + self.parser.on('part', skipPart) + boy.hitPartsLimit = true + boy.emit('partsLimit') + return skipPart(part) + } + + // hack because streams2 _always_ doesn't emit 'end' until nextTick, so let + // us emit 'end' early since we know the part has ended if we are already + // seeing the next part + if (curField) { + const field = curField + field.emit('end') + field.removeAllListeners('end') + } + + part.on('header', function (header) { + let contype + let fieldname + let parsed + let charset + let encoding + let filename + let nsize = 0 + + if (header['content-type']) { + parsed = parseParams(header['content-type'][0]) + if (parsed[0]) { + contype = parsed[0].toLowerCase() + for (i = 0, len = parsed.length; i < len; ++i) { + if (RE_CHARSET.test(parsed[i][0])) { + charset = parsed[i][1].toLowerCase() + break + } + } + } + } + + if (contype === undefined) { contype = 'text/plain' } + if (charset === undefined) { charset = defCharset } + + if (header['content-disposition']) { + parsed = parseParams(header['content-disposition'][0]) + if (!RE_FIELD.test(parsed[0])) { return skipPart(part) } + for (i = 0, len = parsed.length; i < len; ++i) { + if (RE_NAME.test(parsed[i][0])) { + fieldname = parsed[i][1] + } else if (RE_FILENAME.test(parsed[i][0])) { + filename = parsed[i][1] + if (!preservePath) { filename = basename(filename) } + } + } + } else { return skipPart(part) } + + if (header['content-transfer-encoding']) { encoding = header['content-transfer-encoding'][0].toLowerCase() } else { encoding = '7bit' } + + let onData, + onEnd + + if (isPartAFile(fieldname, contype, filename)) { + // file/binary field + if (nfiles === filesLimit) { + if (!boy.hitFilesLimit) { + boy.hitFilesLimit = true + boy.emit('filesLimit') + } + return skipPart(part) + } + + ++nfiles + + if (boy.listenerCount('file') === 0) { + self.parser._ignore() + return + } + + ++nends + const file = new FileStream(fileOpts) + curFile = file + file.on('end', function () { + --nends + self._pause = false + checkFinished() + if (self._cb && !self._needDrain) { + const cb = self._cb + self._cb = undefined + cb() + } + }) + file._read = function (n) { + if (!self._pause) { return } + self._pause = false + if (self._cb && !self._needDrain) { + const cb = self._cb + self._cb = undefined + cb() + } + } + boy.emit('file', fieldname, file, filename, encoding, contype) + + onData = function (data) { + if ((nsize += data.length) > fileSizeLimit) { + const extralen = fileSizeLimit - nsize + data.length + if (extralen > 0) { file.push(data.slice(0, extralen)) } + file.truncated = true + file.bytesRead = fileSizeLimit + part.removeAllListeners('data') + file.emit('limit') + return + } else if (!file.push(data)) { self._pause = true } + + file.bytesRead = nsize + } + + onEnd = function () { + curFile = undefined + file.push(null) + } + } else { + // non-file field + if (nfields === fieldsLimit) { + if (!boy.hitFieldsLimit) { + boy.hitFieldsLimit = true + boy.emit('fieldsLimit') + } + return skipPart(part) + } + + ++nfields + ++nends + let buffer = '' + let truncated = false + curField = part + + onData = function (data) { + if ((nsize += data.length) > fieldSizeLimit) { + const extralen = (fieldSizeLimit - (nsize - data.length)) + buffer += data.toString('binary', 0, extralen) + truncated = true + part.removeAllListeners('data') + } else { buffer += data.toString('binary') } + } + + onEnd = function () { + curField = undefined + if (buffer.length) { buffer = decodeText(buffer, 'binary', charset) } + boy.emit('field', fieldname, buffer, false, truncated, encoding, contype) + --nends + checkFinished() + } + } + + /* As of node@2efe4ab761666 (v0.10.29+/v0.11.14+), busboy had become + broken. Streams2/streams3 is a huge black box of confusion, but + somehow overriding the sync state seems to fix things again (and still + seems to work for previous node versions). + */ + part._readableState.sync = false + + part.on('data', onData) + part.on('end', onEnd) + }).on('error', function (err) { + if (curFile) { curFile.emit('error', err) } + }) + }).on('error', function (err) { + boy.emit('error', err) + }).on('finish', function () { + finished = true + checkFinished() + }) +} + +Multipart.prototype.write = function (chunk, cb) { + const r = this.parser.write(chunk) + if (r && !this._pause) { + cb() + } else { + this._needDrain = !r + this._cb = cb + } +} + +Multipart.prototype.end = function () { + const self = this + + if (self.parser.writable) { + self.parser.end() + } else if (!self._boy._done) { + process.nextTick(function () { + self._boy._done = true + self._boy.emit('finish') + }) + } +} + +function skipPart (part) { + part.resume() +} + +function FileStream (opts) { + Readable.call(this, opts) + + this.bytesRead = 0 + + this.truncated = false +} + +inherits(FileStream, Readable) + +FileStream.prototype._read = function (n) {} + +module.exports = Multipart + + +/***/ }), + +/***/ 8718: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const Decoder = __nccwpck_require__(1395) +const decodeText = __nccwpck_require__(1350) +const getLimit = __nccwpck_require__(8352) + +const RE_CHARSET = /^charset$/i + +UrlEncoded.detect = /^application\/x-www-form-urlencoded/i +function UrlEncoded (boy, cfg) { + const limits = cfg.limits + const parsedConType = cfg.parsedConType + this.boy = boy + + this.fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024) + this.fieldNameSizeLimit = getLimit(limits, 'fieldNameSize', 100) + this.fieldsLimit = getLimit(limits, 'fields', Infinity) + + let charset + for (var i = 0, len = parsedConType.length; i < len; ++i) { // eslint-disable-line no-var + if (Array.isArray(parsedConType[i]) && + RE_CHARSET.test(parsedConType[i][0])) { + charset = parsedConType[i][1].toLowerCase() + break + } + } + + if (charset === undefined) { charset = cfg.defCharset || 'utf8' } + + this.decoder = new Decoder() + this.charset = charset + this._fields = 0 + this._state = 'key' + this._checkingBytes = true + this._bytesKey = 0 + this._bytesVal = 0 + this._key = '' + this._val = '' + this._keyTrunc = false + this._valTrunc = false + this._hitLimit = false +} + +UrlEncoded.prototype.write = function (data, cb) { + if (this._fields === this.fieldsLimit) { + if (!this.boy.hitFieldsLimit) { + this.boy.hitFieldsLimit = true + this.boy.emit('fieldsLimit') + } + return cb() + } + + let idxeq; let idxamp; let i; let p = 0; const len = data.length + + while (p < len) { + if (this._state === 'key') { + idxeq = idxamp = undefined + for (i = p; i < len; ++i) { + if (!this._checkingBytes) { ++p } + if (data[i] === 0x3D/* = */) { + idxeq = i + break + } else if (data[i] === 0x26/* & */) { + idxamp = i + break + } + if (this._checkingBytes && this._bytesKey === this.fieldNameSizeLimit) { + this._hitLimit = true + break + } else if (this._checkingBytes) { ++this._bytesKey } + } + + if (idxeq !== undefined) { + // key with assignment + if (idxeq > p) { this._key += this.decoder.write(data.toString('binary', p, idxeq)) } + this._state = 'val' + + this._hitLimit = false + this._checkingBytes = true + this._val = '' + this._bytesVal = 0 + this._valTrunc = false + this.decoder.reset() + + p = idxeq + 1 + } else if (idxamp !== undefined) { + // key with no assignment + ++this._fields + let key; const keyTrunc = this._keyTrunc + if (idxamp > p) { key = (this._key += this.decoder.write(data.toString('binary', p, idxamp))) } else { key = this._key } + + this._hitLimit = false + this._checkingBytes = true + this._key = '' + this._bytesKey = 0 + this._keyTrunc = false + this.decoder.reset() + + if (key.length) { + this.boy.emit('field', decodeText(key, 'binary', this.charset), + '', + keyTrunc, + false) + } + + p = idxamp + 1 + if (this._fields === this.fieldsLimit) { return cb() } + } else if (this._hitLimit) { + // we may not have hit the actual limit if there are encoded bytes... + if (i > p) { this._key += this.decoder.write(data.toString('binary', p, i)) } + p = i + if ((this._bytesKey = this._key.length) === this.fieldNameSizeLimit) { + // yep, we actually did hit the limit + this._checkingBytes = false + this._keyTrunc = true + } + } else { + if (p < len) { this._key += this.decoder.write(data.toString('binary', p)) } + p = len + } + } else { + idxamp = undefined + for (i = p; i < len; ++i) { + if (!this._checkingBytes) { ++p } + if (data[i] === 0x26/* & */) { + idxamp = i + break + } + if (this._checkingBytes && this._bytesVal === this.fieldSizeLimit) { + this._hitLimit = true + break + } else if (this._checkingBytes) { ++this._bytesVal } + } + + if (idxamp !== undefined) { + ++this._fields + if (idxamp > p) { this._val += this.decoder.write(data.toString('binary', p, idxamp)) } + this.boy.emit('field', decodeText(this._key, 'binary', this.charset), + decodeText(this._val, 'binary', this.charset), + this._keyTrunc, + this._valTrunc) + this._state = 'key' + + this._hitLimit = false + this._checkingBytes = true + this._key = '' + this._bytesKey = 0 + this._keyTrunc = false + this.decoder.reset() + + p = idxamp + 1 + if (this._fields === this.fieldsLimit) { return cb() } + } else if (this._hitLimit) { + // we may not have hit the actual limit if there are encoded bytes... + if (i > p) { this._val += this.decoder.write(data.toString('binary', p, i)) } + p = i + if ((this._val === '' && this.fieldSizeLimit === 0) || + (this._bytesVal = this._val.length) === this.fieldSizeLimit) { + // yep, we actually did hit the limit + this._checkingBytes = false + this._valTrunc = true + } + } else { + if (p < len) { this._val += this.decoder.write(data.toString('binary', p)) } + p = len + } + } + } + cb() +} + +UrlEncoded.prototype.end = function () { + if (this.boy._done) { return } + + if (this._state === 'key' && this._key.length > 0) { + this.boy.emit('field', decodeText(this._key, 'binary', this.charset), + '', + this._keyTrunc, + false) + } else if (this._state === 'val') { + this.boy.emit('field', decodeText(this._key, 'binary', this.charset), + decodeText(this._val, 'binary', this.charset), + this._keyTrunc, + this._valTrunc) + } + this.boy._done = true + this.boy.emit('finish') +} + +module.exports = UrlEncoded + + +/***/ }), + +/***/ 1395: +/***/ ((module) => { + +"use strict"; + + +const RE_PLUS = /\+/g + +const HEX = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +] + +function Decoder () { + this.buffer = undefined +} +Decoder.prototype.write = function (str) { + // Replace '+' with ' ' before decoding + str = str.replace(RE_PLUS, ' ') + let res = '' + let i = 0; let p = 0; const len = str.length + for (; i < len; ++i) { + if (this.buffer !== undefined) { + if (!HEX[str.charCodeAt(i)]) { + res += '%' + this.buffer + this.buffer = undefined + --i // retry character + } else { + this.buffer += str[i] + ++p + if (this.buffer.length === 2) { + res += String.fromCharCode(parseInt(this.buffer, 16)) + this.buffer = undefined + } + } + } else if (str[i] === '%') { + if (i > p) { + res += str.substring(p, i) + p = i + } + this.buffer = '' + ++p + } + } + if (p < len && this.buffer === undefined) { res += str.substring(p) } + return res +} +Decoder.prototype.reset = function () { + this.buffer = undefined +} + +module.exports = Decoder + + +/***/ }), + +/***/ 7461: +/***/ ((module) => { + +"use strict"; + + +module.exports = function basename (path) { + if (typeof path !== 'string') { return '' } + for (var i = path.length - 1; i >= 0; --i) { // eslint-disable-line no-var + switch (path.charCodeAt(i)) { + case 0x2F: // '/' + case 0x5C: // '\' + path = path.slice(i + 1) + return (path === '..' || path === '.' ? '' : path) + } + } + return (path === '..' || path === '.' ? '' : path) +} + + +/***/ }), + +/***/ 1350: +/***/ (function(module) { + +"use strict"; + + +// Node has always utf-8 +const utf8Decoder = new TextDecoder('utf-8') +const textDecoders = new Map([ + ['utf-8', utf8Decoder], + ['utf8', utf8Decoder] +]) + +function getDecoder (charset) { + let lc + while (true) { + switch (charset) { + case 'utf-8': + case 'utf8': + return decoders.utf8 + case 'latin1': + case 'ascii': // TODO: Make these a separate, strict decoder? + case 'us-ascii': + case 'iso-8859-1': + case 'iso8859-1': + case 'iso88591': + case 'iso_8859-1': + case 'windows-1252': + case 'iso_8859-1:1987': + case 'cp1252': + case 'x-cp1252': + return decoders.latin1 + case 'utf16le': + case 'utf-16le': + case 'ucs2': + case 'ucs-2': + return decoders.utf16le + case 'base64': + return decoders.base64 + default: + if (lc === undefined) { + lc = true + charset = charset.toLowerCase() + continue + } + return decoders.other.bind(charset) + } + } +} + +const decoders = { + utf8: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + return data.utf8Slice(0, data.length) + }, + + latin1: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + return data + } + return data.latin1Slice(0, data.length) + }, + + utf16le: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + return data.ucs2Slice(0, data.length) + }, + + base64: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + return data.base64Slice(0, data.length) + }, + + other: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + + if (textDecoders.has(this.toString())) { + try { + return textDecoders.get(this).decode(data) + } catch {} + } + return typeof data === 'string' + ? data + : data.toString() + } +} + +function decodeText (text, sourceEncoding, destEncoding) { + if (text) { + return getDecoder(destEncoding)(text, sourceEncoding) + } + return text +} + +module.exports = decodeText + + +/***/ }), + +/***/ 8352: +/***/ ((module) => { + +"use strict"; + + +module.exports = function getLimit (limits, name, defaultLimit) { + if ( + !limits || + limits[name] === undefined || + limits[name] === null + ) { return defaultLimit } + + if ( + typeof limits[name] !== 'number' || + isNaN(limits[name]) + ) { throw new TypeError('Limit ' + name + ' is not a valid number') } + + return limits[name] +} + + +/***/ }), + +/***/ 8898: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +/* eslint-disable object-property-newline */ + + +const decodeText = __nccwpck_require__(1350) + +const RE_ENCODED = /%[a-fA-F0-9][a-fA-F0-9]/g + +const EncodedLookup = { + '%00': '\x00', '%01': '\x01', '%02': '\x02', '%03': '\x03', '%04': '\x04', + '%05': '\x05', '%06': '\x06', '%07': '\x07', '%08': '\x08', '%09': '\x09', + '%0a': '\x0a', '%0A': '\x0a', '%0b': '\x0b', '%0B': '\x0b', '%0c': '\x0c', + '%0C': '\x0c', '%0d': '\x0d', '%0D': '\x0d', '%0e': '\x0e', '%0E': '\x0e', + '%0f': '\x0f', '%0F': '\x0f', '%10': '\x10', '%11': '\x11', '%12': '\x12', + '%13': '\x13', '%14': '\x14', '%15': '\x15', '%16': '\x16', '%17': '\x17', + '%18': '\x18', '%19': '\x19', '%1a': '\x1a', '%1A': '\x1a', '%1b': '\x1b', + '%1B': '\x1b', '%1c': '\x1c', '%1C': '\x1c', '%1d': '\x1d', '%1D': '\x1d', + '%1e': '\x1e', '%1E': '\x1e', '%1f': '\x1f', '%1F': '\x1f', '%20': '\x20', + '%21': '\x21', '%22': '\x22', '%23': '\x23', '%24': '\x24', '%25': '\x25', + '%26': '\x26', '%27': '\x27', '%28': '\x28', '%29': '\x29', '%2a': '\x2a', + '%2A': '\x2a', '%2b': '\x2b', '%2B': '\x2b', '%2c': '\x2c', '%2C': '\x2c', + '%2d': '\x2d', '%2D': '\x2d', '%2e': '\x2e', '%2E': '\x2e', '%2f': '\x2f', + '%2F': '\x2f', '%30': '\x30', '%31': '\x31', '%32': '\x32', '%33': '\x33', + '%34': '\x34', '%35': '\x35', '%36': '\x36', '%37': '\x37', '%38': '\x38', + '%39': '\x39', '%3a': '\x3a', '%3A': '\x3a', '%3b': '\x3b', '%3B': '\x3b', + '%3c': '\x3c', '%3C': '\x3c', '%3d': '\x3d', '%3D': '\x3d', '%3e': '\x3e', + '%3E': '\x3e', '%3f': '\x3f', '%3F': '\x3f', '%40': '\x40', '%41': '\x41', + '%42': '\x42', '%43': '\x43', '%44': '\x44', '%45': '\x45', '%46': '\x46', + '%47': '\x47', '%48': '\x48', '%49': '\x49', '%4a': '\x4a', '%4A': '\x4a', + '%4b': '\x4b', '%4B': '\x4b', '%4c': '\x4c', '%4C': '\x4c', '%4d': '\x4d', + '%4D': '\x4d', '%4e': '\x4e', '%4E': '\x4e', '%4f': '\x4f', '%4F': '\x4f', + '%50': '\x50', '%51': '\x51', '%52': '\x52', '%53': '\x53', '%54': '\x54', + '%55': '\x55', '%56': '\x56', '%57': '\x57', '%58': '\x58', '%59': '\x59', + '%5a': '\x5a', '%5A': '\x5a', '%5b': '\x5b', '%5B': '\x5b', '%5c': '\x5c', + '%5C': '\x5c', '%5d': '\x5d', '%5D': '\x5d', '%5e': '\x5e', '%5E': '\x5e', + '%5f': '\x5f', '%5F': '\x5f', '%60': '\x60', '%61': '\x61', '%62': '\x62', + '%63': '\x63', '%64': '\x64', '%65': '\x65', '%66': '\x66', '%67': '\x67', + '%68': '\x68', '%69': '\x69', '%6a': '\x6a', '%6A': '\x6a', '%6b': '\x6b', + '%6B': '\x6b', '%6c': '\x6c', '%6C': '\x6c', '%6d': '\x6d', '%6D': '\x6d', + '%6e': '\x6e', '%6E': '\x6e', '%6f': '\x6f', '%6F': '\x6f', '%70': '\x70', + '%71': '\x71', '%72': '\x72', '%73': '\x73', '%74': '\x74', '%75': '\x75', + '%76': '\x76', '%77': '\x77', '%78': '\x78', '%79': '\x79', '%7a': '\x7a', + '%7A': '\x7a', '%7b': '\x7b', '%7B': '\x7b', '%7c': '\x7c', '%7C': '\x7c', + '%7d': '\x7d', '%7D': '\x7d', '%7e': '\x7e', '%7E': '\x7e', '%7f': '\x7f', + '%7F': '\x7f', '%80': '\x80', '%81': '\x81', '%82': '\x82', '%83': '\x83', + '%84': '\x84', '%85': '\x85', '%86': '\x86', '%87': '\x87', '%88': '\x88', + '%89': '\x89', '%8a': '\x8a', '%8A': '\x8a', '%8b': '\x8b', '%8B': '\x8b', + '%8c': '\x8c', '%8C': '\x8c', '%8d': '\x8d', '%8D': '\x8d', '%8e': '\x8e', + '%8E': '\x8e', '%8f': '\x8f', '%8F': '\x8f', '%90': '\x90', '%91': '\x91', + '%92': '\x92', '%93': '\x93', '%94': '\x94', '%95': '\x95', '%96': '\x96', + '%97': '\x97', '%98': '\x98', '%99': '\x99', '%9a': '\x9a', '%9A': '\x9a', + '%9b': '\x9b', '%9B': '\x9b', '%9c': '\x9c', '%9C': '\x9c', '%9d': '\x9d', + '%9D': '\x9d', '%9e': '\x9e', '%9E': '\x9e', '%9f': '\x9f', '%9F': '\x9f', + '%a0': '\xa0', '%A0': '\xa0', '%a1': '\xa1', '%A1': '\xa1', '%a2': '\xa2', + '%A2': '\xa2', '%a3': '\xa3', '%A3': '\xa3', '%a4': '\xa4', '%A4': '\xa4', + '%a5': '\xa5', '%A5': '\xa5', '%a6': '\xa6', '%A6': '\xa6', '%a7': '\xa7', + '%A7': '\xa7', '%a8': '\xa8', '%A8': '\xa8', '%a9': '\xa9', '%A9': '\xa9', + '%aa': '\xaa', '%Aa': '\xaa', '%aA': '\xaa', '%AA': '\xaa', '%ab': '\xab', + '%Ab': '\xab', '%aB': '\xab', '%AB': '\xab', '%ac': '\xac', '%Ac': '\xac', + '%aC': '\xac', '%AC': '\xac', '%ad': '\xad', '%Ad': '\xad', '%aD': '\xad', + '%AD': '\xad', '%ae': '\xae', '%Ae': '\xae', '%aE': '\xae', '%AE': '\xae', + '%af': '\xaf', '%Af': '\xaf', '%aF': '\xaf', '%AF': '\xaf', '%b0': '\xb0', + '%B0': '\xb0', '%b1': '\xb1', '%B1': '\xb1', '%b2': '\xb2', '%B2': '\xb2', + '%b3': '\xb3', '%B3': '\xb3', '%b4': '\xb4', '%B4': '\xb4', '%b5': '\xb5', + '%B5': '\xb5', '%b6': '\xb6', '%B6': '\xb6', '%b7': '\xb7', '%B7': '\xb7', + '%b8': '\xb8', '%B8': '\xb8', '%b9': '\xb9', '%B9': '\xb9', '%ba': '\xba', + '%Ba': '\xba', '%bA': '\xba', '%BA': '\xba', '%bb': '\xbb', '%Bb': '\xbb', + '%bB': '\xbb', '%BB': '\xbb', '%bc': '\xbc', '%Bc': '\xbc', '%bC': '\xbc', + '%BC': '\xbc', '%bd': '\xbd', '%Bd': '\xbd', '%bD': '\xbd', '%BD': '\xbd', + '%be': '\xbe', '%Be': '\xbe', '%bE': '\xbe', '%BE': '\xbe', '%bf': '\xbf', + '%Bf': '\xbf', '%bF': '\xbf', '%BF': '\xbf', '%c0': '\xc0', '%C0': '\xc0', + '%c1': '\xc1', '%C1': '\xc1', '%c2': '\xc2', '%C2': '\xc2', '%c3': '\xc3', + '%C3': '\xc3', '%c4': '\xc4', '%C4': '\xc4', '%c5': '\xc5', '%C5': '\xc5', + '%c6': '\xc6', '%C6': '\xc6', '%c7': '\xc7', '%C7': '\xc7', '%c8': '\xc8', + '%C8': '\xc8', '%c9': '\xc9', '%C9': '\xc9', '%ca': '\xca', '%Ca': '\xca', + '%cA': '\xca', '%CA': '\xca', '%cb': '\xcb', '%Cb': '\xcb', '%cB': '\xcb', + '%CB': '\xcb', '%cc': '\xcc', '%Cc': '\xcc', '%cC': '\xcc', '%CC': '\xcc', + '%cd': '\xcd', '%Cd': '\xcd', '%cD': '\xcd', '%CD': '\xcd', '%ce': '\xce', + '%Ce': '\xce', '%cE': '\xce', '%CE': '\xce', '%cf': '\xcf', '%Cf': '\xcf', + '%cF': '\xcf', '%CF': '\xcf', '%d0': '\xd0', '%D0': '\xd0', '%d1': '\xd1', + '%D1': '\xd1', '%d2': '\xd2', '%D2': '\xd2', '%d3': '\xd3', '%D3': '\xd3', + '%d4': '\xd4', '%D4': '\xd4', '%d5': '\xd5', '%D5': '\xd5', '%d6': '\xd6', + '%D6': '\xd6', '%d7': '\xd7', '%D7': '\xd7', '%d8': '\xd8', '%D8': '\xd8', + '%d9': '\xd9', '%D9': '\xd9', '%da': '\xda', '%Da': '\xda', '%dA': '\xda', + '%DA': '\xda', '%db': '\xdb', '%Db': '\xdb', '%dB': '\xdb', '%DB': '\xdb', + '%dc': '\xdc', '%Dc': '\xdc', '%dC': '\xdc', '%DC': '\xdc', '%dd': '\xdd', + '%Dd': '\xdd', '%dD': '\xdd', '%DD': '\xdd', '%de': '\xde', '%De': '\xde', + '%dE': '\xde', '%DE': '\xde', '%df': '\xdf', '%Df': '\xdf', '%dF': '\xdf', + '%DF': '\xdf', '%e0': '\xe0', '%E0': '\xe0', '%e1': '\xe1', '%E1': '\xe1', + '%e2': '\xe2', '%E2': '\xe2', '%e3': '\xe3', '%E3': '\xe3', '%e4': '\xe4', + '%E4': '\xe4', '%e5': '\xe5', '%E5': '\xe5', '%e6': '\xe6', '%E6': '\xe6', + '%e7': '\xe7', '%E7': '\xe7', '%e8': '\xe8', '%E8': '\xe8', '%e9': '\xe9', + '%E9': '\xe9', '%ea': '\xea', '%Ea': '\xea', '%eA': '\xea', '%EA': '\xea', + '%eb': '\xeb', '%Eb': '\xeb', '%eB': '\xeb', '%EB': '\xeb', '%ec': '\xec', + '%Ec': '\xec', '%eC': '\xec', '%EC': '\xec', '%ed': '\xed', '%Ed': '\xed', + '%eD': '\xed', '%ED': '\xed', '%ee': '\xee', '%Ee': '\xee', '%eE': '\xee', + '%EE': '\xee', '%ef': '\xef', '%Ef': '\xef', '%eF': '\xef', '%EF': '\xef', + '%f0': '\xf0', '%F0': '\xf0', '%f1': '\xf1', '%F1': '\xf1', '%f2': '\xf2', + '%F2': '\xf2', '%f3': '\xf3', '%F3': '\xf3', '%f4': '\xf4', '%F4': '\xf4', + '%f5': '\xf5', '%F5': '\xf5', '%f6': '\xf6', '%F6': '\xf6', '%f7': '\xf7', + '%F7': '\xf7', '%f8': '\xf8', '%F8': '\xf8', '%f9': '\xf9', '%F9': '\xf9', + '%fa': '\xfa', '%Fa': '\xfa', '%fA': '\xfa', '%FA': '\xfa', '%fb': '\xfb', + '%Fb': '\xfb', '%fB': '\xfb', '%FB': '\xfb', '%fc': '\xfc', '%Fc': '\xfc', + '%fC': '\xfc', '%FC': '\xfc', '%fd': '\xfd', '%Fd': '\xfd', '%fD': '\xfd', + '%FD': '\xfd', '%fe': '\xfe', '%Fe': '\xfe', '%fE': '\xfe', '%FE': '\xfe', + '%ff': '\xff', '%Ff': '\xff', '%fF': '\xff', '%FF': '\xff' +} + +function encodedReplacer (match) { + return EncodedLookup[match] +} + +const STATE_KEY = 0 +const STATE_VALUE = 1 +const STATE_CHARSET = 2 +const STATE_LANG = 3 + +function parseParams (str) { + const res = [] + let state = STATE_KEY + let charset = '' + let inquote = false + let escaping = false + let p = 0 + let tmp = '' + const len = str.length + + for (var i = 0; i < len; ++i) { // eslint-disable-line no-var + const char = str[i] + if (char === '\\' && inquote) { + if (escaping) { escaping = false } else { + escaping = true + continue + } + } else if (char === '"') { + if (!escaping) { + if (inquote) { + inquote = false + state = STATE_KEY + } else { inquote = true } + continue + } else { escaping = false } + } else { + if (escaping && inquote) { tmp += '\\' } + escaping = false + if ((state === STATE_CHARSET || state === STATE_LANG) && char === "'") { + if (state === STATE_CHARSET) { + state = STATE_LANG + charset = tmp.substring(1) + } else { state = STATE_VALUE } + tmp = '' + continue + } else if (state === STATE_KEY && + (char === '*' || char === '=') && + res.length) { + state = char === '*' + ? STATE_CHARSET + : STATE_VALUE + res[p] = [tmp, undefined] + tmp = '' + continue + } else if (!inquote && char === ';') { + state = STATE_KEY + if (charset) { + if (tmp.length) { + tmp = decodeText(tmp.replace(RE_ENCODED, encodedReplacer), + 'binary', + charset) + } + charset = '' + } else if (tmp.length) { + tmp = decodeText(tmp, 'binary', 'utf8') + } + if (res[p] === undefined) { res[p] = tmp } else { res[p][1] = tmp } + tmp = '' + ++p + continue + } else if (!inquote && (char === ' ' || char === '\t')) { continue } + } + tmp += char + } + if (charset && tmp.length) { + tmp = decodeText(tmp.replace(RE_ENCODED, encodedReplacer), + 'binary', + charset) + } else if (tmp) { + tmp = decodeText(tmp, 'binary', 'utf8') + } + + if (res[p] === undefined) { + if (tmp) { res[p] = tmp } + } else { res[p][1] = tmp } + + return res +} + +module.exports = parseParams + + +/***/ }) + +/******/ }); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __nccwpck_require__(moduleId) { +/******/ // Check if module is in cache +/******/ var cachedModule = __webpack_module_cache__[moduleId]; +/******/ if (cachedModule !== undefined) { +/******/ return cachedModule.exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ var threw = true; +/******/ try { +/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __nccwpck_require__); +/******/ threw = false; +/******/ } finally { +/******/ if(threw) delete __webpack_module_cache__[moduleId]; +/******/ } +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/compat */ +/******/ +/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/"; +/******/ +/************************************************************************/ +/******/ +/******/ // startup +/******/ // Load entry module and return exports +/******/ // This entry module is referenced by other modules so it can't be inlined +/******/ var __webpack_exports__ = __nccwpck_require__(8465); +/******/ module.exports = __webpack_exports__; +/******/ +/******/ })() +; \ No newline at end of file diff --git a/.github/actions/node/package.json b/.github/actions/node/package.json new file mode 100644 index 0000000000..9a861ab3e3 --- /dev/null +++ b/.github/actions/node/package.json @@ -0,0 +1,26 @@ +{ + "name": "docker", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "tsc && ncc build --no-cache src/index.ts -o dist && cp dist/index.js builder/index.js" + }, + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/exec": "^1.1.1", + "@actions/io": "^1.1.3", + "dotenv": "8.2.0" + }, + "devDependencies": { + "@types/node": "^20.10.6", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "@vercel/ncc": "^0.38.1", + "eslint": "^8.50.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "prettier": "^3.0.3", + "ts-node": "^10.9.1", + "typescript": "^5.6.3" + } +} diff --git a/.github/actions/node/src/index.ts b/.github/actions/node/src/index.ts new file mode 100644 index 0000000000..c7b84b5ccd --- /dev/null +++ b/.github/actions/node/src/index.ts @@ -0,0 +1,48 @@ +import { buildStep, deployStep, pushStep } from './steps' +import { getInputs } from './inputs' +import { IS_POST } from './state' +import * as core from '@actions/core' +import { ActionStep } from './types' +/** + * Runs the action + */ +async function run() { + const inputs = await getInputs() + + for (const step of inputs.steps) { + core.info(`Running step: ${step}`) + switch (step) { + case ActionStep.BUILD: { + await buildStep() + break + } + + case ActionStep.PUSH: { + await pushStep() + break + } + + case ActionStep.DEPLOY: { + await deployStep() + break + } + + default: + core.error(`Unknown action step: ${step}!`) + throw new Error(`Unknown action step: ${step}!`) + } + } +} + +/** + * Runs the post action cleanup step + */ +async function post() {} + +setImmediate(async () => { + if (!IS_POST) { + await run() + } else { + await post() + } +}) diff --git a/.github/actions/node/src/inputs.ts b/.github/actions/node/src/inputs.ts new file mode 100644 index 0000000000..b5e7305124 --- /dev/null +++ b/.github/actions/node/src/inputs.ts @@ -0,0 +1,231 @@ +import * as core from '@actions/core' +import { + ActionStep, + CloudEnvironment, + IActionInputs, + IAwsDeployInput, + IBuildInput, + IDeployInput, + IOracleDeployInput, + IPushInput, +} from './types' +import { getBuilderDefinitions } from './utils' + +const getBuildInputs = (): IBuildInput => { + const tag = core.getInput('tag') + + return { + tag, + images: [], + } +} + +const getPushInputs = (): IPushInput => { + const username = process.env.ORACLE_DOCKER_USERNAME + if (!username) { + core.error('No Oracle Docker username found in ORACLE_DOCKER_USERNAME environment variable!') + throw new Error( + 'No Oracle Docker username found in ORACLE_DOCKER_USERNAME environment variable!', + ) + } + + const password = process.env.ORACLE_DOCKER_PASSWORD + if (!password) { + core.error('No Oracle Docker password found in ORACLE_DOCKER_PASSWORD environment variable!') + throw new Error( + 'No Oracle Docker password found in ORACLE_DOCKER_PASSWORD environment variable!', + ) + } + + return { + dockerUsername: username, + dockerPassword: password, + } +} + +const getDeployIUputs = (): IDeployInput => { + const services = getInputList('services') + + const cloudEnvironment = process.env['CLOUD_ENV'] as CloudEnvironment + if (!cloudEnvironment) { + core.error('No CLOUD_ENV environment variable found!') + throw new Error('No CLOUD_ENV environment variable found!') + } + + let aws: IAwsDeployInput | undefined + let oracle: IOracleDeployInput | undefined + + if ( + cloudEnvironment === CloudEnvironment.LF_ORACLE_PRODUCTION || + cloudEnvironment === CloudEnvironment.LF_ORACLE_STAGING + ) { + const user = process.env.ORACLE_USER + if (!user) { + core.error('No ORACLE_USER environment variable found!') + throw new Error('No ORACLE_USER environment variable found!') + } + + const tenant = process.env.ORACLE_TENANT + if (!tenant) { + core.error('No ORACLE_TENANT environment variable found!') + throw new Error('No ORACLE_TENANT environment variable found!') + } + + const region = process.env.ORACLE_REGION + if (!region) { + core.error('No ORACLE_REGION environment variable found!') + throw new Error('No ORACLE_REGION environment variable found!') + } + + const fingerprint = process.env.ORACLE_FINGERPRINT + if (!fingerprint) { + core.error('No ORACLE_FINGERPRINT environment variable found!') + throw new Error('No ORACLE_FINGERPRINT environment variable found!') + } + + const key = process.env.ORACLE_KEY + if (!key) { + core.error('No ORACLE_KEY environment variable found!') + throw new Error('No ORACLE_KEY environment variable found!') + } + + const cluster = process.env.ORACLE_CLUSTER + if (!cluster) { + core.error('No ORACLE_CLUSTER environment variable found!') + throw new Error('No ORACLE_CLUSTER environment variable found!') + } + + oracle = { + user, + tenant, + region, + fingerprint, + key, + cluster, + } + } else { + const eksClusterName = process.env.CROWD_CLUSTER + if (!eksClusterName) { + core.error('No CROWD_CLUSTER environment variable found!') + throw new Error('No CROWD_CLUSTER environment variable found!') + } + + const awsRoleArn = process.env.CROWD_ROLE_ARN + if (!awsRoleArn) { + core.error('No CROWD_ROLE_ARN environment variable found!') + throw new Error('No CROWD_ROLE_ARN environment variable found!') + } + + const awsAccessKeyId = process.env.AWS_ACCESS_KEY_ID + if (!awsAccessKeyId) { + core.error('No AWS_ACCESS_KEY_ID environment variable found!') + throw new Error('No AWS_ACCESS_KEY_ID environment variable found!') + } + + const awsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY + if (!awsSecretAccessKey) { + core.error('No AWS_SECRET_ACCESS_KEY environment variable found!') + throw new Error('No AWS_SECRET_ACCESS_KEY environment variable found!') + } + + const awsRegion = process.env.AWS_REGION + if (!awsRegion) { + core.error('No AWS_REGION environment variable found!') + throw new Error('No AWS_REGION environment variable found!') + } + + aws = { + eksClusterName, + awsRoleArn, + awsAccessKeyId, + awsSecretAccessKey, + awsRegion, + } + } + + return { + services, + cloudEnvironment, + aws, + oracle, + } +} + +let inputs: IActionInputs | undefined +export const getInputs = async (): Promise => { + if (inputs !== undefined) { + return inputs + } + + const actionSteps = getInputList('steps') as ActionStep[] + + if (actionSteps.length === 0) { + core.error('No action steps provided!') + throw new Error('No action steps provided!') + } + + const results: IActionInputs = { + steps: actionSteps, + } + + for (const step of actionSteps) { + switch (step) { + case ActionStep.BUILD: + results[ActionStep.BUILD] = getBuildInputs() + break + case ActionStep.PUSH: + results[ActionStep.PUSH] = getPushInputs() + break + case ActionStep.DEPLOY: + results[ActionStep.DEPLOY] = getDeployIUputs() + break + + default: + core.error(`Unknown action step: ${step}!`) + throw new Error(`Unknown action step: ${step}!`) + } + } + + if (results[ActionStep.BUILD] !== undefined) { + if (results[ActionStep.BUILD].images.length === 0 && results[ActionStep.DEPLOY] !== undefined) { + // calculate images from services + const buildDefinitions = await getBuilderDefinitions() + + const images: string[] = [] + for (const service of results[ActionStep.DEPLOY].services) { + const definition = buildDefinitions.find((d) => d.services.includes(service)) + if (definition === undefined) { + core.error(`No builder definition found for service: ${service}!`) + throw new Error(`No builder definition found for service: ${service}!`) + } + + if (!images.includes(definition.imageName)) { + images.push(definition.imageName) + } + } + + results[ActionStep.BUILD].images = images + } + } + + if (results[ActionStep.PUSH] !== undefined && results[ActionStep.BUILD] === undefined) { + core.error('Push step provided without build step!') + throw new Error('Push step provided without build step!') + } + + if (results[ActionStep.DEPLOY] !== undefined && results[ActionStep.PUSH] === undefined) { + core.error('Deploy step provided without push step!') + throw new Error('Deploy step provided without push step!') + } + + inputs = results + return results +} + +const getInputList = (name: string): string[] => { + const items = core.getInput(name) + return items + .split(' ') + .map((s) => s.trim()) + .filter((s) => s.length > 0) +} diff --git a/.github/actions/node/src/state.ts b/.github/actions/node/src/state.ts new file mode 100644 index 0000000000..e6e888085c --- /dev/null +++ b/.github/actions/node/src/state.ts @@ -0,0 +1,7 @@ +import * as core from '@actions/core' + +export const IS_POST = !!process.env['STATE_isPost'] + +if (!IS_POST) { + core.saveState('isPost', 'true') +} diff --git a/.github/actions/node/src/steps.ts b/.github/actions/node/src/steps.ts new file mode 100644 index 0000000000..2dece04a69 --- /dev/null +++ b/.github/actions/node/src/steps.ts @@ -0,0 +1,311 @@ +import { getInputs } from './inputs' +import { ActionStep, CloudEnvironment, IBuilderDefinition } from './types' +import * as core from '@actions/core' +import * as exec from '@actions/exec' +import { getBuilderDefinitions } from './utils' +import fs from 'fs' +import os from 'os' +import path from 'path' + +const imageTagMap = new Map() + +export const buildStep = async (): Promise => { + const inputs = await getInputs() + + if (inputs[ActionStep.BUILD] === undefined) { + core.error('No build inputs provided!') + throw new Error('No build inputs provided!') + } + + const { images, tag } = inputs[ActionStep.BUILD] + if (images.length === 0) { + core.error('No images provided!') + throw new Error('No images provided!') + } + + if (!tag) { + core.error('No tag provided!') + throw new Error('No tag provided!') + } + + const timestamp = Math.floor(Date.now() / 1000) + + const actualTag = `${tag}.${timestamp}` + + const alreadyBuilt: string[] = [] + + for (const image of images) { + if (alreadyBuilt.includes(image)) { + core.info(`Skipping already built image: ${image}:${actualTag}`) + continue + } + + core.info(`Building image: ${image}:${actualTag}`) + const exitCode = await exec.exec('bash', ['cli', 'build', image, actualTag], { + cwd: './scripts', + }) + + if (exitCode !== 0) { + core.error(`Failed to build image: ${image}:${actualTag}`) + } else { + alreadyBuilt.push(image) + imageTagMap.set(image, actualTag) + } + } +} + +export const pushStep = async (): Promise => { + const inputs = await getInputs() + const images = inputs[ActionStep.BUILD]?.images ?? [] + const pushInput = inputs[ActionStep.PUSH] + if (!pushInput) { + core.error('No push inputs provided!') + throw new Error('No push inputs provided!') + } + + if (images.length === 0) { + core.error('No images provided!') + throw new Error('No images provided!') + } + + // do a docker login + const exitCode = await exec.exec('docker', [ + 'login', + 'sjc.ocir.io', + '--username', + pushInput.dockerUsername, + '--password', + pushInput.dockerPassword, + ]) + + if (exitCode !== 0) { + core.error('Failed to login to docker!') + throw new Error('Failed to login to docker!') + } + + // now push the images + const alreadyPushed: string[] = [] + for (const image of images) { + if (alreadyPushed.includes(image)) { + core.info(`Skipping already pushed image: ${image}`) + continue + } + + if (!imageTagMap.has(image)) { + core.warning(`No tag found for image: ${image} - image wasn't built successfully!`) + continue + } + + const tag = imageTagMap.get(image) + + core.info(`Pushing image: ${image}:${tag}!`) + + const exitCode = await exec.exec('bash', ['cli', 'push', image, tag], { + cwd: './scripts', + }) + + if (exitCode !== 0) { + core.error(`Failed to push image: ${image}:${tag}`) + imageTagMap.delete(image) + } else { + alreadyPushed.push(image) + } + } +} + +export const deployStep = async (): Promise => { + const inputs = await getInputs() + + const deployInput = inputs[ActionStep.DEPLOY] + if (!deployInput) { + core.error('No deploy inputs provided!') + throw new Error('No deploy inputs provided!') + } + + if (deployInput.services.length === 0) { + core.warning('No services specified for deploy!') + return + } + + // check if any images failed to build + const builderDefinitions = await getBuilderDefinitions() + + const servicesToDeploy: { service: string; tag: string; builderDef: IBuilderDefinition }[] = [] + for (const service of deployInput.services) { + const builderDef = builderDefinitions.find((b) => b.services.includes(service)) + + if (!builderDef) { + core.error(`No builder definition found for service: ${service}`) + throw new Error(`No builder definition found for service: ${service}`) + } + + if (!imageTagMap.has(builderDef.imageName)) { + core.error( + `No tag found for image: ${builderDef.imageName} - image wasn't built successfully!`, + ) + throw new Error( + `No tag found for image: ${builderDef.imageName} - image wasn't built successfully!`, + ) + } + + const tag = imageTagMap.get(builderDef.imageName) + + servicesToDeploy.push({ + service, + tag, + builderDef, + }) + } + + let exitCode: number + + if (deployInput.aws) { + const env = { + AWS_ACCESS_KEY_ID: deployInput.aws.awsAccessKeyId, + AWS_SECRET_ACCESS_KEY: deployInput.aws.awsSecretAccessKey, + AWS_REGION: deployInput.aws.awsRegion, + } + + exitCode = await exec.exec( + 'aws', + [ + 'eks', + 'update-kubeconfig', + '--name', + deployInput.aws.eksClusterName, + '--role-arn', + deployInput.aws.awsRoleArn, + ], + { + env, + }, + ) + + if (exitCode !== 0) { + core.error('Failed to update kubeconfig!') + throw new Error('Failed to update kubeconfig!') + } + } else if (deployInput.oracle) { + const homeDir = os.homedir() + const kubeDir = path.join(homeDir, '.kube') + const ociDir = path.join(homeDir, '.oci') + const configPath = path.join(ociDir, 'config') + const keyPath = path.join(ociDir, 'oci_api_key.pem') + + // prepare oracle config + let config = ` +[DEFAULT] +user=${deployInput.oracle.user} +fingerprint=${deployInput.oracle.fingerprint} +key_file=${keyPath} +tenancy=${deployInput.oracle.tenant} +region=${deployInput.oracle.region} +` + + // create the ~/.oci folder if it doesn't exists + await fs.mkdirSync(ociDir, { recursive: true }) + + // write config to ~/.oci/config + await fs.writeFileSync(configPath, config, 'utf8') + + // write private key to ~/.oci/oci_api_key.pem + await fs.writeFileSync(keyPath, deployInput.oracle.key, 'utf8') + + // chmod 600 to key and config + await fs.chmodSync(configPath, 0o600) + await fs.chmodSync(keyPath, 0o600) + + // get kubernetes context + await fs.mkdirSync(kubeDir, { recursive: true }) + + exitCode = await exec.exec('oci', [ + 'ce', + 'cluster', + 'create-kubeconfig', + '--cluster-id', + deployInput.oracle.cluster, + '--file', + '~/.kube/config', + '--region', + deployInput.oracle.region, + '--token-version', + '2.0.0', + '--kube-endpoint', + 'PUBLIC_ENDPOINT', + '--config-file', + configPath, + ]) + if (exitCode !== 0) { + core.error('Failed to create kubeconfig!') + throw new Error('Failed to create kubeconfig') + } + } else { + core.error('No cloud provider specified!') + throw new Error('No cloud provider specified!') + } + + let failed = [] + + for (const serviceDef of servicesToDeploy) { + const tag = serviceDef.tag + const service = serviceDef.service + const prioritized = serviceDef.builderDef.prioritizedServices.includes(service) + const servicesToUpdate: string[] = [] + + if (prioritized) { + switch (deployInput.cloudEnvironment) { + case CloudEnvironment.PRODUCTION: { + servicesToUpdate.push( + ...[`${service}-system`, `${service}-normal`, `${service}-high`, `${service}-urgent`], + ) + break + } + case CloudEnvironment.LF_ORACLE_PRODUCTION: + case CloudEnvironment.LF_PRODUCTION: { + servicesToUpdate.push(...[`${service}-system`, `${service}-normal`, `${service}-high`]) + break + } + + case CloudEnvironment.LF_ORACLE_STAGING: + case CloudEnvironment.LF_STAGING: + case CloudEnvironment.STAGING: { + servicesToUpdate.push(`${service}-normal`) + break + } + + default: + core.error(`Unknown cloud environment: ${deployInput.cloudEnvironment}`) + throw new Error(`Unknown cloud environment: ${deployInput.cloudEnvironment}`) + } + } else { + servicesToUpdate.push(service) + } + + core.info( + `Deploying service: ${service} with image: ${ + serviceDef.builderDef.dockerRepository + }:${tag} to deployments: ${servicesToUpdate.join(', ')}`, + ) + + for (const toDeploy of servicesToUpdate) { + exitCode = await exec.exec('kubectl', [ + 'set', + 'image', + `deployments/${toDeploy}-dpl`, + `${toDeploy}=${serviceDef.builderDef.dockerRepository}:${tag}`, + ]) + + if (exitCode !== 0) { + core.error(`Failed to deploy service: ${service} to deployment: ${toDeploy}`) + if (!failed.includes(service)) { + failed.push(service) + } + } + } + } + + if (failed.length > 0) { + core.error(`Failed to deploy services: ${failed.join(', ')}`) + throw new Error(`Failed to deploy services: ${failed.join(', ')}`) + } +} diff --git a/.github/actions/node/src/types.ts b/.github/actions/node/src/types.ts new file mode 100644 index 0000000000..27d234edec --- /dev/null +++ b/.github/actions/node/src/types.ts @@ -0,0 +1,71 @@ +export enum ActionStep { + BUILD = 'build', + PUSH = 'push', + DEPLOY = 'deploy', +} + +export enum CloudEnvironment { + PRODUCTION = 'production', + STAGING = 'staging', + LF_PRODUCTION = 'lf-production', + LF_ORACLE_PRODUCTION = 'lf-oracle-production', + LF_ORACLE_STAGING = 'lf-oracle-staging', + LF_STAGING = 'lf-staging', +} + +export interface IBuildInput { + // image version tag + tag: string + + // images to build + images: string[] +} + +export interface IPushInput { + // docker credentials to push image to + dockerUsername: string + dockerPassword: string +} + +export interface IAwsDeployInput { + // aws credentials to use when deploying + eksClusterName: string + awsRoleArn: string + awsAccessKeyId: string + awsSecretAccessKey: string + awsRegion: string +} + +export interface IOracleDeployInput { + cluster: string + user: string + tenant: string + region: string + fingerprint: string + key: string +} + +export interface IDeployInput { + // services to deploy + services: string[] + + // which cloud environment are we deploying to + cloudEnvironment: CloudEnvironment + + aws?: IAwsDeployInput + oracle?: IOracleDeployInput +} + +export interface IBuilderDefinition { + imageName: string + dockerRepository: string + services: string[] + prioritizedServices: string[] +} + +export type IActionInputs = { + steps: ActionStep[] + [ActionStep.BUILD]?: IBuildInput + [ActionStep.PUSH]?: IPushInput + [ActionStep.DEPLOY]?: IDeployInput +} diff --git a/.github/actions/node/src/utils.ts b/.github/actions/node/src/utils.ts new file mode 100644 index 0000000000..c2cd6671bd --- /dev/null +++ b/.github/actions/node/src/utils.ts @@ -0,0 +1,48 @@ +import dotenv from 'dotenv' +import { IBuilderDefinition } from './types' +import fs from 'fs' + +let definitions: IBuilderDefinition[] | undefined + +export const getBuilderDefinitions = async (): Promise => { + if (definitions !== undefined) { + return definitions + } + + const results: IBuilderDefinition[] = [] + const files = fs.readdirSync('./scripts/builders') + + for (const result of files) { + if (result.endsWith('.env')) { + const content = fs.readFileSync(`./scripts/builders/${result}`, 'utf-8') + const parsed = dotenv.parse(content) + + const imageName = result.split('.env')[0] + const dockerRepository = parsed.REPO + let services: string[] = [] + if (parsed.SERVICES !== undefined) { + services = parsed.SERVICES.split(' ') + .map((s) => s.trim()) + .filter((s) => s.length > 0) + } + + let prioritizedServices: string[] = [] + if (parsed.PRIORITIZED !== undefined) { + prioritizedServices = parsed.PRIORITIZED.split(' ') + .map((s) => s.trim()) + .filter((s) => s.length > 0) + } + + results.push({ + imageName, + dockerRepository, + services, + prioritizedServices, + }) + } + } + + definitions = results + + return results +} diff --git a/.github/actions/node/tsconfig.json b/.github/actions/node/tsconfig.json new file mode 100644 index 0000000000..4cf9afb8fc --- /dev/null +++ b/.github/actions/node/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "es2017", + "module": "Node16", + "lib": ["es6", "es7", "es2017", "es2017.object", "es2015.promise", "ES2021.String"], + "skipLibCheck": true, + "sourceMap": true, + "moduleResolution": "node16", + "experimentalDecorators": true, + "esModuleInterop": true, + "baseUrl": "./src", + "outDir": "./out", + "paths": { + "@/*": ["*"] + } + }, + "include": ["src/**/*"] +} diff --git a/.github/workflows/CI-node.yaml b/.github/workflows/CI-node.yaml deleted file mode 100644 index 3172c71084..0000000000 --- a/.github/workflows/CI-node.yaml +++ /dev/null @@ -1,117 +0,0 @@ -name: Node.js CI - -on: - pull_request: - paths: - - 'backend/**' - - 'services/**' -jobs: - lint-format: - runs-on: ubuntu-latest - defaults: - run: - shell: bash - working-directory: ./backend - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 16 - - - name: Install and build libraries - run: ../services/scripts/install_lib_packages.sh - - - name: Install root dependencies - run: npm ci - - - name: Check linting - run: npm run lint - - - name: Check formatting - run: npm run format-check - - - name: Check typescript - run: npm run tsc-check - - lint-format-services: - runs-on: ubuntu-latest - defaults: - run: - shell: bash - working-directory: ./services - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 16 - - - name: Install packages - run: ./scripts/install_all_packages.sh - - - name: Check library linting, format and typescript - run: ./scripts/lint_libs.sh - - - name: Check app linting, format and typescript - run: ./scripts/lint_apps.sh - - tests-main: - needs: lint-format - runs-on: ubuntu-latest - defaults: - run: - shell: bash - working-directory: ./backend - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 16 - - - name: Install and build libraries - run: ../services/scripts/install_lib_packages.sh - - - name: Install root dependencies - run: npm ci - - - name: Run tests - working-directory: ./backend - run: SERVICE=test npm test -- --testPathIgnorePatterns=serverless - - tests-serverless: - needs: lint-format - runs-on: ubuntu-latest - defaults: - run: - shell: bash - working-directory: ./backend - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 16 - - - name: Install and build libraries - run: ../services/scripts/install_lib_packages.sh - - - name: Install dependencies - run: npm ci - - - name: Run tests - working-directory: ./backend - run: SERVICE=test npm test -- --testPathPattern="serverless\/" diff --git a/.github/workflows/api-docs.yaml b/.github/workflows/api-docs.yaml deleted file mode 100644 index 885b97a92b..0000000000 --- a/.github/workflows/api-docs.yaml +++ /dev/null @@ -1,23 +0,0 @@ -name: API docs - -on: - release: - types: [released] -jobs: - tests-main: - runs-on: ubuntu-latest - defaults: - run: - shell: bash - working-directory: ./backend - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - name: Install root dependencies - run: npm ci - - - name: Run docs script - working-directory: ./backend - run: npm run docs -- ${{ secrets.README_API_KEY }} diff --git a/.github/workflows/backend-lint.yaml b/.github/workflows/backend-lint.yaml new file mode 100644 index 0000000000..9818b8bbcf --- /dev/null +++ b/.github/workflows/backend-lint.yaml @@ -0,0 +1,109 @@ +name: Backend Lint +permissions: + contents: read + +on: + pull_request: + paths: + - 'backend/**' + - 'services/**' +jobs: + lint-format: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + working-directory: ./backend + + steps: + - name: Check out repository code + uses: actions/checkout@v2 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 20 + + - name: Install dependencies + run: cd .. && npm install -g corepack@latest && corepack enable pnpm && pnpm i --frozen-lockfile + + - name: Check linting + run: pnpm run lint + + - name: Check formatting + run: pnpm run format-check + + - name: Check typescript + run: pnpm run tsc-check + + lint-format-services: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + working-directory: ./services + + steps: + - name: Check out repository code + uses: actions/checkout@v2 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 20 + + - name: Install dependencies + run: cd .. && npm install -g corepack@latest && corepack enable pnpm && pnpm i --frozen-lockfile + + - name: Check library linting, format and typescript + run: ./scripts/lint_libs.sh + + - name: Check app linting, format and typescript + run: ./scripts/lint_apps.sh + + lint-python-git-integration: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + working-directory: ./services/apps/git_integration + + steps: + - name: Check out repository code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check for Python file changes + id: changes + run: | + if git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -q "^services/apps/git_integration/.*\.py$"; then + echo "python_changed=true" >> $GITHUB_OUTPUT + else + echo "python_changed=false" >> $GITHUB_OUTPUT + fi + + - name: Install uv + if: steps.changes.outputs.python_changed == 'true' + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: "services/apps/git_integration/uv.lock" + + - name: Set up Python + if: steps.changes.outputs.python_changed == 'true' + run: uv python install 3.10 + + - name: Install dependencies + if: steps.changes.outputs.python_changed == 'true' + run: uv sync --group dev --frozen + + - name: Check Python linting and formatting + if: steps.changes.outputs.python_changed == 'true' + run: | + uv run ruff check src/ --output-format=github + uv run ruff format --check src/ + + - name: Skip Python checks + if: steps.changes.outputs.python_changed == 'false' + run: echo "⏭️ No Python files changed, skipping linting checks" diff --git a/.github/workflows/frontend-lint.yml b/.github/workflows/frontend-lint.yml new file mode 100644 index 0000000000..4625c1ad71 --- /dev/null +++ b/.github/workflows/frontend-lint.yml @@ -0,0 +1,20 @@ +name: Frontend Lint + +on: + pull_request: + paths: + - 'frontend/**' + +permissions: + contents: read + +jobs: + lint-frontend: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Lint code + run: npm i && npm run lint + working-directory: frontend diff --git a/.github/workflows/lf-oracle-production-deploy.yaml b/.github/workflows/lf-oracle-production-deploy.yaml new file mode 100644 index 0000000000..e2c55b379f --- /dev/null +++ b/.github/workflows/lf-oracle-production-deploy.yaml @@ -0,0 +1,39 @@ +name: LF Oracle Production Deploy + +on: + workflow_dispatch: + inputs: + services: + description: Space separated list of services to deploy + required: true + +env: + CLOUD_ENV: lf-oracle-production + ORACLE_DOCKER_USERNAME: ${{ secrets.ORACLE_DOCKER_USERNAME }} + ORACLE_DOCKER_PASSWORD: ${{ secrets.ORACLE_DOCKER_PASSWORD }} + ORACLE_USER: ${{ secrets.ORACLE_USER }} + ORACLE_TENANT: ${{ secrets.ORACLE_TENANT }} + ORACLE_REGION: ${{ secrets.ORACLE_REGION }} + ORACLE_FINGERPRINT: ${{ secrets.ORACLE_FINGERPRINT }} + ORACLE_KEY: ${{ secrets.ORACLE_KEY }} + ORACLE_CLUSTER: ${{ secrets.ORACLE_CLUSTER }} + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install OCI + run: | + curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh > install.sh + chmod +x install.sh + ./install.sh --accept-all-defaults + echo "OCI_CLI_DIR=/home/runner/bin" >> $GITHUB_ENV + + - name: Update PATH + run: echo "${{ env.OCI_CLI_DIR }}" >> $GITHUB_PATH + + - uses: ./.github/actions/node/builder + with: + services: ${{ github.event.inputs.services }} diff --git a/.github/workflows/lf-oracle-staging-deploy.yaml b/.github/workflows/lf-oracle-staging-deploy.yaml new file mode 100644 index 0000000000..a223dfc02f --- /dev/null +++ b/.github/workflows/lf-oracle-staging-deploy.yaml @@ -0,0 +1,39 @@ +name: LF Oracle Staging Deploy + +on: + workflow_dispatch: + inputs: + services: + description: Space separated list of services to deploy + required: true + +env: + CLOUD_ENV: lf-oracle-staging + ORACLE_DOCKER_USERNAME: ${{ secrets.ORACLE_DOCKER_USERNAME }} + ORACLE_DOCKER_PASSWORD: ${{ secrets.ORACLE_DOCKER_PASSWORD }} + ORACLE_USER: ${{ secrets.ORACLE_USER }} + ORACLE_TENANT: ${{ secrets.ORACLE_TENANT }} + ORACLE_REGION: ${{ secrets.ORACLE_REGION }} + ORACLE_FINGERPRINT: ${{ secrets.ORACLE_FINGERPRINT }} + ORACLE_KEY: ${{ secrets.ORACLE_KEY }} + ORACLE_CLUSTER: ${{ secrets.ORACLE_STAGING_CLUSTER }} + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install OCI + run: | + curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh > install.sh + chmod +x install.sh + ./install.sh --accept-all-defaults + echo "OCI_CLI_DIR=/home/runner/bin" >> $GITHUB_ENV + + - name: Update PATH + run: echo "${{ env.OCI_CLI_DIR }}" >> $GITHUB_PATH + + - uses: ./.github/actions/node/builder + with: + services: ${{ github.event.inputs.services }} diff --git a/.github/workflows/lf-production-deploy-new.yaml b/.github/workflows/lf-production-deploy-new.yaml deleted file mode 100644 index 8fa7a3a1e4..0000000000 --- a/.github/workflows/lf-production-deploy-new.yaml +++ /dev/null @@ -1,189 +0,0 @@ -name: LF Production Deploy New - -on: - workflow_dispatch: - inputs: - deploy_search_sync_worker: - description: Deploy search-sync-worker service? - required: true - type: boolean - deploy_integration_sync_worker: - description: Deploy integration-sync-worker service? - required: true - type: boolean - deploy_webhook_api: - description: Deploy webhook-api service? - required: true - type: boolean - deploy_script_executor: - description: Deploy script-executor service? - required: true - type: boolean - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.LF_PRODUCTION_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.LF_PRODUCTION_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.LF_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.LF_AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.LF_AWS_REGION }} - SLACK_CHANNEL: deploys-lf - SLACK_WEBHOOK: ${{ secrets.LF_PRODUCTION_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push-search-sync-worker: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_search_sync_worker }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: search-sync-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - build-and-push-integration-sync-worker: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_integration_sync_worker }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: integration-sync-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - build-and-push-webhook-api: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_webhook_api }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: webhook-api - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - build-and-push-script-executor: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_script_executor }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: script-executor - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-search-sync-worker: - needs: build-and-push-search-sync-worker - runs-on: ubuntu-latest - if: ${{ inputs.deploy_search_sync_worker }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: search-sync-worker - image: ${{ needs.build-and-push-search-sync-worker.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-integration-sync-worker: - needs: build-and-push-integration-sync-worker - runs-on: ubuntu-latest - if: ${{ inputs.deploy_integration_sync_worker }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: integration-sync-worker - image: ${{ needs.build-and-push-integration-sync-worker.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-webhook-api: - needs: build-and-push-webhook-api - runs-on: ubuntu-latest - if: ${{ inputs.deploy_webhook_api }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: webhook-api - image: ${{ needs.build-and-push-webhook-api.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-script-executor: - needs: build-and-push-script-executor - runs-on: ubuntu-latest - if: ${{ inputs.deploy_script_executor }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: script-executor - image: ${{ needs.build-and-push-script-executor.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/lf-production-deploy-original.yaml b/.github/workflows/lf-production-deploy-original.yaml deleted file mode 100644 index 3e5fd30a4a..0000000000 --- a/.github/workflows/lf-production-deploy-original.yaml +++ /dev/null @@ -1,383 +0,0 @@ -name: LF Production Deploy Original - -on: - workflow_dispatch: - inputs: - deploy_api: - description: Deploy api service? - required: true - type: boolean - deploy_job_generator: - description: Deploy job-generator service? - required: true - type: boolean - deploy_nodejs_worker: - description: Deploy nodejs-worker service? - required: true - type: boolean - deploy_discord_ws: - description: Deploy discord-ws service? - required: true - type: boolean - deploy_integration_run_worker: - description: Deploy integration-run-worker service? - required: true - type: boolean - deploy_integration_stream_worker: - description: Deploy integration-stream-worker service? - required: true - type: boolean - deploy_integration_data_worker: - description: Deploy integration-data-worker service? - required: true - type: boolean - deploy_data_sink_worker: - description: Deploy data-sink-worker service? - required: true - type: boolean - deploy_python_worker: - description: Deploy python-worker service? - required: true - type: boolean - deploy_frontend: - description: Deploy frontend? - required: true - type: boolean - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.LF_PRODUCTION_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.LF_PRODUCTION_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.LF_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.LF_AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.LF_AWS_REGION }} - SLACK_CHANNEL: deploys-lf - SLACK_WEBHOOK: ${{ secrets.LF_PRODUCTION_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push-backend: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_api || inputs.deploy_job_generator || inputs.deploy_nodejs_worker || inputs.deploy_discord_ws }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: backend - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - build-and-push-integration-run-worker: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_integration_run_worker }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: integration-run-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - build-and-push-integration-stream-worker: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_integration_stream_worker }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: integration-stream-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - build-and-push-integration-data-worker: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_integration_data_worker }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: integration-data-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - build-and-push-data-sink-worker: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_data_sink_worker }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: data-sink-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - build-and-push-frontend: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_frontend }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: frontend - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - build-and-push-python-worker: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_python_worker }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: python-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-api: - needs: build-and-push-backend - runs-on: ubuntu-latest - if: ${{ inputs.deploy_api }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: api - image: ${{ needs.build-and-push-backend.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-nodejs-worker: - needs: build-and-push-backend - runs-on: ubuntu-latest - if: ${{ inputs.deploy_nodejs_worker }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: nodejs-worker - image: ${{ needs.build-and-push-backend.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-discord-ws: - needs: build-and-push-backend - runs-on: ubuntu-latest - if: ${{ inputs.deploy_discord_ws }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: discord-ws - image: ${{ needs.build-and-push-backend.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-job-generator: - needs: build-and-push-backend - runs-on: ubuntu-latest - if: ${{ inputs.deploy_job_generator }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: job-generator - image: ${{ needs.build-and-push-backend.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-integration-run-worker: - needs: build-and-push-integration-run-worker - runs-on: ubuntu-latest - if: ${{ inputs.deploy_integration_run_worker }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: integration-run-worker - image: ${{ needs.build-and-push-integration-run-worker.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-integration-stream-worker: - needs: build-and-push-integration-stream-worker - runs-on: ubuntu-latest - if: ${{ inputs.deploy_integration_stream_worker }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: integration-stream-worker - image: ${{ needs.build-and-push-integration-stream-worker.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-integration-data-worker: - needs: build-and-push-integration-data-worker - runs-on: ubuntu-latest - if: ${{ inputs.deploy_integration_data_worker }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: integration-data-worker - image: ${{ needs.build-and-push-integration-data-worker.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-data-sink-worker: - needs: build-and-push-data-sink-worker - runs-on: ubuntu-latest - if: ${{ inputs.deploy_data_sink_worker }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: data-sink-worker - image: ${{ needs.build-and-push-data-sink-worker.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-frontend: - needs: build-and-push-frontend - runs-on: ubuntu-latest - if: ${{ inputs.deploy_frontend }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: frontend - image: ${{ needs.build-and-push-frontend.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-python-worker: - needs: build-and-push-python-worker - runs-on: ubuntu-latest - if: ${{ inputs.deploy_python_worker }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: python-worker - image: ${{ needs.build-and-push-python-worker.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/lf-staging-deploy-backend.yaml b/.github/workflows/lf-staging-deploy-backend.yaml deleted file mode 100644 index 7cee20ac98..0000000000 --- a/.github/workflows/lf-staging-deploy-backend.yaml +++ /dev/null @@ -1,112 +0,0 @@ -name: LF Staging Deploy Backend services - -on: - push: - branches: - - 'lf-staging/**' - - 'lf-staging-**' - paths: - - 'backend/**' - - 'services/libs/**' - - '!backend/src/serverless/microservices/python/**' - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.LF_STAGING_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.LF_STAGING_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.LF_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.LF_AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.LF_AWS_REGION }} - SLACK_CHANNEL: deploys-lf-staging - SLACK_WEBHOOK: ${{ secrets.LF_STAGING_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: backend - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-api: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: api - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-nodejs-worker: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: nodejs-worker - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-job-generator: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: job-generator - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-discord-ws: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: discord-ws - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/lf-staging-deploy-data-sink-worker.yaml b/.github/workflows/lf-staging-deploy-data-sink-worker.yaml deleted file mode 100644 index 3aa92424ba..0000000000 --- a/.github/workflows/lf-staging-deploy-data-sink-worker.yaml +++ /dev/null @@ -1,60 +0,0 @@ -name: LF Staging Deploy Data Sink Worker - -on: - push: - branches: - - 'lf-staging/**' - - 'lf-staging-**' - paths: - - 'services/libs/**' - - 'services/apps/data_sink_worker/**' - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.LF_STAGING_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.LF_STAGING_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.LF_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.LF_AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.LF_AWS_REGION }} - SLACK_CHANNEL: deploys-lf-staging - SLACK_WEBHOOK: ${{ secrets.LF_STAGING_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: data-sink-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-data-sink-worker: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: data-sink-worker - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/lf-staging-deploy-frontend.yaml b/.github/workflows/lf-staging-deploy-frontend.yaml deleted file mode 100644 index f605dd896e..0000000000 --- a/.github/workflows/lf-staging-deploy-frontend.yaml +++ /dev/null @@ -1,97 +0,0 @@ -name: LF Staging Deploy Frontend service - -on: - push: - branches: - - 'lf-staging/**' - - 'lf-staging-**' - paths: - - 'frontend/**' - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.LF_STAGING_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.LF_STAGING_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.LF_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.LF_AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.LF_AWS_REGION }} - SLACK_CHANNEL: deploys-lf-staging - SLACK_WEBHOOK: ${{ secrets.LF_STAGING_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: frontend - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - build-and-push-dev: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: frontend-dev - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: frontend - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-dev: - needs: build-and-push-dev - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: frontend-dev - image: ${{ needs.build-and-push-dev.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/lf-staging-deploy-integration-data-worker.yaml b/.github/workflows/lf-staging-deploy-integration-data-worker.yaml deleted file mode 100644 index a059404c0c..0000000000 --- a/.github/workflows/lf-staging-deploy-integration-data-worker.yaml +++ /dev/null @@ -1,60 +0,0 @@ -name: LF Staging Deploy Integration Data Worker - -on: - push: - branches: - - 'lf-staging/**' - - 'lf-staging-**' - paths: - - 'services/libs/**' - - 'services/apps/integration_data_worker/**' - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.LF_STAGING_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.LF_STAGING_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.LF_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.LF_AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.LF_AWS_REGION }} - SLACK_CHANNEL: deploys-lf-staging - SLACK_WEBHOOK: ${{ secrets.LF_STAGING_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: integration-data-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-integration-data-worker: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: integration-data-worker - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/lf-staging-deploy-integration-run-worker.yaml b/.github/workflows/lf-staging-deploy-integration-run-worker.yaml deleted file mode 100644 index c26285a4e8..0000000000 --- a/.github/workflows/lf-staging-deploy-integration-run-worker.yaml +++ /dev/null @@ -1,60 +0,0 @@ -name: LF Staging Deploy Integration Run Worker - -on: - push: - branches: - - 'lf-staging/**' - - 'lf-staging-**' - paths: - - 'services/libs/**' - - 'services/apps/integration_data_worker/**' - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.LF_STAGING_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.LF_STAGING_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.LF_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.LF_AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.LF_AWS_REGION }} - SLACK_CHANNEL: deploys-lf-staging - SLACK_WEBHOOK: ${{ secrets.LF_STAGING_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: integration-run-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-integration-run-worker: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: integration-run-worker - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/lf-staging-deploy-integration-stream-worker.yaml b/.github/workflows/lf-staging-deploy-integration-stream-worker.yaml deleted file mode 100644 index e69c76ced3..0000000000 --- a/.github/workflows/lf-staging-deploy-integration-stream-worker.yaml +++ /dev/null @@ -1,60 +0,0 @@ -name: LF Staging Deploy Integration Stream Worker - -on: - push: - branches: - - 'lf-staging/**' - - 'lf-staging-**' - paths: - - 'services/libs/**' - - 'services/apps/integration_stream_worker/**' - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.LF_STAGING_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.LF_STAGING_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.LF_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.LF_AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.LF_AWS_REGION }} - SLACK_CHANNEL: deploys-lf-staging - SLACK_WEBHOOK: ${{ secrets.LF_STAGING_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: integration-stream-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-integration-stream-worker: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: integration-stream-worker - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/lf-staging-deploy-python-worker.yaml b/.github/workflows/lf-staging-deploy-python-worker.yaml deleted file mode 100644 index ade5f61482..0000000000 --- a/.github/workflows/lf-staging-deploy-python-worker.yaml +++ /dev/null @@ -1,59 +0,0 @@ -name: LF Staging Deploy Python Worker service - -on: - push: - branches: - - 'lf-staging/**' - - 'lf-staging-**' - paths: - - 'backend/src/serverless/microservices/python/**' - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.LF_STAGING_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.LF_STAGING_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.LF_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.LF_AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.LF_AWS_REGION }} - SLACK_CHANNEL: deploys-lf-staging - SLACK_WEBHOOK: ${{ secrets.LF_STAGING_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: python-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-python-worker: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: python-worker - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/lf-staging-deploy-search-sync-worker.yaml b/.github/workflows/lf-staging-deploy-search-sync-worker.yaml deleted file mode 100644 index 022c152bd6..0000000000 --- a/.github/workflows/lf-staging-deploy-search-sync-worker.yaml +++ /dev/null @@ -1,60 +0,0 @@ -name: LF Staging Deploy Search Sync Worker - -on: - push: - branches: - - 'lf-staging/**' - - 'lf-staging-**' - paths: - - 'services/libs/**' - - 'services/apps/search_sync_worker/**' - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.LF_STAGING_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.LF_STAGING_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.LF_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.LF_AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.LF_AWS_REGION }} - SLACK_CHANNEL: deploys-lf-staging - SLACK_WEBHOOK: ${{ secrets.LF_STAGING_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: search-sync-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-search-sync-worker: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: search-sync-worker - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/lf-staging-deploy-webhook-api.yaml b/.github/workflows/lf-staging-deploy-webhook-api.yaml deleted file mode 100644 index 36ccd1e07e..0000000000 --- a/.github/workflows/lf-staging-deploy-webhook-api.yaml +++ /dev/null @@ -1,60 +0,0 @@ -name: LF Staging Deploy Webhook API - -on: - push: - branches: - - 'lf-staging/**' - - 'lf-staging-**' - paths: - - 'services/libs/**' - - 'services/apps/webhook_api/**' - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.LF_STAGING_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.LF_STAGING_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.LF_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.LF_AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.LF_AWS_REGION }} - SLACK_CHANNEL: deploys-lf-staging - SLACK_WEBHOOK: ${{ secrets.LF_STAGING_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: webhook-api - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-webhook-api: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: webhook-api - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/lint-frontend.yml b/.github/workflows/lint-frontend.yml deleted file mode 100644 index b4fe721381..0000000000 --- a/.github/workflows/lint-frontend.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Lint Frontend - -on: - pull_request: - paths: - - 'frontend/**' - -jobs: - lint-frontend: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Lint code - run: npm i && npm run lint - working-directory: frontend diff --git a/.github/workflows/pr-title-jira-key-lint.yml b/.github/workflows/pr-title-jira-key-lint.yml new file mode 100644 index 0000000000..b22e57b648 --- /dev/null +++ b/.github/workflows/pr-title-jira-key-lint.yml @@ -0,0 +1,120 @@ +name: PR Title - Jira Key Validation + +on: + pull_request: + types: [opened, edited, reopened] + +jobs: + validate-jira-key-in-pr-title: + runs-on: ubuntu-latest + if: github.event.pull_request.draft == false + permissions: + pull-requests: write + checks: write + steps: + - name: Check for Jira issue key in PR title + uses: actions/github-script@v7 + with: + script: | + const prTitle = context.payload.pull_request.title; + const prNumber = context.issue.number; + const { owner, repo } = context.repo; + const headSha = context.payload.pull_request.head.sha; + + const COMMENT_MARKER = ''; + const jiraKeyRegex = /\b[A-Z]+-\d+\b/; + + console.log(`PR Title: ${prTitle}`); + + const matchingComments = []; + const perPage = 100; + for (let page = 1; ; page += 1) { + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number: prNumber, + per_page: perPage, + page, + }); + + for (const c of comments) { + if (c.user?.type === 'Bot' && c.body?.includes(COMMENT_MARKER)) { + matchingComments.push(c); + } + } + if (comments.length < perPage) break; + } + + if (!jiraKeyRegex.test(prTitle)) { + const warningMessage = [ + COMMENT_MARKER, + '⚠️ **Jira Issue Key Missing**', + '', + "Your PR title doesn't contain a Jira issue key. Consider adding it for better traceability.", + '', + '**Example:**', + '- `feat: add user authentication (CM-123)`', + '- `feat: add user authentication (IN-123)`', + '', + '**Projects:**', + '- CM: Community Data Platform', + '- IN: Insights', + '', + 'Please add a Jira issue key to your PR title.', + ].join('\n'); + + if (matchingComments.length === 0) { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: prNumber, + body: warningMessage, + }); + } else { + const [keep, ...extras] = matchingComments; + if (keep.body !== warningMessage) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: keep.id, + body: warningMessage, + }); + } + for (const extra of extras) { + await github.rest.issues.deleteComment({ owner, repo, comment_id: extra.id }); + } + } + + await github.rest.checks.create({ + owner, + repo, + head_sha: headSha, + name: 'Jira PR Validation', + status: 'completed', + conclusion: 'neutral', + output: { + title: 'Jira Issue Key Missing', + summary: 'PR title does not contain a Jira issue key', + }, + }); + } else { + const match = prTitle.match(jiraKeyRegex); + console.log(`✅ Found Jira issue key: ${match[0]}`); + + for (const c of matchingComments) { + await github.rest.issues.deleteComment({ owner, repo, comment_id: c.id }); + } + + await github.rest.checks.create({ + owner, + repo, + head_sha: headSha, + name: 'Jira PR Validation', + status: 'completed', + conclusion: 'success', + output: { + title: 'Jira Issue Key Found', + summary: `Found Jira issue key: ${match[0]}`, + }, + }); + } diff --git a/.github/workflows/pr-title-lint.yml b/.github/workflows/pr-title-lint.yml new file mode 100644 index 0000000000..04afb9d2ce --- /dev/null +++ b/.github/workflows/pr-title-lint.yml @@ -0,0 +1,14 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT +name: PR Title Lint +on: + pull_request: + # By default, a workflow only runs when a pull_request's activity type is opened, synchronize, or reopened. We + # explicity override here so that PR titles are re-linted when the PR text content is edited. + # + # Possible values: https://help.github.com/en/actions/reference/events-that-trigger-workflows#pull-request-event-pull_request + types: [opened, edited, reopened, synchronize] + +jobs: + pr-title-lint: + uses: linuxfoundation/lfx-ui/.github/workflows/_pr-title-lint.yml@main \ No newline at end of file diff --git a/.github/workflows/production-deploy-new.yaml b/.github/workflows/production-deploy-new.yaml deleted file mode 100644 index 10852465a9..0000000000 --- a/.github/workflows/production-deploy-new.yaml +++ /dev/null @@ -1,189 +0,0 @@ -name: Production Deploy New - -on: - workflow_dispatch: - inputs: - deploy_search_sync_worker: - description: Deploy search-sync-worker service? - required: true - type: boolean - deploy_integration_sync_worker: - description: Deploy integration-sync-worker service? - required: true - type: boolean - deploy_webhook_api: - description: Deploy webhook-api service? - required: true - type: boolean - deploy_script_executor: - description: Deploy script-executor service? - required: true - type: boolean - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.PRODUCTION_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.PRODUCTION_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} - SLACK_CHANNEL: deploys - SLACK_WEBHOOK: ${{ secrets.PRODUCTION_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push-search-sync-worker: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_search_sync_worker }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: search-sync-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - build-and-push-integration-sync-worker: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_integration_sync_worker }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: integration-sync-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - build-and-push-webhook-api: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_webhook_api }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: webhook-api - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - build-and-push-script-executor: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_script_executor }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: script-executor - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-search-sync-worker: - needs: build-and-push-search-sync-worker - runs-on: ubuntu-latest - if: ${{ inputs.deploy_search_sync_worker }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: search-sync-worker - image: ${{ needs.build-and-push-search-sync-worker.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-integration-sync-worker: - needs: build-and-push-integration-sync-worker - runs-on: ubuntu-latest - if: ${{ inputs.deploy_integration_sync_worker }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: integration-sync-worker - image: ${{ needs.build-and-push-integration-sync-worker.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-webhook-api: - needs: build-and-push-webhook-api - runs-on: ubuntu-latest - if: ${{ inputs.deploy_webhook_api }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: webhook-api - image: ${{ needs.build-and-push-webhook-api.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-script-executor: - needs: build-and-push-script-executor - runs-on: ubuntu-latest - if: ${{ inputs.deploy_script_executor }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: script-executor - image: ${{ needs.build-and-push-script-executor.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/production-deploy-original.yaml b/.github/workflows/production-deploy-original.yaml deleted file mode 100644 index 195cf84c20..0000000000 --- a/.github/workflows/production-deploy-original.yaml +++ /dev/null @@ -1,383 +0,0 @@ -name: Production Deploy Original - -on: - workflow_dispatch: - inputs: - deploy_api: - description: Deploy api service? - required: true - type: boolean - deploy_job_generator: - description: Deploy job-generator service? - required: true - type: boolean - deploy_nodejs_worker: - description: Deploy nodejs-worker service? - required: true - type: boolean - deploy_discord_ws: - description: Deploy discord-ws service? - required: true - type: boolean - deploy_integration_run_worker: - description: Deploy integration-run-worker service? - required: true - type: boolean - deploy_integration_stream_worker: - description: Deploy integration-stream-worker service? - required: true - type: boolean - deploy_integration_data_worker: - description: Deploy integration-data-worker service? - required: true - type: boolean - deploy_data_sink_worker: - description: Deploy data-sink-worker service? - required: true - type: boolean - deploy_python_worker: - description: Deploy python-worker service? - required: true - type: boolean - deploy_frontend: - description: Deploy frontend? - required: true - type: boolean - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.PRODUCTION_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.PRODUCTION_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} - SLACK_CHANNEL: deploys - SLACK_WEBHOOK: ${{ secrets.PRODUCTION_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push-backend: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_api || inputs.deploy_job_generator || inputs.deploy_nodejs_worker || inputs.deploy_discord_ws }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: backend - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - build-and-push-integration-run-worker: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_integration_run_worker }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: integration-run-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - build-and-push-integration-stream-worker: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_integration_stream_worker }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: integration-stream-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - build-and-push-integration-data-worker: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_integration_data_worker }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: integration-data-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - build-and-push-data-sink-worker: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_data_sink_worker }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: data-sink-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - build-and-push-frontend: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_frontend }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: frontend - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - build-and-push-python-worker: - runs-on: ubuntu-latest - if: ${{ inputs.deploy_python_worker }} - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: python-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-api: - needs: build-and-push-backend - runs-on: ubuntu-latest - if: ${{ inputs.deploy_api }} - defaults: - run: - shell: bash - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: api - image: ${{ needs.build-and-push-backend.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-nodejs-worker: - needs: build-and-push-backend - runs-on: ubuntu-latest - if: ${{ inputs.deploy_nodejs_worker }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: nodejs-worker - image: ${{ needs.build-and-push-backend.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-discord-ws: - needs: build-and-push-backend - runs-on: ubuntu-latest - if: ${{ inputs.deploy_discord_ws }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: discord-ws - image: ${{ needs.build-and-push-backend.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-job-generator: - needs: build-and-push-backend - runs-on: ubuntu-latest - if: ${{ inputs.deploy_job_generator }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: job-generator - image: ${{ needs.build-and-push-backend.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-integration-run-worker: - needs: build-and-push-integration-run-worker - runs-on: ubuntu-latest - if: ${{ inputs.deploy_integration_run_worker }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: integration-run-worker - image: ${{ needs.build-and-push-integration-run-worker.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-integration-stream-worker: - needs: build-and-push-integration-stream-worker - runs-on: ubuntu-latest - if: ${{ inputs.deploy_integration_stream_worker }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: integration-stream-worker - image: ${{ needs.build-and-push-integration-stream-worker.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-integration-data-worker: - needs: build-and-push-integration-data-worker - runs-on: ubuntu-latest - if: ${{ inputs.deploy_integration_data_worker }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: integration-data-worker - image: ${{ needs.build-and-push-integration-data-worker.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-data-sink-worker: - needs: build-and-push-data-sink-worker - runs-on: ubuntu-latest - if: ${{ inputs.deploy_data_sink_worker }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: data-sink-worker - image: ${{ needs.build-and-push-data-sink-worker.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-frontend: - needs: build-and-push-frontend - runs-on: ubuntu-latest - if: ${{ inputs.deploy_frontend }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: frontend - image: ${{ needs.build-and-push-frontend.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-python-worker: - needs: build-and-push-python-worker - runs-on: ubuntu-latest - if: ${{ inputs.deploy_python_worker }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: python-worker - image: ${{ needs.build-and-push-python-worker.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/python-eagle-eye-manual-build-publish.yaml b/.github/workflows/python-eagle-eye-manual-build-publish.yaml deleted file mode 100644 index a2c7e8fef3..0000000000 --- a/.github/workflows/python-eagle-eye-manual-build-publish.yaml +++ /dev/null @@ -1,51 +0,0 @@ -name: Eagle Eye Image Publish - -on: - workflow_dispatch: - inputs: - branch: - type: string - description: Branch to push/publish - required: true -jobs: - build-eagle-eye-dist: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - ref: ${{inputs.branch}} - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: eu-central-1 - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v1 - - - name: Set env - run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - - name: Version Info - run: | - echo eagle eye lambda image build is starting with version: ${{ env.RELEASE_VERSION }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v2 - - - name: Inspect builder - run: | - echo "Name: ${{ steps.buildx.outputs.name }}" - echo "Endpoint: ${{ steps.buildx.outputs.endpoint }}" - echo "Status: ${{ steps.buildx.outputs.status }}" - echo "Flags: ${{ steps.buildx.outputs.flags }}" - echo "Platforms: ${{ steps.buildx.outputs.platforms }}" - - - name: Build with buildx - working-directory: ./premium/eagle-eye - run: docker buildx build -f ./Dockerfile --platform linux/amd64 -t 359905442998.dkr.ecr.eu-central-1.amazonaws.com/python-eagle-eye-lambda:${{ env.RELEASE_VERSION }} --push . diff --git a/.github/workflows/python-microservices-manual-build-publish.yaml b/.github/workflows/python-microservices-manual-build-publish.yaml deleted file mode 100644 index 9fb5aee9de..0000000000 --- a/.github/workflows/python-microservices-manual-build-publish.yaml +++ /dev/null @@ -1,47 +0,0 @@ -name: Python Microservices Image Publish - -on: - workflow_dispatch: - -jobs: - build-python-microservices-dist: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: eu-central-1 - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v1 - - - name: Version Info 2 - run: | - echo "RELEASE_VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV - echo "crowd version published is:" - echo ${{ env.RELEASE_VERSION }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v2 - - - name: Inspect builder - run: | - echo "Name: ${{ steps.buildx.outputs.name }}" - echo "Endpoint: ${{ steps.buildx.outputs.endpoint }}" - echo "Status: ${{ steps.buildx.outputs.status }}" - echo "Flags: ${{ steps.buildx.outputs.flags }}" - echo "Platforms: ${{ steps.buildx.outputs.platforms }}" - - - name: Build with buildx - working-directory: ./backend/src/serverless/microservices/python - run: docker buildx build -f ./Dockerfile -t 359905442998.dkr.ecr.eu-central-1.amazonaws.com/python-microservices-lambda:${{ env.RELEASE_VERSION}} --push . diff --git a/.github/workflows/release-drafter.yaml b/.github/workflows/release-drafter.yaml deleted file mode 100644 index 72d6769c40..0000000000 --- a/.github/workflows/release-drafter.yaml +++ /dev/null @@ -1,38 +0,0 @@ -name: Release Drafter - -on: - push: - # branches to consider in the event; optional, defaults to all - branches: - - main - # pull_request event is required only for autolabeler - pull_request: - # Only following types are handled by the action, but one can default to all as well - types: [opened, reopened, synchronize] - # pull_request_target event is required for autolabeler to support PRs from forks - pull_request_target: - types: [opened, reopened, synchronize] - -permissions: - contents: read - -jobs: - update_release_draft: - permissions: - contents: write # for release-drafter/release-drafter to create a github release - pull-requests: write # for release-drafter/release-drafter to add label to PR - runs-on: ubuntu-latest - steps: - # (Optional) GitHub Enterprise requires GHE_HOST variable set - #- name: Set GHE_HOST - # run: | - # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV - - # Drafts your next Release notes as Pull Requests are merged into "main" - - uses: release-drafter/release-drafter@v5 - # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml - # with: - # config-name: my-config.yml - # disable-autolabeler: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/reset-staging.yaml b/.github/workflows/reset-staging.yaml deleted file mode 100644 index e28f807500..0000000000 --- a/.github/workflows/reset-staging.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: Reset staging to main - -on: - workflow_dispatch: # added this line to enable manual triggering - schedule: - # Run every Sunday evening at 18:00 - - cron: '0 18 * * 0' - # Run every 5 minutes for testing - # - cron: '*/5 * * * *' - -jobs: - sync-main-to-staging: - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Sync Main to Staging - run: | - git config --local user.email "joan@crowd.dev" # Use your Git email - git config --local user.name "joanreyero" # Use your Git username - - git checkout staging-main - git merge --strategy-option=theirs main --allow-unrelated-histories -m "Sync main with staging-main joanreyero" - git push origin staging-main \ No newline at end of file diff --git a/.github/workflows/staging-deploy-backend.yaml b/.github/workflows/staging-deploy-backend.yaml deleted file mode 100644 index b8c6f73d6d..0000000000 --- a/.github/workflows/staging-deploy-backend.yaml +++ /dev/null @@ -1,112 +0,0 @@ -name: Staging Deploy Backend services - -on: - push: - branches: - - 'staging/**' - - 'staging-**' - paths: - - 'backend/**' - - 'services/libs/**' - - '!backend/src/serverless/microservices/python/**' - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.STAGING_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.STAGING_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} - SLACK_CHANNEL: deploys-staging - SLACK_WEBHOOK: ${{ secrets.STAGING_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: backend - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-api: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: api - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-nodejs-worker: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: nodejs-worker - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-job-generator: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: job-generator - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-discord-ws: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: discord-ws - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/staging-deploy-data-sink-worker.yaml b/.github/workflows/staging-deploy-data-sink-worker.yaml deleted file mode 100644 index 3f9e32557e..0000000000 --- a/.github/workflows/staging-deploy-data-sink-worker.yaml +++ /dev/null @@ -1,60 +0,0 @@ -name: Staging Deploy Data Sink Worker - -on: - push: - branches: - - 'staging/**' - - 'staging-**' - paths: - - 'services/libs/**' - - 'services/apps/data_sink_worker/**' - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.STAGING_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.STAGING_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} - SLACK_CHANNEL: deploys-staging - SLACK_WEBHOOK: ${{ secrets.STAGING_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: data-sink-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-data-sink-worker: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: data-sink-worker - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/staging-deploy-frontend.yaml b/.github/workflows/staging-deploy-frontend.yaml deleted file mode 100644 index ff3e1f290e..0000000000 --- a/.github/workflows/staging-deploy-frontend.yaml +++ /dev/null @@ -1,97 +0,0 @@ -name: Staging Deploy Frontend service - -on: - push: - branches: - - 'staging/**' - - 'staging-**' - paths: - - 'frontend/**' - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.STAGING_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.STAGING_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} - SLACK_CHANNEL: deploys-staging - SLACK_WEBHOOK: ${{ secrets.STAGING_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: frontend - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - build-and-push-dev: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: frontend-dev - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: frontend - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} - - deploy-dev: - needs: build-and-push-dev - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: frontend-dev - image: ${{ needs.build-and-push-dev.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/staging-deploy-integration-data-worker.yaml b/.github/workflows/staging-deploy-integration-data-worker.yaml deleted file mode 100644 index 74b9383984..0000000000 --- a/.github/workflows/staging-deploy-integration-data-worker.yaml +++ /dev/null @@ -1,60 +0,0 @@ -name: Staging Deploy Integration Data Worker - -on: - push: - branches: - - 'staging/**' - - 'staging-**' - paths: - - 'services/libs/**' - - 'services/apps/integration_data_worker/**' - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.STAGING_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.STAGING_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} - SLACK_CHANNEL: deploys-staging - SLACK_WEBHOOK: ${{ secrets.STAGING_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: integration-data-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-integration-data-worker: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: integration-data-worker - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/staging-deploy-integration-run-worker.yaml b/.github/workflows/staging-deploy-integration-run-worker.yaml deleted file mode 100644 index 2f733da9bd..0000000000 --- a/.github/workflows/staging-deploy-integration-run-worker.yaml +++ /dev/null @@ -1,60 +0,0 @@ -name: Staging Deploy Integration Run Worker - -on: - push: - branches: - - 'staging/**' - - 'staging-**' - paths: - - 'services/libs/**' - - 'services/apps/integration_data_worker/**' - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.STAGING_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.STAGING_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} - SLACK_CHANNEL: deploys-staging - SLACK_WEBHOOK: ${{ secrets.STAGING_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: integration-run-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-integration-run-worker: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: integration-run-worker - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/staging-deploy-integration-stream-worker.yaml b/.github/workflows/staging-deploy-integration-stream-worker.yaml deleted file mode 100644 index d7500f1d3a..0000000000 --- a/.github/workflows/staging-deploy-integration-stream-worker.yaml +++ /dev/null @@ -1,60 +0,0 @@ -name: Staging Deploy Integration Stream Worker - -on: - push: - branches: - - 'staging/**' - - 'staging-**' - paths: - - 'services/libs/**' - - 'services/apps/integration_stream_worker/**' - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.STAGING_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.STAGING_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} - SLACK_CHANNEL: deploys-staging - SLACK_WEBHOOK: ${{ secrets.STAGING_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: integration-stream-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-integration-stream-worker: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: integration-stream-worker - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/staging-deploy-integration-sync-worker.yaml b/.github/workflows/staging-deploy-integration-sync-worker.yaml deleted file mode 100644 index e8674313c6..0000000000 --- a/.github/workflows/staging-deploy-integration-sync-worker.yaml +++ /dev/null @@ -1,60 +0,0 @@ -name: Staging Deploy Integration Sync Worker - -on: - push: - branches: - - 'staging/**' - - 'staging-**' - paths: - - 'services/libs/**' - - 'services/apps/integration_sync_worker/**' - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.STAGING_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.STAGING_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} - SLACK_CHANNEL: deploys-staging - SLACK_WEBHOOK: ${{ secrets.STAGING_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: integration-sync-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-integration-sync-worker: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: integration-sync-worker - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/staging-deploy-python-worker.yaml b/.github/workflows/staging-deploy-python-worker.yaml deleted file mode 100644 index 81f58548d0..0000000000 --- a/.github/workflows/staging-deploy-python-worker.yaml +++ /dev/null @@ -1,59 +0,0 @@ -name: Staging Deploy Python Worker service - -on: - push: - branches: - - 'staging/**' - - 'staging-**' - paths: - - 'backend/src/serverless/microservices/python/**' - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.STAGING_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.STAGING_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} - SLACK_CHANNEL: deploys-staging - SLACK_WEBHOOK: ${{ secrets.STAGING_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: python-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-python-worker: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: python-worker - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/staging-deploy-search-sync-worker.yaml b/.github/workflows/staging-deploy-search-sync-worker.yaml deleted file mode 100644 index 21e59a4da7..0000000000 --- a/.github/workflows/staging-deploy-search-sync-worker.yaml +++ /dev/null @@ -1,60 +0,0 @@ -name: Staging Deploy Search Sync Worker - -on: - push: - branches: - - 'staging/**' - - 'staging-**' - paths: - - 'services/libs/**' - - 'services/apps/search_sync_worker/**' - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.STAGING_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.STAGING_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} - SLACK_CHANNEL: deploys-staging - SLACK_WEBHOOK: ${{ secrets.STAGING_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: search-sync-worker - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-search-sync-worker: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: search-sync-worker - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/staging-deploy-webhook-api.yaml b/.github/workflows/staging-deploy-webhook-api.yaml deleted file mode 100644 index 377b16462a..0000000000 --- a/.github/workflows/staging-deploy-webhook-api.yaml +++ /dev/null @@ -1,60 +0,0 @@ -name: Staging Deploy Webhook API - -on: - push: - branches: - - 'staging/**' - - 'staging-**' - paths: - - 'services/libs/**' - - 'services/apps/webhook_api/**' - -env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} - CROWD_CLUSTER: ${{ secrets.STAGING_CLUSTER_NAME }} - CROWD_ROLE_ARN: ${{ secrets.STAGING_CLUSTER_ROLE_ARN }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} - SLACK_CHANNEL: deploys-staging - SLACK_WEBHOOK: ${{ secrets.STAGING_SLACK_CHANNEL_HOOK }} - -jobs: - build-and-push: - runs-on: ubuntu-latest - outputs: - image: ${{ steps.image.outputs.IMAGE }} - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/build-docker-image - id: image-builder - with: - image: webhook-api - - - name: Set docker image output - id: image - run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - - deploy-webhook-api: - needs: build-and-push - runs-on: ubuntu-latest - defaults: - run: - shell: bash - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - uses: ./.github/actions/deploy-service - with: - service: webhook-api - image: ${{ needs.build-and-push.outputs.image }} - cluster: ${{ env.CROWD_CLUSTER }} diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml deleted file mode 100644 index ee9e9e5430..0000000000 --- a/.github/workflows/test-frontend.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Test Frontend - -on: - pull_request: - paths: - - 'frontend/**' - -jobs: - test-frontend: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Create env file - run: | - touch .env.override.local - echo CROWD_SENDGRID_KEY=${{ secrets.CROWD_SENDGRID_KEY }} >> .env.override.local - echo CROWD_SENDGRID_EMAIL_FROM=${{ secrets.CROWD_SENDGRID_EMAIL_FROM }} >> .env.override.local - echo CROWD_SENDGRID_NAME_FROM=${{ secrets.CROWD_SENDGRID_NAME_FROM }} >> .env.override.local - echo CROWD_SENDGRID_TEMPLATE_EMAIL_ADDRESS_VERIFICATION=${{ secrets.CROWD_SENDGRID_TEMPLATE_EMAIL_ADDRESS_VERIFICATION }} >> .env.override.local - echo CROWD_SENDGRID_TEMPLATE_INVITATION=${{ secrets.CROWD_SENDGRID_TEMPLATE_INVITATION }} >> .env.override.local - echo CROWD_SENDGRID_TEMPLATE_PASSWORD_RESET=${{ secrets.CROWD_SENDGRID_TEMPLATE_PASSWORD_RESET }} >> .env.override.local - echo CROWD_SENDGRID_TEMPLATE_WEEKLY_ANALYTICS=${{ secrets.CROWD_SENDGRID_TEMPLATE_WEEKLY_ANALYTICS }} >> .env.override.local - echo CROWD_SENDGRID_WEEKLY_ANALYTICS_UNSUBSCRIBE_GROUP_ID=${{ secrets.CROWD_SENDGRID_WEEKLY_ANALYTICS_UNSUBSCRIBE_GROUP_ID }} >> .env.override.local - working-directory: backend - - - name: Start scaffold - run: ./cli scaffold up - working-directory: scripts - - - name: Start services - run: ./cli service api restart && ./cli service nodejs-worker restart && ./cli service job-generator restart && ./cli service frontend restart - working-directory: scripts - -# - name: Cypress run -# uses: cypress-io/github-action@v4 -# with: -# env: MAILOSAUR_API_KEY=${{ secrets.MAILOSAUR_API_KEY }},MAILOSAUR_SERVER_ID=${{ secrets.MAILOSAUR_SERVER_ID }} -# working-directory: 'frontend' -# browser: chrome -# headed: true -# -# - name: Upload screenshots -# uses: actions/upload-artifact@v2 -# if: failure() -# with: -# name: cypress-screenshots -# path: frontend/cypress/screenshots -# -# - name: Upload video -# uses: actions/upload-artifact@v2 -# if: always() -# with: -# name: cypress-videos -# path: frontend/cypress/videos diff --git a/.github/workflows/tinybird-ci.yml b/.github/workflows/tinybird-ci.yml new file mode 100644 index 0000000000..29918dda0d --- /dev/null +++ b/.github/workflows/tinybird-ci.yml @@ -0,0 +1,232 @@ +name: Tinybird CI + +on: + pull_request: + types: [labeled, unlabeled, synchronize] + paths: + - 'services/libs/tinybird/**' + workflow_dispatch: + +env: + DATA_PROJECT_DIR: services/libs/tinybird + GIT_DEPTH: 300 + USE_LAST_PARTITION: true + +jobs: + check: + name: Datafiles checks + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Tinybird CLI + run: | + if [ -f "${{ env.DATA_PROJECT_DIR }}/requirements.txt" ]; then + pip install -r ${{ env.DATA_PROJECT_DIR }}/requirements.txt + else + pip install tinybird-cli + fi + + - name: Get changed files + id: files + uses: tj-actions/changed-files@v46 + with: + files: | + **/*.{datasource,incl,pipe} + + - name: Check formatting + if: ${{ steps.files.outputs.any_changed == 'true' }} + shell: bash + run: | + for file in ${{ steps.files.outputs.all_changed_files }}; do + tb fmt --diff "$file" + done + + # deploy: + # name: Deploy to CI Branch + # runs-on: ubuntu-latest + # defaults: + # run: + # working-directory: ${{ env.DATA_PROJECT_DIR }} + # steps: + # - uses: actions/checkout@v4 + # with: + # fetch-depth: ${{ env.GIT_DEPTH }} + # ref: ${{ github.event.pull_request.head.sha }} + + # - uses: actions/setup-python@v5 + # with: + # python-version: "3.11" + # architecture: "x64" + # cache: pip + + # - name: Set environment variables + # run: | + # _ENV_FLAGS="${{ env.USE_LAST_PARTITION == 'true' && '--last-partition ' || '' }}--wait" + # _NORMALIZED_BRANCH_NAME=$(echo $DATA_PROJECT_DIR | rev | cut -d "/" -f 1 | rev | tr '.-' '_') + # GIT_BRANCH=${GITHUB_HEAD_REF} + # echo "GIT_BRANCH=$GIT_BRANCH" >> $GITHUB_ENV + # echo "_ENV_FLAGS=$_ENV_FLAGS" >> $GITHUB_ENV + # echo "_NORMALIZED_BRANCH_NAME=$_NORMALIZED_BRANCH_NAME" >> $GITHUB_ENV + # if [ -f .tinyenv ]; then grep -v '^#' .tinyenv >> $GITHUB_ENV; fi + # echo >> $GITHUB_ENV + + # - name: Install Tinybird CLI + # run: | + # if [ -f "requirements.txt" ]; then + # pip install -r requirements.txt + # else + # pip install tinybird-cli + # fi + + # - name: Tinybird version + # run: tb --version + + # - name: Check all the data files syntax + # run: tb check + + # - name: Check auth + # run: tb --host ${{ secrets.TB_HOST }} --token ${{ secrets.TB_ADMIN_TOKEN }} auth info + + # - name: Try delete previous Branch + # run: | + # output=$(tb --host ${{ secrets.TB_HOST }} --token ${{ secrets.TB_ADMIN_TOKEN }} branch ls) + # BRANCH_NAME="tmp_ci_${_NORMALIZED_BRANCH_NAME}_${{ github.event.pull_request.number }}" + # if echo "$output" | grep -q "\b$BRANCH_NAME\b"; then + # tb --host ${{ secrets.TB_HOST }} --token ${{ secrets.TB_ADMIN_TOKEN }} branch rm $BRANCH_NAME --yes + # else + # echo "Skipping clean up: The Branch '$BRANCH_NAME' does not exist." + # fi + + # - name: Create new test Branch + # run: | + # tb \ + # --host ${{ secrets.TB_HOST }} \ + # --token ${{ secrets.TB_ADMIN_TOKEN }} \ + # branch create tmp_ci_${_NORMALIZED_BRANCH_NAME}_${{ github.event.pull_request.number }} \ + # ${_ENV_FLAGS} + + # - name: Deploy changes to the test Branch + # run: | + # source .tinyenv || true + # DEPLOY_FILE=./deploy/${VERSION}/deploy.sh + # if [ ! -f "$DEPLOY_FILE" ]; then + # echo "$DEPLOY_FILE not found, running default tb deploy command" + # tb deploy ${CI_FLAGS} + # tb release ls + # fi + + # - name: Custom deployment to the test Branch + # run: | + # source .tinyenv || true + # DEPLOY_FILE=./deploy/${VERSION}/deploy.sh + # if [ -f "$DEPLOY_FILE" ]; then + # echo "$DEPLOY_FILE found" + # if ! [ -x "$DEPLOY_FILE" ]; then + # echo "Error: You do not have permission to execute '$DEPLOY_FILE'. Run:" + # echo "> chmod +x $DEPLOY_FILE" + # echo "and commit your changes" + # exit 1 + # else + # $DEPLOY_FILE + # fi + # fi + + # test: + # name: Run tests + # runs-on: ubuntu-latest + # needs: + # - deploy + # defaults: + # run: + # working-directory: ${{ env.DATA_PROJECT_DIR }} + # steps: + # - uses: actions/checkout@v4 + # with: + # fetch-depth: 0 + # ref: ${{ github.event.pull_request.head.sha }} + + # - uses: actions/setup-python@v5 + # with: + # python-version: "3.11" + # architecture: "x64" + # cache: pip + + # - name: Set environment variables + # run: | + # _ENV_FLAGS="--last-partition --wait" + # _NORMALIZED_BRANCH_NAME=$(echo $DATA_PROJECT_DIR | rev | cut -d "/" -f 1 | rev | tr '.-' '_') + # GIT_BRANCH=${GITHUB_HEAD_REF} + # echo "GIT_BRANCH=$GIT_BRANCH" >> $GITHUB_ENV + # echo "_ENV_FLAGS=$_ENV_FLAGS" >> $GITHUB_ENV + # echo "_NORMALIZED_BRANCH_NAME=$_NORMALIZED_BRANCH_NAME" >> $GITHUB_ENV + # if [ -f .tinyenv ]; then grep -v '^#' .tinyenv >> $GITHUB_ENV; fi + # echo >> $GITHUB_ENV + + # - name: Install Tinybird CLI + # run: | + # if [ -f "requirements.txt" ]; then + # pip install -r requirements.txt + # else + # pip install tinybird-cli + # fi + + # - name: Tinybird version + # run: tb --version + + # - name: Check auth + # run: tb --host ${{ secrets.TB_HOST }} --token ${{ secrets.TB_ADMIN_TOKEN }} auth info + + # - name: Use Branch + # run: | + # BRANCH_NAME="tmp_ci_${_NORMALIZED_BRANCH_NAME}_${{ github.event.pull_request.number }}" + # tb --host ${{ secrets.TB_HOST }} --token ${{ secrets.TB_ADMIN_TOKEN }} branch use $BRANCH_NAME + + # - name: Post deploy + # run: | + # POSTDEPLOY_FILE=./deploy/${VERSION}/postdeploy.sh + # if [ -f "$POSTDEPLOY_FILE" ]; then + # if ! [ -x "$POSTDEPLOY_FILE" ]; then + # echo "Error: You do not have permission to execute '$POSTDEPLOY_FILE'. Run:" + # echo "> chmod +x $POSTDEPLOY_FILE" + # echo "and commit your changes" + # exit 1 + # else + # $POSTDEPLOY_FILE + # fi + # fi + + # - name: Get regression labels + # id: regression_labels + # uses: alrocar/get-labels-action@v1.0.1 + # with: + # github_token: ${{ secrets.GITHUB_TOKEN }} + # label_key: regression + + # - name: Run pipe regression tests + # run: | + # source .tinyenv || true + # echo ${{ steps.regression_labels.outputs.labels }} + # REGRESSION_LABELS=$(echo "${{ steps.regression_labels.outputs.labels }}" | awk -F, '{for (i=1; i<=NF; i++) if ($i ~ /^--/) print $i}' ORS=',' | sed 's/,$//') + # echo "Regression labels: ${REGRESSION_LABELS}" + + # CONFIG_FILE=./tests/regression.yaml + # BASE_CMD="tb branch regression-tests" + # LABELS_CMD="$(echo ${REGRESSION_LABELS} | tr , ' ')" + # if [ -f ${CONFIG_FILE} ]; then + # echo "Config file '${CONFIG_FILE}' found, adding pull request labels as options" + # ${BASE_CMD} -f ${CONFIG_FILE} --wait ${LABELS_CMD} + # else + # echo "Config file not found at '${CONFIG_FILE}', running with default values" + # ${BASE_CMD} coverage --wait ${LABELS_CMD} + # fi + + # - name: Cleanup test Branch + # if: always() + # run: | + # BRANCH_NAME="tmp_ci_${_NORMALIZED_BRANCH_NAME}_${{ github.event.pull_request.number }}" + # echo "Attempting to delete branch: $BRANCH_NAME" + # tb --host ${{ secrets.TB_HOST }} --token ${{ secrets.TB_ADMIN_TOKEN }} branch rm $BRANCH_NAME --yes || echo "Branch deletion failed or branch may not exist." \ No newline at end of file diff --git a/.gitignore b/.gitignore index b8034b7a91..f543ff5743 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ yarn-error.log* api-test/__pycache__/api_test.cpython-39-pytest-6.2.4.pyc # python +.venv/ **/*-venv **/venv **/venv* @@ -31,6 +32,142 @@ api-test/__pycache__/api_test.cpython-39-pytest-6.2.4.pyc **/__pycache__ **/*.egg-info +# Python bytecode +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +Pipfile.lock + +# poetry +poetry.lock + +# pdm +.pdm.toml + +# PEP 582 +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + **/**/.idea **/.vscode* @@ -46,7 +183,26 @@ api-test/__pycache__/api_test.cpython-39-pytest-6.2.4.pyc /docker/docker-compose.yaml /docker/docker-compose.dev.yaml -/backend/superface/** docker/volume -services/libs/*/dist \ No newline at end of file +services/libs/*/dist + +**/.tinyb +**/.tinyenv +**/*copied_ranges.log* +services/libs/tinybird/.diff_tmp + +# custom cursor rules +.cursor/rules/*.local.mdc + +# claude +.claude/settings.local.json +.claude/cache/ +.claude/tmp/ +.claude/logs/ +.claude/sessions/ +.claude/todos/ + +# git integration test repositories & output +services/apps/git_integration/src/test/repos/ +services/apps/git_integration/src/test/outputs/custom/ diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000000..c48ae6930e --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +pnpm dlx commitlint --edit $1 diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000000..4947849428 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,18 @@ +frontend_files=`git --no-pager diff --name-only --cached | +grep -E "frontend\/.+\.(?:js|ts|vue|scss|html)$" | +wc -l` + +# backend_files=`git --no-pager diff --name-only --cached | +# grep -E "backend\/.+\.(?:js|ts|vue|scss|html)$" | +# wc -l` + + +if [ $frontend_files -gt 0 ] +then + cd frontend && npx lint-staged && cd .. +fi + +# if [ $backend_files -gt 0 ] +# then +# cd backend && npx lint-staged +# fi diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..209e3ef4b6 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 0000000000..681311eb9c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..df941eaa4f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,71 @@ +# CDP — Community Data Platform + +CDP is a community data platform by the Linux Foundation. It ingests millions of +activities and events daily from platforms like GitHub, GitLab, and many others +(not just code hosting). Open-source projects get onboarded by connecting +integrations, and data flows continuously at scale. + +The ingested data is often messy. A big part of what CDP does is improve data quality: deduplicating member and organization profiles through merge and unmerge operations, enriching data via third-party providers, and resolving identities across sources. The cleaned data powers analytics and insights for LFX products. + +The codebase started as crowd.dev, an open-source startup later acquired by the Linux Foundation. Speed was prioritized over standards, but the platform is now stable. The focus has shifted to maintainable patterns, scalability, and good developer experience. Performance matters at this scale, even small inefficiencies compound across millions of data points. + +## Tech stack + +TypeScript, Node.js, Express, PostgreSQL (pg-promise), Temporal, Kafka, Redis, OpenSearch, Zod, Bunyan, AWS S3. + +Vue 3, Vite, Tailwind CSS, Element Plus, Pinia, TanStack Vue Query, Axios. + +Package manager is **pnpm**. Monorepo managed via pnpm workspaces. + +## Codebase structure + +``` +backend/ -> APIs (public endpoints for LFX products + internal for CDP UI) +frontend/ -> CDP Platform UI +services/apps/ -> Microservices — Temporal workers, Node.js workers, webhook APIs +services/libs/ -> Shared libraries used across services +``` + +`services/libs/common` holds shared utilities, error classes, +and helpers. If a piece of logic is reusable (not business logic), it belongs there. + +`services/libs/data-access-layer` holds all +database query functions. Check here before writing new ones — duplicates are +already a problem. + +## Patterns in transition + +Old and new patterns coexist. Always use the new pattern. + +- **Sequelize -> pg-promise**: Sequelize is legacy (backend only). Use + `queryExecutor` from `@crowd/data-access-layer` for all new database code. +- **Classes -> functions**: Class-based services and repos are legacy. Write + plain functions — composable, modular, easy to test. +- **Multi-tenancy -> single tenant**: Multi-tenancy is being phased out. The + tenant table still exists. Code uses `DEFAULT_TENANT_ID` from `@crowd/common`. + Don't add new multi-tenant logic. +- **Legacy auth -> Auth0**: Auth0 is the current auth system. Ignore old JWT + patterns. +- **Zod for validation**: Public API endpoints use Zod schemas with + `validateOrThrow`. Follow this pattern for all new endpoints. + +## Working with the database + +Millions of rows. Every query matters. + +- Look up the table schema and indexes before writing any query. Don't select + or touch columns blindly. +- Check existing functions in `data-access-layer` before writing new ones. + Weigh the blast radius of modifying a shared function — sometimes a new + function is safer. +- Write queries with performance in mind. Think about what indexes exist, what + the query plan looks like, and whether you're scanning more rows than needed. + +## Code quality + +- Functional and modular. Code should be easy to plug in, pull out, and test + independently. +- Think about performance at scale, even for small changes. +- Define types properly — extend and reuse existing types. Don't sprinkle `any`. +- Don't touch working code outside the scope of the current task. +- Prefer doing less over introducing risk. Weigh trade-offs before acting. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 05a65ad2b6..2775e6c3ff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,14 +1,14 @@ # Contributing to crowd.dev -Contributions are what make the open source community such an amazing place to learn, inspire, and create. +Contributions are what make the open-source community such an amazing place to learn, inspire, and create. ## Ways to contribute -- Try the crowd.dev platform & API and give feedback by [creating new issues](https://github.com/CrowdDotDev/crowd.dev/issues/new/choose) -- Help with [open issues](https://github.com/CrowdDotDev/crowd.dev/issues) -- Add a new integration following our [framework](https://docs.crowd.dev/docs/integration-framework) -- Help create tutorials and [blog](https://www.crowd.dev/blog) posts -- Improve [documentation](https://docs.crowd.dev/docs) by fixing incomplete or missing docs, bad wording, examples or explanations +- Try the crowd.dev platform & API and give feedback by [creating new issues](https://github.com/CrowdDotDev/crowd.dev/issues/new/choose). +- Help with [open issues](https://github.com/CrowdDotDev/crowd.dev/issues). +- Add a new integration following our [framework](https://docs.crowd.dev/docs/integration-framework). +- Help create tutorials and [blog](https://www.crowd.dev/blog) posts. +- Improve [documentation](https://docs.crowd.dev/docs) by fixing incomplete or missing docs, bad wording, examples, or explanations. Any contributions you make are **greatly appreciated**. ❤️ @@ -35,7 +35,7 @@ Any contributions you make are **greatly appreciated**. ❤️ - Bug in Core Features (Home, Members, Organizations, Activities, Reports) + Bug in Core Features (Home, Members, Organizations, Activities) @@ -70,10 +70,10 @@ Any contributions you make are **greatly appreciated**. ❤️ We welcome any contribution to crowd.dev. Before you start with your first issue, please consider the following points: -- For your first contribution we recommend taking a look at our [good first issues 🥂](https://github.com/CrowdDotDev/crowd.dev/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue+%F0%9F%A5%82%22). -- Other issues that are well suited for contribution have the tag [help wanted 🙏](https://github.com/CrowdDotDev/crowd.dev/labels/help%20wanted%20%F0%9F%99%8F). +- For your first contribution, we recommend taking a look at our ["good first issues" 🥂](https://github.com/CrowdDotDev/crowd.dev/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue+%F0%9F%A5%82%22). +- Other issues that are well suited for contribution have the tag ["help wanted" 🙏](https://github.com/CrowdDotDev/crowd.dev/labels/help%20wanted%20%F0%9F%99%8F). - If you want to contribute to our codebase, you have to first [sign our Contributor License Agreement](https://cla-assistant.io/CrowdDotDev/crowd.dev). -- If you need help you can reach us either via [Discord](http://crowd.dev/discord) or [Book a 15 min Contributor Onboarding Call](https://cal.com/team/CrowdDotDev/contributor-onboarding?duration=15). +- If you need help, you can reach us either via [Discord](http://crowd.dev/discord) or [Book a 15-min Contributor Onboarding Call](https://cal.com/team/CrowdDotDev/contributor-onboarding?duration=15). #### Requirements @@ -83,7 +83,7 @@ We welcome any contribution to crowd.dev. Before you start with your first issue #### Setup the project -The project is a monorepo, meaning that it is a collection of multiple packages managed in the same repository. In the following steps you'll learn how to get the project up and running for development purposes. +The project is a monorepo, meaning that it is a collection of multiple packages managed in the same repository. In the following steps, you'll learn how to get the project up and running for development purposes. 1. Get the mono repo from GitHub @@ -91,20 +91,20 @@ The project is a monorepo, meaning that it is a collection of multiple packages git clone git@github.com:CrowdDotDev/crowd.dev.git ``` -2. Run the start script +2. Run the start script: ```shell cd scripts ./cli start ``` -For hot reloading, you can run +For hot reloading, you can run: ```shell cd scripts ./cli clean-start-dev ``` -App will be available at http://localhost:8081 +The app will be available at http://localhost:8081 For more information on development, you can check our docs. @@ -120,7 +120,7 @@ To optimize resource usage during development, we would suggest starting only th This will set up the foundational services required for the project. -2. If you are primarily working on the frontend but also need the API without hot reloading +2. If you are primarily working on the frontend but also need the API without hot reloading: ```shell @@ -136,12 +136,61 @@ Feel free to adjust the commands based on the specific services you need for you To ensure consistency throughout the source code, please keep these rules in mind as you are working: -- All features or bug fixes must be tested by one or more specs (unit-tests). -- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier. +- All features or bug fixes must be tested by one or more specs (unit tests). +- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using Prettier. - In-code documentation is required for every function or class that is not self-evident. - All new API endpoints that are relevant to the public API must have in-code documentation to generate OpenAPI specifications. - The pipeline must pass. +##### Commit Message Guidelines + +This project uses [Conventional Commits](https://www.conventionalcommits.org/) enforced by commitlint. + +**Documentation:** +- [Official Conventional Commits Specification](https://www.conventionalcommits.org/) +- [LFX Internal Conventional Commits Guide](https://linuxfoundation.atlassian.net/wiki/spaces/PROD/pages/759726128/Conventional+Commits) + +**Enforcement:** Commitlint runs automatically via Husky on every commit. Invalid commit messages will be rejected. + +##### Pull Request Guidelines + +**PR Title Requirements:** +- Must follow conventional commit format: `type(scope): description` +- Should include JIRA ticket key in title: `feat: add new feature (CDP-123)` + +**PR Process:** +1. Ensure your commits follow the conventional commit format +2. Include JIRA ticket key in PR title +3. Provide clear description of changes +4. Ensure all tests pass +5. Request review from a member of the team + +##### JIRA Integration + +**Using JIRA MCP Server:** +Leverage the JIRA MCP server for efficient ticket management during development. + +**Documentation:** +- [JIRA MCP Server Guide](https://github.com/linuxfoundation/lfx-engineering/blob/main/mcp/jira.md) + +**Ticket Linking:** +- When possible reference JIRA tickets in commit messages and PR titles +- Use format: `type: conventional commit message (TICKET-KEY)` +- This enables automatic linking between code changes and tickets + +##### AI Development Guidelines + +**Leveraging AI Tools:** +The Linux Foundation provides guidelines and best practices for using AI in development workflows. + +**Documentation:** +- [LFX AI Development Guidelines](https://github.com/linuxfoundation/lfx-engineering/tree/main/ai) + +**Best Practices:** +- Follow LFX guidelines when using AI tools for code generation +- Ensure AI-generated code meets our quality standards +- Review and test all AI-assisted contributions thoroughly + ## Need help? 🛟 diff --git a/LICENSE b/LICENSE index 561469f2a4..3c4dfe081c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,14 +1,3 @@ -Copyright (c) 2022-present Crowd Technologies GmbH - -Parts of this software are licensed as follows: - -* All content that is stored in a "premium/" repository path (Premium Edition), is licensed under the license defined in "PREMIUM LICENSE". -* All third-party components incorporated into the crowd.dev Software are licensed under the original license provided by the owner of the applicable component. -* Content outside of the above-mentioned directories or restrictions above is licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - - -------------------------------------------------------------------------- - Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000000..bae29e0cb0 --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,17 @@ +LFX Community Data Platform is a project by the Linux Foundation. + +## Maintainers + +| Role | Name | GitHub ID | Affiliation | +| -----------------| ------------------ | -------------------------------------------------| ---------------------| +| Maintainer | Jonathan Reimer | [jonathimer](https://github.com/jonathimer) | The Linux Foundation | +| Maintainer | Joana Maia | [joanagmaia](https://github.com/joanagmaia) | The Linux Foundation | +| Maintainer | Uroš Marolt | [themarolt](https://github.com/themarolt) | The Linux Foundation | +| Maintainer | Umberto Sgueglia | [ulemons](https://github.com/ulemons) | The Linux Foundation | +| Maintainer | Yeganathan Kumar | [skwowet](https://github.com/skwowet) | The Linux Foundation | +| Maintainer | Mouad Bani | [mbani01](https://github.com/mbani01) | The Linux Foundation | +| Maintainer | Raúl Santos | [borfast](https://github.com/borfast) | The Linux Foundation | +| Maintainer | Gasper Grom | [gaspergrom](https://github.com/gaspergrom) | The Linux Foundation | +| Maintainer | Efren Lim | [emlimlf](https://github.com/emlimlf) | The Linux Foundation | +| Maintainer | Anıl Bostancı | [epipav](https://github.com/epipav) | The Linux Foundation | +| Maintainer | Nuno Eufrásio | [nunoeufrasio](https://github.com/nunoeufrasio) | The Linux Foundation | \ No newline at end of file diff --git a/PREMIUM LICENSE b/PREMIUM LICENSE deleted file mode 100644 index 1662b25164..0000000000 --- a/PREMIUM LICENSE +++ /dev/null @@ -1,35 +0,0 @@ -The crowd.dev Premium Edition license (the "PE License") - -Copyright (c) 2022-present Crowd Technologies GmbH ("crowd.dev") - -The crowd.dev software and associated documentation files (the "Software") may only be -used in production, if you (and any entity that you represent) (i) have a valid crowd.dev -Premium Edition subscription for the correct number of users, (ii) adhere to the -applicable crowd.dev Subscription Terms of Use, available at -https://www.crowd.dev/terms-of-use (the "PE Terms") and (iii) adhere to any other -agreement or terms applicable to the use of the Software. Only under the conditions as -set out in this section, and the applicable terms and conditions, crowd.dev grants you -the right to modify the Software and publish patches to the Software. You agree that -crowd.dev and/or its licensors (as applicable) retain all right, title and interest in -and to all such modifications and/or patches, and all such modifications and/or patches -may only be used, copied, modified, displayed, distributed, or otherwise exploited with -a valid crowd.dev Premium Edition subscription for the correct number of users. -Notwithstanding the foregoing, you may copy and modify the Software for development and -testing purposes without requiring a crowd.dev Premium Edition subscription. You agree -that crowd.dev and/or its licensors (as applicable) retain all right, title and interest -in and to all such modifications. You are not granted any other rights beyond what is -expressly stated herein. Subject to the foregoing, it is forbidden to copy, merge, -publish, distribute, sublicense, and/or sell the Software. - -The Software is provided "AS IS", without warranty of any kind, express or implied, -including but not limited to the warranties of merchantability, fitness for a -particular purpose and non-infringement. In no event shall the authors or copyright -holders be liable for any claim, damages or other liability, whether in an action of -contract, tort or otherwise, arising from, out of or in connection with the Software -or the use or other dealings in the Software. - -The text of this PE License shall be included in all copies or substantial -parts of the Software. - -All third-party components incorporated into the crowd.dev Software are licensed under -the original license provided by the owner of the applicable component. diff --git a/README.md b/README.md index 799361f64e..8f6cda899b 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,22 @@ - -

- - - crowd.dev icon - - -

Effortlessly centralize community, product, and customer data

- -

-
- 🌐 Cloud version (beta) - · - 📖 Docs - · - ❤️ Discord - · - 📣 Newsletter - · - 🗺️ Roadmap -

-

- -
-UI Home screen +# LFX Community Data Platform (fka crowd.dev) +## Background story +This project was launched as part of the startup crowd.dev. crowd.dev was acquired by the Linux Foundation in April 2024. Following the acquisition, crowd.dev was renamed to "Community Data Platform" and is now part of the [LFX platform](https://lfx.linuxfoundation.org/). +## About this project +LFX Community Data Platform collects and stores data from across the communities in a single database for data unification, identity resolution, analysis, and activation. By utilizing this tool, the Linux Foundation can effectively identify key contributors and organizations, facilitating more efficient community support. -## Table of Contents -- [About crowd.dev](#about-crowddev) -- [Features](#✨-features) -- [Getting started](#🚀-getting-started) -- [Roadmap](#🗺️-roadmap) -- [Stay up-to-date](#🔔-stay-up-to-date) -- [Contribution](#✍️-contribution) -- [License](#⚖️-license) -- [Security](#🔒-security) -- [Book a call](#📞-book-a-call) +Key features: +* It consolidates developers' touchpoints with a company or brand. +* It captures data from community platforms, product channels, and commercial channels. +* The data is cleaned, and profiles are matched across platforms and enriched with third-party data. +* The platform provides a unified 360-degree view of developers' engagement, their companies, and their customer journey. -## About crowd.dev -crowd.dev is the developer data platform (DDP) that lets companies centralize all touch points developers have with their product and brand, be it in community (e.g. Stack Overflow or Reddit), product (open-source or SaaS), or commercial channels (e.g. HubSpot). The platform pulls data from a variety of different sources, normalizes it, matches identities across platforms, and enriches it with 3rd party data. The result is a unified 360-view of who the developers are that engage with your product and community, which companies they work for, and where they stand in their personal customer journey. -crowd.dev is open-source, built with developers in mind, available for both hosted and self-hosted deployments, open to extensions, and offers full control over your data. - -**To our **users**:** -- You can get actively involved, contribute to our roadmap, and turn crowd.dev into the tool you always wanted. -- We are open regarding what we are building, allowing you to take a look inside, and making sure we handle your data in a privacy-preserving way. -- You will never be locked in by us. Our interests as a company are aligned with you and we need to make sure that we always deliver enough value to you with our commercial offering in relation to our pricing. - -**To our developer community:** -- You can self-host crowd.dev to centralize data for your community or company while keeping full control over your data. -- Our product is built for extensibility. If you can think of any use cases that you want to build with the data we collect and store for you, please go ahead and build it! We will be here to help out if you need us. -- You can actively contribute to crowd.dev (e.g. integrations), and we will be supporting you along the journey. Just take a look at our [Contributing guide](https://github.com/CrowdDotDev/crowd.dev/blob/main/CONTRIBUTING.md). - -## ✨ Features - -- **Plug & play integrations** to tie all relevant platforms - like GitHub, Discord, Slack, or LinkedIn - together. ([all integrations](https://www.crowd.dev/integrations)) -- **Identity resolution & automated segmentation** to effortlessly understand activities and profiles across platforms. -- **Opinionated analytics & reports** on topics like product-market-fit and open-source community activity to further inform your GTM strategy. -- **Workflows automation** with webhooks. -- **2-way CRM sync & Slack alerts** to get notified about intent events in real-time. [cloud only] -- **User enrichment** with 25+ attributes, including emails, social profiles, work experience, and technical skills. [cloud only] -- **Organization enrichment** with 50+ attributes, including industry, headcount, and revenue. [cloud only] -- **Sentiment analysis and conversation detection** to stay on top of what's going on in your open source community. [cloud only] -- **[Eagle Eye](https://www.crowd.dev/eagle-eye)**: Monitor dev-focused community platforms to find relevant content to engage with, helping you to gain developers’ mindshare and grow your community organically [cloud only] - - -## 🚀 Getting started - -### Cloud version - -Our cloud version is a fast, easy, and free way to get started with crowd.dev. - -### Self-hosted version +## Getting started +⚠️ This documentation is outdated and needs to be reviewed. To get started with self-hosting, take a look at our [self-hosting docs](https://docs.crowd.dev/docs/getting-started-with-self-hosting). @@ -83,13 +26,13 @@ Our services can be deployed using Kubernetes, as well as a lightweight developm #### Integrations -We currently support all our integrations for self-hosting. For each one of them you will need to create your own application. You can see the steps for each integration in our [self-hosting integrations guide](https://docs.crowd.dev/docs/self-hosting). +We currently support all our integrations for self-hosting. For each one of them, you will need to create your own application. You can see the steps for each integration in our [self-hosting integrations guide](https://docs.crowd.dev/docs/self-hosting). ### Development environment #### Requirements -- [Node](https://nodejs.org/en) v16.16.0 +- [Node](https://nodejs.org/en) v20+ - [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/) #### Getting started @@ -114,44 +57,29 @@ cd scripts ./cli clean-start-dev ``` -App will be available at http://localhost:8081 - -For more information on development, you can check our docs. - -## 🗺️ Roadmap - -You can find more features on our [public roadmap](https://crowd.dev/roadmap). Feel free to also [open an issue](https://crowd.dev/open-an-issue) for anything you're missing. - +For starting services required for insights infra (Tinybird, Sequin and Kafka Connect sink), +you can use the `WITH_INSIGHTS` env variable while running cli commands +``` +WITH_INSIGHTS=1 ./cli scaffold up +``` -## 🔔 Stay up-to-date +This app will be available at http://localhost:8081 -crowd.dev is still in beta and we ship new features every week. To stay in the loop, leave us a star and subscribe to our monthly newsletter. Thanks a lot! ❤️ +For more information on development, you can check our docs. -## ✍️ Contribution +## Contribution There are many ways you can contribute to crowd.dev! Here are a few options: - Star this repo - Create issues every time you feel something is missing or goes wrong -- Upvote issues with 👍 reaction so we know what's the demand for particular issue to prioritize it within roadmap +- Upvote issues with 👍 reaction so we know what's the demand for a particular issue to prioritize it within the roadmap If you would like to contribute to the development of the project, please refer to our [Contributing guide](https://github.com/CrowdDotDev/crowd.dev/blob/main/CONTRIBUTING.md). All contributions are highly appreciated. 🙏 -## ⚖️ License - -Distributed under the Apache 2.0 License. See `LICENSE` for more information. - -Our self-hosted version can be run and deployed by default following the permissive Apache 2.0 license. All premium components will be hidden and inactive with the default configuration. You can run, deploy, and contribute to the app without fearing to violate the premium license. Check out the [premium self-hosted features docs](https://docs.crowd.dev/docs/premium-self-hosted-apps) to know more about the premium self-hosted features. - -## 🔒 Security - -We take security very seriously. If you come across any security vulnerabilities, please disclose them by sending an email to security@crowd.dev. We appreciate your help in making our platform as secure as possible and are committed to working with you to resolve any issues quickly and efficiently. - -## 📞 Book a call - -Call with a crowd.dev team member to learn more about our product and make sure you get the most out of it. +## License -Book us with Cal.com +Distributed under the Apache 2.0 License. diff --git a/backend/.dockerignore b/backend/.dockerignore index 2dc202455d..e367da5e19 100644 --- a/backend/.dockerignore +++ b/backend/.dockerignore @@ -3,6 +3,5 @@ **/venv* **/.webpack **/.serverless -**/.cubestore **/.idea **/.vscode \ No newline at end of file diff --git a/backend/.env.dist.composed b/backend/.env.dist.composed index 8a36964926..0bf0dd50d7 100644 --- a/backend/.env.dist.composed +++ b/backend/.env.dist.composed @@ -1,9 +1,6 @@ -# SQS settings -CROWD_SQS_HOST="sqs" -CROWD_SQS_ENDPOINT=http://sqs:9324 -CROWD_SQS_NODEJS_WORKER_QUEUE="http://sqs:9324/000000000000/nodejs-worker.fifo" -CROWD_SQS_NODEJS_WORKER_DELAYABLE_QUEUE=http://sqs:9324/000000000000/nodejs-worker -CROWD_SQS_PYTHON_WORKER_QUEUE="http://sqs:9324/000000000000/python-worker.fifo" +# Kafka settings +CROWD_KAFKA_BROKERS=kafka:9092 +CROWD_KAFKA_TOPIC=data-sink-worker-normal # Redis settings CROWD_REDIS_HOST=redis @@ -14,12 +11,27 @@ CROWD_S3_HOST="s3" # Db settings CROWD_DB_READ_HOST="db" CROWD_DB_WRITE_HOST="db" +INSIGHTS_DB_WRITE_HOST="db" -# CubeJS settings -CROWD_CUBEJS_URL="http://cubejs:4000/cubejs-api/v1" +# Product DB settings +PRODUCT_DB_HOST=product +PRODUCT_DB_PORT=5432 # Nango settings CROWD_NANGO_URL=http://nango:3003 # OpenSearch settings -CROWD_OPENSEARCH_NODE=http://open-search:9200 \ No newline at end of file +CROWD_OPENSEARCH_NODE=http://open-search:9200 + +# Temporal +CROWD_TEMPORAL_SERVER_URL=temporal:7233 + +# Seach sync api +CROWD_SEARCH_SYNC_API_URL=http://search-sync-api:8083 +# packages DB (osspckgs) +CROWD_PACKAGES_DB_READ_HOST=packages +CROWD_PACKAGES_DB_WRITE_HOST=packages +CROWD_PACKAGES_DB_PORT=5432 +CROWD_PACKAGES_DB_USERNAME=postgres +CROWD_PACKAGES_DB_PASSWORD=example +CROWD_PACKAGES_DB_DATABASE=packages-db diff --git a/backend/.env.dist.local b/backend/.env.dist.local index aff5e593d0..18c65f92ef 100755 --- a/backend/.env.dist.local +++ b/backend/.env.dist.local @@ -1,7 +1,8 @@ # Global settings KUBE_MODE=1 -CROWD_EDITION=community +CROWD_EDITION=lfx-ee TENANT_MODE=multi +QUEUE_PRIORITY_LEVEL=normal # API settings CROWD_API_URL=https://localhost/api @@ -10,17 +11,8 @@ CROWD_API_FRONTEND_URL_WITH_SUBDOMAINS= CROWD_API_JWT_SECRET=your-secret CROWD_API_JWT_EXPIRES_IN='100 years' -# SQS settings -CROWD_SQS_HOST=localhost -CROWD_SQS_PORT=9324 -CROWD_SQS_ENDPOINT=http://localhost:9324 -CROWD_SQS_NODEJS_WORKER_QUEUE=http://localhost:9324/000000000000/nodejs-worker.fifo -CROWD_SQS_NODEJS_WORKER_DELAYABLE_QUEUE=http://localhost:9324/000000000000/nodejs-worker -CROWD_SQS_PYTHON_WORKER_QUEUE=http://localhost:9324/000000000000/python-worker.fifo -CROWD_SQS_AWS_ACCOUNT_ID=000000000000 -CROWD_SQS_AWS_ACCESS_KEY_ID=x -CROWD_SQS_AWS_SECRET_ACCESS_KEY=x -CROWD_SQS_AWS_REGION=elasticmq +# Kafka settings +CROWD_KAFKA_BROKERS=localhost:9093 # Redis settings CROWD_REDIS_USERNAME=default @@ -30,7 +22,7 @@ CROWD_REDIS_PORT=6379 # S3 settings CROWD_S3_HOST=localhost -CROWD_S3_PORT=9000 +CROWD_S3_PORT=9100 CROWD_S3_INTEGRATION_ASSETS_BUCKET=crowd-integrations-assets CROWD_S3_MICROSERVICES_ASSETS_BUCKET=crowd-microservices-assets CROWD_S3_AWS_ACCOUNT_ID=000000000000 @@ -46,14 +38,24 @@ CROWD_DB_USERNAME=postgres CROWD_DB_PASSWORD=example CROWD_DB_DATABASE=crowd-web +INSIGHTS_DB_WRITE_HOST=localhost +INSIGHTS_DB_USERNAME=postgres +INSIGHTS_DB_PASSWORD=example +INSIGHTS_DB_DATABASE=insights +INSIGHTS_DB_PORT=5432 +INSIGHTS_DB_POOL_MAX=10 +INSIGHTS_DB_SSLMODE=disable + +# Product DB settings +PRODUCT_DB_HOST=localhost +PRODUCT_DB_PORT=5433 +PRODUCT_DB_USERNAME=postgres +PRODUCT_DB_PASSWORD=example +PRODUCT_DB_DATABASE=product-db + # OpenSearch settings CROWD_OPENSEARCH_NODE=http://localhost:9200 -# CubeJS settings -CROWD_CUBEJS_URL=http://localhost:4000/cubejs-api/v1 -CROWD_CUBEJS_JWT_SECRET=137ea167812145c6d77452a58d7dd29b -CROWD_CUBEJS_JWT_EXPIRY=2h - # AWS Comprehend settings CROWD_COMPREHEND_AWS_ACCOUNT_ID= CROWD_COMPREHEND_AWS_ACCESS_KEY_ID= @@ -71,23 +73,10 @@ CROWD_NETLIFY_SITE_DOMAIN=localhost:3000 # Sendgrid settings CROWD_SENDGRID_KEY= -CROWD_SENDGRID_WEBHOOK_SIGNING_SECRET= CROWD_SENDGRID_EMAIL_FROM= CROWD_SENDGRID_NAME_FROM= -CROWD_SENDGRID_TEMPLATE_EMAIL_ADDRESS_VERIFICATION= -CROWD_SENDGRID_TEMPLATE_INVITATION= -CROWD_SENDGRID_TEMPLATE_PASSWORD_RESET= -CROWD_SENDGRID_TEMPLATE_WEEKLY_ANALYTICS= -CROWD_SENDGRID_TEMPLATE_INTEGRATION_DONE= -CROWD_SENDGRID_WEEKLY_ANALYTICS_UNSUBSCRIBE_GROUP_ID= - -# Stripe settings -CROWD_STRIPE_PRICE_PREMIUM= -CROWD_STRIPE_PRICE_ENTERPRISE= -CROWD_STRIPE_SECRET_KEY= -CROWD_STRIPE_WEBHOOK_SIGNING_SECRET= -CROWD_STRIPE_EAGLE_EYE_PLAN_PRODUCT_ID= -CROWD_STRIPE_GROWTH_PLAN_PRODUCT_ID= +CROWD_SENDGRID_TEMPLATE_EAGLE_EYE_DIGEST= +CROWD_SENDGRID_TEMPLATE_CSV_EXPORT= # Twitter settings CROWD_TWITTER_CLIENT_ID= @@ -128,14 +117,16 @@ CROWD_STACKEXCHANGE_KEY= # Nango settings CROWD_NANGO_URL=http://localhost:3003 CROWD_NANGO_SECRET_KEY=424242 -CROWD_NANGO_INTEGRATIONS=reddit,linkedin,stackexchange,hubspot +CROWD_NANGO_INTEGRATIONS=reddit,linkedin,stackexchange # Cohere settings CROWD_COHERE_API_KEY= # Enrichment settings -CROWD_ENRICHMENT_URL= -CROWD_ENRICHMENT_API_KEY= +CROWD_ENRICHMENT_PROGAI_URL= +CROWD_ENRICHMENT_PROGAI_API_KEY= +CROWD_ENRICHMENT_CLEARBIT_URL= +CROWD_ENRICHMENT_CLEARBIT_API_KEY= # PDL Organization Enrichment settings CROWD_ORGANIZATION_ENRICHMENT_API_KEY= @@ -144,20 +135,6 @@ CROWD_ORGANIZATION_ENRICHMENT_API_KEY= CROWD_EAGLE_EYE_URL= CROWD_EAGLE_EYE_API_KEY= -# Slack alerting settings -CROWD_SLACK_ALERTING_URL= - -# Unleash settings -CROWD_UNLEASH_URL= -CROWD_UNLEASH_ADMIN_API_KEY= -CROWD_UNLEASH_FRONTEND_API_KEY= -CROWD_UNLEASH_BACKEND_API_KEY= -CROWD_UNLEASH_DB_HOST= -CROWD_UNLEASH_DB_PORT= -CROWD_UNLEASH_DB_USERNAME= -CROWD_UNLEASH_DB_PASSWORD= -CROWD_UNLEASH_DB_DATABASE= - # Weekly emails settings CROWD_WEEKLY_EMAILS_ENABLED="true" @@ -166,3 +143,77 @@ CROWD_ANALYTICS_IS_ENABLED= CROWD_ANALYTICS_TENANT_ID= CROWD_ANALYTICS_BASE_URL= CROWD_ANALYTICS_API_TOKEN= + +# Temporal +CROWD_TEMPORAL_SERVER_URL=localhost:7233 +CROWD_TEMPORAL_NAMESPACE=default +CROWD_TEMPORAL_ENCRYPTION_KEY_ID=local +CROWD_TEMPORAL_ENCRYPTION_KEY=FweBMRnGCLshER8FlSvNusQA6G3MRUKt + +# Temporal — packages namespace +CROWD_PACKAGES_TEMPORAL_NAMESPACE=default + +# Seach sync api +CROWD_SEARCH_SYNC_API_URL=http://localhost:8083 + +CROWD_MV_OTHERS_REFRESH_PERIOD='* * * * *' + +# Loki +CROWD_LOKI_DB_TURSO_URL=dummy +CROWD_LOKI_DB_TURSO_TOKEN=dummy + +CROWD_GITHUB_IS_SNOWFLAKE_ENABLED=false + +# Tinybird +CROWD_TINYBIRD_BASE_URL=http://localhost:7181/ + +# Auth0 +CROWD_AUTH0_ISSUER_BASE_URLS= +CROWD_AUTH0_AUDIENCE= + +# packages DB (osspckgs) +CROWD_PACKAGES_DB_READ_HOST=localhost +CROWD_PACKAGES_DB_WRITE_HOST=localhost +CROWD_PACKAGES_DB_PORT=5434 +CROWD_PACKAGES_DB_USERNAME=postgres +CROWD_PACKAGES_DB_PASSWORD=example +CROWD_PACKAGES_DB_DATABASE=packages-db + +# github-repos-enricher +ENRICHER_GITHUB_TOKENS= +ENRICHER_BATCH_SIZE=100 +ENRICHER_REPO_UPDATE_INTERVAL_HOURS=24 +ENRICHER_IDLE_SLEEP_SEC=60 + +OSSPCKGS_GCP_PROJECT=local-dev +OSSPCKGS_GCS_BUCKET=local-dev +OSSPCKGS_GCP_CREDENTIALS_B64=e30= + +# osv-sync (Temporal-scheduled; see services/apps/packages_worker/src/osv/schedule.ts) +# OSV_ECOSYSTEMS uses OSV's canonical bucket case (npm lowercase, Maven titlecase) because +# the bucket URL //all.zip is case-sensitive (Maven/all.zip exists, +# maven/all.zip 404s). The allowlist check and DB storage normalize to lowercase +# internally per ADR-0001 §OSV "Ecosystem normalization", so downstream stays lowercase. +OSV_BULK_BASE_URL=https://osv-vulnerabilities.storage.googleapis.com +OSV_ECOSYSTEMS=npm,Maven,cargo +OSV_TMP_DIR=/tmp/osv +OSV_BATCH_SIZE=500 +OSV_DERIVE_BATCH_SIZE=1000 + +# maven enricher + +MAVEN_FETCHER_BATCH_SIZE=2000 +MAVEN_FETCHER_CONCURRENCY=10 +MAVEN_FETCHER_NON_CRITICAL_BATCH_SIZE=500 +MAVEN_FETCHER_NON_CRITICAL_CONCURRENCY=20 +MAVEN_FETCHER_REFRESH_DAYS=1 +MAVEN_FETCHER_GROUP_DELAY_MS=100 +MAVEN_FETCHER_BASE_URL_BACKFILL=https://maven-central.storage-download.googleapis.com/maven2 +MAVEN_FETCHER_BASE_URL_INCREMENTAL=https://repo1.maven.org/maven2 + +# dockerhub-sync (see services/apps/packages_worker/src/dockerhub/) +DOCKERHUB_API_BASE_URL=https://hub.docker.com/v2 +DOCKERHUB_BATCH_SIZE=100 +DOCKERHUB_REFRESH_INTERVAL_HOURS=24 +DOCKERHUB_DISCOVERY_INTERVAL_DAYS=14 +DOCKERHUB_IDLE_SLEEP_SEC=60 diff --git a/backend/.env.test b/backend/.env.test deleted file mode 100755 index f477793a60..0000000000 --- a/backend/.env.test +++ /dev/null @@ -1,8 +0,0 @@ -# DB settings -CROWD_DB_PORT=5433 - -# Redis settings -CROWD_REDIS_PORT=6380 - -# Sqs settings -CROWD_SQS_PORT=9325 \ No newline at end of file diff --git a/backend/.eslintrc.js b/backend/.eslintrc.js index 9a38b602a7..1b3253a7b7 100644 --- a/backend/.eslintrc.js +++ b/backend/.eslintrc.js @@ -3,7 +3,7 @@ module.exports = { node: true, es2021: true, }, - extends: ['airbnb-base', 'prettier', 'plugin:openapi/recommended'], + extends: ['airbnb-base', 'prettier'], parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint'], ignorePatterns: ['dist/*', '**/*.test.ts'], @@ -12,6 +12,10 @@ module.exports = { 'prefer-destructuring': ['error', { object: false, array: false }], 'no-param-reassign': 0, 'no-underscore-dangle': 0, + 'no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_' }, + ], '@typescript-eslint/naming-convention': [ 'error', { @@ -46,6 +50,14 @@ module.exports = { 'prefer-destructuring': ['error', { object: false, array: false }], 'no-param-reassign': 0, 'no-underscore-dangle': 0, + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + }, + ], '@typescript-eslint/naming-convention': [ 'error', { diff --git a/backend/.gitignore b/backend/.gitignore index fd67665434..e53aa3843f 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -5,8 +5,10 @@ dist/ **/.env* !.env.test !.env.dist* -**/.cubestore **/local-events **/local_events -**/try.ts \ No newline at end of file +**/try.ts + +*.tmp + diff --git a/backend/.openapirc.js b/backend/.openapirc.js deleted file mode 100644 index 3f9aead4ac..0000000000 --- a/backend/.openapirc.js +++ /dev/null @@ -1,24 +0,0 @@ -module.exports = { - extension: ['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx', '.yaml', '.yml'], - include: ['**'], - exclude: [ - 'coverage/**', - 'packages/*/test{,s}/**', - '**/docker/**', - '**/serverless/**', - '**/*.d.ts', - 'test{,s}/**', - 'test{,-*}.{js,cjs,mjs,ts,tsx,jsx,yaml,yml}', - '**/*{.,-}test.{js,cjs,mjs,ts,tsx,jsx,yaml,yml}', - '**/__tests__/**', - '**/{ava,babel,nyc}.config.{js,cjs,mjs}', - '**/jest.config.{js,cjs,mjs,ts}', - '**/{karma,rollup,webpack}.config.js', - '**/.{eslint,mocha}rc.{js,cjs}', - '**/.{travis,yarnrc}.yml', - '**/{docker-compose}.yml', - ], - excludeNodeModules: true, - verbose: true, - throwLevel: 'off', -} diff --git a/backend/.prettierignore b/backend/.prettierignore index 24d2ee90a3..9f7de4795e 100644 --- a/backend/.prettierignore +++ b/backend/.prettierignore @@ -2,4 +2,5 @@ dist node_modules admin venv-* -.serverless \ No newline at end of file +.serverless +.claude \ No newline at end of file diff --git a/backend/.prettierrc b/backend/.prettierrc deleted file mode 100644 index 529f793325..0000000000 --- a/backend/.prettierrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "singleQuote": true, - "arrowParens": "always", - "printWidth": 100, - "trailingComma": "all", - "semi": false -} diff --git a/backend/.prettierrc.cjs b/backend/.prettierrc.cjs new file mode 100644 index 0000000000..ec0d179d4e --- /dev/null +++ b/backend/.prettierrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + singleQuote: true, + arrowParens: 'always', + printWidth: 100, + trailingComma: 'all', + semi: false, + importOrder: [ + '^(?!(\\.|@crowd/|@/|\\.\\./))(.*)$', // 3rd-party imports + '^@crowd/(.*)$', // crowd packages + '^@/(.*)$', // local package absolute imports that start with @/ + '^\\.\\./', // relative imports that start with ../ + '^\\./', // same directory imports that start with ./ + '^\\.$', // current directory + ], + importOrderSeparation: true, + importOrderSortSpecifiers: true, + plugins: ['@trivago/prettier-plugin-sort-imports'], +} diff --git a/backend/Makefile b/backend/Makefile deleted file mode 100644 index 6ef791039e..0000000000 --- a/backend/Makefile +++ /dev/null @@ -1,57 +0,0 @@ -SSH=crowd-aws --call-in=public -RSYNC=crowd-aws --send-to-public - -build: - npm run build - -set-environment-staging: - npm run build:setenv:staging - -set-environment-prod: - npm run build:setenv:prod - -send-deploy: - $(RSYNC) util/deploy.sh deploy/ - -pull: - git pull - - -deploy-all: deploy deploy-serverless - -deploy-all-prod: deploy-prod deploy-serverless-prod - -deploy-serverless: - cd src/serverless/integrations && npm run sls-deploy - cd src/serverless/dbOperations && npm run sls-deploy - cd src/serverless/microservices/nodejs && npm run sls-deploy - cd src/serverless/microservices/python/serverless && npm run sls-deploy - -deploy-serverless-prod: - cd src/serverless/integrations && npm run sls-deploy-prod - cd src/serverless/dbOperations && npm run sls-deploy-prod - cd src/serverless/microservices/nodejs && npm run sls-deploy-prod - cd src/serverless/microservices/python/serverless && npm run sls-deploy-prod - -deploy: pull send-deploy build set-environment-staging - echo "Deploying staging" - $(RSYNC) dist/ deploy/pre-dist/ - $(SSH) "cd deploy ; ./deploy.sh" - -deploy-prod: pull send-deploy build set-environment-prod - echo "Deploying production" - $(RSYNC) dist/ deploy/pre-dist-prod/ - $(SSH) "cd deploy ; ./deploy.sh -prod" - -remote-install: - $(RSYNC) server-config/ conf/ - $(SSH) "cd conf/nginx ; sudo ./setup.sh" - $(SSH) "pm2 reload conf/pm2.config.js" - -pm2-status: - $(SSH) "pm2 list" - -nginx-status: - $(SSH) "systemctl status nginx" - -status: pm2-status nginx-status diff --git a/backend/babel.config.json b/backend/babel.config.json deleted file mode 100644 index b05fea5024..0000000000 --- a/backend/babel.config.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "presets": [ - [ - "@babel/preset-env", - { - "targets": { - "node": "current" - } - } - ], - "@babel/preset-typescript" - ] -} diff --git a/backend/base.yaml b/backend/base.yaml deleted file mode 100644 index bcc964aa72..0000000000 --- a/backend/base.yaml +++ /dev/null @@ -1,33 +0,0 @@ -openapi: 3.0.3 -info: - title: crowd.dev API - version: 1.0.5 - description: > - crowd.dev API - contact: - email: joan@crowd.dev - license: - name: Apache 2.0 - url: http://www.apache.org/licenses/LICENSE-2.0.html - x-github: https://github.com/crowdHQ - -servers: - - url: https://app.crowd.dev/api - -tags: - - name: Members - description: Everything about members - - name: Member Attributes - description: Settings for member's attributes - - name: Activities - description: Everything about activities - - name: Organizations - description: Everything about organizations - - name: Conversations - description: Everything about conversations - - name: Tags - description: Everything about tags - - name: Automations - description: Everything about automations - - name: Notes - description: Everything about notes diff --git a/backend/config/custom-environment-variables.json b/backend/config/custom-environment-variables.json index 7ec18c3020..4d1d01b6f5 100644 --- a/backend/config/custom-environment-variables.json +++ b/backend/config/custom-environment-variables.json @@ -13,19 +13,9 @@ "host": "CROWD_REDIS_HOST", "port": "CROWD_REDIS_PORT" }, - "sqs": { - "host": "CROWD_SQS_HOST", - "port": "CROWD_SQS_PORT", - "nodejsWorkerQueue": "CROWD_SQS_NODEJS_WORKER_QUEUE", - "nodejsWorkerDelayableQueue": "CROWD_SQS_NODEJS_WORKER_DELAYABLE_QUEUE", - "integrationRunWorkerQueue": "CROWD_SQS_INTEGRATION_RUN_WORKER_QUEUE", - "pythonWorkerQueue": "CROWD_SQS_PYTHON_WORKER_QUEUE", - "aws": { - "accountId": "CROWD_SQS_AWS_ACCOUNT_ID", - "accessKeyId": "CROWD_SQS_AWS_ACCESS_KEY_ID", - "secretAccessKey": "CROWD_SQS_AWS_SECRET_ACCESS_KEY", - "region": "CROWD_SQS_AWS_REGION" - } + "queue": { + "brokers": "CROWD_KAFKA_BROKERS", + "extra": "CROWD_KAFKA_EXTRA" }, "s3": { "host": "CROWD_S3_HOST", @@ -33,7 +23,7 @@ "integrationsAssetsBucket": "CROWD_S3_INTEGRATION_ASSETS_BUCKET", "microservicesAssetsBucket": "CROWD_S3_MICROSERVICES_ASSETS_BUCKET", "aws": { - "accountId": "CROWD_S3_AWS_ACCOUNT_ID", + "endpoint": "CROWD_S3_ENDPOINT", "accessKeyId": "CROWD_S3_AWS_ACCESS_KEY_ID", "secretAccessKey": "CROWD_S3_AWS_SECRET_ACCESS_KEY", "region": "CROWD_S3_AWS_REGION" @@ -47,17 +37,24 @@ "password": "CROWD_DB_PASSWORD", "apiUsername": "CROWD_DB_API_USERNAME", "apiPassword": "CROWD_DB_API_PASSWORD", - "nodejsWorkerUsername": "CROWD_DB_NODEJS_WORKER_USERNAME", - "nodejsWorkerPassword": "CROWD_DB_NODEJS_WORKER_PASSWORD", "jobGeneratorUsername": "CROWD_DB_JOB_GENERATOR_USERNAME", "jobGeneratorPassword": "CROWD_DB_JOB_GENERATOR_PASSWORD", "database": "CROWD_DB_DATABASE", "logging": "CROWD_DB_LOGGING" }, - "cubejs": { - "url": "CROWD_CUBEJS_URL", - "jwtSecret": "CROWD_CUBEJS_JWT_SECRET", - "jwtExpiry": "CROWD_CUBEJS_JWT_EXPIRY" + "productDb": { + "host": "PRODUCT_DB_HOST", + "port": "PRODUCT_DB_PORT", + "user": "PRODUCT_DB_USERNAME", + "password": "PRODUCT_DB_PASSWORD", + "database": "PRODUCT_DB_DATABASE" + }, + "packagesDb": { + "host": "CROWD_PACKAGES_DB_WRITE_HOST", + "port": "CROWD_PACKAGES_DB_PORT", + "user": "CROWD_PACKAGES_DB_USERNAME", + "password": "CROWD_PACKAGES_DB_PASSWORD", + "database": "CROWD_PACKAGES_DB_DATABASE" }, "segment": { "writeKey": "CROWD_SEGMENT_WRITE_KEY" @@ -73,31 +70,12 @@ "clearbit": { "apiKey": "CROWD_CLEARBIT_API_KEY" }, - "netlify": { - "apiKey": "CROWD_NETLIFY_API_KEY", - "siteDomain": "CROWD_NETLIFY_SITE_DOMAIN" - }, "sendgrid": { "key": "CROWD_SENDGRID_KEY", - "webhookSigningSecret": "CROWD_SENDGRID_WEBHOOK_SIGNING_SECRET", "emailFrom": "CROWD_SENDGRID_EMAIL_FROM", "nameFrom": "CROWD_SENDGRID_NAME_FROM", - "templateEmailAddressVerification": "CROWD_SENDGRID_TEMPLATE_EMAIL_ADDRESS_VERIFICATION", - "templateInvitation": "CROWD_SENDGRID_TEMPLATE_INVITATION", - "templatePasswordReset": "CROWD_SENDGRID_TEMPLATE_PASSWORD_RESET", - "templateWeeklyAnalytics": "CROWD_SENDGRID_TEMPLATE_WEEKLY_ANALYTICS", - "templateIntegrationDone": "CROWD_SENDGRID_TEMPLATE_INTEGRATION_DONE", "templateCsvExport": "CROWD_SENDGRID_TEMPLATE_CSV_EXPORT", - "templateEagleEyeDigest": "CROWD_SENDGRID_TEMPLATE_EAGLE_EYE_DIGEST", - "weeklyAnalyticsUnsubscribeGroupId": "CROWD_SENDGRID_WEEKLY_ANALYTICS_UNSUBSCRIBE_GROUP_ID" - }, - "plans": { - "stripePricePremium": "CROWD_STRIPE_PRICE_PREMIUM", - "stripePriceEnterprise": "CROWD_STRIPE_PRICE_ENTERPRISE", - "stripeSecretKey": "CROWD_STRIPE_SECRET_KEY", - "stripWebhookSigningSecret": "CROWD_STRIPE_WEBHOOK_SIGNING_SECRET", - "stripeEagleEyePlanProductId": "CROWD_STRIPE_EAGLE_EYE_PLAN_PRODUCT_ID", - "stripeGrowthPlanProductId": "CROWD_STRIPE_GROWTH_PLAN_PRODUCT_ID" + "templateEagleEyeDigest": "CROWD_SENDGRID_TEMPLATE_EAGLE_EYE_DIGEST" }, "twitter": { "clientId": "CROWD_TWITTER_CLIENT_ID", @@ -113,10 +91,6 @@ "appId": "CROWD_SLACK_APP_ID", "appToken": "CROWD_SLACK_APP_TOKEN" }, - "slackNotifier": { - "clientId": "CROWD_SLACK_NOTIFIER_CLIENT_ID", - "clientSecret": "CROWD_SLACK_NOTIFIER_CLIENT_SECRET" - }, "google": { "clientId": "CROWD_GOOGLE_CLIENT_ID", "clientSecret": "CROWD_GOOGLE_CLIENT_SECRET", @@ -133,23 +107,36 @@ "callbackUrl": "CROWD_GITHUB_CALLBACK_URL", "privateKey": "CROWD_GITHUB_PRIVATE_KEY", "webhookSecret": "CROWD_GITHUB_WEBHOOK_SECRET", - "isCommitDataEnabled": "CROWD_GITHUB_IS_COMMIT_DATA_ENABLED" + "isCommitDataEnabled": "CROWD_GITHUB_IS_COMMIT_DATA_ENABLED", + "isSnowflakeEnabled": "CROWD_GITHUB_IS_SNOWFLAKE_ENABLED" + }, + "githubIssueReporter": { + "appId": "CROWD_GITHUB_ISSUE_REPORTER_APP_ID", + "privateKey": "CROWD_GITHUB_ISSUE_REPORTER_PRIVATE_KEY", + "installationId": "CROWD_GITHUB_ISSUE_REPORTER_INSTALLATION_ID" + }, + "jiraIssueReporter": { + "apiUrl": "CROWD_JIRA_ISSUE_REPORTER_API_URL", + "apiTokenEmail": "CROWD_JIRA_ISSUE_REPORTER_API_TOKEN_EMAIL", + "token": "CROWD_JIRA_ISSUE_REPORTER_API_TOKEN", + "projectKey": "CROWD_JIRA_ISSUE_REPORTER_PROJECT_KEY" }, "stackexchange": { "key": "CROWD_STACKEXCHANGE_KEY" }, - "hubspot": { - "appId": "CROWD_HUBSPOT_APP_ID", - "clientId": "CROWD_HUBSPOT_CLIENT_ID", - "clientSecret": "CROWD_HUBSPOT_CLIENT_SECRET" + "reddit": { + "clientId": "CROWD_REDDIT_CLIENT_ID", + "clientSecret": "CROWD_REDDIT_CLIENT_SECRET" }, "nango": { "url": "CROWD_NANGO_URL", - "secretKey": "CROWD_NANGO_SECRET_KEY" + "secretKey": "CROWD_NANGO_SECRET_KEY", + "cloudSecretKey": "NANGO_CLOUD_SECRET_KEY", + "cloudIntegrations": "NANGO_CLOUD_INTEGRATIONS" }, "enrichment": { - "url": "CROWD_ENRICHMENT_URL", - "apiKey": "CROWD_ENRICHMENT_API_KEY" + "url": "CROWD_ENRICHMENT_PROGAI_URL", + "apiKey": "CROWD_ENRICHMENT_PROGAI_API_KEY" }, "organizationEnrichment": { "apiKey": "CROWD_ORGANIZATION_ENRICHMENT_API_KEY" @@ -158,42 +145,63 @@ "url": "CROWD_EAGLE_EYE_URL", "apiKey": "CROWD_EAGLE_EYE_API_KEY" }, - "slackAlerting": { - "url": "CROWD_SLACK_ALERTING_URL" - }, - "sampleData": { - "tenantId": "CROWD_SAMPLE_DATA_TENANT_ID" - }, - "unleash": { - "url": "CROWD_UNLEASH_URL", - "adminApiKey": "CROWD_UNLEASH_ADMIN_API_KEY", - "frontendApiKey": "CROWD_UNLEASH_FRONTEND_API_KEY", - "backendApiKey": "CROWD_UNLEASH_BACKEND_API_KEY", - "db": { - "host": "CROWD_UNLEASH_DB_HOST", - "port": "CROWD_UNLEASH_DB_PORT", - "username": "CROWD_UNLEASH_DB_USERNAME", - "password": "CROWD_UNLEASH_DB_PASSWORD", - "database": "CROWD_UNLEASH_DB_DATABASE" - } + "githubToken": { + "clientId": "GITHUB_TOKEN_CLIENT_ID", + "installationId": "GITHUB_TOKEN_INSTALLATION_ID", + "privateKey": "GITHUB_TOKEN_PRIVATE_KEY" }, "weeklyEmails": { "enabled": "CROWD_WEEKLY_EMAILS_ENABLED" }, "opensearch": { "node": "CROWD_OPENSEARCH_NODE", - "region": "CROWD_OPENSEARCH_AWS_REGION", - "accessKeyId": "CROWD_OPENSEARCH_AWS_ACCESS_KEY_ID", - "secretAccessKey": "CROWD_OPENSEARCH_AWS_SECRET_ACCESS_KEY" + "username": "CROWD_OPENSEARCH_USERNAME", + "password": "CROWD_OPENSEARCH_PASSWORD" }, "auth0": { "clientId": "CROWD_AUTH0_CLIENT_ID", - "jwks": "CROWD_AUTH0_JWKS" + "jwks": "CROWD_AUTH0_JWKS", + "issuerBaseURLs": "CROWD_AUTH0_ISSUER_BASE_URLS", + "audience": "CROWD_AUTH0_AUDIENCE" + }, + "sso": { + "crowdTenantId": "CROWD_SSO_CROWD_TENANT_ID", + "lfTenantId": "CROWD_SSO_LF_TENANT_ID" }, "crowdAnalytics": { "isEnabled": "CROWD_ANALYTICS_IS_ENABLED", "tenantId": "CROWD_ANALYTICS_TENANT_ID", "baseUrl": "CROWD_ANALYTICS_BASE_URL", "apiToken": "CROWD_ANALYTICS_API_TOKEN" + }, + "temporal": { + "serverUrl": "CROWD_TEMPORAL_SERVER_URL", + "namespace": "CROWD_TEMPORAL_NAMESPACE", + "certificate": "CROWD_TEMPORAL_CERTIFICATE", + "privateKey": "CROWD_TEMPORAL_PRIVATE_KEY" + }, + "searchSyncApi": { + "baseUrl": "CROWD_SEARCH_SYNC_API_URL" + }, + "encryption": { + "secretKey": "CROWD_ENCRYPTION_SECRET_KEY", + "initVector": "CROWD_ENCRYPTION_INIT_VECTOR" + }, + "openStatusApi": { + "baseUrl": "CROWD_OPENSTATUS_URL" + }, + "gitlab": { + "clientId": "CROWD_GITLAB_CLIENT_ID", + "clientSecret": "CROWD_GITLAB_CLIENT_SECRET", + "callbackUrl": "CROWD_GITLAB_CALLBACK_URL", + "webhookToken": "CROWD_GITLAB_WEBHOOK_TOKEN" + }, + "snowflake": { + "privateKey": "CROWD_SNOWFLAKE_PRIVATE_KEY", + "account": "CROWD_SNOWFLAKE_ACCOUNT", + "username": "CROWD_SNOWFLAKE_USERNAME", + "database": "CROWD_SNOWFLAKE_DATABASE", + "warehouse": "CROWD_SNOWFLAKE_WAREHOUSE", + "role": "CROWD_SNOWFLAKE_ROLE" } } diff --git a/backend/config/default.json b/backend/config/default.json index c930bef9f9..e60af044e2 100644 --- a/backend/config/default.json +++ b/backend/config/default.json @@ -6,24 +6,19 @@ "integrationProcessing": { "maxRetries": 5 }, - "sqs": {}, "s3": {}, "db": { "dialect": "postgres", "logging": false, "transactions": false }, - "cubejs": {}, "searchEngine": {}, "segment": {}, "comprehend": { "aws": {} }, "clearbit": {}, - "netlify": {}, "sendgrid": {}, - "plans": {}, - "devto": {}, "twitter": { "maxRetrospectInSeconds": 7380, "limitResetFrequencyDays": 30 @@ -31,30 +26,29 @@ "slack": { "maxRetrospectInSeconds": 3600 }, - "slackNotifier": {}, "google": {}, "discord": { "maxRetrospectInSeconds": 3600 }, "github": {}, "stackexchange": {}, + "reddit": {}, "enrichment": {}, "organizationEnrichment": {}, "eagleEye": {}, - "unleash": { - "db": {} - }, - "slackAlerting": { - "url": "" - }, - "sampleData": { - "tenantId": "" - }, - "weeklyEmails": { - "enabled": "true" - }, + "githubToken": {}, "auth0": {}, + "sso": {}, "crowdAnalytics": { "isEnabled": "false" - } + }, + "temporal": {}, + "searchSyncApi": {}, + "encryption": {}, + "openStatusApi": {}, + "gitlab": {}, + "jiraIssueReporter": {}, + "snowflake": {}, + "nango": {}, + "linuxFoundation": {} } diff --git a/backend/config/production.json b/backend/config/production.json index d289ecc853..13d9868d02 100644 --- a/backend/config/production.json +++ b/backend/config/production.json @@ -8,5 +8,8 @@ }, "slack": { "maxRetrospectInSeconds": 86400 + }, + "linuxFoundation": { + "collectionId": "1606ad11-c96d-4177-8147-8f990b76b35d" } } diff --git a/backend/config/staging.json b/backend/config/staging.json index 0967ef424b..74e7a01f7a 100644 --- a/backend/config/staging.json +++ b/backend/config/staging.json @@ -1 +1,5 @@ -{} +{ + "linuxFoundation": { + "collectionId": "5ffc867e-067a-4018-82ca-dbbade2c95f3" + } +} diff --git a/backend/config/test.json b/backend/config/test.json deleted file mode 100644 index 8a2bf7cd25..0000000000 --- a/backend/config/test.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "redis": {}, - "nango": {} -} diff --git a/backend/docker-compose.test.yaml b/backend/docker-compose.test.yaml deleted file mode 100644 index c4e2587531..0000000000 --- a/backend/docker-compose.test.yaml +++ /dev/null @@ -1,47 +0,0 @@ -version: '3.1' - -services: - db-test: - image: postgres:13.6-alpine - environment: - POSTGRES_PASSWORD: example - POSTGRES_DB: crowd-web - ports: - - 5433:5432 - networks: - - crowd-bridge-test - - sqs: - build: - context: ../scripts/scaffold/sqs - ports: - - 9325:9324 - - 9326:9325 - networks: - - crowd-bridge-test - - open-search-test: - image: opensearchproject/opensearch:2.7.0 - environment: - - discovery.type=single-node - - bootstrap.memory_lock=true - ulimits: - memlock: - soft: -1 - hard: -1 - ports: - - 9201:9200 - - 9601:9600 - networks: - - crowd-bridge-test - - redis-test: - image: redis - ports: - - 6380:6379 - networks: - - crowd-bridge-test - -networks: - crowd-bridge-test: - external: true diff --git a/backend/id-openapi.yaml b/backend/id-openapi.yaml new file mode 100644 index 0000000000..7ad781c8e4 --- /dev/null +++ b/backend/id-openapi.yaml @@ -0,0 +1,403 @@ +openapi: 3.0.0 +info: + title: CM API Docs + version: 1.0.0 + description: API endpoints for LFX Community Data Platform application + +servers: + - url: https://cm.lfx.dev/api/v1 + description: LFX CM Production + +tags: + - name: Member Organizations API + description: API endpoints for managing work history organizations, including creating, reading, updating, and deleting organization relationships for profiles. + - name: Member Affiliations API + description: API endpoints for managing project affiliations, including listing and bulk updating affiliation relationships within a profile. + +security: + - BearerAuth: [] + +paths: + /member/{memberId}/organization: + get: + security: + - BearerAuth: [] + tags: + - Member Organizations API + summary: List Work History Organizations + description: Retrieve a list of organizations for a specific profile + parameters: + - name: memberId + in: path + required: true + schema: + type: string + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Organization' + '400': + description: Bad request + '404': + description: Member not found + + post: + security: + - BearerAuth: [] + tags: + - Member Organizations API + summary: Create Work History Organization + description: Create a new organization for a specific profile + parameters: + - name: memberId + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - organizationId + - source + properties: + organizationId: + type: string + example: '550e8400-e29b-41d4-a716-446655440000' + description: Organization ID + source: + type: string + enum: ['ui', 'email-domain', 'enrichment', 'github'] + example: 'ui' + description: Data source. For manual updates, always use 'ui' + title: + type: string + example: 'Software Engineer' + description: Member role within the organization + dateStart: + type: string + format: date-time + example: '2023-01-01T00:00:00.000Z' + description: Organization role start date + dateEnd: + type: string + format: date-time + example: '2024-01-01T00:00:00.000Z' + description: Organization role end date + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Organization' + '400': + description: Bad request + '404': + description: Member not found + + /member/{memberId}/organization/{workHistoryId}: + patch: + security: + - BearerAuth: [] + tags: + - Member Organizations API + summary: Update Work History Organization + description: Update an existing organization for a specific profile + parameters: + - name: memberId + in: path + required: true + schema: + type: string + - name: workHistoryId + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - id + - workHistoryId + - source + properties: + id: + type: string + example: '550e8400-e29b-41d4-a716-446655440000' + description: Work experience ID + organizationId: + type: string + example: '550e8400-e29b-41d4-a716-446655440000' + description: Organization ID + source: + type: string + enum: ['ui', 'email-domain', 'enrichment', 'github'] + example: 'ui' + description: Data source. For manual updates, always use 'ui' + title: + type: string + example: 'Software Engineer' + description: Member role within the organization + dateStart: + type: string + format: date-time + example: '2023-01-01T00:00:00.000Z' + description: Organization role start date + dateEnd: + type: string + format: date-time + example: '2024-01-01T00:00:00.000Z' + description: Organization role end date + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Organization' + '400': + description: Bad request + '404': + description: Organization or member not found + + delete: + security: + - BearerAuth: [] + tags: + - Member Organizations API + summary: Delete Work History Organization + description: Delete an organization for a specific profile + parameters: + - name: memberId + in: path + required: true + schema: + type: string + - name: workHistoryId + in: path + required: true + schema: + type: string + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Organization' + '404': + description: Organization or member not found + + /member/{memberId}/affiliation: + get: + security: + - BearerAuth: [] + tags: + - Member Affiliations API + summary: List Project Affiliations + description: Retrieve a list of project affiliations for a specific profile + parameters: + - name: memberId + in: path + required: true + schema: + type: string + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Affiliation' + '400': + description: Bad request + '404': + description: Member not found + + patch: + security: + - BearerAuth: [] + tags: + - Member Affiliations API + summary: Update Multiple Project Affiliations + description: Bulk update project affiliations for a specific profile + parameters: + - name: memberId + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - affiliations + properties: + affiliations: + type: array + items: + type: object + required: + - organizationId + - segmentId + properties: + organizationId: + type: string + description: Organization ID associated with this affiliation + segmentId: + type: string + description: ID of the segment + dateEnd: + type: string + format: date-time + description: End date of the affiliation + dateStart: + type: string + format: date-time + description: Start date of the affiliation + responses: + '200': + description: Affiliations updated successfully + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Affiliation' + '400': + description: Bad request + '404': + description: Member not found + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT token obtained from LFX authentication service + + schemas: + Organization: + type: object + properties: + id: + type: string + example: '550e8400-e29b-41d4-a716-446655440000' + description: Unique identifier for the organization + displayName: + type: string + example: 'The Linux Foundation' + description: Display name of the organization + logo: + type: string + example: 'https://avatars.githubusercontent.com/u/1040002?v=4' + description: URL of the organization's logo + memberOrganizations: + type: array + items: + $ref: '#/components/schemas/MemberOrganization' + + MemberOrganization: + type: object + properties: + id: + type: string + example: '550e8400-e29b-41d4-a716-446655440000' + description: Unique identifier for the organization + organizationId: + type: string + example: '550e8400-e29b-41d4-a716-446655440000' + description: Organization ID associated with this affiliation + dateEnd: + type: string + example: '2024-01-01T00:00:00.000Z' + format: date-time + description: End date of the affiliation + dateStart: + type: string + example: '2023-01-01T00:00:00.000Z' + format: date-time + description: Start date of the affiliation + memberId: + type: string + example: '550e8400-e29b-41d4-a716-446655440000' + description: Member ID associated with this affiliation + source: + type: string + example: 'ui' + description: Data source. For manual updates, always use 'ui' + title: + type: string + example: 'Software Engineer' + description: Member role within the organization + + Affiliation: + type: object + properties: + id: + type: string + example: '550e8400-e29b-41d4-a716-446655440000' + description: Unique identifier for the affiliation + organizationId: + type: string + example: '550e8400-e29b-41d4-a716-446655440000' + description: Organization ID associated with this affiliation + organizationLogo: + type: string + example: 'https://avatars.githubusercontent.com/u/1040002?v=4' + description: URL of the organization's logo + organizationName: + type: string + example: 'The Linux Foundation' + description: Name of the organization + segmentId: + type: string + example: '550e8400-e29b-41d4-a716-446655440000' + description: ID of the segment + segmentName: + type: string + example: 'Kubernetes' + description: Name of the segment + segmentParentName: + type: string + example: 'Cloud Native Computing Foundation' + description: Name of the parent segment + segmentSlug: + type: string + example: 'kubernetes' + description: Slug identifier for the segment + dateStart: + type: string + example: '2023-01-01T00:00:00.000Z' + format: date-time + description: Start date of the affiliation + dateEnd: + type: string + example: '2024-01-01T00:00:00.000Z' + format: date-time + description: End date of the affiliation diff --git a/backend/jest.config.js b/backend/jest.config.js deleted file mode 100644 index 6fee5b31b9..0000000000 --- a/backend/jest.config.js +++ /dev/null @@ -1,41 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ - -const tsconfig = require('./tsconfig.json') - -const fromPairs = (pairs) => pairs.reduce((res, [key, value]) => ({ ...res, [key]: value }), {}) - -/** - * tsconfig の paths の設定から moduleNameMapper を生成する - * {"@app/*": ["src/*"]} -> {"@app/(.*)": "/src/$1"} - */ -function moduleNameMapperFromTSPaths(tsconf) { - return fromPairs( - Object.entries(tsconf.compilerOptions.paths).map(([k, [v]]) => [ - k.replace(/\*/, '(.*)'), - `/${tsconf.compilerOptions.baseUrl}/${v.replace(/\*/, '$1')}`, - ]), - ) -} - -/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */ -module.exports = { - preset: 'ts-jest', - transform: { - '^.+\\.(ts|tsx)$': [ - 'ts-jest', - { - babelConfig: true, - isolatedModules: true, - }, - ], - }, - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - testEnvironment: 'node', - testPathIgnorePatterns: ['/dist'], - testTimeout: 90000, - testRegex: ['__tests__/.*tests?.ts$'], - bail: false, - roots: [''], - moduleNameMapper: moduleNameMapperFromTSPaths(tsconfig), - transformIgnorePatterns: ['node_modules/(?!(axios|@crowd/))/'], -} diff --git a/backend/openapi.json b/backend/openapi.json deleted file mode 100644 index a7a02745ed..0000000000 --- a/backend/openapi.json +++ /dev/null @@ -1,5852 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "crowd.dev API", - "version": "1.0.5", - "description": "crowd.dev API\n", - "contact": { "email": "joan@crowd.dev" }, - "license": { "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" }, - "x-github": "https://github.com/crowdHQ" - }, - "servers": [{ "url": "https://app.crowd.dev/api" }], - "paths": { - "/tenant/{tenantId}/activity/with-member": { - "post": { - "summary": "Create or update an activity with a member", - "tags": ["Activities"], - "security": [{ "Bearer": [] }], - "description": "Create or update an activity with a member\nActivity existence is checked by sourceId and tenantId\nMember existence is checked by platform and username", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ActivityUpsertWithMemberInput" } - } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Activity" }, - "examples": { "Activity": { "$ref": "#/components/examples/ActivityUpsert" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/activity": { - "post": { - "summary": "Create or update an activity", - "tags": ["Activities"], - "security": [{ "Bearer": [] }], - "description": "Create or update an activity. Existence is checked by sourceId and tenantId", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/ActivityUpsertInput" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Activity" }, - "examples": { "Activity": { "$ref": "#/components/examples/ActivityUpsert" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/activity/{id}": { - "delete": { - "summary": "Delete an activity", - "tags": ["Activities"], - "security": [{ "Bearer": [] }], - "description": "Delete a activity given an ID", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the activity", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { "description": "Ok" }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "Find an activity", - "tags": ["Activities"], - "security": [{ "Bearer": [] }], - "description": "Find a single activity by ID", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the activity", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ActivityResponse" }, - "examples": { "Activity": { "$ref": "#/components/examples/ActivityFind" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "put": { - "summary": "Update an activity", - "tags": ["Activities"], - "security": [{ "Bearer": [] }], - "description": "Update an activity given an ID.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the activity", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/ActivityUpsertInput" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Activity" }, - "examples": { "Activity": { "$ref": "#/components/examples/ActivityFind" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/activity/query": { - "post": { - "summary": "Query activities", - "tags": ["Activities"], - "security": [{ "Bearer": [] }], - "description": "Query activities. It accepts filters, sorting options and pagination.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/ActivityQuery" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ActivityList" }, - "examples": { "Activity": { "$ref": "#/components/examples/ActivityList" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/automation": { - "post": { - "summary": "Create an automation", - "tags": ["Automations"], - "security": [{ "Bearer": [] }], - "description": "Create a new automation for the tenant.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/AutomationCreateInput" } - } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Automation" }, - "examples": { "Automation": { "$ref": "#/components/examples/Automation" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "List automations", - "tags": ["Automations"], - "security": [{ "Bearer": [] }], - "description": "Get all existing automation data for tenant.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "filter[type]", - "in": "query", - "description": "Filter by type of automation", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[trigger]", - "in": "query", - "description": "Filter by trigger type of automation", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[state]", - "in": "query", - "description": "Filter by state of automation", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "offset", - "in": "query", - "description": "Skip the first n results. Default 0.", - "required": false, - "schema": { "type": "number" } - }, - { - "name": "limit", - "in": "query", - "description": "Limit the number of results. Default 50.", - "required": false, - "schema": { "type": "number" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/AutomationPage" }, - "examples": { "AutomationPage": { "$ref": "#/components/examples/AutomationPage" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/automation/{automationId}": { - "delete": { - "summary": "Destroy an automation", - "tags": ["Automations"], - "security": [{ "Bearer": [] }], - "description": "Destroys an existing automation in the tenant.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "automationId", - "in": "path", - "description": "Automation ID that you want to update", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "204": { "description": "Ok - No content" }, - "401": { "description": "Unauthorized" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "Find an automation", - "tags": ["Automations"], - "security": [{ "Bearer": [] }], - "description": "Get an existing automation data in the tenant.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "automationId", - "in": "path", - "description": "Automation ID that you want to find", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Automation" }, - "examples": { "Automation": { "$ref": "#/components/examples/Automation" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "429": { "description": "Too many requests" } - } - }, - "put": { - "summary": "Update an automation", - "tags": ["Automations"], - "security": [{ "Bearer": [] }], - "description": "Updates an existing automation in the tenant.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "automationId", - "in": "path", - "description": "Automation ID that you want to update", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/AutomationUpdateInput" } - } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Automation" }, - "examples": { "Automation": { "$ref": "#/components/examples/Automation" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/automation/{automationId}/executions": { - "get": { - "summary": "Get automation history", - "tags": ["Automations"], - "security": [{ "Bearer": [] }], - "description": "Get all automation execution history for tenant and automation", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "automationId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "offset", - "in": "query", - "description": "How many elements from the beginning would you like to skip", - "required": false, - "schema": { "type": "integer", "default": 0 } - }, - { - "name": "limit", - "in": "query", - "description": "How many elements would you like to fetch", - "required": false, - "schema": { "type": "integer", "default": 10 } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/AutomationExecutionPage" }, - "examples": { - "AutomationExecutionPage": { - "$ref": "#/components/examples/AutomationExecutionPage" - } - } - } - } - }, - "401": { "description": "Unauthorized" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/conversation": { - "post": { - "summary": "Create a conversation", - "tags": ["Conversations"], - "security": [{ "Bearer": [] }], - "description": "Create a conversation.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/ConversationNoId" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Conversation" }, - "examples": { "Conversation": { "$ref": "#/components/examples/Conversation" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "List conversations", - "tags": ["Conversations"], - "security": [{ "Bearer": [] }], - "description": "Get a list of conversations with filtering, sorting and offsetting.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "filter[title]", - "in": "query", - "description": "Filter by the title of the conversation.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[slug]", - "in": "query", - "description": "Filter by the slug of the conversation.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[published]", - "in": "query", - "description": "Filter by whether it is published or not.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[platform]", - "in": "query", - "description": "Filter by the platform of the conversation.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[channel]", - "in": "query", - "description": "Filter by the channel of the conversation.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[activitiesCountRange]", - "in": "query", - "description": "activitiesCount lower bound. If you want a range, send this parameter twice with [min] and [max]. If you send it once it will be interpreted as a lower bound.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[createdAtRange]", - "in": "query", - "description": "Send this parameter twice with [min] and [max].", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "orderBy", - "in": "query", - "description": "Sort the results. Default timestamp_DESC.", - "required": false, - "schema": { "$ref": "#/components/schemas/ConversationSort" } - }, - { - "name": "offset", - "in": "query", - "description": "Skip the first n results. Default 0.", - "required": false, - "schema": { "type": "number" } - }, - { - "name": "limit", - "in": "query", - "description": "Limit the number of results. Default 50.", - "required": false, - "schema": { "type": "number" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ConversationList" }, - "examples": { - "Conversations": { "$ref": "#/components/examples/ConversationList" } - } - } - } - }, - "401": { "description": "Unauthorized" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/conversation/{id}": { - "delete": { - "summary": "Delete a conversation", - "tags": ["Conversations"], - "security": [{ "Bearer": [] }], - "description": "Delete a conversation.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the conversation", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { "description": "Ok" }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "Find a conversation", - "tags": ["Conversations"], - "security": [{ "Bearer": [] }], - "description": "Find a conversation by ID.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID.", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the conversation.", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Conversation" }, - "examples": { "Conversation": { "$ref": "#/components/examples/Conversation" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "put": { - "summary": "Update an conversation", - "tags": ["Conversations"], - "security": [{ "Bearer": [] }], - "description": "Update a conversation given an ID.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the conversation", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/ConversationNoId" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Conversation" }, - "examples": { "Conversation": { "$ref": "#/components/examples/Conversation" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/member/active": { - "get": { - "summary": "List active members", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "List active members. It accepts filters, sorting options and pagination.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "filter[platforms]", - "in": "query", - "description": "Filter by activity platforms (comma separated list without spaces)", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[isTeamMember]", - "in": "query", - "description": "If true we will return just team members, if false we will return just non-team members, if undefined we will return both.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[isBot]", - "in": "query", - "description": "If true we will return just members who are bots, if false we will return just non-bot members, if undefined we will return both.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[isOrganization]", - "in": "query", - "description": "If true we will return just members who are organizations (such as linkedin organizations that post), if false we will return just non-organization members, if undefined we will return both.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[activityTimestampFrom]", - "in": "query", - "description": "Filter by activity timestamp from (required)", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[activityTimestampTo]", - "in": "query", - "description": "Filter by activity timestamp to (required)", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[activityIsContribution]", - "in": "query", - "description": "Filter by activities that are contributions", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "orderBy", - "in": "query", - "description": "How to sort results. Available values: activityCount_DESC, activityCount_ASC, activeDaysCount_DESC, activeDaysCount_ASC (default activityCount_DESC)", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "offset", - "in": "query", - "description": "Skip the first n results. Default 0.", - "required": false, - "schema": { "type": "number" } - }, - { - "name": "limit", - "in": "query", - "description": "Limit the number of results. Default 20.", - "required": false, - "schema": { "type": "number" } - } - ], - "responses": { - "200": { "description": "Ok" }, - "401": { "description": "Unauthorized" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/member": { - "post": { - "summary": "Create or update a member", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Create or update a member. Existence is checked by platform and username.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/MemberUpsertInput" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Member" }, - "examples": { "Member": { "$ref": "#/components/examples/MemberUpsert" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/member/{id}": { - "delete": { - "summary": "Delete a member", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Delete a member given an ID", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the member", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { "description": "Ok" }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "Find a member", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Find a single member by ID.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the member", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/MemberResponse" }, - "examples": { "Member": { "$ref": "#/components/examples/MemberFind" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "put": { - "summary": "Update a member", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Update a member", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the member", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/MemberUpdateInput" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Member" }, - "examples": { "Member": { "$ref": "#/components/examples/MemberUpsert" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/member/export": { - "post": { - "summary": "Export members as CSV", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Export members. It accepts filters, sorting options and pagination.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/MemberQuery" } } - } - }, - "responses": { - "200": { "description": "Ok" }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/member/query": { - "post": { - "summary": "Query members", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Query members. It accepts filters, sorting options and pagination.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/MemberQuery" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/MemberList" }, - "examples": { "Member": { "$ref": "#/components/examples/MemberList" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/note": { - "post": { - "summary": "Create a note", - "tags": ["Notes"], - "security": [{ "Bearer": [] }], - "description": "Create a note", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/NoteNoId" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Note" }, - "examples": { "Note": { "$ref": "#/components/examples/Note" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/note/{id}": { - "delete": { - "summary": "Delete a note", - "tags": ["Notes"], - "security": [{ "Bearer": [] }], - "description": "Delete a note.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the note", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { "description": "Ok" }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "Find a note", - "tags": ["Notes"], - "security": [{ "Bearer": [] }], - "description": "Find a note by ID.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID.", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the note.", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/NoteResponse" }, - "examples": { "Note": { "$ref": "#/components/examples/Note" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "put": { - "summary": "Update a note", - "tags": ["Notes"], - "security": [{ "Bearer": [] }], - "description": "Update a note", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the note", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/NoteInput" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Note" }, - "examples": { "Note": { "$ref": "#/components/examples/Note" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/note/query": { - "post": { - "summary": "Query notes", - "tags": ["Notes"], - "security": [{ "Bearer": [] }], - "description": "Query notes. It accepts filters, sorting options and pagination.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/NoteQuery" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/NoteList" }, - "examples": { "Note": { "$ref": "#/components/examples/NoteList" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/organization": { - "post": { - "summary": "Create a organization", - "tags": ["Organizations"], - "security": [{ "Bearer": [] }], - "description": "Create a organization", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/OrganizationInput" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Organization" }, - "examples": { - "Organization": { "$ref": "#/components/examples/OrganizationCreate" } - } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/organization/{id}": { - "delete": { - "summary": "Delete a organization", - "tags": ["Organizations"], - "security": [{ "Bearer": [] }], - "description": "Delete a organization.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the organization", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { "description": "Ok" }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "Find an organization", - "tags": ["Organizations"], - "security": [{ "Bearer": [] }], - "description": "Find an organization by ID.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the organization", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/OrganizationResponse" }, - "examples": { "Organization": { "$ref": "#/components/examples/Organization" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "put": { - "summary": "Update an organization", - "tags": ["Organizations"], - "security": [{ "Bearer": [] }], - "description": "Update a organization", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the organization", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/OrganizationInput" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Organization" }, - "examples": { - "Organization": { "$ref": "#/components/examples/OrganizationCreate" } - } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/organization/query": { - "post": { - "summary": "Query organizations", - "tags": ["Organizations"], - "security": [{ "Bearer": [] }], - "description": "Query organizations. It accepts filters, sorting options and pagination.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/OrganizationQuery" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/OrganizationList" }, - "examples": { "Organization": { "$ref": "#/components/examples/OrganizationList" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/settings/activity/types": { - "post": { - "summary": "Create an activity type", - "tags": ["Activities"], - "security": [{ "Bearer": [] }], - "description": "Create a custom activity type", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ActivityTypesCreateInput" } - } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ActivityTypes" }, - "examples": { "ActivityTypes": { "$ref": "#/components/examples/ActivityTypes" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "List all activity types", - "tags": ["Activities"], - "security": [{ "Bearer": [] }], - "description": "List all activity types", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ActivityTypes" }, - "examples": { "ActivityTypes": { "$ref": "#/components/examples/ActivityTypes" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/settings/activity/types/{key}": { - "put": { - "summary": "Update an activity type", - "tags": ["Activities"], - "security": [{ "Bearer": [] }], - "description": "Update a custom activity type", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "key", - "in": "path", - "description": "The key of the activity type", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ActivityTypesUpdateInput" } - } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ActivityTypes" }, - "examples": { "ActivityTypes": { "$ref": "#/components/examples/ActivityTypes" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/settings/members/attributes": { - "post": { - "summary": "Attribute settings: create", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Create a members' attribute setting", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/MemberAttributeSettingsCreateInput" } - } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/MemberAttributeSettings" }, - "examples": { - "MemberAttributeSettings": { - "$ref": "#/components/examples/MemberAttributeSettings" - } - } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "delete": { - "summary": "Attribute settings: delete", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Delete a members' attribute setting", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "query", - "description": "Id to destroy", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { "description": "Ok" }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "Attributes settings: list", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Get a list of members' attribute settings", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "filter[label]", - "in": "query", - "description": "Filter by label of member attribute settings", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[name]", - "in": "query", - "description": "Filter by name of member attribute settings", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[type]", - "in": "query", - "description": "Filter by type of member attribute settings", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[canDelete]", - "in": "query", - "description": "Filter by canDelete: \"true\" or \"false\"", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[show]", - "in": "query", - "description": "Filter by show: \"true\" or \"false\"", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[createdAtRange]", - "in": "query", - "description": "CreatedAt lower bound. If you want a range, send this parameter twice with [min] and [max]. If you send it once it will be interpreted as a lower bound.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "orderBy", - "in": "query", - "description": "Sort the results. Default createdAt_DESC.", - "required": false, - "schema": { "$ref": "#/components/schemas/MemberAttributeSettingsSort" } - }, - { - "name": "offset", - "in": "query", - "description": "Skip the first n results. Default 0.", - "required": false, - "schema": { "type": "number" } - }, - { - "name": "limit", - "in": "query", - "description": "Limit the number of results. Default 50.", - "required": false, - "schema": { "type": "number" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/MemberAttributeSettingsList" }, - "examples": { - "MemberAttributeSettings": { - "$ref": "#/components/examples/MemberAttributeSettingsList" - } - } - } - } - }, - "401": { "description": "Unauthorized" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/settings/members/attributes/{id}": { - "get": { - "summary": "Attributes settings: find", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Find a single members' attribute setting by ID", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the member attribute's settings", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/MemberAttributeSettings" }, - "examples": { - "MemberAttributeSettings": { - "$ref": "#/components/examples/MemberAttributeSettings" - } - } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "put": { - "summary": "Attribute settings: update", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Update a members' attribute setting", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the member attribute settings", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/MemberAttributeSettingsUpdateInput" } - } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/MemberAttributeSettings" }, - "examples": { - "MemberAttributeSettings": { - "$ref": "#/components/examples/MemberAttributeSettings" - } - } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/tag": { - "post": { - "summary": "Create a tag", - "tags": ["Tags"], - "security": [{ "Bearer": [] }], - "description": "Create a tag", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/TagNoId" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Tag" }, - "examples": { "Tag": { "$ref": "#/components/examples/Tag" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "List tags", - "tags": ["Tags"], - "security": [{ "Bearer": [] }], - "description": "Get a list of tags with filtering, sorting and offsetting.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "filter[name]", - "in": "query", - "description": "Filter by the name of the tag.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[createdAtRange]", - "in": "query", - "description": "Created at lower bound. If you want a range, send this parameter twice with [min] and [max]. If you send it once it will be interpreted as a lower bound.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "orderBy", - "in": "query", - "description": "Sort the results. Default timestamp_DESC.", - "required": false, - "schema": { "$ref": "#/components/schemas/TagSort" } - }, - { - "name": "offset", - "in": "query", - "description": "Skip the first n results. Default 0.", - "required": false, - "schema": { "type": "number" } - }, - { - "name": "limit", - "in": "query", - "description": "Limit the number of results. Default 50.", - "required": false, - "schema": { "type": "number" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/TagList" }, - "examples": { "Tags": { "$ref": "#/components/examples/TagList" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/tag/{id}": { - "delete": { - "summary": "Delete a tag", - "tags": ["Tags"], - "security": [{ "Bearer": [] }], - "description": "Delete a tag.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the tag", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { "description": "Ok" }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "Find a tag", - "tags": ["Tags"], - "security": [{ "Bearer": [] }], - "description": "Find a tag by ID", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the tag", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Tag" }, - "examples": { "Tag": { "$ref": "#/components/examples/Tag" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "put": { - "summary": "Update an tag", - "tags": ["Tags"], - "security": [{ "Bearer": [] }], - "description": "Update a tag", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the tag", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/TagNoId" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Tag" }, - "examples": { "Tag": { "$ref": "#/components/examples/Tag" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/task/batch": { - "post": { - "summary": "Make batch operations on tasks", - "tags": ["Tasks"], - "security": [{ "Bearer": [] }], - "description": "Make batch operations on tasks", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/TaskBatchInput" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/TaskFindAndUpdateAll" }, - "examples": { "Task": { "$ref": "#/components/examples/TaskFindAndUpdateAll" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/task": { - "post": { - "summary": "Create a task", - "tags": ["Tasks"], - "security": [{ "Bearer": [] }], - "description": "Create a task", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/TaskInput" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Task" }, - "examples": { "Task": { "$ref": "#/components/examples/Task" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/task/{id}": { - "delete": { - "summary": "Delete a task", - "tags": ["Tasks"], - "security": [{ "Bearer": [] }], - "description": "Delete a task.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the task", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { "description": "Ok" }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "Find a task", - "tags": ["Tasks"], - "security": [{ "Bearer": [] }], - "description": "Find a task by ID", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the task", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/TaskResponse" }, - "examples": { "Task": { "$ref": "#/components/examples/Task" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "put": { - "summary": "Update an task", - "tags": ["Tasks"], - "security": [{ "Bearer": [] }], - "description": "Update a task", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the task", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/TaskInput" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Task" }, - "examples": { "Task": { "$ref": "#/components/examples/Task" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/task/query": { - "post": { - "summary": "Query tasks", - "tags": ["Tasks"], - "security": [{ "Bearer": [] }], - "description": "Query tasks. It accepts filters, sorting options and pagination.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/TaskQuery" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/TaskList" }, - "examples": { "Task": { "$ref": "#/components/examples/TaskList" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/enrichment/member/{id}": { - "put": { - "summary": "Enrich a member", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Enrich a member.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the member", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/MemberResponse" }, - "examples": { "Member": { "$ref": "#/components/examples/MemberFind" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - } - }, - "components": { - "securitySchemes": { "Bearer": { "type": "http", "scheme": "bearer" } }, - "schemas": { - "MemberType": { "type": "string", "enum": ["member"] }, - "MemberScore": { "type": "integer", "minimum": -1, "maximum": 10 }, - "MemberSort": { - "type": "string", - "enum": [ - "activitiesCount_ASC", - "activitiesCount_DESC", - "score_ASC", - "score_ASC", - "joinedAt_ASC", - "joinedAt_DESC", - "createdAt_ASC", - "createdAt_DESC", - "organisation_ASC", - "organisation_DESC", - "location_ASC", - "location_DESC" - ] - }, - "ActivitySort": { - "type": "string", - "enum": [ - "timestamp_DESC", - "timestamp_ASC", - "createdAt_DESC", - "createdAt_ASC", - "score_DESC", - "score_ASC", - "type_DESC", - "type_ASC", - "platform_DESC", - "platform_ASC", - "createdBy_DESC", - "createdBy_ASC" - ] - }, - "ConversationSort": { - "type": "string", - "enum": [ - "createdAt_DESC", - "createdAt_ASC", - "activityCount_DESC", - "activityCount_ASC", - "platform_DESC", - "platform_ASC", - "channel_DESC", - "channel_ASC", - "createdBy_DESC", - "createdBy_ASC" - ] - }, - "TagSort": { - "type": "string", - "enum": ["name_ASC", "name_DESC", "createdAt_DESC", "createdAt_ASC"] - }, - "MemberAttributeSettingsSort": { - "type": "string", - "enum": [ - "label_ASC", - "label_DESC", - "type_ASC", - "type_DESC", - "createdAt_DESC", - "createdAt_ASC" - ] - }, - "ActivityRelationsInput": { - "description": "Relations of an activity.", - "type": "object", - "properties": { - "tasks": { - "description": "Tasks associated with the activity", - "type": "array", - "items": { "$ref": "#/components/schemas/TaskNoId" } - } - } - }, - "ActivityUpsertInput": { - "required": ["memberId"], - "description": "An activity performed by a member of your community. The member is sent as an ID.", - "allOf": [ - { "$ref": "#/components/schemas/ActivityNoId" }, - { "$ref": "#/components/schemas/ActivityRelationsInput" } - ], - "properties": { - "memberId": { "description": "The ID of the member that performed the activity" } - } - }, - "ActivityUpsertWithMemberInput": { - "type": "object", - "description": "An activity performed by a member of your community. The member is sent as a whole object.", - "allOf": [ - { "$ref": "#/components/schemas/ActivityNoId" }, - { "$ref": "#/components/schemas/ActivityRelationsInput" } - ], - "properties": { "member": { "$ref": "#/components/schemas/MemberNoId" } } - }, - "ActivityNoId": { - "description": "An activity performed by a member of your community.", - "type": "object", - "required": ["type", "platform", "timestamp", "sourceId"], - "properties": { - "type": { "description": "Type of activity", "type": "string" }, - "timestamp": { - "description": "Date and time when the activity took place", - "type": "string", - "format": "date-time" - }, - "platform": { - "description": "Platform on which the activity took place", - "type": "string" - }, - "title": { "description": "Title of the activity", "type": "string" }, - "body": { "description": "Body of the activity", "type": "string" }, - "channel": { "description": "Channel of the activity", "type": "string" }, - "sentiment": { - "description": "Sentiment of the activity", - "type": "object", - "properties": { - "sentiment": { - "description": "Default sentiment score.
Computed by mapping (positive - negative) from 0 to 100", - "type": "number", - "minimum": 0, - "maximum": 100 - }, - "label": { - "description": "Sentiment label", - "type": "string", - "enum": ["positive", "negative", "neutral", "mixed"] - }, - "positive": { - "description": "Positive sentiment score", - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "negative": { - "description": "Negative sentiment score", - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "neutral": { - "description": "Neutral sentiment score", - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "mixed": { - "description": "Mixed sentiment score. Mixed contains both positive and negative sentiments", - "type": "number", - "minimum": 0, - "maximum": 1 - } - } - }, - "sourceId": { - "description": "The id of the activity in the platform (e.g. the id of the message in Discord)", - "type": "string" - }, - "sourceParentId": { - "description": "The id of the parent activity in the platform (e.g. the id of the parent message in Discord)", - "type": "string" - }, - "parentId": { - "description": "Id of the parent activity, if the activity has a parent", - "type": "string", - "format": "uuid" - }, - "score": { "description": "Score associated with the activity", "type": "number" }, - "isContribution": { - "description": "Whether the activity was a contribution", - "type": "boolean" - }, - "attributes": { - "description": "Extra attributes of the activity", - "type": "object", - "additionalProperties": true - }, - "createdAt": { - "description": "Date the activity was created", - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "description": "Date the activity was last updated", - "type": "string", - "format": "date-time" - } - }, - "xml": { "name": "ActivityNoId" } - }, - "FilterType": { - "type": "object", - "additionalProperties": { - "oneOf": [{ "type": "string" }, { "$ref": "#/components/schemas/FilterType" }] - } - }, - "ActivityQuery": { - "description": "All the parameters you can use to query activitys.", - "properties": { - "filter": { - "description": "Filter. Please refer to filter docs.", - "type": "string", - "format": "blob" - }, - "orderBy": { - "type": "string", - "enum": [ - "activitiesCount_DESC", - "score_ASC", - "score_ASC", - "joinedAt_ASC", - "joinedAt_DESC", - "createdAt_ASC", - "createdAt_DESC", - "organisation_ASC", - "organisation_DESC", - "location_ASC", - "location_DESC" - ] - }, - "limit": { - "description": "Limit the number of records returned. Default is 10.", - "type": "integer", - "minimum": 1, - "maximum": 200, - "default": 10 - }, - "offset": { - "description": "Offset the number of records returned. Default is 0.", - "type": "integer", - "minimum": 0, - "default": 0 - } - } - }, - "Activity": { - "type": "object", - "allOf": [{ "$ref": "#/components/schemas/ActivityNoId" }], - "properties": { "id": { "description": "The unique identifier for an activity." } } - }, - "ActivityRelationsResponse": { - "description": "Relations of an activity.", - "type": "object", - "properties": { - "member": { - "description": "Member that performed the activity", - "$ref": "#/components/schemas/Member" - }, - "tasks": { - "description": "Tasks associated with the activity.", - "type": "array", - "items": { "$ref": "#/components/schemas/Task" } - } - } - }, - "ActivityResponse": { - "description": "An activity performed by a member.", - "type": "object", - "allOf": [ - { "$ref": "#/components/schemas/Activity" }, - { "$ref": "#/components/schemas/ActivityRelationsResponse" } - ] - }, - "ActivityList": { - "description": "List and count of activities.", - "type": "object", - "properties": { - "rows": { - "description": "List of activities", - "type": "array", - "items": { "$ref": "#/components/schemas/ActivityResponse" } - }, - "count": { "description": "Count", "type": "integer" }, - "limit": { "description": "Limit of records returned", "type": "integer" }, - "offset": { "description": "Offset, for pagination", "type": "integer" } - }, - "xml": { "name": "ActivitiesList" } - }, - "AutomationCreateInput": { - "type": "object", - "description": "Data to create a new automation.", - "required": ["type", "trigger", "settings"], - "properties": { - "type": { "$ref": "#/components/schemas/AutomationType" }, - "trigger": { "$ref": "#/components/schemas/AutomationTrigger" }, - "settings": { "$ref": "#/components/schemas/AutomationSettings" } - } - }, - "AutomationUpdateInput": { - "type": "object", - "description": "Data to update an existing automation.", - "required": ["trigger", "settings", "state"], - "properties": { - "trigger": { "$ref": "#/components/schemas/AutomationTrigger" }, - "settings": { "$ref": "#/components/schemas/AutomationSettings" }, - "state": { "$ref": "#/components/schemas/AutomationState" } - } - }, - "AutomationType": { "description": "Automation type", "type": "string", "enum": ["webhook"] }, - "AutomationState": { - "description": "Automation state", - "type": "string", - "enum": ["active", "disabled"] - }, - "AutomationTrigger": { - "description": "What will trigger an automation", - "type": "string", - "enum": ["new_activity", "new_member"] - }, - "AutomationExecutionState": { - "description": "What was the state of the automation execution", - "type": "string", - "enum": ["success", "error"] - }, - "WebhookAutomationSettings": { - "description": "Settings used by automation with type webhook", - "type": "object", - "required": ["url"], - "properties": { - "url": { "description": "URL to POST webhook data to", "type": "string", "format": "uri" } - } - }, - "NewActivityAutomationSettings": { - "description": "Settings used by automation that is triggered by new activities", - "type": "object", - "required": ["types", "platforms", "keywords", "teamMemberActivities"], - "properties": { - "types": { - "description": "If activity type matches any of these we should trigger this automation", - "type": "array", - "items": { "type": "string" } - }, - "platforms": { - "description": "If activity came from any of these platforms we should trigger this automation", - "type": "array", - "items": { "type": "string" } - }, - "keywords": { - "description": "If activity content contains any of these keywords we should trigger this automation", - "type": "array", - "items": { "type": "string" } - }, - "teamMemberActivities": { - "description": "If activity came from any of our team members - should we trigger automation or not?", - "type": "boolean" - } - } - }, - "AutomationSettings": { - "description": "Settings based on automation type and trigger - you need to provide union object of both automation type based settings and trigger based settings", - "type": "object", - "anyOf": [ - { "$ref": "#/components/schemas/WebhookAutomationSettings" }, - { "$ref": "#/components/schemas/NewActivityAutomationSettings" } - ] - }, - "Automation": { - "type": "object", - "required": ["id", "type", "tenantId", "trigger", "settings", "state", "createdAt"], - "properties": { - "id": { "description": "Automation unique ID", "type": "string", "format": "uuid" }, - "type": { "$ref": "#/components/schemas/AutomationType" }, - "tenantId": { - "description": "Automation tenant unique ID", - "type": "string", - "format": "uuid" - }, - "trigger": { "$ref": "#/components/schemas/AutomationTrigger" }, - "settings": { "$ref": "#/components/schemas/AutomationSettings" }, - "state": { "$ref": "#/components/schemas/AutomationState" }, - "createdAt": { - "description": "When was automation created", - "type": "string", - "format": "date-time" - }, - "lastExecutionAt": { - "description": "When was automation last executed", - "type": "string", - "format": "date-time" - }, - "lastExecutionState": { - "description": "State of the last automation execution", - "$ref": "#/components/schemas/AutomationExecutionState" - }, - "lastExecutionError": { - "description": "Error information if last automation execution failed", - "type": "object" - } - } - }, - "AutomationPage": { - "type": "object", - "required": ["rows", "count", "offset", "limit"], - "properties": { - "rows": { - "description": "Array of automations that were fetched", - "type": "array", - "items": { "$ref": "#/components/schemas/Automation" } - }, - "count": { "description": "How many total automations there are", "type": "integer" }, - "offset": { - "description": "What offset was used when preparing this response", - "type": "integer" - }, - "limit": { - "description": "What limit was used when preparing this response", - "type": "integer" - } - } - }, - "AutomationExecution": { - "type": "object", - "required": ["id", "automationId", "state", "executedAt", "eventId", "payload"], - "properties": { - "id": { - "description": "Automation execution unique ID", - "type": "string", - "format": "uuid" - }, - "automationId": { - "description": "Automation unique ID", - "type": "string", - "format": "uuid" - }, - "state": { - "description": "Automation execution state", - "$ref": "#/components/schemas/AutomationExecutionState" - }, - "error": { - "description": "If execution was not successful this object will contain error information", - "type": "object" - }, - "executedAt": { - "description": "Automation execution timestamp", - "type": "string", - "format": "date-time" - }, - "eventId": { - "description": "Unique ID of the event that triggered this automation execution.", - "type": "string" - }, - "payload": { - "description": "Payload that was sent when this execution was processed", - "type": "object" - } - } - }, - "AutomationExecutionPage": { - "type": "object", - "required": ["rows", "count", "offset", "limit"], - "properties": { - "rows": { - "description": "Automation Execution List", - "type": "array", - "items": { "$ref": "#/components/schemas/AutomationExecution" } - }, - "count": { "description": "How many items are there in total", "type": "integer" }, - "offset": { - "description": "What offset was used when preparing this response", - "type": "integer" - }, - "limit": { - "description": "What limit was used when preparing this response", - "type": "integer" - } - } - }, - "ConversationNoId": { - "type": "object", - "required": ["platform", "slug", "tenantId"], - "description": "A conversation is a group of activities. Some attributes, like slug, are mostly used in public pages.", - "properties": { - "title": { "description": "Title of the conversation", "type": "string" }, - "slug": { "description": "Unique slug of the conversation", "type": "string" }, - "published": { - "description": "Whether the conversation is publicaly visible from open pages.", - "type": "boolean", - "default": false - }, - "conversationStarter": { - "description": "The conversation starter activity", - "type": "object", - "additionalProperties": { "$ref": "#/components/schemas/Activity" } - }, - "memberCount": { - "description": "Number of participating members in the conversation.", - "type": "integer" - }, - "lastActive": { - "description": "Last activity time in the conversation", - "type": "string", - "format": "date-time" - }, - "createdAt": { - "description": "Date the conversation was created", - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "description": "Date the conversation was last updated", - "type": "string", - "format": "date-time" - }, - "tenantId": { - "description": "Your workspace/tenant id", - "type": "string", - "format": "uuid" - } - }, - "xml": { "name": "Conversation" } - }, - "Conversation": { - "allOf": [{ "$ref": "#/components/schemas/ConversationNoId" }], - "properties": { - "id": { "description": "Unique identifier of the conversation", "type": "string" }, - "activities": { - "description": "List of IDs of the activities in the conversation", - "type": "array", - "items": { "type": "string" } - } - } - }, - "ConversationList": { - "type": "object", - "properties": { - "rows": { "type": "array", "items": { "$ref": "#/components/schemas/Conversation" } }, - "count": { "description": "Count", "type": "integer" }, - "limit": { "description": "Limit of records returned", "type": "integer" }, - "offset": { "description": "Offset, for pagination", "type": "integer" } - } - }, - "MemberPlatformHelper": { - "type": "object", - "required": ["platform"], - "properties": { - "platform": { - "type": "string", - "description": "Platform for which to check member existence." - } - } - }, - "MemberOrganizations": { - "type": "object", - "properties": { - "organizations": { - "description": "Organizations associated with the member. Each element in the array is the name of the organization, or an organization object. If the organization does not exist, it will be created.", - "type": "array", - "items": { "$ref": "#/components/schemas/OrganizationNoId" } - } - } - }, - "MemberOrganizationsUpdate": { - "type": "object", - "properties": { - "organizations": { - "description": "Organizations associated with the member. Each element in the array is the name of the organization, or an organization object. If the organization does not exist, it will be created.", - "type": "array", - "items": { "type": "string" } - } - } - }, - "MemberInputRelations": { - "type": "object", - "properties": { - "tags": { - "description": "Tags associated with the member. Each element in the array is the ID of the tag.", - "type": "array", - "items": { "type": "string" } - }, - "tasks": { - "description": "Tasks associated with the member. Each element in the array is the ID of the task.", - "type": "array", - "items": { "type": "string" } - }, - "notes": { - "description": "Notes associated with the member. Each element in the array is the ID of the note.", - "type": "array", - "items": { "type": "string" } - }, - "activities": { - "description": "Activities associated with the member. Each element in the array is the ID of the activity.", - "type": "array", - "items": { "type": "string" } - } - } - }, - "MemberUpsertInput": { - "allOf": [ - { "$ref": "#/components/schemas/MemberPlatformHelper" }, - { "$ref": "#/components/schemas/MemberNoId" }, - { "$ref": "#/components/schemas/MemberOrganizations" }, - { "$ref": "#/components/schemas/MemberInputRelations" } - ] - }, - "MemberUpdateInput": { - "allOf": [ - { "$ref": "#/components/schemas/MemberPlatformHelper" }, - { "$ref": "#/components/schemas/MemberNoId" }, - { "$ref": "#/components/schemas/MemberInputRelations" }, - { "$ref": "#/components/schemas/MemberOrganizationsUpdate" } - ] - }, - "MemberNoId": { - "description": "A member of your community.", - "type": "object", - "required": ["username"], - "properties": { - "username": { - "description": "Usernames of the member in each platform. Exactly one for each platform in which the member is active.
Example: ```{ github: 'iamgilfoyle', discord: 'gilfoyle '}```", - "type": "object", - "additionalProperties": true - }, - "displayName": { "description": "UI friendly name of the member", "type": "string" }, - "emails": { - "description": "Email addresses of the member", - "type": "array", - "items": { "type": "string" } - }, - "joinedAt": { - "description": "Date of joining the community", - "type": "string", - "format": "date-time" - }, - "score": { - "description": "Engagement score of the member. From 0 to 10. Set -1 for not yet calculated.", - "type": "number" - }, - "reach": { - "description": "Reach of the member in each platform. At most one for each platform in which the member is active.
Example: ```{ github: 10, twitter: 250, total: 260 }```", - "type": "object", - "properties": { - "total": { "description": "Sum of all the platform reaches.", "type": "number" } - }, - "additionalProperties": true - }, - "attributes": { - "description": "Attributes associated to the member. Each attribute must be an object with it's value for each platform, and a default.
For example: ```{\"location\": {\"github\": \"San Francisco\", \"twitter\": \"California\", \"default\": \"San Francisco\"}}```", - "type": "object", - "additionalProperties": { "$ref": "#/components/schemas/MemberAttribute" } - }, - "createdAt": { - "description": "Date the member was created", - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "description": "Date the member was last updated", - "type": "string", - "format": "date-time" - } - }, - "xml": { "name": "Member" } - }, - "MemberAttribute": { - "description": "A key for each platform.
- ```default``` is the value that will be displayed by default in the app
- ```custom``` is the value that will be displayed if the user has set a custom value for the attribute", - "type": "object", - "properties": { - "default": { - "description": "Default value for the attribute. This is set automatically according to crowd.dev rules.", - "type": "string" - }, - "custom": { - "description": "Custom value for the attribute. This is optionally set by the user. It will always be picked as the default when sent.", - "type": "string" - } - }, - "additionalProperties": true - }, - "MemberQuery": { - "description": "All the parameters you can use to query members.", - "properties": { - "filter": { - "description": "Filter. Please refer to filter docs.", - "type": "string", - "format": "blob" - }, - "orderBy": { - "type": "string", - "enum": [ - "activityCount_ASC", - "activityCount_DESC", - "score_ASC", - "score_DESC", - "joinedAt_ASC", - "joinedAt_DESC", - "createdAt_ASC", - "createdAt_DESC", - "organisation_ASC", - "organisation_DESC", - "location_ASC", - "location_DESC" - ] - }, - "limit": { - "description": "Limit the number of records returned. Default is 10.", - "type": "integer", - "minimum": 1, - "maximum": 200, - "default": 10 - }, - "offset": { - "description": "Offset the number of records returned. Default is 0.", - "type": "integer", - "minimum": 0, - "default": 0 - } - } - }, - "Member": { - "type": "object", - "allOf": [{ "$ref": "#/components/schemas/MemberNoId" }], - "properties": { - "id": { "description": "The unique identifier for a member of your community." }, - "activityCount": { - "description": "Number of activities performed by the member.", - "type": "integer" - }, - "lastActivity": { - "description": "Timestamp, type and platform of the last activity performed by the member.", - "type": "object", - "properties": { - "type": { "description": "Type of the last activity", "type": "string" }, - "timestamp": { - "description": "Date and time of the last activity", - "type": "string", - "format": "date-time" - }, - "platform": { "description": "Platform of the last activity", "type": "string" } - } - }, - "averageSentiment": { - "description": "Average sentiment of the member. From 0 to 100.", - "type": "number" - }, - "identities": { - "description": "List of platforms the member has identities in.", - "type": "array", - "items": { "type": "string" } - }, - "activeOn": { - "description": "List of platforms the member is active on.", - "type": "array", - "items": { "type": "string" } - } - } - }, - "MemberRelationsResponse": { - "description": "Relations of a member.", - "type": "object", - "properties": { - "tags": { - "description": "Tags associated with the member.", - "type": "array", - "items": { "$ref": "#/components/schemas/Tag" } - }, - "notes": { - "description": "Notes associated with the member.", - "type": "array", - "items": { "$ref": "#/components/schemas/Note" } - }, - "tasks": { - "description": "Tasks associated with the member.", - "type": "array", - "items": { "$ref": "#/components/schemas/Task" } - }, - "organizations": { - "description": "Organizations associated with the member.", - "type": "array", - "items": { "$ref": "#/components/schemas/Organization" } - } - } - }, - "MemberResponse": { - "description": "A member of your community.", - "type": "object", - "allOf": [ - { "$ref": "#/components/schemas/Member" }, - { "$ref": "#/components/schemas/MemberRelationsResponse" } - ] - }, - "MemberList": { - "description": "List and count of members.", - "type": "object", - "properties": { - "rows": { - "description": "List of members", - "type": "array", - "items": { "$ref": "#/components/schemas/MemberResponse" } - }, - "count": { "description": "Count", "type": "integer" }, - "limit": { "description": "Limit of records returned", "type": "integer" }, - "offset": { "description": "Offset, for pagination", "type": "integer" } - }, - "xml": { "name": "MembersList" } - }, - "NoteInputRelations": { - "type": "object", - "properties": { - "members": { - "description": "Members associated with the note. Each element in the array is the ID of the member.", - "type": "array", - "items": { "type": "string" } - } - } - }, - "NoteInput": { - "allOf": [ - { "$ref": "#/components/schemas/NoteNoId" }, - { "$ref": "#/components/schemas/NoteInputRelations" } - ] - }, - "NoteNoId": { - "description": "A created note.", - "type": "object", - "properties": { - "body": { "description": "The body of the note.", "type": "string", "format": "blob" }, - "createdAt": { - "description": "Date the note was created.", - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "description": "Date the note was last updated.", - "type": "string", - "format": "date-time" - } - }, - "xml": { "name": "Note" } - }, - "NoteQuery": { - "description": "All the parameters you can use to query notes.", - "properties": { - "filter": { - "description": "Filter. Please refer to filter docs.", - "type": "string", - "format": "blob" - }, - "orderBy": { "type": "string", "enum": ["createdAt_ASC", "createdAt_DESC"] }, - "limit": { - "description": "Limit the number of records returned. Default is 10.", - "type": "integer", - "minimum": 1, - "maximum": 200, - "default": 10 - }, - "offset": { - "description": "Offset the number of records returned. Default is 0.", - "type": "integer", - "minimum": 0, - "default": 0 - } - } - }, - "Note": { - "type": "object", - "allOf": [{ "$ref": "#/components/schemas/NoteNoId" }], - "properties": { - "id": { "description": "The ID of the note." }, - "body": { "description": "The body of the note.", "type": "string", "format": "blob" } - } - }, - "NoteRelationsResponse": { - "description": "Relations of a note.", - "type": "object", - "properties": { - "members": { - "description": "Members associated with the note.", - "type": "array", - "items": { "$ref": "#/components/schemas/Member" } - } - } - }, - "NoteResponse": { - "description": "A note of your community.", - "type": "object", - "allOf": [ - { "$ref": "#/components/schemas/Note" }, - { "$ref": "#/components/schemas/NoteRelationsResponse" } - ] - }, - "NoteList": { - "description": "List and count of notes.", - "type": "object", - "properties": { - "rows": { - "description": "List of notes", - "type": "array", - "items": { "$ref": "#/components/schemas/NoteResponse" } - }, - "count": { "description": "Count", "type": "integer" }, - "limit": { "description": "Limit of records returned", "type": "integer" }, - "offset": { "description": "Offset, for pagination", "type": "integer" } - }, - "xml": { "name": "NotesList" } - }, - "OrganizationInputRelations": { - "type": "object", - "properties": { - "members": { - "description": "Members associated with the organization. Each element in the array is the ID of the member.", - "type": "array", - "items": { "type": "string", "format": "uuid" } - } - } - }, - "OrganizationInput": { - "allOf": [ - { "$ref": "#/components/schemas/OrganizationNoId" }, - { "$ref": "#/components/schemas/OrganizationInputRelations" } - ] - }, - "OrganizationNoId": { - "description": "A created organization.", - "type": "object", - "required": ["name"], - "properties": { - "name": { "description": "The name of the organization.", "type": "string" }, - "url": { "description": "The URL of the organization.", "type": "string" }, - "description": { - "description": "A short description of the organization.", - "type": "string", - "format": "blob" - }, - "logo": { "description": "A URL for logo of the organization.", "type": "string" }, - "emails": { - "description": "The emails for contacting the organization.", - "type": "array", - "items": { "type": "string" } - }, - "phoneNumbers": { - "description": "The phone numbers for contacting for the organization.", - "type": "array", - "items": { "type": "string" } - }, - "parentUrl": { - "description": "The URL of the parent organization if it has one (for example if it has been acquired).", - "type": "string" - }, - "tags": { - "description": "Tags associated with the organization.", - "type": "array", - "items": { "type": "string" } - }, - "twitter": { - "description": "Twitter information for the organization.", - "type": "object", - "properties": { - "handle": { - "description": "The Twitter handle for the organization.", - "type": "string" - }, - "id": { "description": "The Twitter ID for the organization.", "type": "string" }, - "bio": { "description": "The Twitter bio for the organization.", "type": "string" }, - "followers": { - "description": "The number of followers on Twitter.", - "type": "integer" - }, - "location": { - "description": "The Twitter location for the organization.", - "type": "string" - }, - "site": { - "description": "The website linked to the organization's Twitter profile.", - "type": "string" - }, - "avatar": { - "description": "The URL for the organization's Twitter avatar.", - "type": "string" - } - } - }, - "employees": { - "description": "The number of employees of the organization.", - "type": "integer" - }, - "revenueRange": { - "description": "The estimated revenue range of the organization.", - "type": "object", - "properties": { - "min": { - "description": "The minimum estimated revenue of the organization.", - "type": "integer" - }, - "max": { - "description": "The maximum estimated revenue of the organization.", - "type": "integer" - } - } - }, - "linkedin": { - "description": "LinkedIn information for the organization.", - "type": "object", - "properties": { - "handle": { - "description": "The LinkedIn handle for the organization.", - "type": "string" - } - } - }, - "crunchbase": { - "description": "Crunchbase information for the organization.", - "type": "object", - "properties": { - "handle": { - "description": "The Crunchbase handle for the organization.", - "type": "string" - } - } - }, - "activeOn": { - "description": "List of platforms the organization members are active on.", - "type": "array", - "items": { "type": "string" } - }, - "identities": { - "description": "List of platforms the organization members have identities in.", - "type": "array", - "items": { "type": "string" } - }, - "memberCount": { - "description": "Number of members organization has.", - "type": "integer" - }, - "createdAt": { - "description": "Date the organization was created.", - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "description": "Date the organization was last updated.", - "type": "string", - "format": "date-time" - } - }, - "xml": { "name": "Organization" } - }, - "OrganizationQuery": { - "description": "All the parameters you can use to query organizations.", - "properties": { - "filter": { - "description": "Filter. Please refer to filter docs.", - "type": "string", - "format": "blob" - }, - "orderBy": { - "type": "string", - "enum": [ - "createdAt_ASC", - "createdAt_DESC", - "memberCount_ASC", - "memberCount_DESC", - "activityCount_ASC", - "activityCount_DESC", - "joinedAt_ASC", - "joinedAt_DESC", - "lastActive_ASC", - "lastActive_DESC" - ] - }, - "limit": { - "description": "Limit the number of records returned. Default is 10.", - "type": "integer", - "minimum": 1, - "maximum": 200, - "default": 10 - }, - "offset": { - "description": "Offset the number of records returned. Default is 0.", - "type": "integer", - "minimum": 0, - "default": 0 - } - } - }, - "Organization": { - "type": "object", - "allOf": [{ "$ref": "#/components/schemas/OrganizationNoId" }], - "properties": { - "id": { "description": "The ID of the organization." }, - "body": { - "description": "The body of the organization.", - "type": "string", - "format": "blob" - } - } - }, - "OrganizationRelationsResponse": { - "description": "Relations of a organization.", - "type": "object", - "properties": { - "members": { - "description": "Members associated with the organization.", - "type": "array", - "items": { "$ref": "#/components/schemas/Member" } - }, - "activeOn": { - "description": "The platforms where the organization is active.", - "type": "array", - "items": { "type": "string" } - }, - "identities": { - "description": "The list of identities of the members in the organization.", - "type": "array", - "items": { "type": "string" } - }, - "lastActive": { - "description": "The last time the organization was active.", - "type": "string", - "format": "date-time" - }, - "joinedAt": { - "description": "The date the first member from the organization joined the community.", - "type": "string", - "format": "date-time" - } - } - }, - "OrganizationResponse": { - "description": "A organization of your community.", - "type": "object", - "allOf": [ - { "$ref": "#/components/schemas/Organization" }, - { "$ref": "#/components/schemas/OrganizationRelationsResponse" } - ] - }, - "OrganizationList": { - "description": "List and count of organizations.", - "type": "object", - "properties": { - "rows": { - "description": "List of organizations", - "type": "array", - "items": { "$ref": "#/components/schemas/OrganizationResponse" } - }, - "count": { "description": "Count", "type": "integer" }, - "limit": { "description": "Limit of records returned", "type": "integer" }, - "offset": { "description": "Offset, for pagination", "type": "integer" } - }, - "xml": { "name": "OrganizationsList" } - }, - "TagNoId": { - "description": "A tag associated with a member.", - "type": "object", - "required": ["name", "tenantId"], - "properties": { - "name": { "description": "The name of the tag", "type": "string" }, - "createdAt": { - "description": "Date the tag was created", - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "description": "Date the tag was last updated", - "type": "string", - "format": "date-time" - }, - "tenantId": { - "description": "Your workspace/tenant id", - "type": "string", - "format": "uuid" - } - }, - "xml": { "name": "Tag" } - }, - "Tag": { - "type": "object", - "allOf": [{ "$ref": "#/components/schemas/TagNoId" }], - "properties": { "id": { "description": "The unique identifier for a tag." } } - }, - "TagList": { - "description": "List and count of tags.", - "type": "object", - "properties": { - "rows": { - "description": "List of tags", - "type": "array", - "items": { "$ref": "#/components/schemas/Tag" } - }, - "count": { "description": "Count", "type": "integer" }, - "limit": { "description": "Limit of records returned", "type": "integer" }, - "offset": { "description": "Offset, for pagination", "type": "integer" } - }, - "xml": { "name": "TagsList" } - }, - "TaskInputRelations": { - "type": "object", - "properties": { - "members": { - "description": "Members associated with the task. Each element in the array is the ID of the member.", - "type": "array", - "items": { "type": "string", "format": "uuid" } - }, - "activities": { - "description": "Activities associated with the task. Each element in the array is the ID of the activity.", - "type": "array", - "items": { "type": "string", "format": "uuid" } - }, - "assignees": { - "description": "Users assigned with the task. Each element in the array is the ID of the user.", - "type": "string", - "format": "uuid", - "default": null - } - } - }, - "TaskInput": { - "allOf": [ - { "$ref": "#/components/schemas/TaskNoId" }, - { "$ref": "#/components/schemas/TaskInputRelations" } - ] - }, - "TaskBatchInput": { - "type": "object", - "properties": { - "operation": { - "description": "Batch operation name.", - "type": "string", - "enum": ["findAndUpdateAll"] - }, - "payload": { - "type": "object", - "description": "Payload to send to the batch operation", - "properties": { - "filter": { - "description": "Filter to select the task entities. Please refer to filter docs.", - "type": "string", - "format": "blob" - }, - "update": { - "description": "key value object with desired updated fields.", - "type": "object" - } - } - } - } - }, - "TaskNoId": { - "description": "A created task.", - "type": "object", - "properties": { - "name": { "description": "The name of the task.", "type": "string" }, - "body": { "description": "The body of the task.", "type": "string", "format": "blob" }, - "status": { - "description": "The status of the task.", - "type": "string", - "enum": ["in-progress", "done"], - "default": null - }, - "createdAt": { - "description": "Date the task was created.", - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "description": "Date the task was last updated.", - "type": "string", - "format": "date-time" - } - }, - "xml": { "name": "Task" } - }, - "TaskQuery": { - "description": "All the parameters you can use to query tasks.", - "properties": { - "filter": { - "description": "Filter. Please refer to filter docs.", - "type": "string", - "format": "blob" - }, - "orderBy": { "type": "string", "enum": ["createdAt_ASC", "createdAt_DESC"] }, - "limit": { - "description": "Limit the number of records returned. Default is 10.", - "type": "integer", - "minimum": 1, - "maximum": 200, - "default": 10 - }, - "offset": { - "description": "Offset the number of records returned. Default is 0.", - "type": "integer", - "minimum": 0, - "default": 0 - } - } - }, - "Task": { - "type": "object", - "allOf": [{ "$ref": "#/components/schemas/TaskNoId" }], - "properties": { - "id": { "description": "The ID of the task." }, - "body": { "description": "The body of the task.", "type": "string", "format": "blob" } - } - }, - "TaskRelationsResponse": { - "description": "Relations of a task.", - "type": "object", - "properties": { - "members": { - "description": "Members associated with the task.", - "type": "array", - "items": { "$ref": "#/components/schemas/Member" } - }, - "activities": { - "description": "Activities associated with the task.", - "type": "array", - "items": { "$ref": "#/components/schemas/Activity" } - }, - "assignedTo": { - "description": "The workspace member assigned to the task.", - "$ref": "#/components/schemas/Member" - } - } - }, - "TaskResponse": { - "description": "A task of your community.", - "type": "object", - "allOf": [ - { "$ref": "#/components/schemas/Task" }, - { "$ref": "#/components/schemas/TaskRelationsResponse" } - ] - }, - "TaskList": { - "description": "List and count of tasks.", - "type": "object", - "properties": { - "rows": { - "description": "List of tasks", - "type": "array", - "items": { "$ref": "#/components/schemas/TaskResponse" } - }, - "count": { "description": "Count", "type": "integer" }, - "limit": { "description": "Limit of records returned", "type": "integer" }, - "offset": { "description": "Offset, for pagination", "type": "integer" } - }, - "xml": { "name": "TasksList" } - }, - "TaskFindAndUpdateAll": { - "description": "Returns number of tasks updated", - "type": "object", - "properties": { - "rowsUpdated": { "description": "Number of tasks updated", "type": "integer" } - } - }, - "ActivityTypesCreateInput": { - "description": "An activity type.", - "properties": { - "type": { - "description": "Human-friendly type of the activity. Default and short displays will set to this and key will be generated using this value." - } - } - }, - "ActivityTypesUpdateInput": { - "description": "An activity type.", - "properties": { - "type": { - "description": "Human-friendly type of the activity. Default and short displays will set to this and key will be generated using this value." - } - } - }, - "ActivityTypeDisplayOptions": { - "type": "object", - "required": ["default", "short", "channel"], - "description": "Activity type display options.", - "properties": { - "default": { - "description": "Default display of an activity type. Used in the activity module in the app.", - "type": "string" - }, - "short": { - "description": "Short display version of an activity type. Used in the member list -> last activity.", - "type": "string" - }, - "channel": { - "description": "Channel display of an activity type. Used in Dashboard -> trending conversations.", - "type": "string" - } - }, - "xml": { "name": "ActivityTypeDisplayOptions" } - }, - "ActivityTypes": { - "type": "object", - "properties": { - "custom": { - "type": "object", - "description": "Custom activity types defined by the user.", - "additionalProperties": { "$ref": "#/components/schemas/ActivityTypeDisplayOptions" } - }, - "default": { - "type": "object", - "description": "Default activity types used by the integrations.", - "additionalProperties": { "$ref": "#/components/schemas/ActivityTypeDisplayOptions" } - } - } - }, - "MemberAttributeSettingsCreateInput": { - "description": "A member attribute.", - "allOf": [{ "$ref": "#/components/schemas/MemberAttributeSettingsNoId" }] - }, - "MemberAttributeSettingsUpdateInput": { - "description": "A member attribute.", - "properties": { - "label": { - "description": "Human-friendly name of the attribute. Label is unique in workspaces.", - "type": "string" - }, - "show": { - "description": "Whether to show the member attribute in the web app or not.", - "type": "boolean", - "default": true - } - } - }, - "MemberAttributeSettingsNoId": { - "type": "object", - "required": ["label", "type"], - "description": "A member attribute that can be created dynamically.", - "properties": { - "label": { - "description": "Human-friendly name of the attribute. Label is unique in workspaces.", - "type": "string" - }, - "name": { - "description": "Camel-case code friendly name of the attribute. If ommited, name will be generated from the label. Name is unique in workspaces.", - "type": "string" - }, - "type": { - "description": "Type of the attribute's value", - "type": "string", - "enum": ["boolean", "number", "email", "string", "url", "date"] - }, - "canDelete": { - "description": "If set to false, member attribute can not be deleted in future requests.", - "type": "boolean", - "default": false - }, - "show": { - "description": "Whether to show the member attribute in the web app or not.", - "type": "boolean", - "default": true - }, - "createdAt": { - "description": "Date the member attribute was created.", - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "description": "Date the member attribute was last updated.", - "type": "string", - "format": "date-time" - } - }, - "xml": { "name": "MemberAttributeSettings" } - }, - "MemberAttributeSettings": { - "type": "object", - "allOf": [{ "$ref": "#/components/schemas/MemberAttributeSettingsNoId" }], - "properties": { "id": { "description": "The attribute settings ID." } } - }, - "MemberAttributeSettingsList": { - "description": "List and count member attribute settings.", - "type": "object", - "properties": { - "rows": { - "description": "List of member attribute settings", - "type": "array", - "items": { "$ref": "#/components/schemas/MemberAttributeSettings" } - }, - "count": { "description": "Count", "type": "integer" }, - "limit": { "description": "Limit of records returned", "type": "integer" }, - "offset": { "description": "Offset, for pagination", "type": "integer" } - }, - "xml": { "name": "MemberAttributeSettingsList" } - } - }, - "examples": { - "ActivityUpsert": { - "value": { - "id": "782b426d-adc8-4fb4-a4ee-ab0bb07ffca0", - "type": "message", - "timestamp": "2020-05-27T15:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "1234", - "sourceParentId": null, - "attributes": { "reactions": 43 }, - "channel": "dev", - "body": "It's not magic. It's talend and sweat.", - "title": null, - "url": "discord.gg/1234", - "sentiment": { - "label": "negative", - "mixed": 1.1410574428737164, - "neutral": 11.00325882434845, - "negative": 85.99738478660583, - "positive": 1.8582981079816818, - "sentiment": 2 - }, - "importHash": null, - "createdAt": "2022-10-03T15:18:11.294Z", - "updatedAt": "2022-10-03T15:21:49.402Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": null, - "parentId": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": -1, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-03T15:17:27.073Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - "parent": null, - "tasks": [] - } - }, - "ActivityFind": { - "value": { - "id": "462ddc6b-5672-43b2-9018-4e3fd7332228", - "type": "pull_request-closed", - "timestamp": "2021-07-27T20:20:30.000Z", - "platform": "github", - "isContribution": true, - "score": 10, - "sourceId": "gh_1", - "sourceParentId": null, - "attributes": {}, - "channel": "piedpiper", - "body": "Last one to finish the code sprint! But I will have fewer bugs than Gilfoyle.", - "title": "Code sprint over!", - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "positive", - "mixed": 0.7594161201268435, - "neutral": 39.13898766040802, - "negative": 12.336093187332153, - "positive": 47.76550233364105, - "sentiment": 79 - }, - "createdAt": "2022-10-03T15:36:43.775Z", - "updatedAt": "2022-10-03T15:39:38.199Z", - "deletedAt": null, - "memberId": "2effc566-1932-44f3-a821-2d692933a953", - "conversationId": "291af008-7717-457e-9242-f5c507c8987b", - "parentId": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "2effc566-1932-44f3-a821-2d692933a953", - "username": { "github": "dinesh", "twitter": "dinesh.chugtai" }, - "attributes": { - "bio": { - "github": "Lead developer at Pied Piper", - "default": "Pakistani Denzel. Tesla and gold chain owner.", - "twitter": "Pakistani Denzel. Tesla and gold chain owner." - }, - "url": { - "github": "https://github.com/dinesh", - "default": "https://t.co/d", - "twitter": "https://t.co/d" - }, - "location": { - "custom": "Silicon Valley", - "github": "Palo alto", - "default": "Silicon Valley" - } - }, - "displayName": "Dinesh", - "email": "dinesh@piedpiper.io", - "score": 9, - "joinedAt": "2022-10-03T15:30:55.672Z", - "importHash": null, - "reach": { "total": 100, "github": 60, "twitter": 40 }, - "createdAt": "2022-10-03T15:30:55.679Z", - "updatedAt": "2022-10-03T15:30:55.679Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - "parent": null, - "tasks": [] - } - }, - "ActivityFind2": { - "value": { - "id": "73aa13b7-1ef9-4987-a273-e560edff94ca", - "type": "pull_request-comment", - "timestamp": "2021-07-27T20:22:30.000Z", - "platform": "github", - "isContribution": true, - "score": 3, - "sourceId": "gh_2", - "sourceParentId": "gh_1", - "attributes": {}, - "channel": "piedpiper", - "body": "I will never underestimate my talents again.", - "title": null, - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "positive", - "mixed": 14.308956265449524, - "neutral": 14.437079429626465, - "negative": 9.826807677745819, - "positive": 61.42715811729431, - "sentiment": 86 - }, - "importHash": null, - "createdAt": "2022-10-03T15:38:05.847Z", - "updatedAt": "2022-10-03T15:46:34.610Z", - "deletedAt": null, - "memberId": "2effc566-1932-44f3-a821-2d692933a953", - "conversationId": "291af008-7717-457e-9242-f5c507c8987b", - "parentId": "462ddc6b-5672-43b2-9018-4e3fd7332228", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "2effc566-1932-44f3-a821-2d692933a953", - "username": { "github": "dinesh", "twitter": "dinesh.chugtai" }, - "attributes": { - "bio": { - "github": "Lead developer at Pied Piper", - "default": "Pakistani Denzel. Tesla and gold chain owner.", - "twitter": "Pakistani Denzel. Tesla and gold chain owner." - }, - "url": { - "github": "https://github.com/dinesh", - "default": "https://t.co/d", - "twitter": "https://t.co/d" - }, - "location": { - "custom": "Silicon Valley", - "github": "Palo alto", - "default": "Silicon Valley" - } - }, - "displayName": "Dinesh", - "email": "dinesh@piedpiper.io", - "score": -1, - "joinedAt": "2022-10-03T15:30:55.672Z", - "importHash": null, - "reach": { "total": 100, "github": 60, "twitter": 40 }, - "createdAt": "2022-10-03T15:30:55.679Z", - "updatedAt": "2022-10-03T15:30:55.679Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - "parent": { - "id": "462ddc6b-5672-43b2-9018-4e3fd7332228", - "type": "pull_request-closed", - "timestamp": "2021-07-27T20:22:30.000Z", - "platform": "github", - "isContribution": true, - "score": 10, - "sourceId": "gh_1", - "sourceParentId": null, - "attributes": {}, - "channel": "piedpiper", - "body": "Last one to finish the code sprint! But I will have less bugs than Gilfoyle.", - "title": "Code sprint over!", - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "positive", - "mixed": 0.7594161201268435, - "neutral": 39.13898766040802, - "negative": 12.336093187332153, - "positive": 47.76550233364105, - "sentiment": 79 - }, - "importHash": null, - "createdAt": "2022-10-03T15:36:43.775Z", - "updatedAt": "2022-10-03T15:39:38.199Z", - "deletedAt": null, - "memberId": "2effc566-1932-44f3-a821-2d692933a953", - "conversationId": "291af008-7717-457e-9242-f5c507c8987b", - "parentId": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - "tasks": [] - } - }, - "ActivityFind3": { - "value": { - "id": "2dcbe40e-36e0-4929-ab21-a30467fd9a65", - "type": "pull_request-comment", - "timestamp": "2021-07-27T20:23:30.000Z", - "platform": "github", - "isContribution": true, - "score": 3, - "sourceId": "gh_3", - "sourceParentId": "gh_1", - "attributes": {}, - "channel": "piedpiper", - "body": "Don't worry. I will continue to do it for you.", - "title": null, - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "positive", - "mixed": 2.9098065569996834, - "neutral": 25.578168034553528, - "negative": 2.241993509232998, - "positive": 69.27002668380737, - "sentiment": 97 - }, - "importHash": null, - "createdAt": "2022-10-03T15:47:20.151Z", - "updatedAt": "2022-10-03T15:47:20.220Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "291af008-7717-457e-9242-f5c507c8987b", - "parentId": "462ddc6b-5672-43b2-9018-4e3fd7332228", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": -1, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-03T15:17:27.073Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - "parent": { - "id": "462ddc6b-5672-43b2-9018-4e3fd7332228", - "type": "pull_request-closed", - "timestamp": "2021-07-27T20:22:30.000Z", - "platform": "github", - "isContribution": true, - "score": 10, - "sourceId": "gh_1", - "sourceParentId": null, - "attributes": {}, - "channel": "piedpiper", - "body": "Last one to finish the code sprint! But I will have less bugs than Gilfoyle.", - "title": "Code sprint over!", - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "positive", - "mixed": 0.7594161201268435, - "neutral": 39.13898766040802, - "negative": 12.336093187332153, - "positive": 47.76550233364105, - "sentiment": 79 - }, - "importHash": null, - "createdAt": "2022-10-03T15:36:43.775Z", - "updatedAt": "2022-10-03T15:39:38.199Z", - "deletedAt": null, - "memberId": "2effc566-1932-44f3-a821-2d692933a953", - "conversationId": "291af008-7717-457e-9242-f5c507c8987b", - "parentId": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - "tasks": [] - } - }, - "ActivityList": { - "value": { - "rows": [ - { "$ref": "#/components/examples/ActivityFind" }, - { "$ref": "#/components/examples/ActivityFind2" }, - { "$ref": "#/components/examples/ActivityFind3" } - ], - "count": 3, - "limit": 10, - "offset": 0 - } - }, - "Automation": { - "value": { - "id": "b3297f3b-6924-4e92-80e7-ef2e0d87a120", - "type": "webhook", - "tenantId": "a3297f3b-6924-4e92-80e7-ef2e0d87a120", - "trigger": "new_activity", - "settings": { "url": "https://webhook.url/new_activities" }, - "createdAt": "2022-03-29T09:22:31.989Z" - } - }, - "AutomationPage": { - "value": { - "count": 1, - "offset": 0, - "limit": 10, - "rows": [ - { - "id": "b3297f3b-6924-4e92-80e7-ef2e0d87a120", - "type": "webhook", - "tenantId": "a3297f3b-6924-4e92-80e7-ef2e0d87a120", - "trigger": "new_activity", - "settings": { "url": "https://webhook.url/new_activities" }, - "createdAt": "2022-03-29T09:22:31.989Z" - } - ] - } - }, - "AutomationExecutionPage": { - "value": { - "count": 1, - "offset": 0, - "limit": 10, - "rows": [ - { - "id": "b3297f3b-6924-4e92-80e7-ef2e0d87a120", - "automationId": "a3297f3b-6924-4e92-80e7-ef2e0d87a120", - "state": "success", - "executedAt": "2022-03-29T09:22:31.989Z", - "eventId": "a3297f3b-6924-4e92-80e7-ef2e0d87a121", - "payload": [ - { - "id": "a3297f3b-6924-4e92-80e7-ef2e0d87a121", - "type": "comment", - "timestamp": "2022-03-29T09:22:31.989Z", - "platform": "twitter" - } - ] - } - ] - } - }, - "Conversation": { - "value": { - "id": "24bdea79-3125-4950-bb38-07fa4a555012", - "title": "Best of dinesh and Gilfoyle", - "slug": "best-of-dinesh-and-gilfoyle", - "published": true, - "createdAt": "2022-10-05T12:21:53.271Z", - "updatedAt": "2022-10-05T12:21:53.271Z", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "activities": [ - { - "id": "89a136ed-336d-4586-8842-790775465212", - "type": "message", - "timestamp": "2020-06-27T14:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "d42", - "sourceParentId": null, - "attributes": {}, - "channel": "piedpiper", - "body": "Sooner or later Gilfoyle's servers are going to fail and then it's all done", - "title": null, - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "negative", - "mixed": 3.6482997238636017, - "neutral": 19.5749893784523, - "negative": 75.36468505859375, - "positive": 1.4120269566774368, - "sentiment": 2 - }, - "importHash": null, - "createdAt": "2022-10-05T12:09:44.414Z", - "updatedAt": "2022-10-05T12:21:53.279Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "24bdea79-3125-4950-bb38-07fa4a555012", - "parentId": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - }, - { - "id": "c39dc046-da1d-4a25-8624-6b78aad00f30", - "type": "message", - "timestamp": "2020-06-27T15:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "2345", - "sourceParentId": "1234", - "attributes": { "reactions": 68 }, - "channel": "dev", - "body": "My servers could handle 10x the traffic, if they weren't busy apologizing for your sh*t codebase.", - "title": null, - "url": "discord.gg/2345", - "sentiment": { - "label": "negative", - "mixed": 5.963129922747612, - "neutral": 20.673033595085144, - "negative": 69.99874711036682, - "positive": 3.365083411335945, - "sentiment": 5 - }, - "importHash": null, - "createdAt": "2022-10-03T15:19:30.415Z", - "updatedAt": "2022-10-05T12:21:53.279Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "24bdea79-3125-4950-bb38-07fa4a555012", - "parentId": "782b426d-adc8-4fb4-a4ee-ab0bb07ffca0", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - } - ], - "conversationStarter": { - "id": "89a136ed-336d-4586-8842-790775465212", - "type": "message", - "timestamp": "2020-06-27T14:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "d42", - "sourceParentId": null, - "attributes": {}, - "channel": "piedpiper", - "body": "Sooner or later Gilfoyle's servers are going to fail and then it's all done", - "title": null, - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "negative", - "mixed": 3.6482997238636017, - "neutral": 19.5749893784523, - "negative": 75.36468505859375, - "positive": 1.4120269566774368, - "sentiment": 2 - }, - "importHash": null, - "createdAt": "2022-10-05T12:09:44.414Z", - "updatedAt": "2022-10-05T12:21:53.279Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "24bdea79-3125-4950-bb38-07fa4a555012", - "parentId": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - }, - "activityCount": 2, - "memberCount": 2, - "platform": "discord", - "channel": "piedpiper", - "lastActive": "2020-06-27T15:13:30.000Z" - } - }, - "ConversationList": { - "value": { - "rows": [ - { - "id": "291af008-7717-457e-9242-f5c507c8987b", - "title": "Code sprint over!", - "slug": "code-sprint-over", - "published": false, - "createdAt": "2022-10-03T15:38:05.900Z", - "updatedAt": "2022-10-03T15:38:05.900Z", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "platform": "github", - "activityCount": 3, - "lastActive": "2021-07-27T20:23:30.000Z", - "conversationStarter": { - "id": "89a136ed-336d-4586-8842-790775465212", - "type": "message", - "timestamp": "2020-06-27T14:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "d42", - "sourceParentId": null, - "attributes": {}, - "channel": "piedpiper", - "body": "Sooner or later Gilfoyle's servers are going to fail and then it's all done", - "title": null, - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "negative", - "mixed": 3.6482997238636017, - "neutral": 19.5749893784523, - "negative": 75.36468505859375, - "positive": 1.4120269566774368, - "sentiment": 2 - }, - "importHash": null, - "createdAt": "2022-10-05T12:09:44.414Z", - "updatedAt": "2022-10-05T12:21:53.279Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "24bdea79-3125-4950-bb38-07fa4a555012", - "parentId": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - }, - "lastReplies": [ - { - "id": "c39dc046-da1d-4a25-8624-6b78aad00f30", - "type": "message", - "timestamp": "2020-06-27T15:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "2345", - "sourceParentId": "1234", - "attributes": { "reactions": 68 }, - "channel": "dev", - "body": "My servers could handle 10x the traffic, if they weren't busy apologizing for your sh*t codebase.", - "title": null, - "url": "discord.gg/2345", - "sentiment": { - "label": "negative", - "mixed": 5.963129922747612, - "neutral": 20.673033595085144, - "negative": 69.99874711036682, - "positive": 3.365083411335945, - "sentiment": 5 - }, - "importHash": null, - "createdAt": "2022-10-03T15:19:30.415Z", - "updatedAt": "2022-10-05T12:21:53.279Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "24bdea79-3125-4950-bb38-07fa4a555012", - "parentId": "782b426d-adc8-4fb4-a4ee-ab0bb07ffca0", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - } - ], - "memberCount": 2, - "channel": null - }, - { - "id": "24bdea79-3125-4950-bb38-07fa4a555012", - "title": "Best of dinesh and Gilfoyle", - "slug": "best-of-dinesh-and-gilfoyle", - "published": true, - "createdAt": "2022-10-05T12:21:53.271Z", - "updatedAt": "2022-10-05T12:21:53.271Z", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "platform": "discord", - "activityCount": 1, - "lastActive": "2020-06-29T15:13:30.000Z", - "conversationStarter": { - "id": "89a136ed-336d-4586-8842-790775465212", - "type": "message", - "timestamp": "2020-05-27T14:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "d42", - "sourceParentId": null, - "attributes": {}, - "channel": "piedpiper", - "body": "Best of Dinesh and gilfoyle", - "title": null, - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "negative", - "mixed": 1.6482997238636017, - "neutral": 12.5749893784523, - "negative": 62.36468505859375, - "positive": 1.4120269566774368, - "sentiment": 2 - }, - "importHash": null, - "createdAt": "2022-10-05T12:09:44.414Z", - "updatedAt": "2022-10-05T12:21:53.279Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "24bdea79-3125-4950-bb38-07fa4a555012", - "parentId": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - }, - "lastReplies": [ - { - "id": "c39dc046-da1d-4a25-8624-6b78aad00f30", - "type": "message", - "timestamp": "2020-06-29T15:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "2345", - "sourceParentId": "1234", - "attributes": { "reactions": 68 }, - "channel": "dev", - "body": "A very last reply to the conversation.", - "title": null, - "url": "discord.gg/2345", - "sentiment": { - "label": "negative", - "mixed": 5.963129922747612, - "neutral": 20.673033595085144, - "negative": 69.99874711036682, - "positive": 3.365083411335945, - "sentiment": 5 - }, - "importHash": null, - "createdAt": "2022-10-03T15:19:30.415Z", - "updatedAt": "2022-10-05T12:21:53.279Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "24bdea79-3125-4950-bb38-07fa4a555012", - "parentId": "782b426d-adc8-4fb4-a4ee-ab0bb07ffca0", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - } - ], - "memberCount": 2, - "channel": "dev" - } - ], - "count": 2, - "limit": 10, - "offset": 0 - } - }, - "MemberUpsert": { - "value": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": -1, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-03T15:17:27.073Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - }, - "MemberFind": { - "value": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "activities": [ - { - "id": "2dcbe40e-36e0-4929-ab21-a30467fd9a65", - "type": "pull_request-comment", - "timestamp": "2021-07-27T20:23:30.000Z", - "platform": "github", - "isContribution": true, - "score": 3, - "sourceId": "gh_3", - "sourceParentId": "gh_1", - "attributes": {}, - "channel": "piedpiper", - "body": "Don't worry. I will continue to do it for you.", - "title": null, - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "positive", - "mixed": 2.9098065569996834, - "neutral": 25.578168034553528, - "negative": 2.241993509232998, - "positive": 69.27002668380737, - "sentiment": 97 - }, - "importHash": null, - "createdAt": "2022-10-03T15:47:20.151Z", - "updatedAt": "2022-10-03T15:47:20.220Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "291af008-7717-457e-9242-f5c507c8987b", - "parentId": "462ddc6b-5672-43b2-9018-4e3fd7332228", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - { - "id": "c39dc046-da1d-4a25-8624-6b78aad00f30", - "type": "message", - "timestamp": "2020-06-27T15:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "2345", - "sourceParentId": "1234", - "attributes": { "reactions": 68 }, - "channel": "dev", - "body": "My servers could handle 10x the traffic, if they weren't busy apologizing for your sh*t codebase.", - "title": null, - "url": "discord.gg/2345", - "sentiment": { - "label": "negative", - "mixed": 5.963129922747612, - "neutral": 20.673033595085144, - "negative": 69.99874711036682, - "positive": 3.365083411335945, - "sentiment": 5 - }, - "importHash": null, - "createdAt": "2022-10-03T15:19:30.415Z", - "updatedAt": "2022-10-03T15:26:02.599Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": null, - "parentId": "782b426d-adc8-4fb4-a4ee-ab0bb07ffca0", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - { - "id": "782b426d-adc8-4fb4-a4ee-ab0bb07ffca0", - "type": "message", - "timestamp": "2020-05-27T15:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "1234", - "sourceParentId": null, - "attributes": { "reactions": 43 }, - "channel": "dev", - "body": "It's not magic. It's talend and sweat.", - "title": null, - "url": "discord.gg/1234", - "sentiment": { - "label": "negative", - "mixed": 1.1410574428737164, - "neutral": 11.00325882434845, - "negative": 85.99738478660583, - "positive": 1.8582981079816818, - "sentiment": 2 - }, - "importHash": null, - "createdAt": "2022-10-03T15:18:11.294Z", - "updatedAt": "2022-10-03T15:21:49.402Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": null, - "parentId": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ], - "lastActivity": { - "id": "2dcbe40e-36e0-4929-ab21-a30467fd9a65", - "type": "pull_request-comment", - "timestamp": "2021-07-27T20:23:30.000Z", - "platform": "github", - "isContribution": true, - "score": 3, - "sourceId": "gh_3", - "sourceParentId": "gh_1", - "attributes": {}, - "channel": "piedpiper", - "body": "Don't worry. I will continue to do it for you.", - "title": null, - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "positive", - "mixed": 2.9098065569996834, - "neutral": 25.578168034553528, - "negative": 2.241993509232998, - "positive": 69.27002668380737, - "sentiment": 97 - }, - "importHash": null, - "createdAt": "2022-10-03T15:47:20.151Z", - "updatedAt": "2022-10-03T15:47:20.220Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "291af008-7717-457e-9242-f5c507c8987b", - "parentId": "462ddc6b-5672-43b2-9018-4e3fd7332228", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - "lastActive": "2021-07-27T20:23:30.000Z", - "activityCount": 3, - "averageSentiment": 34.67, - "tags": [ - { - "id": "38807625-6302-47b5-9f35-58566ddec83b", - "name": "developer", - "importHash": null, - "createdAt": "2022-10-05T11:41:20.162Z", - "updatedAt": "2022-10-05T11:41:20.162Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - { - "id": "dca36c33-38cd-4e68-8ba8-515167e00971", - "name": "attended-hooli-con", - "importHash": null, - "createdAt": "2022-10-05T11:42:17.414Z", - "updatedAt": "2022-10-05T11:42:17.414Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ], - "organizations": [ - { - "id": "31bff99a-2eac-49f5-b015-cba95aa6e530", - "name": "Pied Piper", - "url": "https://piedpiper.io", - "description": "The new internet", - "parentUrl": null, - "emails": ["richard@piedpiper.io", "hello@piedpiper.io"], - "phoneNumbers": null, - "logo": null, - "tags": ["new-internet", "making-the-world-a-better-place", "not-like-hooli"], - "twitter": { - "bio": "The internet we deserve", - "handle": "PiedPiper", - "location": "The valley", - "followers": 5000, - "following": 20 - }, - "linkedin": { "handle": "company/PiedPiper" }, - "crunchbase": { "handle": "company/PiedPiper" }, - "employees": 50, - "revenueRange": { "max": 50, "min": 10 }, - "importHash": null, - "createdAt": "2022-10-03T16:15:21.812Z", - "updatedAt": "2022-10-03T16:15:21.812Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ], - "tasks": [], - "notes": [], - "noMerge": [], - "toMerge": [] - } - }, - "MemberList": { - "value": { - "rows": [ - { - "id": "2effc566-1932-44f3-a821-2d692933a953", - "username": { "github": "dinesh", "twitter": "dinesh.chugtai" }, - "attributes": { - "bio": { - "github": "Lead developer at Pied Piper", - "default": "Pakistani Denzel. Tesla and gold chain owner.", - "twitter": "Pakistani Denzel. Tesla and gold chain owner." - }, - "url": { - "github": "https://github.com/dinesh", - "default": "https://t.co/d", - "twitter": "https://t.co/d" - }, - "location": { - "custom": "Silicon Valley", - "github": "Palo alto", - "default": "Silicon Valley" - } - }, - "displayName": "Dinesh", - "email": "dinesh@piedpiper.io", - "score": 9, - "joinedAt": "2022-10-03T15:30:55.672Z", - "importHash": null, - "reach": { "total": 100, "github": 60, "twitter": 40 }, - "createdAt": "2022-10-03T15:30:55.679Z", - "updatedAt": "2022-10-05T11:39:58.095Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "identities": ["github", "twitter"], - "activeOn": ["github"], - "activityCount": "2", - "lastActive": "2021-07-27T20:22:30.000Z", - "averageSentiment": "82.50", - "noMerge": [], - "toMerge": [], - "lastActivity": { - "id": "73aa13b7-1ef9-4987-a273-e560edff94ca", - "type": "pull_request-comment", - "timestamp": "2021-07-27T20:22:30.000Z", - "platform": "github", - "isContribution": true, - "score": 3, - "sourceId": "gh_2", - "sourceParentId": "gh_1", - "attributes": {}, - "channel": "piedpiper", - "body": "I will never underestimate my talents again.", - "title": null, - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "positive", - "mixed": 14.308956265449524, - "neutral": 14.437079429626465, - "negative": 9.826807677745819, - "positive": 61.42715811729431, - "sentiment": 86 - }, - "importHash": null, - "createdAt": "2022-10-03T15:38:05.847Z", - "updatedAt": "2022-10-03T15:46:34.610Z", - "deletedAt": null, - "memberId": "2effc566-1932-44f3-a821-2d692933a953", - "conversationId": "291af008-7717-457e-9242-f5c507c8987b", - "parentId": "462ddc6b-5672-43b2-9018-4e3fd7332228", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - "organizations": [ - { - "id": "31bff99a-2eac-49f5-b015-cba95aa6e530", - "name": "Pied Piper", - "url": "https://piedpiper.io", - "description": "The new internet", - "parentUrl": null, - "emails": ["richard@piedpiper.io", "hello@piedpiper.io"], - "phoneNumbers": null, - "logo": null, - "tags": ["new-internet", "making-the-world-a-better-place", "not-like-hooli"], - "twitter": { - "bio": "The internet we deserve", - "handle": "PiedPiper", - "location": "The valley", - "followers": 5000, - "following": 20 - }, - "linkedin": { "handle": "company/PiedPiper" }, - "crunchbase": { "handle": "company/PiedPiper" }, - "employees": 50, - "revenueRange": { "max": 50, "min": 10 }, - "importHash": null, - "createdAt": "2022-10-03T16:15:21.812Z", - "updatedAt": "2022-10-03T16:15:21.812Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ], - "tags": [ - { - "id": "38807625-6302-47b5-9f35-58566ddec83b", - "name": "developer", - "importHash": null, - "createdAt": "2022-10-05T11:41:20.162Z", - "updatedAt": "2022-10-05T11:41:20.162Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ] - }, - { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "activityCount": "3", - "lastActive": "2021-07-27T20:23:30.000Z", - "averageSentiment": "34.67", - "noMerge": [], - "toMerge": [], - "lastActivity": { - "id": "2dcbe40e-36e0-4929-ab21-a30467fd9a65", - "type": "pull_request-comment", - "timestamp": "2021-07-27T20:23:30.000Z", - "platform": "github", - "isContribution": true, - "score": 3, - "sourceId": "gh_3", - "sourceParentId": "gh_1", - "attributes": {}, - "channel": "piedpiper", - "body": "Don't worry. I will continue to do it for you.", - "title": null, - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "positive", - "mixed": 2.9098065569996834, - "neutral": 25.578168034553528, - "negative": 2.241993509232998, - "positive": 69.27002668380737, - "sentiment": 97 - }, - "importHash": null, - "createdAt": "2022-10-03T15:47:20.151Z", - "updatedAt": "2022-10-03T15:47:20.220Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "291af008-7717-457e-9242-f5c507c8987b", - "parentId": "462ddc6b-5672-43b2-9018-4e3fd7332228", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - "organizations": [ - { - "id": "31bff99a-2eac-49f5-b015-cba95aa6e530", - "name": "Pied Piper", - "url": "https://piedpiper.io", - "description": "The new internet", - "parentUrl": null, - "emails": ["richard@piedpiper.io", "hello@piedpiper.io"], - "phoneNumbers": null, - "logo": null, - "tags": ["new-internet", "making-the-world-a-better-place", "not-like-hooli"], - "twitter": { - "bio": "The internet we deserve", - "handle": "PiedPiper", - "location": "The valley", - "followers": 5000, - "following": 20 - }, - "linkedin": { "handle": "company/PiedPiper" }, - "crunchbase": { "handle": "company/PiedPiper" }, - "employees": 50, - "revenueRange": { "max": 50, "min": 10 }, - "importHash": null, - "createdAt": "2022-10-03T16:15:21.812Z", - "updatedAt": "2022-10-03T16:15:21.812Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ], - "tags": [ - { - "id": "38807625-6302-47b5-9f35-58566ddec83b", - "name": "developer", - "importHash": null, - "createdAt": "2022-10-05T11:41:20.162Z", - "updatedAt": "2022-10-05T11:41:20.162Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - { - "id": "dca36c33-38cd-4e68-8ba8-515167e00971", - "name": "attended-hooli-con", - "importHash": null, - "createdAt": "2022-10-05T11:42:17.414Z", - "updatedAt": "2022-10-05T11:42:17.414Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ] - } - ], - "count": 2, - "offset": 0, - "limit": 10 - } - }, - "Note2": { - "value": { - "id": "39c850f6-fb96-4d16-8e8c-cd7072e33925", - "body": "Refused to have a user feedback call", - "importHash": null, - "createdAt": "2022-10-03T16:00:57.867Z", - "updatedAt": "2022-10-03T16:00:57.867Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "members": [ - { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": -1, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-03T15:17:27.073Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ] - } - }, - "Note": { - "value": { - "id": "196c07da-14e0-419e-bd9a-5f15c721a694", - "body": "Likes frunks", - "importHash": null, - "createdAt": "2022-10-05T11:58:30.977Z", - "updatedAt": "2022-10-05T11:58:30.977Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "members": [ - { - "id": "2effc566-1932-44f3-a821-2d692933a953", - "username": { "github": "dinesh", "twitter": "dinesh.chugtai" }, - "attributes": { - "bio": { - "github": "Lead developer at Pied Piper", - "default": "Pakistani Denzel. Tesla and gold chain owner.", - "twitter": "Pakistani Denzel. Tesla and gold chain owner." - }, - "url": { - "github": "https://github.com/dinesh", - "default": "https://t.co/d", - "twitter": "https://t.co/d" - }, - "location": { - "custom": "Silicon Valley", - "github": "Palo alto", - "default": "Silicon Valley" - } - }, - "displayName": "Dinesh", - "email": "dinesh@piedpiper.io", - "score": 9, - "joinedAt": "2022-10-03T15:30:55.672Z", - "importHash": null, - "reach": { "total": 100, "github": 60, "twitter": 40 }, - "createdAt": "2022-10-03T15:30:55.679Z", - "updatedAt": "2022-10-05T11:39:58.095Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ] - } - }, - "NoteList": { - "value": { - "rows": [ - { "$ref": "#/components/examples/Note" }, - { "$ref": "#/components/examples/Note2" } - ], - "count": 2, - "limit": 10, - "offset": 0 - } - }, - "OrganizationCreate": { - "value": { - "id": "31bff99a-2eac-49f5-b015-cba95aa6e530", - "name": "Pied Piper", - "url": "https://piedpiper.io", - "description": "The new internet", - "parentUrl": null, - "emails": ["richard@piedpiper.io", "hello@piedpiper.io"], - "phoneNumbers": null, - "logo": null, - "tags": ["new-internet", "making-the-world-a-better-place", "not-like-hooli"], - "twitter": { - "bio": "The internet we deserve", - "handle": "PiedPiper", - "location": "The valley", - "followers": 5000, - "following": 20 - }, - "linkedin": { "handle": "company/PiedPiper" }, - "crunchbase": { "handle": "company/PiedPiper" }, - "employees": 50, - "revenueRange": { "max": 50, "min": 10 }, - "importHash": null, - "createdAt": "2022-10-03T16:15:21.812Z", - "updatedAt": "2022-10-03T16:15:21.812Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "memberCount": 2, - "activityCount": 4 - } - }, - "Organization": { - "value": { - "id": "31bff99a-2eac-49f5-b015-cba95aa6e530", - "name": "Pied Piper", - "url": "https://piedpiper.io", - "description": "The new internet", - "parentUrl": null, - "emails": ["richard@piedpiper.io", "hello@piedpiper.io"], - "phoneNumbers": null, - "logo": null, - "tags": ["new-internet", "making-the-world-a-better-place", "not-like-hooli"], - "identities": ["github", "twitter"], - "activeOn": ["github"], - "lastActive": "2022-10-03T16:15:21.812Z", - "joinedAt": "2022-05-03T11:16:32.812Z", - "twitter": { - "bio": "The internet we deserve", - "handle": "PiedPiper", - "location": "The valley", - "followers": 5000, - "following": 20 - }, - "linkedin": { "handle": "company/PiedPiper" }, - "crunchbase": { "handle": "company/PiedPiper" }, - "employees": 50, - "revenueRange": { "max": 50, "min": 10 }, - "importHash": null, - "createdAt": "2022-10-03T16:15:21.812Z", - "updatedAt": "2022-10-03T16:15:21.812Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "memberCount": 2, - "activityCount": 4 - } - }, - "Organization2": { - "value": { - "id": "65257687-0bfa-498e-8b2f-53559f41522b", - "name": "Hooli", - "url": "https://hooli.xyz", - "description": "Hooli is an international corporation founded by Gavin Belson and Peter Gregory", - "parentUrl": null, - "emails": ["gavin@hooli.xyz"], - "phoneNumbers": null, - "logo": null, - "tags": ["hooli", "tethics", "not-google"], - "identities": ["devto", "github", "twitter"], - "activeOn": ["devto"], - "lastActive": "2022-10-04", - "joinedAt": "2020-01-30", - "twitter": { - "bio": "Hooli is an international corporation founded by Gavin Belson and Peter Gregory", - "handle": "hooli", - "location": "Menlo Park", - "followers": 500000, - "following": 0 - }, - "linkedin": { "handle": "company/Hooli" }, - "crunchbase": { "handle": "company/Hooli" }, - "employees": 4000, - "revenueRange": { "max": 500, "min": 100 }, - "importHash": null, - "createdAt": "2022-10-05T12:03:11.228Z", - "updatedAt": "2022-10-05T12:03:11.228Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "memberCount": 0, - "activityCount": 0 - } - }, - "OrganizationList": { - "value": { - "rows": [ - { "$ref": "#/components/examples/Organization" }, - { "$ref": "#/components/examples/Organization2" } - ], - "count": 2, - "limit": 10, - "offset": 0 - } - }, - "Tag": { - "value": { - "id": "dca36c33-38cd-4e68-8ba8-515167e00971", - "name": "attended-hooli-con", - "importHash": null, - "createdAt": "2022-10-05T11:42:17.414Z", - "updatedAt": "2022-10-05T11:42:17.414Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "members": [ - { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ] - } - }, - "Tag2": { - "value": { - "id": "38807625-6302-47b5-9f35-58566ddec83b", - "name": "developer", - "createdAt": "2022-10-05T11:41:20.162Z", - "updatedAt": "2022-10-05T11:41:20.162Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "members": [ - { - "id": "2effc566-1932-44f3-a821-2d692933a953", - "username": { "github": "dinesh", "twitter": "dinesh.chugtai" }, - "attributes": { - "bio": { - "github": "Lead developer at Pied Piper", - "default": "Pakistani Denzel. Tesla and gold chain owner.", - "twitter": "Pakistani Denzel. Tesla and gold chain owner." - }, - "url": { - "github": "https://github.com/dinesh", - "default": "https://t.co/d", - "twitter": "https://t.co/d" - }, - "location": { - "custom": "Silicon Valley", - "github": "Palo alto", - "default": "Silicon Valley" - } - }, - "displayName": "Dinesh", - "email": "dinesh@piedpiper.io", - "score": 9, - "joinedAt": "2022-10-03T15:30:55.672Z", - "reach": { "total": 100, "github": 60, "twitter": 40 }, - "createdAt": "2022-10-03T15:30:55.679Z", - "updatedAt": "2022-10-05T11:39:58.095Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ] - } - }, - "TagList": { - "value": { - "rows": [ - { "$ref": "#/components/examples/Tag" }, - { "$ref": "#/components/examples/Tag2" } - ], - "count": 2, - "limit": 10, - "offset": 0 - } - }, - "Task": { - "value": { - "id": "8a127785-f11d-4102-804d-5b79ccddd4cc", - "name": "Ask for tips on building a new Anton", - "body": null, - "status": null, - "dueDate": "2022-05-27T15:13:30.000Z", - "importHash": null, - "createdAt": "2022-10-03T16:00:18.701Z", - "updatedAt": "2022-10-03T16:00:18.701Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "assignedToId": null, - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "members": [ - { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": -1, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-03T15:17:27.073Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ], - "activities": [] - } - }, - "Task2": { - "value": { - "id": "ef22fb05-a41b-472e-9917-a4d10d19fcc6", - "name": "Ask if we can use as quote", - "body": null, - "status": null, - "dueDate": "2022-08-27T00:00:00.000Z", - "importHash": null, - "createdAt": "2022-10-05T11:55:55.606Z", - "updatedAt": "2022-10-05T11:55:55.606Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "assignedToId": null, - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "members": [], - "activities": [ - { - "id": "782b426d-adc8-4fb4-a4ee-ab0bb07ffca0", - "type": "message", - "timestamp": "2020-05-27T15:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "1234", - "sourceParentId": null, - "attributes": { "reactions": 43 }, - "channel": "dev", - "body": "It's not magic. It's talend and sweat.", - "title": null, - "url": "discord.gg/1234", - "sentiment": { - "label": "negative", - "mixed": 1.1410574428737164, - "neutral": 11.00325882434845, - "negative": 85.99738478660583, - "positive": 1.8582981079816818, - "sentiment": 2 - }, - "importHash": null, - "createdAt": "2022-10-03T15:18:11.294Z", - "updatedAt": "2022-10-03T15:21:49.402Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": null, - "parentId": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ] - } - }, - "TaskList": { - "value": { - "rows": [ - { "$ref": "#/components/examples/Task" }, - { "$ref": "#/components/examples/Task2" } - ], - "count": 2, - "limit": 10, - "offset": 0 - } - }, - "TaskFindAndUpdateAll": { "value": { "rowsUpdated": 5 } }, - "ActivityTypes": { - "value": { - "default": { - "github": { - "discussion-started": { - "default": "started a discussion in {channel}", - "short": "started a discussion", - "channel": "{channel}" - }, - "discussion-comment": { - "default": "commented on a discussion in {channel}", - "short": "commented on a discussion", - "channel": "{channel}" - }, - "fork": { "default": "forked {channel}", "short": "forked", "channel": "{channel}" }, - "issues-closed": { - "default": "closed an issue in {channel}", - "short": "closed an issue", - "channel": "{channel}" - }, - "issues-opened": { - "default": "opened a new issue in {channel}", - "short": "opened an issue", - "channel": "{channel}" - }, - "issue-comment": { - "default": "commented on an issue in {channel}", - "short": "commented on an issue", - "channel": "{channel}" - }, - "pull_request-closed": { - "default": "closed a pull request in {channel}", - "short": "closed a pull request", - "channel": "{channel}" - }, - "pull_request-opened": { - "default": "opened a new pull request in {channel}", - "short": "opened a pull request", - "channel": "{channel}" - }, - "pull_request-comment": { - "default": "commented on a pull request in {channel}", - "short": "commented on a pull request", - "channel": "{channel}" - }, - "star": { - "default": "starred {channel}", - "short": "starred", - "channel": "{channel}" - }, - "unstar": { - "default": "unstarred {channel}", - "short": "unstarred", - "channel": "{channel}" - } - }, - "devto": { - "comment": { - "default": "commented on {attributes.articleTitle}", - "short": "commented", - "channel": "{attributes.articleTitle}" - } - }, - "discord": { - "joined_guild": { - "default": "joined server", - "short": "joined server", - "channel": "" - }, - "message": { - "default": "sent a message in #{channel}", - "short": "sent a message", - "channel": "#{channel}" - }, - "thread_started": { - "default": "started a new thread", - "short": "started a new thread", - "channel": "" - }, - "thread_message": { - "default": "replied to a message in thread #{channel} -> {attributes.parentChannel}", - "short": "replied to a message", - "channel": "thread #{channel} -> #{attributes.parentChannel}" - } - }, - "hackernews": { - "comment": { - "default": "commented on {attributes.parentTitle}", - "short": "commented", - "channel": "{channel}" - }, - "post": { - "default": "posted mentioning {channel}", - "short": "posted", - "channel": "{channel}" - } - }, - "linkedin": { - "comment": { - "default": "commented on a post {attributes.postBody}", - "short": "commented", - "channel": "{attributes.postBody}" - }, - "message": { "default": "sent a message", "short": "sent a message", "channel": "" }, - "reaction": { - "default": "reacted with on a post {attributes.postBody}", - "short": "reacted", - "channel": "{attributes.postBody}" - } - }, - "reddit": { - "comment": { - "default": "commented in subreddit r/{channel}", - "short": "commented on a post", - "channel": "r/{channel}" - }, - "post": { - "default": "posted in subreddit r/{channel}", - "short": "posted in subreddit", - "channel": "r/{channel}" - } - }, - "slack": { - "channel_joined": { - "default": "joined channel {channel}", - "short": "joined channel", - "channel": "{channel}" - }, - "message": { - "default": "sent a message in {channel}", - "short": "sent a message", - "channel": "{channel}" - } - }, - "twitter": { - "hashtag": { "default": "posted a tweet", "short": "posted a tweet", "channel": "" }, - "follow": { "default": "followed you", "short": "followed you", "channel": "" }, - "mention": { - "default": "mentioned you in a tweet", - "short": "mentioned you", - "channel": "" - } - }, - "stackoverflow": { - "question": { - "default": "Asked a question {self}", - "short": "asked a question", - "channel": "" - }, - "answer": { - "default": "Answered a question {self}", - "short": "answered a question", - "channel": "" - } - } - }, - "custom": { - "other": { - "attended-a-meeting": { - "short": "Attended a meeting", - "channel": "", - "default": "Attended a meeting" - }, - "asked-question-in-webinar": { - "default": "Asked question in webinar", - "short": "Asked question in webinar", - "channel": "" - } - } - } - } - }, - "MemberAttributeSettings": { - "value": { - "id": "9eaedce9-1f3a-4a75-adc8-e475cbc47553'", - "type": "string", - "canDelete": false, - "show": true, - "label": "Url", - "name": "url", - "createdAt": "2022-09-07", - "updatedAt": "2022-09-07", - "tenantId": "fcd5b9cc-144b-4687-8fd9-34818f35e70d" - } - }, - "MemberAttributeSettings2": { - "value": { - "id": "13bb9e12-c371-44ad-8806-0678c2f53dd1", - "type": "boolean", - "canDelete": false, - "show": true, - "label": "is Hireable", - "name": "isHireable", - "createdAt": "2022-09-07", - "updatedAt": "2022-09-07", - "tenantId": "fcd5b9cc-144b-4687-8fd9-34818f35e70d" - } - }, - "MemberAttributeSettingsList": { - "value": { - "rows": [ - { "$ref": "#/components/examples/MemberAttributeSettings" }, - { "$ref": "#/components/examples/MemberAttributeSettings2" } - ], - "count": 2 - } - } - } - }, - "tags": [ - { "name": "Members", "description": "Everything about members" }, - { "name": "Member Attributes", "description": "Settings for member's attributes" }, - { "name": "Activities", "description": "Everything about activities" }, - { "name": "Organizations", "description": "Everything about organizations" }, - { "name": "Conversations", "description": "Everything about conversations" }, - { "name": "Tags", "description": "Everything about tags" }, - { "name": "Automations", "description": "Everything about automations" }, - { "name": "Notes", "description": "Everything about notes" } - ] -} diff --git a/backend/package-lock.json b/backend/package-lock.json deleted file mode 100644 index 5e87b678f9..0000000000 --- a/backend/package-lock.json +++ /dev/null @@ -1,67120 +0,0 @@ -{ - "name": "app-backend", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "app-backend", - "dependencies": { - "@aws-sdk/client-comprehend": "^3.159.0", - "@aws-sdk/hash-node": "^3.226.0", - "@aws-sdk/protocol-http": "^3.226.0", - "@aws-sdk/s3-request-presigner": "^3.229.0", - "@aws-sdk/url-parser": "^3.226.0", - "@aws-sdk/util-format-url": "^3.226.0", - "@crowd/alerting": "file:../services/libs/alerting", - "@crowd/common": "file:../services/libs/common", - "@crowd/integrations": "file:../services/libs/integrations", - "@crowd/logging": "file:../services/libs/logging", - "@crowd/opensearch": "file:../services/libs/opensearch", - "@crowd/redis": "file:../services/libs/redis", - "@crowd/sqs": "file:../services/libs/sqs", - "@crowd/tracing": "file:../services/libs/tracing", - "@crowd/types": "file:../services/libs/types", - "@cubejs-client/core": "^0.30.4", - "@google-cloud/storage": "5.3.0", - "@octokit/auth-app": "^3.6.1", - "@octokit/graphql": "^4.8.0", - "@octokit/request": "^5.6.3", - "@opensearch-project/opensearch": "^1.2.0", - "@pm2/io": "^5.0.0", - "@sendgrid/eventwebhook": "^7.7.0", - "@sendgrid/mail": "7.2.6", - "@slack/web-api": "^6.7.2", - "@superfaceai/one-sdk": "^1.3.0", - "@superfaceai/passport-twitter-oauth2": "^1.0.0", - "analytics-node": "^6.2.0", - "aws-sdk": "2.814.0", - "axios": "^0.27.2", - "bcrypt": "5.0.0", - "body-parser": "^1.20.1", - "bufferutil": "^4.0.7", - "bunyan": "^1.8.15", - "bunyan-format": "^0.2.1", - "bunyan-middleware": "^1.0.2", - "clearbit": "^1.3.5", - "cli-highlight": "2.1.6", - "command-line-args": "^5.2.1", - "command-line-usage": "^6.1.3", - "config": "^3.3.8", - "cors": "2.8.5", - "cron": "^2.1.0", - "cron-time-generator": "^1.3.0", - "crowd-sentiment": "^1.1.7", - "crypto-js": "^4.1.1", - "discord.js": "^14.7.1", - "dotenv": "8.2.0", - "dotenv-expand": "^8.0.3", - "emoji-dictionary": "^1.0.11", - "erlpack": "^0.1.4", - "express": "4.17.1", - "express-rate-limit": "6.5.1", - "fast-levenshtein": "^3.0.0", - "formidable-serverless": "1.1.1", - "he": "^1.2.0", - "helmet": "4.1.1", - "html-to-mrkdwn-ts": "^1.1.0", - "html-to-text": "^8.2.1", - "json2csv": "^5.0.7", - "jsonwebtoken": "8.5.1", - "jwks-rsa": "^3.0.1", - "lodash": "4.17.21", - "moment": "2.29.4", - "moment-timezone": "^0.5.34", - "mv": "2.1.1", - "node-fetch": "^2.6.7", - "omit-deep-by-values": "^1.0.2", - "openapi-comment-parser": "^1.0.0", - "passport": "0.6.0", - "passport-facebook": "3.0.0", - "passport-github2": "^0.1.12", - "passport-google-oauth": "2.0.0", - "passport-google-oauth20": "^2.0.0", - "passport-slack": "0.0.7", - "peopledatalabs": "^5.0.3", - "pg": "^8.7.3", - "pm2": "^5.2.0", - "sanitize-html": "^2.7.1", - "sequelize": "6.21.2", - "sequelize-cli-typescript": "^3.2.0-c", - "slack-block-builder": "^2.7.2", - "socket.io": "^4.5.4", - "stripe": "^10.0.0", - "superagent": "^8.0.0", - "swagger-ui-dist": "4.1.3", - "tsconfig-paths": "^4.2.0", - "unleash-client": "^3.18.1", - "utf-8-validate": "^5.0.10", - "uuid": "^9.0.0", - "validator": "^13.7.0", - "verify-github-webhook": "^1.0.1", - "zlib-sync": "^0.1.8" - }, - "devDependencies": { - "@babel/core": "^7.21.8", - "@babel/preset-env": "^7.21.5", - "@babel/preset-typescript": "^7.21.5", - "@types/bunyan": "^1.8.8", - "@types/bunyan-format": "^0.2.5", - "@types/config": "^3.3.0", - "@types/cron": "^2.0.0", - "@types/html-to-text": "^8.1.1", - "@types/jest": "^29.5.1", - "@types/node": "^17.0.21", - "@types/sanitize-html": "^2.6.2", - "@types/superagent": "^4.1.15", - "@types/uuid": "^9.0.2", - "@typescript-eslint/eslint-plugin": "^5.17.0", - "@typescript-eslint/parser": "^5.17.0", - "copyfiles": "2.4.1", - "cross-env": "7.0.2", - "eslint": "^8.12.0", - "eslint-config-airbnb-base": "^15.0.0", - "eslint-config-airbnb-typescript": "^16.1.4", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-import": "^2.25.4", - "eslint-plugin-openapi": "^0.0.4", - "jest": "^29.5.0", - "node-mocks-http": "1.9.0", - "nodemon": "2.0.4", - "prettier": "^2.5.1", - "rdme": "^7.2.0", - "supertest": "^6.2.2", - "ts-jest": "^29.1.0", - "ts-node": "10.6.0", - "typescript": "^4.7.4" - } - }, - "../services/libs/alerting": { - "name": "@crowd/alerting", - "version": "1.0.0", - "dependencies": { - "@types/node": "^20.3.1", - "@typescript-eslint/eslint-plugin": "^5.59.11", - "@typescript-eslint/parser": "^5.59.11", - "axios": "^1.4.0", - "eslint": "^8.42.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.1.3" - } - }, - "../services/libs/alerting/node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "../services/libs/alerting/node_modules/@eslint-community/regexpp": { - "version": "4.5.1", - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "../services/libs/alerting/node_modules/@eslint/eslintrc": { - "version": "2.0.3", - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/alerting/node_modules/@eslint/js": { - "version": "8.42.0", - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "../services/libs/alerting/node_modules/@humanwhocodes/config-array": { - "version": "0.11.10", - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "../services/libs/alerting/node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "../services/libs/alerting/node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "license": "BSD-3-Clause" - }, - "../services/libs/alerting/node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/alerting/node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "../services/libs/alerting/node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/alerting/node_modules/@types/json-schema": { - "version": "7.0.12", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/@types/node": { - "version": "20.3.1", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/@types/semver": { - "version": "7.5.0", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.11", - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.11", - "@typescript-eslint/type-utils": "5.59.11", - "@typescript-eslint/utils": "5.59.11", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/alerting/node_modules/@typescript-eslint/parser": { - "version": "5.59.11", - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "5.59.11", - "@typescript-eslint/types": "5.59.11", - "@typescript-eslint/typescript-estree": "5.59.11", - "debug": "^4.3.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/alerting/node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.11", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.59.11", - "@typescript-eslint/visitor-keys": "5.59.11" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/alerting/node_modules/@typescript-eslint/type-utils": { - "version": "5.59.11", - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.11", - "@typescript-eslint/utils": "5.59.11", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/alerting/node_modules/@typescript-eslint/types": { - "version": "5.59.11", - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/alerting/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.11", - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "5.59.11", - "@typescript-eslint/visitor-keys": "5.59.11", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/alerting/node_modules/@typescript-eslint/utils": { - "version": "5.59.11", - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.11", - "@typescript-eslint/types": "5.59.11", - "@typescript-eslint/typescript-estree": "5.59.11", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "../services/libs/alerting/node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { - "version": "5.1.1", - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "../services/libs/alerting/node_modules/@typescript-eslint/utils/node_modules/estraverse": { - "version": "4.3.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/alerting/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.11", - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.59.11", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/alerting/node_modules/acorn": { - "version": "8.8.2", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "../services/libs/alerting/node_modules/acorn-jsx": { - "version": "5.3.2", - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "../services/libs/alerting/node_modules/ajv": { - "version": "6.12.6", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "../services/libs/alerting/node_modules/ansi-regex": { - "version": "5.0.1", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/alerting/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "../services/libs/alerting/node_modules/argparse": { - "version": "2.0.1", - "license": "Python-2.0" - }, - "../services/libs/alerting/node_modules/array-union": { - "version": "2.1.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/alerting/node_modules/asynckit": { - "version": "0.4.0", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/axios": { - "version": "1.4.0", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "../services/libs/alerting/node_modules/balanced-match": { - "version": "1.0.2", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/brace-expansion": { - "version": "1.1.11", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "../services/libs/alerting/node_modules/braces": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/alerting/node_modules/callsites": { - "version": "3.1.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "../services/libs/alerting/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "../services/libs/alerting/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "../services/libs/alerting/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/combined-stream": { - "version": "1.0.8", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "../services/libs/alerting/node_modules/concat-map": { - "version": "0.0.1", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/cross-spawn": { - "version": "7.0.3", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/alerting/node_modules/debug": { - "version": "4.3.4", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "../services/libs/alerting/node_modules/deep-is": { - "version": "0.1.4", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/delayed-stream": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "../services/libs/alerting/node_modules/dir-glob": { - "version": "3.0.1", - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/alerting/node_modules/doctrine": { - "version": "3.0.0", - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "../services/libs/alerting/node_modules/escape-string-regexp": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/alerting/node_modules/eslint": { - "version": "8.42.0", - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.42.0", - "@humanwhocodes/config-array": "^0.11.10", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/alerting/node_modules/eslint-config-prettier": { - "version": "8.8.0", - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "../services/libs/alerting/node_modules/eslint-plugin-prettier": { - "version": "4.2.1", - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "eslint": ">=7.28.0", - "prettier": ">=2.0.0" - }, - "peerDependenciesMeta": { - "eslint-config-prettier": { - "optional": true - } - } - }, - "../services/libs/alerting/node_modules/eslint-scope": { - "version": "7.2.0", - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/alerting/node_modules/eslint-visitor-keys": { - "version": "3.4.1", - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/alerting/node_modules/espree": { - "version": "9.5.2", - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/alerting/node_modules/esquery": { - "version": "1.5.0", - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "../services/libs/alerting/node_modules/esrecurse": { - "version": "4.3.0", - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/alerting/node_modules/estraverse": { - "version": "5.3.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/alerting/node_modules/esutils": { - "version": "2.0.3", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/alerting/node_modules/fast-deep-equal": { - "version": "3.1.3", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/fast-diff": { - "version": "1.3.0", - "license": "Apache-2.0" - }, - "../services/libs/alerting/node_modules/fast-glob": { - "version": "3.2.12", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "../services/libs/alerting/node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "../services/libs/alerting/node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/fast-levenshtein": { - "version": "2.0.6", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/fastq": { - "version": "1.15.0", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "../services/libs/alerting/node_modules/file-entry-cache": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "../services/libs/alerting/node_modules/fill-range": { - "version": "7.0.1", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/alerting/node_modules/find-up": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/alerting/node_modules/flat-cache": { - "version": "3.0.4", - "license": "MIT", - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "../services/libs/alerting/node_modules/flatted": { - "version": "3.2.7", - "license": "ISC" - }, - "../services/libs/alerting/node_modules/follow-redirects": { - "version": "1.15.2", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "../services/libs/alerting/node_modules/form-data": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "../services/libs/alerting/node_modules/fs.realpath": { - "version": "1.0.0", - "license": "ISC" - }, - "../services/libs/alerting/node_modules/glob": { - "version": "7.2.3", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "../services/libs/alerting/node_modules/glob-parent": { - "version": "6.0.2", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "../services/libs/alerting/node_modules/globals": { - "version": "13.20.0", - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/alerting/node_modules/globby": { - "version": "11.1.0", - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/alerting/node_modules/grapheme-splitter": { - "version": "1.0.4", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/graphemer": { - "version": "1.4.0", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/alerting/node_modules/ignore": { - "version": "5.2.4", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "../services/libs/alerting/node_modules/import-fresh": { - "version": "3.3.0", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/alerting/node_modules/imurmurhash": { - "version": "0.1.4", - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "../services/libs/alerting/node_modules/inflight": { - "version": "1.0.6", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "../services/libs/alerting/node_modules/inherits": { - "version": "2.0.4", - "license": "ISC" - }, - "../services/libs/alerting/node_modules/is-extglob": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/alerting/node_modules/is-glob": { - "version": "4.0.3", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/alerting/node_modules/is-number": { - "version": "7.0.0", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "../services/libs/alerting/node_modules/is-path-inside": { - "version": "3.0.3", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/alerting/node_modules/isexe": { - "version": "2.0.0", - "license": "ISC" - }, - "../services/libs/alerting/node_modules/js-yaml": { - "version": "4.1.0", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "../services/libs/alerting/node_modules/json-schema-traverse": { - "version": "0.4.1", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/levn": { - "version": "0.4.1", - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/alerting/node_modules/locate-path": { - "version": "6.0.0", - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/alerting/node_modules/lodash.merge": { - "version": "4.6.2", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/lru-cache": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "../services/libs/alerting/node_modules/merge2": { - "version": "1.4.1", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "../services/libs/alerting/node_modules/micromatch": { - "version": "4.0.5", - "license": "MIT", - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "../services/libs/alerting/node_modules/mime-db": { - "version": "1.52.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "../services/libs/alerting/node_modules/mime-types": { - "version": "2.1.35", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "../services/libs/alerting/node_modules/minimatch": { - "version": "3.1.2", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "../services/libs/alerting/node_modules/ms": { - "version": "2.1.2", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/natural-compare": { - "version": "1.4.0", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/natural-compare-lite": { - "version": "1.4.0", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/once": { - "version": "1.4.0", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "../services/libs/alerting/node_modules/optionator": { - "version": "0.9.1", - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/alerting/node_modules/p-limit": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/alerting/node_modules/p-locate": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/alerting/node_modules/parent-module": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "../services/libs/alerting/node_modules/path-exists": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/alerting/node_modules/path-is-absolute": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/alerting/node_modules/path-key": { - "version": "3.1.1", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/alerting/node_modules/path-type": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/alerting/node_modules/picomatch": { - "version": "2.3.1", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "../services/libs/alerting/node_modules/prelude-ls": { - "version": "1.2.1", - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/alerting/node_modules/prettier": { - "version": "2.8.8", - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "../services/libs/alerting/node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "../services/libs/alerting/node_modules/proxy-from-env": { - "version": "1.1.0", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/punycode": { - "version": "2.3.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "../services/libs/alerting/node_modules/queue-microtask": { - "version": "1.2.3", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "../services/libs/alerting/node_modules/resolve-from": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "../services/libs/alerting/node_modules/reusify": { - "version": "1.0.4", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "../services/libs/alerting/node_modules/rimraf": { - "version": "3.0.2", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "../services/libs/alerting/node_modules/run-parallel": { - "version": "1.2.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "../services/libs/alerting/node_modules/semver": { - "version": "7.5.1", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "../services/libs/alerting/node_modules/shebang-command": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/alerting/node_modules/shebang-regex": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/alerting/node_modules/slash": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/alerting/node_modules/strip-ansi": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/alerting/node_modules/strip-json-comments": { - "version": "3.1.1", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/alerting/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/alerting/node_modules/text-table": { - "version": "0.2.0", - "license": "MIT" - }, - "../services/libs/alerting/node_modules/to-regex-range": { - "version": "5.0.1", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "../services/libs/alerting/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "../services/libs/alerting/node_modules/tsutils": { - "version": "3.21.0", - "license": "MIT", - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "../services/libs/alerting/node_modules/type-check": { - "version": "0.4.0", - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/alerting/node_modules/type-fest": { - "version": "0.20.2", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/alerting/node_modules/typescript": { - "version": "5.1.3", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "../services/libs/alerting/node_modules/uri-js": { - "version": "4.4.1", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "../services/libs/alerting/node_modules/which": { - "version": "2.0.2", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/alerting/node_modules/word-wrap": { - "version": "1.2.3", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/alerting/node_modules/wrappy": { - "version": "1.0.2", - "license": "ISC" - }, - "../services/libs/alerting/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "../services/libs/alerting/node_modules/yocto-queue": { - "version": "0.1.0", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/common": { - "name": "@crowd/common", - "version": "1.0.0", - "dependencies": { - "@crowd/logging": "file:../logging", - "@crowd/types": "file:../types", - "psl": "^1.9.0", - "uuid": "^9.0.0" - }, - "devDependencies": { - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - } - }, - "../services/libs/common/node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "../services/libs/common/node_modules/@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "../services/libs/common/node_modules/@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/common/node_modules/@eslint/js": { - "version": "8.40.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "../services/libs/common/node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "../services/libs/common/node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "../services/libs/common/node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true, - "license": "BSD-3-Clause" - }, - "../services/libs/common/node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/common/node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "../services/libs/common/node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/common/node_modules/@types/json-schema": { - "version": "7.0.11", - "dev": true, - "license": "MIT" - }, - "../services/libs/common/node_modules/@types/node": { - "version": "18.16.7", - "dev": true, - "license": "MIT" - }, - "../services/libs/common/node_modules/@types/semver": { - "version": "7.5.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/common/node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/type-utils": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/common/node_modules/@typescript-eslint/parser": { - "version": "5.59.6", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "debug": "^4.3.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/common/node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/common/node_modules/@typescript-eslint/type-utils": { - "version": "5.59.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/common/node_modules/@typescript-eslint/types": { - "version": "5.59.6", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/common/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.6", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/common/node_modules/@typescript-eslint/utils": { - "version": "5.59.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "../services/libs/common/node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { - "version": "5.1.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "../services/libs/common/node_modules/@typescript-eslint/utils/node_modules/estraverse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/common/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.59.6", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/common/node_modules/acorn": { - "version": "8.8.2", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "../services/libs/common/node_modules/acorn-jsx": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "../services/libs/common/node_modules/ajv": { - "version": "6.12.6", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "../services/libs/common/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/common/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "../services/libs/common/node_modules/argparse": { - "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, - "../services/libs/common/node_modules/array-union": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/common/node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "../services/libs/common/node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "../services/libs/common/node_modules/braces": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/common/node_modules/callsites": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "../services/libs/common/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "../services/libs/common/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "../services/libs/common/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/common/node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "../services/libs/common/node_modules/cross-spawn": { - "version": "7.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/common/node_modules/debug": { - "version": "4.3.4", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "../services/libs/common/node_modules/deep-is": { - "version": "0.1.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/common/node_modules/dir-glob": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/common/node_modules/doctrine": { - "version": "3.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "../services/libs/common/node_modules/escape-string-regexp": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/common/node_modules/eslint": { - "version": "8.40.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/common/node_modules/eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "../services/libs/common/node_modules/eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "eslint": ">=7.28.0", - "prettier": ">=2.0.0" - }, - "peerDependenciesMeta": { - "eslint-config-prettier": { - "optional": true - } - } - }, - "../services/libs/common/node_modules/eslint-scope": { - "version": "7.2.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/common/node_modules/eslint-visitor-keys": { - "version": "3.4.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/common/node_modules/espree": { - "version": "9.5.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/common/node_modules/esquery": { - "version": "1.5.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "../services/libs/common/node_modules/esrecurse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/common/node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/common/node_modules/esutils": { - "version": "2.0.3", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/common/node_modules/fast-deep-equal": { - "version": "3.1.3", - "dev": true, - "license": "MIT" - }, - "../services/libs/common/node_modules/fast-diff": { - "version": "1.2.0", - "dev": true, - "license": "Apache-2.0" - }, - "../services/libs/common/node_modules/fast-glob": { - "version": "3.2.12", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "../services/libs/common/node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "../services/libs/common/node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/common/node_modules/fast-levenshtein": { - "version": "2.0.6", - "dev": true, - "license": "MIT" - }, - "../services/libs/common/node_modules/fastq": { - "version": "1.15.0", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "../services/libs/common/node_modules/file-entry-cache": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "../services/libs/common/node_modules/fill-range": { - "version": "7.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/common/node_modules/find-up": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/common/node_modules/flat-cache": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "../services/libs/common/node_modules/flatted": { - "version": "3.2.7", - "dev": true, - "license": "ISC" - }, - "../services/libs/common/node_modules/fs.realpath": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "../services/libs/common/node_modules/glob": { - "version": "7.2.3", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "../services/libs/common/node_modules/glob-parent": { - "version": "6.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "../services/libs/common/node_modules/globals": { - "version": "13.20.0", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/common/node_modules/globby": { - "version": "11.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/common/node_modules/grapheme-splitter": { - "version": "1.0.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/common/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/common/node_modules/ignore": { - "version": "5.2.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "../services/libs/common/node_modules/import-fresh": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/common/node_modules/imurmurhash": { - "version": "0.1.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "../services/libs/common/node_modules/inflight": { - "version": "1.0.6", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "../services/libs/common/node_modules/inherits": { - "version": "2.0.4", - "dev": true, - "license": "ISC" - }, - "../services/libs/common/node_modules/is-extglob": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/common/node_modules/is-glob": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/common/node_modules/is-number": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "../services/libs/common/node_modules/is-path-inside": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/common/node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "../services/libs/common/node_modules/js-sdsl": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, - "../services/libs/common/node_modules/js-yaml": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "../services/libs/common/node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "../services/libs/common/node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "../services/libs/common/node_modules/levn": { - "version": "0.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/common/node_modules/locate-path": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/common/node_modules/lodash.merge": { - "version": "4.6.2", - "dev": true, - "license": "MIT" - }, - "../services/libs/common/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "../services/libs/common/node_modules/merge2": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "../services/libs/common/node_modules/micromatch": { - "version": "4.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "../services/libs/common/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "../services/libs/common/node_modules/ms": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "../services/libs/common/node_modules/natural-compare": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/common/node_modules/natural-compare-lite": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/common/node_modules/once": { - "version": "1.4.0", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "../services/libs/common/node_modules/optionator": { - "version": "0.9.1", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/common/node_modules/p-limit": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/common/node_modules/p-locate": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/common/node_modules/parent-module": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "../services/libs/common/node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/common/node_modules/path-is-absolute": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/common/node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/common/node_modules/path-type": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/common/node_modules/picomatch": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "../services/libs/common/node_modules/prelude-ls": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/common/node_modules/prettier": { - "version": "2.8.8", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "../services/libs/common/node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "../services/libs/common/node_modules/punycode": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "../services/libs/common/node_modules/queue-microtask": { - "version": "1.2.3", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "../services/libs/common/node_modules/resolve-from": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "../services/libs/common/node_modules/reusify": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "../services/libs/common/node_modules/rimraf": { - "version": "3.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "../services/libs/common/node_modules/run-parallel": { - "version": "1.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "../services/libs/common/node_modules/semver": { - "version": "7.5.1", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "../services/libs/common/node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/common/node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/common/node_modules/slash": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/common/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/common/node_modules/strip-json-comments": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/common/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/common/node_modules/text-table": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/common/node_modules/to-regex-range": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "../services/libs/common/node_modules/tslib": { - "version": "1.14.1", - "dev": true, - "license": "0BSD" - }, - "../services/libs/common/node_modules/tsutils": { - "version": "3.21.0", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "../services/libs/common/node_modules/type-check": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/common/node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/common/node_modules/typescript": { - "version": "5.0.4", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=12.20" - } - }, - "../services/libs/common/node_modules/uri-js": { - "version": "4.4.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "../services/libs/common/node_modules/uuid": { - "version": "9.0.0", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "../services/libs/common/node_modules/which": { - "version": "2.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/common/node_modules/word-wrap": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/common/node_modules/wrappy": { - "version": "1.0.2", - "dev": true, - "license": "ISC" - }, - "../services/libs/common/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "../services/libs/common/node_modules/yocto-queue": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/integrations": { - "name": "@crowd/integrations", - "version": "1.0.0", - "dependencies": { - "@crowd/common": "file:../common", - "@crowd/logging": "file:../logging", - "@crowd/types": "file:../types", - "@octokit/auth-app": "^4.0.13", - "@octokit/graphql": "^5.0.6", - "axios": "^1.4.0", - "he": "^1.2.0", - "sanitize-html": "^2.10.0", - "verify-github-webhook": "^1.0.1" - }, - "devDependencies": { - "@types/he": "^1.2.0", - "@types/node": "^18.16.3", - "@types/sanitize-html": "^2.9.0", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - } - }, - "../services/libs/integrations/node_modules/@crowd/common": { - "resolved": "../services/libs/common", - "link": true - }, - "../services/libs/integrations/node_modules/@crowd/logging": { - "resolved": "../services/libs/logging", - "link": true - }, - "../services/libs/integrations/node_modules/@crowd/types": { - "resolved": "../services/libs/types", - "link": true - }, - "../services/libs/integrations/node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "../services/libs/integrations/node_modules/@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "../services/libs/integrations/node_modules/@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/integrations/node_modules/@eslint/js": { - "version": "8.40.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "../services/libs/integrations/node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "../services/libs/integrations/node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "../services/libs/integrations/node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true, - "license": "BSD-3-Clause" - }, - "../services/libs/integrations/node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/integrations/node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "../services/libs/integrations/node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/integrations/node_modules/@types/he": { - "version": "1.2.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/integrations/node_modules/@types/json-schema": { - "version": "7.0.11", - "dev": true, - "license": "MIT" - }, - "../services/libs/integrations/node_modules/@types/node": { - "version": "18.16.9", - "dev": true, - "license": "MIT" - }, - "../services/libs/integrations/node_modules/@types/sanitize-html": { - "version": "2.9.0", - "dev": true, - "license": "MIT", - "dependencies": { - "htmlparser2": "^8.0.0" - } - }, - "../services/libs/integrations/node_modules/@types/semver": { - "version": "7.5.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/integrations/node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/type-utils": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/integrations/node_modules/@typescript-eslint/parser": { - "version": "5.59.5", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "debug": "^4.3.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/integrations/node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/integrations/node_modules/@typescript-eslint/type-utils": { - "version": "5.59.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/integrations/node_modules/@typescript-eslint/types": { - "version": "5.59.5", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/integrations/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.5", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/integrations/node_modules/@typescript-eslint/utils": { - "version": "5.59.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "../services/libs/integrations/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.59.5", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/integrations/node_modules/acorn": { - "version": "8.8.2", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "../services/libs/integrations/node_modules/acorn-jsx": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "../services/libs/integrations/node_modules/ajv": { - "version": "6.12.6", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "../services/libs/integrations/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/integrations/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "../services/libs/integrations/node_modules/argparse": { - "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, - "../services/libs/integrations/node_modules/array-union": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/integrations/node_modules/asynckit": { - "version": "0.4.0", - "license": "MIT" - }, - "../services/libs/integrations/node_modules/axios": { - "version": "1.4.0", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "../services/libs/integrations/node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "../services/libs/integrations/node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "../services/libs/integrations/node_modules/braces": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/integrations/node_modules/callsites": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "../services/libs/integrations/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "../services/libs/integrations/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "../services/libs/integrations/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/integrations/node_modules/combined-stream": { - "version": "1.0.8", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "../services/libs/integrations/node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "../services/libs/integrations/node_modules/cross-spawn": { - "version": "7.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/integrations/node_modules/debug": { - "version": "4.3.4", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "../services/libs/integrations/node_modules/deep-is": { - "version": "0.1.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/integrations/node_modules/deepmerge": { - "version": "4.3.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/integrations/node_modules/delayed-stream": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "../services/libs/integrations/node_modules/dir-glob": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/integrations/node_modules/doctrine": { - "version": "3.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "../services/libs/integrations/node_modules/dom-serializer": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "../services/libs/integrations/node_modules/domelementtype": { - "version": "2.3.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "../services/libs/integrations/node_modules/domhandler": { - "version": "5.0.3", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "../services/libs/integrations/node_modules/domutils": { - "version": "3.1.0", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "../services/libs/integrations/node_modules/entities": { - "version": "4.5.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "../services/libs/integrations/node_modules/escape-string-regexp": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/integrations/node_modules/eslint": { - "version": "8.40.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/integrations/node_modules/eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "../services/libs/integrations/node_modules/eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "eslint": ">=7.28.0", - "prettier": ">=2.0.0" - }, - "peerDependenciesMeta": { - "eslint-config-prettier": { - "optional": true - } - } - }, - "../services/libs/integrations/node_modules/eslint-scope": { - "version": "5.1.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "../services/libs/integrations/node_modules/eslint-visitor-keys": { - "version": "3.4.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/integrations/node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/integrations/node_modules/eslint/node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/integrations/node_modules/espree": { - "version": "9.5.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/integrations/node_modules/esquery": { - "version": "1.5.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "../services/libs/integrations/node_modules/esquery/node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/integrations/node_modules/esrecurse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/integrations/node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/integrations/node_modules/estraverse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/integrations/node_modules/esutils": { - "version": "2.0.3", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/integrations/node_modules/fast-deep-equal": { - "version": "3.1.3", - "dev": true, - "license": "MIT" - }, - "../services/libs/integrations/node_modules/fast-diff": { - "version": "1.2.0", - "dev": true, - "license": "Apache-2.0" - }, - "../services/libs/integrations/node_modules/fast-glob": { - "version": "3.2.12", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "../services/libs/integrations/node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "../services/libs/integrations/node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/integrations/node_modules/fast-levenshtein": { - "version": "2.0.6", - "dev": true, - "license": "MIT" - }, - "../services/libs/integrations/node_modules/fastq": { - "version": "1.15.0", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "../services/libs/integrations/node_modules/file-entry-cache": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "../services/libs/integrations/node_modules/fill-range": { - "version": "7.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/integrations/node_modules/find-up": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/integrations/node_modules/flat-cache": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "../services/libs/integrations/node_modules/flatted": { - "version": "3.2.7", - "dev": true, - "license": "ISC" - }, - "../services/libs/integrations/node_modules/follow-redirects": { - "version": "1.15.2", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "../services/libs/integrations/node_modules/form-data": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "../services/libs/integrations/node_modules/fs.realpath": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "../services/libs/integrations/node_modules/glob": { - "version": "7.2.3", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "../services/libs/integrations/node_modules/glob-parent": { - "version": "6.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "../services/libs/integrations/node_modules/globals": { - "version": "13.20.0", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/integrations/node_modules/globby": { - "version": "11.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/integrations/node_modules/grapheme-splitter": { - "version": "1.0.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/integrations/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/integrations/node_modules/he": { - "version": "1.2.0", - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, - "../services/libs/integrations/node_modules/htmlparser2": { - "version": "8.0.2", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, - "../services/libs/integrations/node_modules/ignore": { - "version": "5.2.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "../services/libs/integrations/node_modules/import-fresh": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/integrations/node_modules/imurmurhash": { - "version": "0.1.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "../services/libs/integrations/node_modules/inflight": { - "version": "1.0.6", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "../services/libs/integrations/node_modules/inherits": { - "version": "2.0.4", - "dev": true, - "license": "ISC" - }, - "../services/libs/integrations/node_modules/is-extglob": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/integrations/node_modules/is-glob": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/integrations/node_modules/is-number": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "../services/libs/integrations/node_modules/is-path-inside": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/integrations/node_modules/is-plain-object": { - "version": "5.0.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/integrations/node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "../services/libs/integrations/node_modules/js-sdsl": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, - "../services/libs/integrations/node_modules/js-yaml": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "../services/libs/integrations/node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "../services/libs/integrations/node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "../services/libs/integrations/node_modules/levn": { - "version": "0.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/integrations/node_modules/locate-path": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/integrations/node_modules/lodash.merge": { - "version": "4.6.2", - "dev": true, - "license": "MIT" - }, - "../services/libs/integrations/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "../services/libs/integrations/node_modules/merge2": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "../services/libs/integrations/node_modules/micromatch": { - "version": "4.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "../services/libs/integrations/node_modules/mime-db": { - "version": "1.52.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "../services/libs/integrations/node_modules/mime-types": { - "version": "2.1.35", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "../services/libs/integrations/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "../services/libs/integrations/node_modules/ms": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "../services/libs/integrations/node_modules/nanoid": { - "version": "3.3.6", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "../services/libs/integrations/node_modules/natural-compare": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/integrations/node_modules/natural-compare-lite": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/integrations/node_modules/once": { - "version": "1.4.0", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "../services/libs/integrations/node_modules/optionator": { - "version": "0.9.1", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/integrations/node_modules/p-limit": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/integrations/node_modules/p-locate": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/integrations/node_modules/parent-module": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "../services/libs/integrations/node_modules/parse-srcset": { - "version": "1.0.2", - "license": "MIT" - }, - "../services/libs/integrations/node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/integrations/node_modules/path-is-absolute": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/integrations/node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/integrations/node_modules/path-type": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/integrations/node_modules/picocolors": { - "version": "1.0.0", - "license": "ISC" - }, - "../services/libs/integrations/node_modules/picomatch": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "../services/libs/integrations/node_modules/postcss": { - "version": "8.4.23", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "../services/libs/integrations/node_modules/prelude-ls": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/integrations/node_modules/prettier": { - "version": "2.8.8", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "../services/libs/integrations/node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "../services/libs/integrations/node_modules/proxy-from-env": { - "version": "1.1.0", - "license": "MIT" - }, - "../services/libs/integrations/node_modules/punycode": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "../services/libs/integrations/node_modules/queue-microtask": { - "version": "1.2.3", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "../services/libs/integrations/node_modules/resolve-from": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "../services/libs/integrations/node_modules/reusify": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "../services/libs/integrations/node_modules/rimraf": { - "version": "3.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "../services/libs/integrations/node_modules/run-parallel": { - "version": "1.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "../services/libs/integrations/node_modules/sanitize-html": { - "version": "2.10.0", - "license": "MIT", - "dependencies": { - "deepmerge": "^4.2.2", - "escape-string-regexp": "^4.0.0", - "htmlparser2": "^8.0.0", - "is-plain-object": "^5.0.0", - "parse-srcset": "^1.0.2", - "postcss": "^8.3.11" - } - }, - "../services/libs/integrations/node_modules/semver": { - "version": "7.5.1", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "../services/libs/integrations/node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/integrations/node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/integrations/node_modules/slash": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/integrations/node_modules/source-map-js": { - "version": "1.0.2", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/integrations/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/integrations/node_modules/strip-json-comments": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/integrations/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/integrations/node_modules/text-table": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/integrations/node_modules/to-regex-range": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "../services/libs/integrations/node_modules/tslib": { - "version": "1.14.1", - "dev": true, - "license": "0BSD" - }, - "../services/libs/integrations/node_modules/tsutils": { - "version": "3.21.0", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "../services/libs/integrations/node_modules/type-check": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/integrations/node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/integrations/node_modules/typescript": { - "version": "5.0.4", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=12.20" - } - }, - "../services/libs/integrations/node_modules/uri-js": { - "version": "4.4.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "../services/libs/integrations/node_modules/which": { - "version": "2.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/integrations/node_modules/word-wrap": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/integrations/node_modules/wrappy": { - "version": "1.0.2", - "dev": true, - "license": "ISC" - }, - "../services/libs/integrations/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "../services/libs/integrations/node_modules/yocto-queue": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/logging": { - "name": "@crowd/logging", - "version": "1.0.0", - "dependencies": { - "@crowd/common": "file:../common", - "@crowd/tracing": "file:../tracing", - "bunyan": "^1.8.15", - "bunyan-format": "^0.2.1" - }, - "devDependencies": { - "@types/bunyan": "^1.8.8", - "@types/bunyan-format": "^0.2.5", - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - } - }, - "../services/libs/logging/node_modules/@crowd/common": { - "resolved": "../services/libs/common", - "link": true - }, - "../services/libs/logging/node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "../services/libs/logging/node_modules/@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "../services/libs/logging/node_modules/@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/logging/node_modules/@eslint/js": { - "version": "8.40.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "../services/libs/logging/node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "../services/libs/logging/node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "../services/libs/logging/node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true, - "license": "BSD-3-Clause" - }, - "../services/libs/logging/node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/logging/node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "../services/libs/logging/node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/logging/node_modules/@types/bunyan": { - "version": "1.8.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "../services/libs/logging/node_modules/@types/bunyan-format": { - "version": "0.2.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "../services/libs/logging/node_modules/@types/json-schema": { - "version": "7.0.11", - "dev": true, - "license": "MIT" - }, - "../services/libs/logging/node_modules/@types/node": { - "version": "18.16.8", - "dev": true, - "license": "MIT" - }, - "../services/libs/logging/node_modules/@types/semver": { - "version": "7.5.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/logging/node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/type-utils": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/logging/node_modules/@typescript-eslint/parser": { - "version": "5.59.6", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "debug": "^4.3.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/logging/node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/logging/node_modules/@typescript-eslint/type-utils": { - "version": "5.59.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/logging/node_modules/@typescript-eslint/types": { - "version": "5.59.6", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/logging/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.6", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/logging/node_modules/@typescript-eslint/utils": { - "version": "5.59.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "../services/libs/logging/node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { - "version": "5.1.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "../services/libs/logging/node_modules/@typescript-eslint/utils/node_modules/estraverse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/logging/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.59.6", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/logging/node_modules/acorn": { - "version": "8.8.2", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "../services/libs/logging/node_modules/acorn-jsx": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "../services/libs/logging/node_modules/ajv": { - "version": "6.12.6", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "../services/libs/logging/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/logging/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "../services/libs/logging/node_modules/ansicolors": { - "version": "0.2.1", - "license": "MIT" - }, - "../services/libs/logging/node_modules/ansistyles": { - "version": "0.1.3", - "license": "MIT" - }, - "../services/libs/logging/node_modules/argparse": { - "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, - "../services/libs/logging/node_modules/array-union": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/logging/node_modules/balanced-match": { - "version": "1.0.2", - "devOptional": true, - "license": "MIT" - }, - "../services/libs/logging/node_modules/brace-expansion": { - "version": "1.1.11", - "devOptional": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "../services/libs/logging/node_modules/braces": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/logging/node_modules/bunyan": { - "version": "1.8.15", - "engines": [ - "node >=0.10.0" - ], - "license": "MIT", - "bin": { - "bunyan": "bin/bunyan" - }, - "optionalDependencies": { - "dtrace-provider": "~0.8", - "moment": "^2.19.3", - "mv": "~2", - "safe-json-stringify": "~1" - } - }, - "../services/libs/logging/node_modules/bunyan-format": { - "version": "0.2.1", - "license": "MIT", - "dependencies": { - "ansicolors": "~0.2.1", - "ansistyles": "~0.1.1", - "xtend": "~2.1.1" - } - }, - "../services/libs/logging/node_modules/callsites": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "../services/libs/logging/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "../services/libs/logging/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "../services/libs/logging/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/logging/node_modules/concat-map": { - "version": "0.0.1", - "devOptional": true, - "license": "MIT" - }, - "../services/libs/logging/node_modules/cross-spawn": { - "version": "7.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/logging/node_modules/debug": { - "version": "4.3.4", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "../services/libs/logging/node_modules/deep-is": { - "version": "0.1.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/logging/node_modules/dir-glob": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/logging/node_modules/doctrine": { - "version": "3.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "../services/libs/logging/node_modules/dtrace-provider": { - "version": "0.8.8", - "hasInstallScript": true, - "license": "BSD-2-Clause", - "optional": true, - "dependencies": { - "nan": "^2.14.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "../services/libs/logging/node_modules/escape-string-regexp": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/logging/node_modules/eslint": { - "version": "8.40.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/logging/node_modules/eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "../services/libs/logging/node_modules/eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "eslint": ">=7.28.0", - "prettier": ">=2.0.0" - }, - "peerDependenciesMeta": { - "eslint-config-prettier": { - "optional": true - } - } - }, - "../services/libs/logging/node_modules/eslint-scope": { - "version": "7.2.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/logging/node_modules/eslint-visitor-keys": { - "version": "3.4.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/logging/node_modules/espree": { - "version": "9.5.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/logging/node_modules/esquery": { - "version": "1.5.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "../services/libs/logging/node_modules/esrecurse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/logging/node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/logging/node_modules/esutils": { - "version": "2.0.3", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/logging/node_modules/fast-deep-equal": { - "version": "3.1.3", - "dev": true, - "license": "MIT" - }, - "../services/libs/logging/node_modules/fast-diff": { - "version": "1.2.0", - "dev": true, - "license": "Apache-2.0" - }, - "../services/libs/logging/node_modules/fast-glob": { - "version": "3.2.12", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "../services/libs/logging/node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "../services/libs/logging/node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/logging/node_modules/fast-levenshtein": { - "version": "2.0.6", - "dev": true, - "license": "MIT" - }, - "../services/libs/logging/node_modules/fastq": { - "version": "1.15.0", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "../services/libs/logging/node_modules/file-entry-cache": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "../services/libs/logging/node_modules/fill-range": { - "version": "7.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/logging/node_modules/find-up": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/logging/node_modules/flat-cache": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "../services/libs/logging/node_modules/flatted": { - "version": "3.2.7", - "dev": true, - "license": "ISC" - }, - "../services/libs/logging/node_modules/fs.realpath": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "../services/libs/logging/node_modules/glob": { - "version": "7.2.3", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "../services/libs/logging/node_modules/glob-parent": { - "version": "6.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "../services/libs/logging/node_modules/globals": { - "version": "13.20.0", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/logging/node_modules/globby": { - "version": "11.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/logging/node_modules/grapheme-splitter": { - "version": "1.0.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/logging/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/logging/node_modules/ignore": { - "version": "5.2.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "../services/libs/logging/node_modules/import-fresh": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/logging/node_modules/imurmurhash": { - "version": "0.1.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "../services/libs/logging/node_modules/inflight": { - "version": "1.0.6", - "devOptional": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "../services/libs/logging/node_modules/inherits": { - "version": "2.0.4", - "devOptional": true, - "license": "ISC" - }, - "../services/libs/logging/node_modules/is-extglob": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/logging/node_modules/is-glob": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/logging/node_modules/is-number": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "../services/libs/logging/node_modules/is-path-inside": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/logging/node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "../services/libs/logging/node_modules/js-sdsl": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, - "../services/libs/logging/node_modules/js-yaml": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "../services/libs/logging/node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "../services/libs/logging/node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "../services/libs/logging/node_modules/levn": { - "version": "0.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/logging/node_modules/locate-path": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/logging/node_modules/lodash.merge": { - "version": "4.6.2", - "dev": true, - "license": "MIT" - }, - "../services/libs/logging/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "../services/libs/logging/node_modules/merge2": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "../services/libs/logging/node_modules/micromatch": { - "version": "4.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "../services/libs/logging/node_modules/minimatch": { - "version": "3.1.2", - "devOptional": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "../services/libs/logging/node_modules/minimist": { - "version": "1.2.8", - "license": "MIT", - "optional": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "../services/libs/logging/node_modules/mkdirp": { - "version": "0.5.6", - "license": "MIT", - "optional": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "../services/libs/logging/node_modules/moment": { - "version": "2.29.4", - "license": "MIT", - "optional": true, - "engines": { - "node": "*" - } - }, - "../services/libs/logging/node_modules/ms": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "../services/libs/logging/node_modules/mv": { - "version": "2.1.1", - "license": "MIT", - "optional": true, - "dependencies": { - "mkdirp": "~0.5.1", - "ncp": "~2.0.0", - "rimraf": "~2.4.0" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "../services/libs/logging/node_modules/mv/node_modules/glob": { - "version": "6.0.4", - "license": "ISC", - "optional": true, - "dependencies": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "../services/libs/logging/node_modules/mv/node_modules/rimraf": { - "version": "2.4.5", - "license": "ISC", - "optional": true, - "dependencies": { - "glob": "^6.0.1" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "../services/libs/logging/node_modules/nan": { - "version": "2.17.0", - "license": "MIT", - "optional": true - }, - "../services/libs/logging/node_modules/natural-compare": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/logging/node_modules/natural-compare-lite": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/logging/node_modules/ncp": { - "version": "2.0.0", - "license": "MIT", - "optional": true, - "bin": { - "ncp": "bin/ncp" - } - }, - "../services/libs/logging/node_modules/object-keys": { - "version": "0.4.0", - "license": "MIT" - }, - "../services/libs/logging/node_modules/once": { - "version": "1.4.0", - "devOptional": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "../services/libs/logging/node_modules/optionator": { - "version": "0.9.1", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/logging/node_modules/p-limit": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/logging/node_modules/p-locate": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/logging/node_modules/parent-module": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "../services/libs/logging/node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/logging/node_modules/path-is-absolute": { - "version": "1.0.1", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/logging/node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/logging/node_modules/path-type": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/logging/node_modules/picomatch": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "../services/libs/logging/node_modules/prelude-ls": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/logging/node_modules/prettier": { - "version": "2.8.8", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "../services/libs/logging/node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "../services/libs/logging/node_modules/punycode": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "../services/libs/logging/node_modules/queue-microtask": { - "version": "1.2.3", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "../services/libs/logging/node_modules/resolve-from": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "../services/libs/logging/node_modules/reusify": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "../services/libs/logging/node_modules/rimraf": { - "version": "3.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "../services/libs/logging/node_modules/run-parallel": { - "version": "1.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "../services/libs/logging/node_modules/safe-json-stringify": { - "version": "1.2.0", - "license": "MIT", - "optional": true - }, - "../services/libs/logging/node_modules/semver": { - "version": "7.5.1", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "../services/libs/logging/node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/logging/node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/logging/node_modules/slash": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/logging/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/logging/node_modules/strip-json-comments": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/logging/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/logging/node_modules/text-table": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/logging/node_modules/to-regex-range": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "../services/libs/logging/node_modules/tslib": { - "version": "1.14.1", - "dev": true, - "license": "0BSD" - }, - "../services/libs/logging/node_modules/tsutils": { - "version": "3.21.0", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "../services/libs/logging/node_modules/type-check": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/logging/node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/logging/node_modules/typescript": { - "version": "5.0.4", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=12.20" - } - }, - "../services/libs/logging/node_modules/uri-js": { - "version": "4.4.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "../services/libs/logging/node_modules/which": { - "version": "2.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/logging/node_modules/word-wrap": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/logging/node_modules/wrappy": { - "version": "1.0.2", - "devOptional": true, - "license": "ISC" - }, - "../services/libs/logging/node_modules/xtend": { - "version": "2.1.2", - "dependencies": { - "object-keys": "~0.4.0" - }, - "engines": { - "node": ">=0.4" - } - }, - "../services/libs/logging/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "../services/libs/logging/node_modules/yocto-queue": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/opensearch": { - "name": "@crowd/opensearch", - "version": "1.0.0", - "dependencies": { - "@crowd/types": "file:../types", - "@opensearch-project/opensearch": "^1.2.0" - }, - "devDependencies": { - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - } - }, - "../services/libs/opensearch/node_modules/@crowd/types": { - "resolved": "../services/libs/types", - "link": true - }, - "../services/libs/opensearch/node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "../services/libs/opensearch/node_modules/@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "../services/libs/opensearch/node_modules/@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/opensearch/node_modules/@eslint/js": { - "version": "8.42.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "../services/libs/opensearch/node_modules/@humanwhocodes/config-array": { - "version": "0.11.10", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "../services/libs/opensearch/node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "../services/libs/opensearch/node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true, - "license": "BSD-3-Clause" - }, - "../services/libs/opensearch/node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/opensearch/node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "../services/libs/opensearch/node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/opensearch/node_modules/@opensearch-project/opensearch": { - "version": "1.2.0", - "license": "Apache-2.0", - "dependencies": { - "aws4": "^1.11.0", - "debug": "^4.3.1", - "hpagent": "^0.1.1", - "ms": "^2.1.3", - "secure-json-parse": "^2.4.0" - }, - "engines": { - "node": ">=10" - } - }, - "../services/libs/opensearch/node_modules/@opensearch-project/opensearch/node_modules/ms": { - "version": "2.1.3", - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/@types/json-schema": { - "version": "7.0.12", - "dev": true, - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/@types/node": { - "version": "18.16.18", - "dev": true, - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/@types/semver": { - "version": "7.5.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.11", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.11", - "@typescript-eslint/type-utils": "5.59.11", - "@typescript-eslint/utils": "5.59.11", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/opensearch/node_modules/@typescript-eslint/parser": { - "version": "5.59.11", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "5.59.11", - "@typescript-eslint/types": "5.59.11", - "@typescript-eslint/typescript-estree": "5.59.11", - "debug": "^4.3.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/opensearch/node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.11", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.59.11", - "@typescript-eslint/visitor-keys": "5.59.11" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/opensearch/node_modules/@typescript-eslint/type-utils": { - "version": "5.59.11", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.11", - "@typescript-eslint/utils": "5.59.11", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/opensearch/node_modules/@typescript-eslint/types": { - "version": "5.59.11", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/opensearch/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.11", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "5.59.11", - "@typescript-eslint/visitor-keys": "5.59.11", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/opensearch/node_modules/@typescript-eslint/utils": { - "version": "5.59.11", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.11", - "@typescript-eslint/types": "5.59.11", - "@typescript-eslint/typescript-estree": "5.59.11", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "../services/libs/opensearch/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.11", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.59.11", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/opensearch/node_modules/acorn": { - "version": "8.8.2", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "../services/libs/opensearch/node_modules/acorn-jsx": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "../services/libs/opensearch/node_modules/ajv": { - "version": "6.12.6", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "../services/libs/opensearch/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/opensearch/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "../services/libs/opensearch/node_modules/argparse": { - "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, - "../services/libs/opensearch/node_modules/array-union": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/opensearch/node_modules/aws4": { - "version": "1.12.0", - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "../services/libs/opensearch/node_modules/braces": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/opensearch/node_modules/callsites": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "../services/libs/opensearch/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "../services/libs/opensearch/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "../services/libs/opensearch/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/cross-spawn": { - "version": "7.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/opensearch/node_modules/debug": { - "version": "4.3.4", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "../services/libs/opensearch/node_modules/deep-is": { - "version": "0.1.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/dir-glob": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/opensearch/node_modules/doctrine": { - "version": "3.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "../services/libs/opensearch/node_modules/escape-string-regexp": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/opensearch/node_modules/eslint": { - "version": "8.42.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.42.0", - "@humanwhocodes/config-array": "^0.11.10", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/opensearch/node_modules/eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "../services/libs/opensearch/node_modules/eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "eslint": ">=7.28.0", - "prettier": ">=2.0.0" - }, - "peerDependenciesMeta": { - "eslint-config-prettier": { - "optional": true - } - } - }, - "../services/libs/opensearch/node_modules/eslint-scope": { - "version": "5.1.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "../services/libs/opensearch/node_modules/eslint-visitor-keys": { - "version": "3.4.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/opensearch/node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/opensearch/node_modules/eslint/node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/opensearch/node_modules/espree": { - "version": "9.5.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/opensearch/node_modules/esquery": { - "version": "1.5.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "../services/libs/opensearch/node_modules/esquery/node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/opensearch/node_modules/esrecurse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/opensearch/node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/opensearch/node_modules/estraverse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/opensearch/node_modules/esutils": { - "version": "2.0.3", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/opensearch/node_modules/fast-deep-equal": { - "version": "3.1.3", - "dev": true, - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/fast-diff": { - "version": "1.3.0", - "dev": true, - "license": "Apache-2.0" - }, - "../services/libs/opensearch/node_modules/fast-glob": { - "version": "3.2.12", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "../services/libs/opensearch/node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "../services/libs/opensearch/node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/fast-levenshtein": { - "version": "2.0.6", - "dev": true, - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/fastq": { - "version": "1.15.0", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "../services/libs/opensearch/node_modules/file-entry-cache": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "../services/libs/opensearch/node_modules/fill-range": { - "version": "7.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/opensearch/node_modules/find-up": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/opensearch/node_modules/flat-cache": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "../services/libs/opensearch/node_modules/flatted": { - "version": "3.2.7", - "dev": true, - "license": "ISC" - }, - "../services/libs/opensearch/node_modules/fs.realpath": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "../services/libs/opensearch/node_modules/glob": { - "version": "7.2.3", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "../services/libs/opensearch/node_modules/glob-parent": { - "version": "6.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "../services/libs/opensearch/node_modules/globals": { - "version": "13.20.0", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/opensearch/node_modules/globby": { - "version": "11.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/opensearch/node_modules/grapheme-splitter": { - "version": "1.0.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/graphemer": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/opensearch/node_modules/hpagent": { - "version": "0.1.2", - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/ignore": { - "version": "5.2.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "../services/libs/opensearch/node_modules/import-fresh": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/opensearch/node_modules/imurmurhash": { - "version": "0.1.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "../services/libs/opensearch/node_modules/inflight": { - "version": "1.0.6", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "../services/libs/opensearch/node_modules/inherits": { - "version": "2.0.4", - "dev": true, - "license": "ISC" - }, - "../services/libs/opensearch/node_modules/is-extglob": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/opensearch/node_modules/is-glob": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/opensearch/node_modules/is-number": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "../services/libs/opensearch/node_modules/is-path-inside": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/opensearch/node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "../services/libs/opensearch/node_modules/js-yaml": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "../services/libs/opensearch/node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/levn": { - "version": "0.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/opensearch/node_modules/locate-path": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/opensearch/node_modules/lodash.merge": { - "version": "4.6.2", - "dev": true, - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "../services/libs/opensearch/node_modules/merge2": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "../services/libs/opensearch/node_modules/micromatch": { - "version": "4.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "../services/libs/opensearch/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "../services/libs/opensearch/node_modules/ms": { - "version": "2.1.2", - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/natural-compare": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/natural-compare-lite": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/once": { - "version": "1.4.0", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "../services/libs/opensearch/node_modules/optionator": { - "version": "0.9.1", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/opensearch/node_modules/p-limit": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/opensearch/node_modules/p-locate": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/opensearch/node_modules/parent-module": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "../services/libs/opensearch/node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/opensearch/node_modules/path-is-absolute": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/opensearch/node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/opensearch/node_modules/path-type": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/opensearch/node_modules/picomatch": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "../services/libs/opensearch/node_modules/prelude-ls": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/opensearch/node_modules/prettier": { - "version": "2.8.8", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "../services/libs/opensearch/node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "../services/libs/opensearch/node_modules/punycode": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "../services/libs/opensearch/node_modules/queue-microtask": { - "version": "1.2.3", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/resolve-from": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "../services/libs/opensearch/node_modules/reusify": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "../services/libs/opensearch/node_modules/rimraf": { - "version": "3.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "../services/libs/opensearch/node_modules/run-parallel": { - "version": "1.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "../services/libs/opensearch/node_modules/secure-json-parse": { - "version": "2.7.0", - "license": "BSD-3-Clause" - }, - "../services/libs/opensearch/node_modules/semver": { - "version": "7.5.1", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "../services/libs/opensearch/node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/opensearch/node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/opensearch/node_modules/slash": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/opensearch/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/opensearch/node_modules/strip-json-comments": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/opensearch/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/opensearch/node_modules/text-table": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/opensearch/node_modules/to-regex-range": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "../services/libs/opensearch/node_modules/tslib": { - "version": "1.14.1", - "dev": true, - "license": "0BSD" - }, - "../services/libs/opensearch/node_modules/tsutils": { - "version": "3.21.0", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "../services/libs/opensearch/node_modules/type-check": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/opensearch/node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/opensearch/node_modules/typescript": { - "version": "5.1.3", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "../services/libs/opensearch/node_modules/uri-js": { - "version": "4.4.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "../services/libs/opensearch/node_modules/which": { - "version": "2.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/opensearch/node_modules/word-wrap": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/opensearch/node_modules/wrappy": { - "version": "1.0.2", - "dev": true, - "license": "ISC" - }, - "../services/libs/opensearch/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "../services/libs/opensearch/node_modules/yocto-queue": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/redis": { - "name": "@crowd/redis", - "version": "1.0.0", - "dependencies": { - "@crowd/common": "file:../common", - "@crowd/logging": "file:../logging", - "@crowd/types": "file:../types", - "redis": "^4.6.6" - }, - "devDependencies": { - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - } - }, - "../services/libs/redis/node_modules/@crowd/common": { - "resolved": "../services/libs/common", - "link": true - }, - "../services/libs/redis/node_modules/@crowd/logging": { - "resolved": "../services/libs/logging", - "link": true - }, - "../services/libs/redis/node_modules/@crowd/types": { - "resolved": "../services/libs/types", - "link": true - }, - "../services/libs/redis/node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "../services/libs/redis/node_modules/@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "../services/libs/redis/node_modules/@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/redis/node_modules/@eslint/js": { - "version": "8.40.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "../services/libs/redis/node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "../services/libs/redis/node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "../services/libs/redis/node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true, - "license": "BSD-3-Clause" - }, - "../services/libs/redis/node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/redis/node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "../services/libs/redis/node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/redis/node_modules/@redis/bloom": { - "version": "1.2.0", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, - "../services/libs/redis/node_modules/@redis/client": { - "version": "1.5.7", - "license": "MIT", - "dependencies": { - "cluster-key-slot": "1.1.2", - "generic-pool": "3.9.0", - "yallist": "4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "../services/libs/redis/node_modules/@redis/graph": { - "version": "1.1.0", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, - "../services/libs/redis/node_modules/@redis/json": { - "version": "1.0.4", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, - "../services/libs/redis/node_modules/@redis/search": { - "version": "1.1.2", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, - "../services/libs/redis/node_modules/@redis/time-series": { - "version": "1.0.4", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, - "../services/libs/redis/node_modules/@types/json-schema": { - "version": "7.0.11", - "dev": true, - "license": "MIT" - }, - "../services/libs/redis/node_modules/@types/node": { - "version": "18.16.9", - "dev": true, - "license": "MIT" - }, - "../services/libs/redis/node_modules/@types/semver": { - "version": "7.5.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/redis/node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/type-utils": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/redis/node_modules/@typescript-eslint/parser": { - "version": "5.59.6", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "debug": "^4.3.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/redis/node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/redis/node_modules/@typescript-eslint/type-utils": { - "version": "5.59.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/redis/node_modules/@typescript-eslint/types": { - "version": "5.59.6", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/redis/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.6", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/redis/node_modules/@typescript-eslint/utils": { - "version": "5.59.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "../services/libs/redis/node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { - "version": "5.1.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "../services/libs/redis/node_modules/@typescript-eslint/utils/node_modules/estraverse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/redis/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.59.6", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/redis/node_modules/acorn": { - "version": "8.8.2", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "../services/libs/redis/node_modules/acorn-jsx": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "../services/libs/redis/node_modules/ajv": { - "version": "6.12.6", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "../services/libs/redis/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/redis/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "../services/libs/redis/node_modules/argparse": { - "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, - "../services/libs/redis/node_modules/array-union": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/redis/node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "../services/libs/redis/node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "../services/libs/redis/node_modules/braces": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/redis/node_modules/callsites": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "../services/libs/redis/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "../services/libs/redis/node_modules/cluster-key-slot": { - "version": "1.1.2", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/redis/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "../services/libs/redis/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/redis/node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "../services/libs/redis/node_modules/cross-spawn": { - "version": "7.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/redis/node_modules/debug": { - "version": "4.3.4", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "../services/libs/redis/node_modules/deep-is": { - "version": "0.1.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/redis/node_modules/dir-glob": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/redis/node_modules/doctrine": { - "version": "3.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "../services/libs/redis/node_modules/escape-string-regexp": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/redis/node_modules/eslint": { - "version": "8.40.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/redis/node_modules/eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "../services/libs/redis/node_modules/eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "eslint": ">=7.28.0", - "prettier": ">=2.0.0" - }, - "peerDependenciesMeta": { - "eslint-config-prettier": { - "optional": true - } - } - }, - "../services/libs/redis/node_modules/eslint-scope": { - "version": "7.2.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/redis/node_modules/eslint-visitor-keys": { - "version": "3.4.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/redis/node_modules/espree": { - "version": "9.5.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/redis/node_modules/esquery": { - "version": "1.5.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "../services/libs/redis/node_modules/esrecurse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/redis/node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/redis/node_modules/esutils": { - "version": "2.0.3", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/redis/node_modules/fast-deep-equal": { - "version": "3.1.3", - "dev": true, - "license": "MIT" - }, - "../services/libs/redis/node_modules/fast-diff": { - "version": "1.2.0", - "dev": true, - "license": "Apache-2.0" - }, - "../services/libs/redis/node_modules/fast-glob": { - "version": "3.2.12", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "../services/libs/redis/node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "../services/libs/redis/node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/redis/node_modules/fast-levenshtein": { - "version": "2.0.6", - "dev": true, - "license": "MIT" - }, - "../services/libs/redis/node_modules/fastq": { - "version": "1.15.0", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "../services/libs/redis/node_modules/file-entry-cache": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "../services/libs/redis/node_modules/fill-range": { - "version": "7.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/redis/node_modules/find-up": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/redis/node_modules/flat-cache": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "../services/libs/redis/node_modules/flatted": { - "version": "3.2.7", - "dev": true, - "license": "ISC" - }, - "../services/libs/redis/node_modules/fs.realpath": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "../services/libs/redis/node_modules/generic-pool": { - "version": "3.9.0", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "../services/libs/redis/node_modules/glob": { - "version": "7.2.3", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "../services/libs/redis/node_modules/glob-parent": { - "version": "6.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "../services/libs/redis/node_modules/globals": { - "version": "13.20.0", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/redis/node_modules/globby": { - "version": "11.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/redis/node_modules/grapheme-splitter": { - "version": "1.0.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/redis/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/redis/node_modules/ignore": { - "version": "5.2.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "../services/libs/redis/node_modules/import-fresh": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/redis/node_modules/imurmurhash": { - "version": "0.1.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "../services/libs/redis/node_modules/inflight": { - "version": "1.0.6", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "../services/libs/redis/node_modules/inherits": { - "version": "2.0.4", - "dev": true, - "license": "ISC" - }, - "../services/libs/redis/node_modules/is-extglob": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/redis/node_modules/is-glob": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/redis/node_modules/is-number": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "../services/libs/redis/node_modules/is-path-inside": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/redis/node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "../services/libs/redis/node_modules/js-sdsl": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, - "../services/libs/redis/node_modules/js-yaml": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "../services/libs/redis/node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "../services/libs/redis/node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "../services/libs/redis/node_modules/levn": { - "version": "0.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/redis/node_modules/locate-path": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/redis/node_modules/lodash.merge": { - "version": "4.6.2", - "dev": true, - "license": "MIT" - }, - "../services/libs/redis/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "../services/libs/redis/node_modules/merge2": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "../services/libs/redis/node_modules/micromatch": { - "version": "4.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "../services/libs/redis/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "../services/libs/redis/node_modules/ms": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "../services/libs/redis/node_modules/natural-compare": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/redis/node_modules/natural-compare-lite": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/redis/node_modules/once": { - "version": "1.4.0", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "../services/libs/redis/node_modules/optionator": { - "version": "0.9.1", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/redis/node_modules/p-limit": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/redis/node_modules/p-locate": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/redis/node_modules/parent-module": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "../services/libs/redis/node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/redis/node_modules/path-is-absolute": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/redis/node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/redis/node_modules/path-type": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/redis/node_modules/picomatch": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "../services/libs/redis/node_modules/prelude-ls": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/redis/node_modules/prettier": { - "version": "2.8.8", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "../services/libs/redis/node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "../services/libs/redis/node_modules/punycode": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "../services/libs/redis/node_modules/queue-microtask": { - "version": "1.2.3", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "../services/libs/redis/node_modules/redis": { - "version": "4.6.6", - "license": "MIT", - "workspaces": [ - "./packages/*" - ], - "dependencies": { - "@redis/bloom": "1.2.0", - "@redis/client": "1.5.7", - "@redis/graph": "1.1.0", - "@redis/json": "1.0.4", - "@redis/search": "1.1.2", - "@redis/time-series": "1.0.4" - } - }, - "../services/libs/redis/node_modules/resolve-from": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "../services/libs/redis/node_modules/reusify": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "../services/libs/redis/node_modules/rimraf": { - "version": "3.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "../services/libs/redis/node_modules/run-parallel": { - "version": "1.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "../services/libs/redis/node_modules/semver": { - "version": "7.5.1", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "../services/libs/redis/node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/redis/node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/redis/node_modules/slash": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/redis/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/redis/node_modules/strip-json-comments": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/redis/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/redis/node_modules/text-table": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/redis/node_modules/to-regex-range": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "../services/libs/redis/node_modules/tslib": { - "version": "1.14.1", - "dev": true, - "license": "0BSD" - }, - "../services/libs/redis/node_modules/tsutils": { - "version": "3.21.0", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "../services/libs/redis/node_modules/type-check": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/redis/node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/redis/node_modules/typescript": { - "version": "5.0.4", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=12.20" - } - }, - "../services/libs/redis/node_modules/uri-js": { - "version": "4.4.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "../services/libs/redis/node_modules/which": { - "version": "2.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/redis/node_modules/word-wrap": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/redis/node_modules/wrappy": { - "version": "1.0.2", - "dev": true, - "license": "ISC" - }, - "../services/libs/redis/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "../services/libs/redis/node_modules/yocto-queue": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/sqs": { - "name": "@crowd/sqs", - "version": "1.0.0", - "dependencies": { - "@aws-sdk/client-sqs": "^3.332.0", - "@aws-sdk/types": "^3.329.0", - "@crowd/common": "file:../common", - "@crowd/logging": "file:../logging", - "@crowd/tracing": "file:../tracing", - "@crowd/types": "file:../types", - "@smithy/util-retry": "^2.0.1" - }, - "devDependencies": { - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - } - }, - "../services/libs/sqs/node_modules/@aws-crypto/ie11-detection": { - "version": "3.0.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^1.11.1" - } - }, - "../services/libs/sqs/node_modules/@aws-crypto/ie11-detection/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "../services/libs/sqs/node_modules/@aws-crypto/sha256-browser": { - "version": "3.0.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/ie11-detection": "^3.0.0", - "@aws-crypto/sha256-js": "^3.0.0", - "@aws-crypto/supports-web-crypto": "^3.0.0", - "@aws-crypto/util": "^3.0.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@aws-sdk/util-utf8-browser": "^3.0.0", - "tslib": "^1.11.1" - } - }, - "../services/libs/sqs/node_modules/@aws-crypto/sha256-browser/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "../services/libs/sqs/node_modules/@aws-crypto/sha256-js": { - "version": "3.0.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^3.0.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^1.11.1" - } - }, - "../services/libs/sqs/node_modules/@aws-crypto/sha256-js/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "../services/libs/sqs/node_modules/@aws-crypto/supports-web-crypto": { - "version": "3.0.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^1.11.1" - } - }, - "../services/libs/sqs/node_modules/@aws-crypto/supports-web-crypto/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "../services/libs/sqs/node_modules/@aws-crypto/util": { - "version": "3.0.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-utf8-browser": "^3.0.0", - "tslib": "^1.11.1" - } - }, - "../services/libs/sqs/node_modules/@aws-crypto/util/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "../services/libs/sqs/node_modules/@aws-sdk/abort-controller": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/client-sqs": { - "version": "3.332.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.332.0", - "@aws-sdk/config-resolver": "3.329.0", - "@aws-sdk/credential-provider-node": "3.332.0", - "@aws-sdk/fetch-http-handler": "3.329.0", - "@aws-sdk/hash-node": "3.329.0", - "@aws-sdk/invalid-dependency": "3.329.0", - "@aws-sdk/md5-js": "3.329.0", - "@aws-sdk/middleware-content-length": "3.329.0", - "@aws-sdk/middleware-endpoint": "3.329.0", - "@aws-sdk/middleware-host-header": "3.329.0", - "@aws-sdk/middleware-logger": "3.329.0", - "@aws-sdk/middleware-recursion-detection": "3.329.0", - "@aws-sdk/middleware-retry": "3.329.0", - "@aws-sdk/middleware-sdk-sqs": "3.329.0", - "@aws-sdk/middleware-serde": "3.329.0", - "@aws-sdk/middleware-signing": "3.329.0", - "@aws-sdk/middleware-stack": "3.329.0", - "@aws-sdk/middleware-user-agent": "3.332.0", - "@aws-sdk/node-config-provider": "3.329.0", - "@aws-sdk/node-http-handler": "3.329.0", - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/smithy-client": "3.329.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/url-parser": "3.329.0", - "@aws-sdk/util-base64": "3.310.0", - "@aws-sdk/util-body-length-browser": "3.310.0", - "@aws-sdk/util-body-length-node": "3.310.0", - "@aws-sdk/util-defaults-mode-browser": "3.329.0", - "@aws-sdk/util-defaults-mode-node": "3.329.0", - "@aws-sdk/util-endpoints": "3.332.0", - "@aws-sdk/util-retry": "3.329.0", - "@aws-sdk/util-user-agent-browser": "3.329.0", - "@aws-sdk/util-user-agent-node": "3.329.0", - "@aws-sdk/util-utf8": "3.310.0", - "fast-xml-parser": "4.1.2", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/client-sso": { - "version": "3.332.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/config-resolver": "3.329.0", - "@aws-sdk/fetch-http-handler": "3.329.0", - "@aws-sdk/hash-node": "3.329.0", - "@aws-sdk/invalid-dependency": "3.329.0", - "@aws-sdk/middleware-content-length": "3.329.0", - "@aws-sdk/middleware-endpoint": "3.329.0", - "@aws-sdk/middleware-host-header": "3.329.0", - "@aws-sdk/middleware-logger": "3.329.0", - "@aws-sdk/middleware-recursion-detection": "3.329.0", - "@aws-sdk/middleware-retry": "3.329.0", - "@aws-sdk/middleware-serde": "3.329.0", - "@aws-sdk/middleware-stack": "3.329.0", - "@aws-sdk/middleware-user-agent": "3.332.0", - "@aws-sdk/node-config-provider": "3.329.0", - "@aws-sdk/node-http-handler": "3.329.0", - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/smithy-client": "3.329.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/url-parser": "3.329.0", - "@aws-sdk/util-base64": "3.310.0", - "@aws-sdk/util-body-length-browser": "3.310.0", - "@aws-sdk/util-body-length-node": "3.310.0", - "@aws-sdk/util-defaults-mode-browser": "3.329.0", - "@aws-sdk/util-defaults-mode-node": "3.329.0", - "@aws-sdk/util-endpoints": "3.332.0", - "@aws-sdk/util-retry": "3.329.0", - "@aws-sdk/util-user-agent-browser": "3.329.0", - "@aws-sdk/util-user-agent-node": "3.329.0", - "@aws-sdk/util-utf8": "3.310.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.332.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/config-resolver": "3.329.0", - "@aws-sdk/fetch-http-handler": "3.329.0", - "@aws-sdk/hash-node": "3.329.0", - "@aws-sdk/invalid-dependency": "3.329.0", - "@aws-sdk/middleware-content-length": "3.329.0", - "@aws-sdk/middleware-endpoint": "3.329.0", - "@aws-sdk/middleware-host-header": "3.329.0", - "@aws-sdk/middleware-logger": "3.329.0", - "@aws-sdk/middleware-recursion-detection": "3.329.0", - "@aws-sdk/middleware-retry": "3.329.0", - "@aws-sdk/middleware-serde": "3.329.0", - "@aws-sdk/middleware-stack": "3.329.0", - "@aws-sdk/middleware-user-agent": "3.332.0", - "@aws-sdk/node-config-provider": "3.329.0", - "@aws-sdk/node-http-handler": "3.329.0", - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/smithy-client": "3.329.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/url-parser": "3.329.0", - "@aws-sdk/util-base64": "3.310.0", - "@aws-sdk/util-body-length-browser": "3.310.0", - "@aws-sdk/util-body-length-node": "3.310.0", - "@aws-sdk/util-defaults-mode-browser": "3.329.0", - "@aws-sdk/util-defaults-mode-node": "3.329.0", - "@aws-sdk/util-endpoints": "3.332.0", - "@aws-sdk/util-retry": "3.329.0", - "@aws-sdk/util-user-agent-browser": "3.329.0", - "@aws-sdk/util-user-agent-node": "3.329.0", - "@aws-sdk/util-utf8": "3.310.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/client-sts": { - "version": "3.332.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/config-resolver": "3.329.0", - "@aws-sdk/credential-provider-node": "3.332.0", - "@aws-sdk/fetch-http-handler": "3.329.0", - "@aws-sdk/hash-node": "3.329.0", - "@aws-sdk/invalid-dependency": "3.329.0", - "@aws-sdk/middleware-content-length": "3.329.0", - "@aws-sdk/middleware-endpoint": "3.329.0", - "@aws-sdk/middleware-host-header": "3.329.0", - "@aws-sdk/middleware-logger": "3.329.0", - "@aws-sdk/middleware-recursion-detection": "3.329.0", - "@aws-sdk/middleware-retry": "3.329.0", - "@aws-sdk/middleware-sdk-sts": "3.329.0", - "@aws-sdk/middleware-serde": "3.329.0", - "@aws-sdk/middleware-signing": "3.329.0", - "@aws-sdk/middleware-stack": "3.329.0", - "@aws-sdk/middleware-user-agent": "3.332.0", - "@aws-sdk/node-config-provider": "3.329.0", - "@aws-sdk/node-http-handler": "3.329.0", - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/smithy-client": "3.329.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/url-parser": "3.329.0", - "@aws-sdk/util-base64": "3.310.0", - "@aws-sdk/util-body-length-browser": "3.310.0", - "@aws-sdk/util-body-length-node": "3.310.0", - "@aws-sdk/util-defaults-mode-browser": "3.329.0", - "@aws-sdk/util-defaults-mode-node": "3.329.0", - "@aws-sdk/util-endpoints": "3.332.0", - "@aws-sdk/util-retry": "3.329.0", - "@aws-sdk/util-user-agent-browser": "3.329.0", - "@aws-sdk/util-user-agent-node": "3.329.0", - "@aws-sdk/util-utf8": "3.310.0", - "fast-xml-parser": "4.1.2", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/config-resolver": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.329.0", - "@aws-sdk/util-config-provider": "3.310.0", - "@aws-sdk/util-middleware": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/credential-provider-imds": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/node-config-provider": "3.329.0", - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/url-parser": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.332.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.329.0", - "@aws-sdk/credential-provider-imds": "3.329.0", - "@aws-sdk/credential-provider-process": "3.329.0", - "@aws-sdk/credential-provider-sso": "3.332.0", - "@aws-sdk/credential-provider-web-identity": "3.329.0", - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/shared-ini-file-loader": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.332.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.329.0", - "@aws-sdk/credential-provider-imds": "3.329.0", - "@aws-sdk/credential-provider-ini": "3.332.0", - "@aws-sdk/credential-provider-process": "3.329.0", - "@aws-sdk/credential-provider-sso": "3.332.0", - "@aws-sdk/credential-provider-web-identity": "3.329.0", - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/shared-ini-file-loader": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/shared-ini-file-loader": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.332.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sso": "3.332.0", - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/shared-ini-file-loader": "3.329.0", - "@aws-sdk/token-providers": "3.332.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/fetch-http-handler": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/querystring-builder": "3.329.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/util-base64": "3.310.0", - "tslib": "^2.5.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/hash-node": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.329.0", - "@aws-sdk/util-buffer-from": "3.310.0", - "@aws-sdk/util-utf8": "3.310.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/invalid-dependency": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/is-array-buffer": { - "version": "3.310.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/md5-js": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.329.0", - "@aws-sdk/util-utf8": "3.310.0", - "tslib": "^2.5.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/middleware-content-length": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/middleware-endpoint": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-serde": "3.329.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/url-parser": "3.329.0", - "@aws-sdk/util-middleware": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/middleware-logger": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/middleware-retry": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/service-error-classification": "3.329.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/util-middleware": "3.329.0", - "@aws-sdk/util-retry": "3.329.0", - "tslib": "^2.5.0", - "uuid": "^8.3.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/middleware-sdk-sqs": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.329.0", - "@aws-sdk/util-hex-encoding": "3.310.0", - "@aws-sdk/util-utf8": "3.310.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/middleware-sdk-sts": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-signing": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/middleware-serde": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/middleware-signing": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/signature-v4": "3.329.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/util-middleware": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/middleware-stack": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.332.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/util-endpoints": "3.332.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/node-config-provider": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/shared-ini-file-loader": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/node-http-handler": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/abort-controller": "3.329.0", - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/querystring-builder": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/property-provider": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/protocol-http": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/querystring-builder": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.329.0", - "@aws-sdk/util-uri-escape": "3.310.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/querystring-parser": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/service-error-classification": { - "version": "3.329.0", - "license": "Apache-2.0", - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/shared-ini-file-loader": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/signature-v4": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/is-array-buffer": "3.310.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/util-hex-encoding": "3.310.0", - "@aws-sdk/util-middleware": "3.329.0", - "@aws-sdk/util-uri-escape": "3.310.0", - "@aws-sdk/util-utf8": "3.310.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/smithy-client": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-stack": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/token-providers": { - "version": "3.332.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sso-oidc": "3.332.0", - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/shared-ini-file-loader": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/types": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/url-parser": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/querystring-parser": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/util-base64": { - "version": "3.310.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/util-buffer-from": "3.310.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/util-body-length-browser": { - "version": "3.310.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/util-body-length-node": { - "version": "3.310.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/util-buffer-from": { - "version": "3.310.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/is-array-buffer": "3.310.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/util-config-provider": { - "version": "3.310.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/util-defaults-mode-browser": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/types": "3.329.0", - "bowser": "^2.11.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/util-defaults-mode-node": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/config-resolver": "3.329.0", - "@aws-sdk/credential-provider-imds": "3.329.0", - "@aws-sdk/node-config-provider": "3.329.0", - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/util-endpoints": { - "version": "3.332.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/util-hex-encoding": { - "version": "3.310.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/util-locate-window": { - "version": "3.310.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/util-middleware": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/util-retry": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/service-error-classification": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/util-uri-escape": { - "version": "3.310.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.329.0", - "bowser": "^2.11.0", - "tslib": "^2.5.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.329.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/node-config-provider": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/util-utf8": { - "version": "3.310.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/util-buffer-from": "3.310.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "../services/libs/sqs/node_modules/@aws-sdk/util-utf8-browser": { - "version": "3.259.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.3.1" - } - }, - "../services/libs/sqs/node_modules/@crowd/common": { - "resolved": "../services/libs/common", - "link": true - }, - "../services/libs/sqs/node_modules/@crowd/logging": { - "resolved": "../services/libs/logging", - "link": true - }, - "../services/libs/sqs/node_modules/@crowd/types": { - "resolved": "../services/libs/types", - "link": true - }, - "../services/libs/sqs/node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "../services/libs/sqs/node_modules/@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "../services/libs/sqs/node_modules/@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/sqs/node_modules/@eslint/js": { - "version": "8.40.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "../services/libs/sqs/node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "../services/libs/sqs/node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "../services/libs/sqs/node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true, - "license": "BSD-3-Clause" - }, - "../services/libs/sqs/node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/sqs/node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "../services/libs/sqs/node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/sqs/node_modules/@types/json-schema": { - "version": "7.0.11", - "dev": true, - "license": "MIT" - }, - "../services/libs/sqs/node_modules/@types/node": { - "version": "18.16.8", - "dev": true, - "license": "MIT" - }, - "../services/libs/sqs/node_modules/@types/semver": { - "version": "7.5.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/sqs/node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/type-utils": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/sqs/node_modules/@typescript-eslint/parser": { - "version": "5.59.5", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "debug": "^4.3.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/sqs/node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/sqs/node_modules/@typescript-eslint/type-utils": { - "version": "5.59.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/sqs/node_modules/@typescript-eslint/types": { - "version": "5.59.5", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/sqs/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.5", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/sqs/node_modules/@typescript-eslint/utils": { - "version": "5.59.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "../services/libs/sqs/node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { - "version": "5.1.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "../services/libs/sqs/node_modules/@typescript-eslint/utils/node_modules/estraverse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/sqs/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.59.5", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/sqs/node_modules/acorn": { - "version": "8.8.2", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "../services/libs/sqs/node_modules/acorn-jsx": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "../services/libs/sqs/node_modules/ajv": { - "version": "6.12.6", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "../services/libs/sqs/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/sqs/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "../services/libs/sqs/node_modules/argparse": { - "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, - "../services/libs/sqs/node_modules/array-union": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/sqs/node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "../services/libs/sqs/node_modules/bowser": { - "version": "2.11.0", - "license": "MIT" - }, - "../services/libs/sqs/node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "../services/libs/sqs/node_modules/braces": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/sqs/node_modules/callsites": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "../services/libs/sqs/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "../services/libs/sqs/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "../services/libs/sqs/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/sqs/node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "../services/libs/sqs/node_modules/cross-spawn": { - "version": "7.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/sqs/node_modules/debug": { - "version": "4.3.4", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "../services/libs/sqs/node_modules/deep-is": { - "version": "0.1.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/sqs/node_modules/dir-glob": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/sqs/node_modules/doctrine": { - "version": "3.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "../services/libs/sqs/node_modules/escape-string-regexp": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/sqs/node_modules/eslint": { - "version": "8.40.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/sqs/node_modules/eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "../services/libs/sqs/node_modules/eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "eslint": ">=7.28.0", - "prettier": ">=2.0.0" - }, - "peerDependenciesMeta": { - "eslint-config-prettier": { - "optional": true - } - } - }, - "../services/libs/sqs/node_modules/eslint-scope": { - "version": "7.2.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/sqs/node_modules/eslint-visitor-keys": { - "version": "3.4.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/sqs/node_modules/espree": { - "version": "9.5.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/sqs/node_modules/esquery": { - "version": "1.5.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "../services/libs/sqs/node_modules/esrecurse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/sqs/node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/sqs/node_modules/esutils": { - "version": "2.0.3", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/sqs/node_modules/fast-deep-equal": { - "version": "3.1.3", - "dev": true, - "license": "MIT" - }, - "../services/libs/sqs/node_modules/fast-diff": { - "version": "1.2.0", - "dev": true, - "license": "Apache-2.0" - }, - "../services/libs/sqs/node_modules/fast-glob": { - "version": "3.2.12", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "../services/libs/sqs/node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "../services/libs/sqs/node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/sqs/node_modules/fast-levenshtein": { - "version": "2.0.6", - "dev": true, - "license": "MIT" - }, - "../services/libs/sqs/node_modules/fast-xml-parser": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "strnum": "^1.0.5" - }, - "bin": { - "fxparser": "src/cli/cli.js" - }, - "funding": { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" - } - }, - "../services/libs/sqs/node_modules/fastq": { - "version": "1.15.0", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "../services/libs/sqs/node_modules/file-entry-cache": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "../services/libs/sqs/node_modules/fill-range": { - "version": "7.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/sqs/node_modules/find-up": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/sqs/node_modules/flat-cache": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "../services/libs/sqs/node_modules/flatted": { - "version": "3.2.7", - "dev": true, - "license": "ISC" - }, - "../services/libs/sqs/node_modules/fs.realpath": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "../services/libs/sqs/node_modules/glob": { - "version": "7.2.3", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "../services/libs/sqs/node_modules/glob-parent": { - "version": "6.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "../services/libs/sqs/node_modules/globals": { - "version": "13.20.0", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/sqs/node_modules/globby": { - "version": "11.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/sqs/node_modules/grapheme-splitter": { - "version": "1.0.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/sqs/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/sqs/node_modules/ignore": { - "version": "5.2.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "../services/libs/sqs/node_modules/import-fresh": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/sqs/node_modules/imurmurhash": { - "version": "0.1.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "../services/libs/sqs/node_modules/inflight": { - "version": "1.0.6", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "../services/libs/sqs/node_modules/inherits": { - "version": "2.0.4", - "dev": true, - "license": "ISC" - }, - "../services/libs/sqs/node_modules/is-extglob": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/sqs/node_modules/is-glob": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/sqs/node_modules/is-number": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "../services/libs/sqs/node_modules/is-path-inside": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/sqs/node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "../services/libs/sqs/node_modules/js-sdsl": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, - "../services/libs/sqs/node_modules/js-yaml": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "../services/libs/sqs/node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "../services/libs/sqs/node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "../services/libs/sqs/node_modules/levn": { - "version": "0.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/sqs/node_modules/locate-path": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/sqs/node_modules/lodash.merge": { - "version": "4.6.2", - "dev": true, - "license": "MIT" - }, - "../services/libs/sqs/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "../services/libs/sqs/node_modules/merge2": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "../services/libs/sqs/node_modules/micromatch": { - "version": "4.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "../services/libs/sqs/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "../services/libs/sqs/node_modules/ms": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "../services/libs/sqs/node_modules/natural-compare": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/sqs/node_modules/natural-compare-lite": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/sqs/node_modules/once": { - "version": "1.4.0", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "../services/libs/sqs/node_modules/optionator": { - "version": "0.9.1", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/sqs/node_modules/p-limit": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/sqs/node_modules/p-locate": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/sqs/node_modules/parent-module": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "../services/libs/sqs/node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/sqs/node_modules/path-is-absolute": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/sqs/node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/sqs/node_modules/path-type": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/sqs/node_modules/picomatch": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "../services/libs/sqs/node_modules/prelude-ls": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/sqs/node_modules/prettier": { - "version": "2.8.8", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "../services/libs/sqs/node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "../services/libs/sqs/node_modules/punycode": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "../services/libs/sqs/node_modules/queue-microtask": { - "version": "1.2.3", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "../services/libs/sqs/node_modules/resolve-from": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "../services/libs/sqs/node_modules/reusify": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "../services/libs/sqs/node_modules/rimraf": { - "version": "3.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "../services/libs/sqs/node_modules/run-parallel": { - "version": "1.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "../services/libs/sqs/node_modules/semver": { - "version": "7.5.0", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "../services/libs/sqs/node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/sqs/node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/sqs/node_modules/slash": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/sqs/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/sqs/node_modules/strip-json-comments": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/sqs/node_modules/strnum": { - "version": "1.0.5", - "license": "MIT" - }, - "../services/libs/sqs/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/sqs/node_modules/text-table": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/sqs/node_modules/to-regex-range": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "../services/libs/sqs/node_modules/tslib": { - "version": "2.5.0", - "license": "0BSD" - }, - "../services/libs/sqs/node_modules/tsutils": { - "version": "3.21.0", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "../services/libs/sqs/node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "dev": true, - "license": "0BSD" - }, - "../services/libs/sqs/node_modules/type-check": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/sqs/node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/sqs/node_modules/typescript": { - "version": "5.0.4", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=12.20" - } - }, - "../services/libs/sqs/node_modules/uri-js": { - "version": "4.4.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "../services/libs/sqs/node_modules/uuid": { - "version": "8.3.2", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "../services/libs/sqs/node_modules/which": { - "version": "2.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/sqs/node_modules/word-wrap": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/sqs/node_modules/wrappy": { - "version": "1.0.2", - "dev": true, - "license": "ISC" - }, - "../services/libs/sqs/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "../services/libs/sqs/node_modules/yocto-queue": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/tracing": { - "name": "@crowd/tracing", - "version": "1.0.0", - "dependencies": { - "@crowd/common": "file:../common", - "@opentelemetry/api": "~1.6.0", - "@opentelemetry/exporter-trace-otlp-grpc": "~0.43.0", - "@opentelemetry/instrumentation-aws-sdk": "~0.36.0", - "@opentelemetry/instrumentation-bunyan": "~0.32.1", - "@opentelemetry/instrumentation-express": "~0.33.1", - "@opentelemetry/instrumentation-http": "~0.43.0", - "@opentelemetry/instrumentation-redis": "~0.35.1", - "@opentelemetry/resource-detector-aws": "~1.3.1", - "@opentelemetry/resources": "~1.17.0", - "@opentelemetry/sdk-node": "~0.43.0", - "@opentelemetry/semantic-conventions": "~1.17.0", - "opentelemetry-instrumentation-sequelize": "~0.39.1" - }, - "devDependencies": { - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - } - }, - "../services/libs/types": { - "name": "@crowd/types", - "version": "1.0.0", - "devDependencies": { - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - } - }, - "../services/libs/types/node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "../services/libs/types/node_modules/@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "../services/libs/types/node_modules/@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/types/node_modules/@eslint/js": { - "version": "8.40.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "../services/libs/types/node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "../services/libs/types/node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "../services/libs/types/node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true, - "license": "BSD-3-Clause" - }, - "../services/libs/types/node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/types/node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "../services/libs/types/node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/types/node_modules/@types/json-schema": { - "version": "7.0.11", - "dev": true, - "license": "MIT" - }, - "../services/libs/types/node_modules/@types/node": { - "version": "18.16.9", - "dev": true, - "license": "MIT" - }, - "../services/libs/types/node_modules/@types/semver": { - "version": "7.5.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/types/node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/type-utils": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/types/node_modules/@typescript-eslint/parser": { - "version": "5.59.5", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "debug": "^4.3.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/types/node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/types/node_modules/@typescript-eslint/type-utils": { - "version": "5.59.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/types/node_modules/@typescript-eslint/types": { - "version": "5.59.5", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/types/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.5", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "../services/libs/types/node_modules/@typescript-eslint/utils": { - "version": "5.59.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "../services/libs/types/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.59.5", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "../services/libs/types/node_modules/acorn": { - "version": "8.8.2", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "../services/libs/types/node_modules/acorn-jsx": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "../services/libs/types/node_modules/ajv": { - "version": "6.12.6", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "../services/libs/types/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/types/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "../services/libs/types/node_modules/argparse": { - "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, - "../services/libs/types/node_modules/array-union": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/types/node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "../services/libs/types/node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "../services/libs/types/node_modules/braces": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/types/node_modules/callsites": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "../services/libs/types/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "../services/libs/types/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "../services/libs/types/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/types/node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "../services/libs/types/node_modules/cross-spawn": { - "version": "7.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/types/node_modules/debug": { - "version": "4.3.4", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "../services/libs/types/node_modules/deep-is": { - "version": "0.1.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/types/node_modules/dir-glob": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/types/node_modules/doctrine": { - "version": "3.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "../services/libs/types/node_modules/escape-string-regexp": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/types/node_modules/eslint": { - "version": "8.40.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/types/node_modules/eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "../services/libs/types/node_modules/eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "eslint": ">=7.28.0", - "prettier": ">=2.0.0" - }, - "peerDependenciesMeta": { - "eslint-config-prettier": { - "optional": true - } - } - }, - "../services/libs/types/node_modules/eslint-scope": { - "version": "5.1.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "../services/libs/types/node_modules/eslint-visitor-keys": { - "version": "3.4.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/types/node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/types/node_modules/eslint/node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/types/node_modules/espree": { - "version": "9.5.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "../services/libs/types/node_modules/esquery": { - "version": "1.5.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "../services/libs/types/node_modules/esquery/node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/types/node_modules/esrecurse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/types/node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/types/node_modules/estraverse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "../services/libs/types/node_modules/esutils": { - "version": "2.0.3", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/types/node_modules/fast-deep-equal": { - "version": "3.1.3", - "dev": true, - "license": "MIT" - }, - "../services/libs/types/node_modules/fast-diff": { - "version": "1.2.0", - "dev": true, - "license": "Apache-2.0" - }, - "../services/libs/types/node_modules/fast-glob": { - "version": "3.2.12", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "../services/libs/types/node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "../services/libs/types/node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/types/node_modules/fast-levenshtein": { - "version": "2.0.6", - "dev": true, - "license": "MIT" - }, - "../services/libs/types/node_modules/fastq": { - "version": "1.15.0", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "../services/libs/types/node_modules/file-entry-cache": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "../services/libs/types/node_modules/fill-range": { - "version": "7.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/types/node_modules/find-up": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/types/node_modules/flat-cache": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "../services/libs/types/node_modules/flatted": { - "version": "3.2.7", - "dev": true, - "license": "ISC" - }, - "../services/libs/types/node_modules/fs.realpath": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "../services/libs/types/node_modules/glob": { - "version": "7.2.3", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "../services/libs/types/node_modules/glob-parent": { - "version": "6.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "../services/libs/types/node_modules/globals": { - "version": "13.20.0", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/types/node_modules/globby": { - "version": "11.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/types/node_modules/grapheme-splitter": { - "version": "1.0.4", - "dev": true, - "license": "MIT" - }, - "../services/libs/types/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/types/node_modules/ignore": { - "version": "5.2.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "../services/libs/types/node_modules/import-fresh": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/types/node_modules/imurmurhash": { - "version": "0.1.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "../services/libs/types/node_modules/inflight": { - "version": "1.0.6", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "../services/libs/types/node_modules/inherits": { - "version": "2.0.4", - "dev": true, - "license": "ISC" - }, - "../services/libs/types/node_modules/is-extglob": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/types/node_modules/is-glob": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/types/node_modules/is-number": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "../services/libs/types/node_modules/is-path-inside": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/types/node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "../services/libs/types/node_modules/js-sdsl": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, - "../services/libs/types/node_modules/js-yaml": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "../services/libs/types/node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "../services/libs/types/node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "../services/libs/types/node_modules/levn": { - "version": "0.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/types/node_modules/locate-path": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/types/node_modules/lodash.merge": { - "version": "4.6.2", - "dev": true, - "license": "MIT" - }, - "../services/libs/types/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "../services/libs/types/node_modules/merge2": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "../services/libs/types/node_modules/micromatch": { - "version": "4.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "../services/libs/types/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "../services/libs/types/node_modules/ms": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "../services/libs/types/node_modules/natural-compare": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/types/node_modules/natural-compare-lite": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/types/node_modules/once": { - "version": "1.4.0", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "../services/libs/types/node_modules/optionator": { - "version": "0.9.1", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/types/node_modules/p-limit": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/types/node_modules/p-locate": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/types/node_modules/parent-module": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "../services/libs/types/node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/types/node_modules/path-is-absolute": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/types/node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/types/node_modules/path-type": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/types/node_modules/picomatch": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "../services/libs/types/node_modules/prelude-ls": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/types/node_modules/prettier": { - "version": "2.8.8", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "../services/libs/types/node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "../services/libs/types/node_modules/punycode": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "../services/libs/types/node_modules/queue-microtask": { - "version": "1.2.3", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "../services/libs/types/node_modules/resolve-from": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "../services/libs/types/node_modules/reusify": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "../services/libs/types/node_modules/rimraf": { - "version": "3.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "../services/libs/types/node_modules/run-parallel": { - "version": "1.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "../services/libs/types/node_modules/semver": { - "version": "7.5.1", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "../services/libs/types/node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/types/node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/types/node_modules/slash": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../services/libs/types/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/types/node_modules/strip-json-comments": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/types/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../services/libs/types/node_modules/text-table": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, - "../services/libs/types/node_modules/to-regex-range": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "../services/libs/types/node_modules/tslib": { - "version": "1.14.1", - "dev": true, - "license": "0BSD" - }, - "../services/libs/types/node_modules/tsutils": { - "version": "3.21.0", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "../services/libs/types/node_modules/type-check": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "../services/libs/types/node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../services/libs/types/node_modules/typescript": { - "version": "5.0.4", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=12.20" - } - }, - "../services/libs/types/node_modules/uri-js": { - "version": "4.4.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "../services/libs/types/node_modules/which": { - "version": "2.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "../services/libs/types/node_modules/word-wrap": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../services/libs/types/node_modules/wrappy": { - "version": "1.0.2", - "dev": true, - "license": "ISC" - }, - "../services/libs/types/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "../services/libs/types/node_modules/yocto-queue": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@actions/core": { - "version": "1.10.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" - } - }, - "node_modules/@actions/core/node_modules/uuid": { - "version": "8.3.2", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@actions/http-client": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "tunnel": "^0.0.6" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@apidevtools/openapi-schemas": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/@apidevtools/swagger-methods": { - "version": "3.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@aws-crypto/crc32": { - "version": "3.0.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^3.0.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/crc32/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@aws-crypto/ie11-detection": { - "version": "3.0.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/ie11-detection/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@aws-crypto/sha256-browser": { - "version": "3.0.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/ie11-detection": "^3.0.0", - "@aws-crypto/sha256-js": "^3.0.0", - "@aws-crypto/supports-web-crypto": "^3.0.0", - "@aws-crypto/util": "^3.0.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@aws-sdk/util-utf8-browser": "^3.0.0", - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@aws-crypto/sha256-js": { - "version": "3.0.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^3.0.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/sha256-js/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@aws-crypto/supports-web-crypto": { - "version": "3.0.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/supports-web-crypto/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@aws-crypto/util": { - "version": "3.0.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-utf8-browser": "^3.0.0", - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/util/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@aws-sdk/abort-controller": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-comprehend": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.357.0", - "@aws-sdk/config-resolver": "3.357.0", - "@aws-sdk/credential-provider-node": "3.357.0", - "@aws-sdk/fetch-http-handler": "3.357.0", - "@aws-sdk/hash-node": "3.357.0", - "@aws-sdk/invalid-dependency": "3.357.0", - "@aws-sdk/middleware-content-length": "3.357.0", - "@aws-sdk/middleware-endpoint": "3.357.0", - "@aws-sdk/middleware-host-header": "3.357.0", - "@aws-sdk/middleware-logger": "3.357.0", - "@aws-sdk/middleware-recursion-detection": "3.357.0", - "@aws-sdk/middleware-retry": "3.357.0", - "@aws-sdk/middleware-serde": "3.357.0", - "@aws-sdk/middleware-signing": "3.357.0", - "@aws-sdk/middleware-stack": "3.357.0", - "@aws-sdk/middleware-user-agent": "3.357.0", - "@aws-sdk/node-config-provider": "3.357.0", - "@aws-sdk/node-http-handler": "3.357.0", - "@aws-sdk/smithy-client": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/url-parser": "3.357.0", - "@aws-sdk/util-base64": "3.310.0", - "@aws-sdk/util-body-length-browser": "3.310.0", - "@aws-sdk/util-body-length-node": "3.310.0", - "@aws-sdk/util-defaults-mode-browser": "3.357.0", - "@aws-sdk/util-defaults-mode-node": "3.357.0", - "@aws-sdk/util-endpoints": "3.357.0", - "@aws-sdk/util-retry": "3.357.0", - "@aws-sdk/util-user-agent-browser": "3.357.0", - "@aws-sdk/util-user-agent-node": "3.357.0", - "@aws-sdk/util-utf8": "3.310.0", - "@smithy/protocol-http": "^1.0.1", - "@smithy/types": "^1.0.0", - "tslib": "^2.5.0", - "uuid": "^8.3.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-comprehend/node_modules/uuid": { - "version": "8.3.2", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/config-resolver": "3.357.0", - "@aws-sdk/fetch-http-handler": "3.357.0", - "@aws-sdk/hash-node": "3.357.0", - "@aws-sdk/invalid-dependency": "3.357.0", - "@aws-sdk/middleware-content-length": "3.357.0", - "@aws-sdk/middleware-endpoint": "3.357.0", - "@aws-sdk/middleware-host-header": "3.357.0", - "@aws-sdk/middleware-logger": "3.357.0", - "@aws-sdk/middleware-recursion-detection": "3.357.0", - "@aws-sdk/middleware-retry": "3.357.0", - "@aws-sdk/middleware-serde": "3.357.0", - "@aws-sdk/middleware-stack": "3.357.0", - "@aws-sdk/middleware-user-agent": "3.357.0", - "@aws-sdk/node-config-provider": "3.357.0", - "@aws-sdk/node-http-handler": "3.357.0", - "@aws-sdk/smithy-client": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/url-parser": "3.357.0", - "@aws-sdk/util-base64": "3.310.0", - "@aws-sdk/util-body-length-browser": "3.310.0", - "@aws-sdk/util-body-length-node": "3.310.0", - "@aws-sdk/util-defaults-mode-browser": "3.357.0", - "@aws-sdk/util-defaults-mode-node": "3.357.0", - "@aws-sdk/util-endpoints": "3.357.0", - "@aws-sdk/util-retry": "3.357.0", - "@aws-sdk/util-user-agent-browser": "3.357.0", - "@aws-sdk/util-user-agent-node": "3.357.0", - "@aws-sdk/util-utf8": "3.310.0", - "@smithy/protocol-http": "^1.0.1", - "@smithy/types": "^1.0.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/config-resolver": "3.357.0", - "@aws-sdk/fetch-http-handler": "3.357.0", - "@aws-sdk/hash-node": "3.357.0", - "@aws-sdk/invalid-dependency": "3.357.0", - "@aws-sdk/middleware-content-length": "3.357.0", - "@aws-sdk/middleware-endpoint": "3.357.0", - "@aws-sdk/middleware-host-header": "3.357.0", - "@aws-sdk/middleware-logger": "3.357.0", - "@aws-sdk/middleware-recursion-detection": "3.357.0", - "@aws-sdk/middleware-retry": "3.357.0", - "@aws-sdk/middleware-serde": "3.357.0", - "@aws-sdk/middleware-stack": "3.357.0", - "@aws-sdk/middleware-user-agent": "3.357.0", - "@aws-sdk/node-config-provider": "3.357.0", - "@aws-sdk/node-http-handler": "3.357.0", - "@aws-sdk/smithy-client": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/url-parser": "3.357.0", - "@aws-sdk/util-base64": "3.310.0", - "@aws-sdk/util-body-length-browser": "3.310.0", - "@aws-sdk/util-body-length-node": "3.310.0", - "@aws-sdk/util-defaults-mode-browser": "3.357.0", - "@aws-sdk/util-defaults-mode-node": "3.357.0", - "@aws-sdk/util-endpoints": "3.357.0", - "@aws-sdk/util-retry": "3.357.0", - "@aws-sdk/util-user-agent-browser": "3.357.0", - "@aws-sdk/util-user-agent-node": "3.357.0", - "@aws-sdk/util-utf8": "3.310.0", - "@smithy/protocol-http": "^1.0.1", - "@smithy/types": "^1.0.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-sts": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/config-resolver": "3.357.0", - "@aws-sdk/credential-provider-node": "3.357.0", - "@aws-sdk/fetch-http-handler": "3.357.0", - "@aws-sdk/hash-node": "3.357.0", - "@aws-sdk/invalid-dependency": "3.357.0", - "@aws-sdk/middleware-content-length": "3.357.0", - "@aws-sdk/middleware-endpoint": "3.357.0", - "@aws-sdk/middleware-host-header": "3.357.0", - "@aws-sdk/middleware-logger": "3.357.0", - "@aws-sdk/middleware-recursion-detection": "3.357.0", - "@aws-sdk/middleware-retry": "3.357.0", - "@aws-sdk/middleware-sdk-sts": "3.357.0", - "@aws-sdk/middleware-serde": "3.357.0", - "@aws-sdk/middleware-signing": "3.357.0", - "@aws-sdk/middleware-stack": "3.357.0", - "@aws-sdk/middleware-user-agent": "3.357.0", - "@aws-sdk/node-config-provider": "3.357.0", - "@aws-sdk/node-http-handler": "3.357.0", - "@aws-sdk/smithy-client": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/url-parser": "3.357.0", - "@aws-sdk/util-base64": "3.310.0", - "@aws-sdk/util-body-length-browser": "3.310.0", - "@aws-sdk/util-body-length-node": "3.310.0", - "@aws-sdk/util-defaults-mode-browser": "3.357.0", - "@aws-sdk/util-defaults-mode-node": "3.357.0", - "@aws-sdk/util-endpoints": "3.357.0", - "@aws-sdk/util-retry": "3.357.0", - "@aws-sdk/util-user-agent-browser": "3.357.0", - "@aws-sdk/util-user-agent-node": "3.357.0", - "@aws-sdk/util-utf8": "3.310.0", - "@smithy/protocol-http": "^1.0.1", - "@smithy/types": "^1.0.0", - "fast-xml-parser": "4.2.4", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/config-resolver": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-config-provider": "3.310.0", - "@aws-sdk/util-middleware": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-imds": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/node-config-provider": "3.357.0", - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/url-parser": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.357.0", - "@aws-sdk/credential-provider-imds": "3.357.0", - "@aws-sdk/credential-provider-process": "3.357.0", - "@aws-sdk/credential-provider-sso": "3.357.0", - "@aws-sdk/credential-provider-web-identity": "3.357.0", - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/shared-ini-file-loader": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.357.0", - "@aws-sdk/credential-provider-imds": "3.357.0", - "@aws-sdk/credential-provider-ini": "3.357.0", - "@aws-sdk/credential-provider-process": "3.357.0", - "@aws-sdk/credential-provider-sso": "3.357.0", - "@aws-sdk/credential-provider-web-identity": "3.357.0", - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/shared-ini-file-loader": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/shared-ini-file-loader": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sso": "3.357.0", - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/shared-ini-file-loader": "3.357.0", - "@aws-sdk/token-providers": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/eventstream-codec": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/crc32": "3.0.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-hex-encoding": "3.310.0", - "tslib": "^2.5.0" - } - }, - "node_modules/@aws-sdk/fetch-http-handler": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/protocol-http": "3.357.0", - "@aws-sdk/querystring-builder": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-base64": "3.310.0", - "tslib": "^2.5.0" - } - }, - "node_modules/@aws-sdk/hash-node": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-buffer-from": "3.310.0", - "@aws-sdk/util-utf8": "3.310.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/invalid-dependency": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "node_modules/@aws-sdk/is-array-buffer": { - "version": "3.310.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-content-length": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/protocol-http": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-endpoint": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-serde": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/url-parser": "3.357.0", - "@aws-sdk/util-middleware": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/protocol-http": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-logger": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/protocol-http": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-retry": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/protocol-http": "3.357.0", - "@aws-sdk/service-error-classification": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-middleware": "3.357.0", - "@aws-sdk/util-retry": "3.357.0", - "tslib": "^2.5.0", - "uuid": "^8.3.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-retry/node_modules/uuid": { - "version": "8.3.2", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@aws-sdk/middleware-sdk-sts": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-signing": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-serde": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-signing": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/protocol-http": "3.357.0", - "@aws-sdk/signature-v4": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-middleware": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-stack": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/protocol-http": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-endpoints": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/node-config-provider": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/shared-ini-file-loader": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/node-http-handler": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/abort-controller": "3.357.0", - "@aws-sdk/protocol-http": "3.357.0", - "@aws-sdk/querystring-builder": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/property-provider": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/protocol-http": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/querystring-builder": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-uri-escape": "3.310.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/querystring-parser": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-endpoint": "3.357.0", - "@aws-sdk/protocol-http": "3.357.0", - "@aws-sdk/signature-v4-multi-region": "3.357.0", - "@aws-sdk/smithy-client": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-format-url": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/service-error-classification": { - "version": "3.357.0", - "license": "Apache-2.0", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/shared-ini-file-loader": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/signature-v4": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/eventstream-codec": "3.357.0", - "@aws-sdk/is-array-buffer": "3.310.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-hex-encoding": "3.310.0", - "@aws-sdk/util-middleware": "3.357.0", - "@aws-sdk/util-uri-escape": "3.310.0", - "@aws-sdk/util-utf8": "3.310.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/protocol-http": "3.357.0", - "@aws-sdk/signature-v4": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "@aws-sdk/signature-v4-crt": "^3.118.0" - }, - "peerDependenciesMeta": { - "@aws-sdk/signature-v4-crt": { - "optional": true - } - } - }, - "node_modules/@aws-sdk/smithy-client": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-stack": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-stream": "3.357.0", - "@smithy/types": "^1.0.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/token-providers": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sso-oidc": "3.357.0", - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/shared-ini-file-loader": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/types": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/url-parser": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/querystring-parser": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "node_modules/@aws-sdk/util-base64": { - "version": "3.310.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/util-buffer-from": "3.310.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-body-length-browser": { - "version": "3.310.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - } - }, - "node_modules/@aws-sdk/util-body-length-node": { - "version": "3.310.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-buffer-from": { - "version": "3.310.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/is-array-buffer": "3.310.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-config-provider": { - "version": "3.310.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-defaults-mode-browser": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/types": "3.357.0", - "bowser": "^2.11.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@aws-sdk/util-defaults-mode-node": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/config-resolver": "3.357.0", - "@aws-sdk/credential-provider-imds": "3.357.0", - "@aws-sdk/node-config-provider": "3.357.0", - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@aws-sdk/util-endpoints": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-format-url": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/querystring-builder": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-hex-encoding": { - "version": "3.310.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-locate-window": { - "version": "3.310.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-middleware": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-retry": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/service-error-classification": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@aws-sdk/util-stream": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/fetch-http-handler": "3.357.0", - "@aws-sdk/node-http-handler": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-base64": "3.310.0", - "@aws-sdk/util-buffer-from": "3.310.0", - "@aws-sdk/util-hex-encoding": "3.310.0", - "@aws-sdk/util-utf8": "3.310.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-uri-escape": { - "version": "3.310.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.357.0", - "bowser": "^2.11.0", - "tslib": "^2.5.0" - } - }, - "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.357.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/node-config-provider": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, - "node_modules/@aws-sdk/util-utf8": { - "version": "3.310.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/util-buffer-from": "3.310.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/util-utf8-browser": { - "version": "3.259.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.3.1" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helpers": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.22.5", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "browserslist": "^4.21.3", - "lru-cache": "^5.1.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "regexpu-core": "^5.3.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0-0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-wrap-function": "^7.22.5", - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "regenerator-transform": "^0.15.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-typescript": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.22.5", - "@babel/plugin-syntax-import-attributes": "^7.22.5", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.22.5", - "@babel/plugin-transform-async-generator-functions": "^7.22.5", - "@babel/plugin-transform-async-to-generator": "^7.22.5", - "@babel/plugin-transform-block-scoped-functions": "^7.22.5", - "@babel/plugin-transform-block-scoping": "^7.22.5", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-class-static-block": "^7.22.5", - "@babel/plugin-transform-classes": "^7.22.5", - "@babel/plugin-transform-computed-properties": "^7.22.5", - "@babel/plugin-transform-destructuring": "^7.22.5", - "@babel/plugin-transform-dotall-regex": "^7.22.5", - "@babel/plugin-transform-duplicate-keys": "^7.22.5", - "@babel/plugin-transform-dynamic-import": "^7.22.5", - "@babel/plugin-transform-exponentiation-operator": "^7.22.5", - "@babel/plugin-transform-export-namespace-from": "^7.22.5", - "@babel/plugin-transform-for-of": "^7.22.5", - "@babel/plugin-transform-function-name": "^7.22.5", - "@babel/plugin-transform-json-strings": "^7.22.5", - "@babel/plugin-transform-literals": "^7.22.5", - "@babel/plugin-transform-logical-assignment-operators": "^7.22.5", - "@babel/plugin-transform-member-expression-literals": "^7.22.5", - "@babel/plugin-transform-modules-amd": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.22.5", - "@babel/plugin-transform-modules-systemjs": "^7.22.5", - "@babel/plugin-transform-modules-umd": "^7.22.5", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.22.5", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5", - "@babel/plugin-transform-numeric-separator": "^7.22.5", - "@babel/plugin-transform-object-rest-spread": "^7.22.5", - "@babel/plugin-transform-object-super": "^7.22.5", - "@babel/plugin-transform-optional-catch-binding": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.5", - "@babel/plugin-transform-parameters": "^7.22.5", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/plugin-transform-private-property-in-object": "^7.22.5", - "@babel/plugin-transform-property-literals": "^7.22.5", - "@babel/plugin-transform-regenerator": "^7.22.5", - "@babel/plugin-transform-reserved-words": "^7.22.5", - "@babel/plugin-transform-shorthand-properties": "^7.22.5", - "@babel/plugin-transform-spread": "^7.22.5", - "@babel/plugin-transform-sticky-regex": "^7.22.5", - "@babel/plugin-transform-template-literals": "^7.22.5", - "@babel/plugin-transform-typeof-symbol": "^7.22.5", - "@babel/plugin-transform-unicode-escapes": "^7.22.5", - "@babel/plugin-transform-unicode-property-regex": "^7.22.5", - "@babel/plugin-transform-unicode-regex": "^7.22.5", - "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.22.5", - "babel-plugin-polyfill-corejs2": "^0.4.3", - "babel-plugin-polyfill-corejs3": "^0.8.1", - "babel-plugin-polyfill-regenerator": "^0.5.0", - "core-js-compat": "^3.30.2", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.22.5", - "@babel/plugin-transform-typescript": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/runtime": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.13.11" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "dev": true, - "license": "MIT" - }, - "node_modules/@crowd/alerting": { - "resolved": "../services/libs/alerting", - "link": true - }, - "node_modules/@crowd/common": { - "resolved": "../services/libs/common", - "link": true - }, - "node_modules/@crowd/integrations": { - "resolved": "../services/libs/integrations", - "link": true - }, - "node_modules/@crowd/logging": { - "resolved": "../services/libs/logging", - "link": true - }, - "node_modules/@crowd/opensearch": { - "resolved": "../services/libs/opensearch", - "link": true - }, - "node_modules/@crowd/redis": { - "resolved": "../services/libs/redis", - "link": true - }, - "node_modules/@crowd/sqs": { - "resolved": "../services/libs/sqs", - "link": true - }, - "node_modules/@crowd/tracing": { - "resolved": "../services/libs/tracing", - "link": true - }, - "node_modules/@crowd/types": { - "resolved": "../services/libs/types", - "link": true - }, - "node_modules/@cspotcode/source-map-consumer": { - "version": "0.8.0", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-consumer": "0.8.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cubejs-client/core": { - "version": "0.30.74", - "license": "MIT", - "dependencies": { - "core-js": "^3.6.5", - "cross-fetch": "^3.0.2", - "dayjs": "^1.10.4", - "ramda": "^0.27.0", - "url-search-params-polyfill": "^7.0.0", - "uuid": "^8.3.2" - } - }, - "node_modules/@cubejs-client/core/node_modules/uuid": { - "version": "8.3.2", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@discordjs/builders": { - "version": "1.6.3", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/formatters": "^0.3.1", - "@discordjs/util": "^0.3.1", - "@sapphire/shapeshift": "^3.8.2", - "discord-api-types": "^0.37.41", - "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.3", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/@discordjs/collection": { - "version": "1.5.1", - "license": "Apache-2.0", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/@discordjs/formatters": { - "version": "0.3.1", - "license": "Apache-2.0", - "dependencies": { - "discord-api-types": "^0.37.41" - }, - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/@discordjs/rest": { - "version": "1.7.1", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/collection": "^1.5.1", - "@discordjs/util": "^0.3.0", - "@sapphire/async-queue": "^1.5.0", - "@sapphire/snowflake": "^3.4.2", - "discord-api-types": "^0.37.41", - "file-type": "^18.3.0", - "tslib": "^2.5.0", - "undici": "^5.22.0" - }, - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/@discordjs/util": { - "version": "0.3.1", - "license": "Apache-2.0", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/@discordjs/ws": { - "version": "0.8.3", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/collection": "^1.5.1", - "@discordjs/rest": "^1.7.1", - "@discordjs/util": "^0.3.1", - "@sapphire/async-queue": "^1.5.0", - "@types/ws": "^8.5.4", - "@vladfrangu/async_event_emitter": "^2.2.1", - "discord-api-types": "^0.37.41", - "tslib": "^2.5.0", - "ws": "^8.13.0" - }, - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.20.0", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "8.43.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@exodus/schemasafe": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "license": "MIT" - }, - "node_modules/@google-cloud/common": { - "version": "3.10.0", - "license": "Apache-2.0", - "dependencies": { - "@google-cloud/projectify": "^2.0.0", - "@google-cloud/promisify": "^2.0.0", - "arrify": "^2.0.1", - "duplexify": "^4.1.1", - "ent": "^2.2.0", - "extend": "^3.0.2", - "google-auth-library": "^7.14.0", - "retry-request": "^4.2.2", - "teeny-request": "^7.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@google-cloud/common/node_modules/duplexify": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" - } - }, - "node_modules/@google-cloud/paginator": { - "version": "3.0.7", - "license": "Apache-2.0", - "dependencies": { - "arrify": "^2.0.0", - "extend": "^3.0.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@google-cloud/projectify": { - "version": "2.1.1", - "license": "Apache-2.0", - "engines": { - "node": ">=10" - } - }, - "node_modules/@google-cloud/promisify": { - "version": "2.0.4", - "license": "Apache-2.0", - "engines": { - "node": ">=10" - } - }, - "node_modules/@google-cloud/storage": { - "version": "5.3.0", - "license": "Apache-2.0", - "dependencies": { - "@google-cloud/common": "^3.3.0", - "@google-cloud/paginator": "^3.0.0", - "@google-cloud/promisify": "^2.0.0", - "arrify": "^2.0.0", - "compressible": "^2.0.12", - "concat-stream": "^2.0.0", - "date-and-time": "^0.14.0", - "duplexify": "^3.5.0", - "extend": "^3.0.2", - "gaxios": "^3.0.0", - "gcs-resumable-upload": "^3.1.0", - "hash-stream-validation": "^0.2.2", - "mime": "^2.2.0", - "mime-types": "^2.0.8", - "onetime": "^5.1.0", - "p-limit": "^3.0.1", - "pumpify": "^2.0.0", - "snakeize": "^0.1.0", - "stream-events": "^1.0.1", - "xdg-basedir": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.10", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/momoa": { - "version": "2.0.4", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { - "version": "1.0.3", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.5.0", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/console/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/console/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/console/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/console/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/console/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/core": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.5.0", - "@jest/reporters": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.5.0", - "jest-config": "^29.5.0", - "jest-haste-map": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-resolve-dependencies": "^29.5.0", - "jest-runner": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", - "jest-watcher": "^29.5.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.5.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/core/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/core/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/core/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/core/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/environment": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/node": "*", - "jest-mock": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.5.0", - "jest-snapshot": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.4.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.5.0", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.5.0", - "jest-mock": "^29.5.0", - "jest-util": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/expect": "^29.5.0", - "@jest/types": "^29.5.0", - "jest-mock": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@jridgewell/trace-mapping": "^0.3.15", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", - "jest-worker": "^29.5.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/reporters/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/reporters/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/reporters/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/reporters/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/reporters/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/schemas": { - "version": "29.4.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.25.16" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "29.4.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.15", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.5.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.5.0", - "@jridgewell/trace-mapping": "^0.3.15", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.5.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/transform/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/transform/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/transform/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/transform/node_modules/convert-source-map": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/transform/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/transform/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/transform/node_modules/write-file-atomic": { - "version": "4.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/@jest/types": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.4.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/types/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/types/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/types/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "dev": true, - "license": "MIT" - }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@npmcli/fs": { - "version": "2.1.2", - "license": "ISC", - "dependencies": { - "@gar/promisify": "^1.1.3", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/@npmcli/fs/node_modules/lru-cache": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.5.2", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@npmcli/fs/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/@npmcli/move-file": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/@octokit/auth-app": { - "version": "3.6.1", - "license": "MIT", - "dependencies": { - "@octokit/auth-oauth-app": "^4.3.0", - "@octokit/auth-oauth-user": "^1.2.3", - "@octokit/request": "^5.6.0", - "@octokit/request-error": "^2.1.0", - "@octokit/types": "^6.0.3", - "@types/lru-cache": "^5.1.0", - "deprecation": "^2.3.1", - "lru-cache": "^6.0.0", - "universal-github-app-jwt": "^1.0.1", - "universal-user-agent": "^6.0.0" - } - }, - "node_modules/@octokit/auth-app/node_modules/lru-cache": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@octokit/auth-app/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/@octokit/auth-oauth-app": { - "version": "4.3.4", - "license": "MIT", - "dependencies": { - "@octokit/auth-oauth-device": "^3.1.1", - "@octokit/auth-oauth-user": "^2.0.0", - "@octokit/request": "^5.6.3", - "@octokit/types": "^6.0.3", - "@types/btoa-lite": "^1.0.0", - "btoa-lite": "^1.0.0", - "universal-user-agent": "^6.0.0" - } - }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/auth-oauth-user": { - "version": "2.1.2", - "license": "MIT", - "dependencies": { - "@octokit/auth-oauth-device": "^4.0.0", - "@octokit/oauth-methods": "^2.0.0", - "@octokit/request": "^6.0.0", - "@octokit/types": "^9.0.0", - "btoa-lite": "^1.0.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/auth-oauth-user/node_modules/@octokit/auth-oauth-device": { - "version": "4.0.5", - "license": "MIT", - "dependencies": { - "@octokit/oauth-methods": "^2.0.0", - "@octokit/request": "^6.0.0", - "@octokit/types": "^9.0.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/auth-oauth-user/node_modules/@octokit/request": { - "version": "6.2.8", - "license": "MIT", - "dependencies": { - "@octokit/endpoint": "^7.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": { - "version": "9.3.2", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" - } - }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/endpoint": { - "version": "7.0.6", - "license": "MIT", - "dependencies": { - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/endpoint/node_modules/@octokit/types": { - "version": "9.3.2", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" - } - }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": { - "version": "18.0.0", - "license": "MIT" - }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/request-error": { - "version": "3.0.3", - "license": "MIT", - "dependencies": { - "@octokit/types": "^9.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/request-error/node_modules/@octokit/types": { - "version": "9.3.2", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" - } - }, - "node_modules/@octokit/auth-oauth-device": { - "version": "3.1.4", - "license": "MIT", - "dependencies": { - "@octokit/oauth-methods": "^2.0.0", - "@octokit/request": "^6.0.0", - "@octokit/types": "^6.10.0", - "universal-user-agent": "^6.0.0" - } - }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/endpoint": { - "version": "7.0.6", - "license": "MIT", - "dependencies": { - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/endpoint/node_modules/@octokit/types": { - "version": "9.3.2", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" - } - }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": { - "version": "18.0.0", - "license": "MIT" - }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request": { - "version": "6.2.8", - "license": "MIT", - "dependencies": { - "@octokit/endpoint": "^7.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request-error": { - "version": "3.0.3", - "license": "MIT", - "dependencies": { - "@octokit/types": "^9.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request-error/node_modules/@octokit/types": { - "version": "9.3.2", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" - } - }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request/node_modules/@octokit/types": { - "version": "9.3.2", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" - } - }, - "node_modules/@octokit/auth-oauth-user": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "@octokit/auth-oauth-device": "^3.1.1", - "@octokit/oauth-methods": "^1.1.0", - "@octokit/request": "^5.4.14", - "@octokit/types": "^6.12.2", - "btoa-lite": "^1.0.0", - "universal-user-agent": "^6.0.0" - } - }, - "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/oauth-authorization-url": { - "version": "4.3.3", - "license": "MIT" - }, - "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/oauth-methods": { - "version": "1.2.6", - "license": "MIT", - "dependencies": { - "@octokit/oauth-authorization-url": "^4.3.1", - "@octokit/request": "^5.4.14", - "@octokit/request-error": "^2.0.5", - "@octokit/types": "^6.12.2", - "btoa-lite": "^1.0.0" - } - }, - "node_modules/@octokit/endpoint": { - "version": "6.0.12", - "license": "MIT", - "dependencies": { - "@octokit/types": "^6.0.3", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - } - }, - "node_modules/@octokit/graphql": { - "version": "4.8.0", - "license": "MIT", - "dependencies": { - "@octokit/request": "^5.6.0", - "@octokit/types": "^6.0.3", - "universal-user-agent": "^6.0.0" - } - }, - "node_modules/@octokit/oauth-authorization-url": { - "version": "5.0.0", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/oauth-methods": { - "version": "2.0.6", - "license": "MIT", - "dependencies": { - "@octokit/oauth-authorization-url": "^5.0.0", - "@octokit/request": "^6.2.3", - "@octokit/request-error": "^3.0.3", - "@octokit/types": "^9.0.0", - "btoa-lite": "^1.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/oauth-methods/node_modules/@octokit/endpoint": { - "version": "7.0.6", - "license": "MIT", - "dependencies": { - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": { - "version": "18.0.0", - "license": "MIT" - }, - "node_modules/@octokit/oauth-methods/node_modules/@octokit/request": { - "version": "6.2.8", - "license": "MIT", - "dependencies": { - "@octokit/endpoint": "^7.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/oauth-methods/node_modules/@octokit/request-error": { - "version": "3.0.3", - "license": "MIT", - "dependencies": { - "@octokit/types": "^9.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/oauth-methods/node_modules/@octokit/types": { - "version": "9.3.2", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" - } - }, - "node_modules/@octokit/openapi-types": { - "version": "12.11.0", - "license": "MIT" - }, - "node_modules/@octokit/request": { - "version": "5.6.3", - "license": "MIT", - "dependencies": { - "@octokit/endpoint": "^6.0.1", - "@octokit/request-error": "^2.1.0", - "@octokit/types": "^6.16.1", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" - } - }, - "node_modules/@octokit/request-error": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "@octokit/types": "^6.0.3", - "deprecation": "^2.0.0", - "once": "^1.4.0" - } - }, - "node_modules/@octokit/types": { - "version": "6.41.0", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^12.11.0" - } - }, - "node_modules/@opencensus/core": { - "version": "0.0.9", - "license": "Apache-2.0", - "dependencies": { - "continuation-local-storage": "^3.2.1", - "log-driver": "^1.2.7", - "semver": "^5.5.0", - "shimmer": "^1.2.0", - "uuid": "^3.2.1" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/@opencensus/core/node_modules/semver": { - "version": "5.7.1", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/@opencensus/core/node_modules/uuid": { - "version": "3.4.0", - "license": "MIT", - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/@opencensus/propagation-b3": { - "version": "0.0.8", - "license": "Apache-2.0", - "dependencies": { - "@opencensus/core": "^0.0.8", - "uuid": "^3.2.1" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/@opencensus/propagation-b3/node_modules/@opencensus/core": { - "version": "0.0.8", - "license": "Apache-2.0", - "dependencies": { - "continuation-local-storage": "^3.2.1", - "log-driver": "^1.2.7", - "semver": "^5.5.0", - "shimmer": "^1.2.0", - "uuid": "^3.2.1" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/@opencensus/propagation-b3/node_modules/semver": { - "version": "5.7.1", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/@opencensus/propagation-b3/node_modules/uuid": { - "version": "3.4.0", - "license": "MIT", - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/@opensearch-project/opensearch": { - "version": "1.2.0", - "license": "Apache-2.0", - "dependencies": { - "aws4": "^1.11.0", - "debug": "^4.3.1", - "hpagent": "^0.1.1", - "ms": "^2.1.3", - "secure-json-parse": "^2.4.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@pm2/agent": { - "version": "2.0.1", - "license": "AGPL-3.0", - "dependencies": { - "async": "~3.2.0", - "chalk": "~3.0.0", - "dayjs": "~1.8.24", - "debug": "~4.3.1", - "eventemitter2": "~5.0.1", - "fast-json-patch": "^3.0.0-1", - "fclone": "~1.0.11", - "nssocket": "0.6.0", - "pm2-axon": "~4.0.1", - "pm2-axon-rpc": "~0.7.0", - "proxy-agent": "~5.0.0", - "semver": "~7.2.0", - "ws": "~7.4.0" - } - }, - "node_modules/@pm2/agent/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@pm2/agent/node_modules/async": { - "version": "3.2.4", - "license": "MIT" - }, - "node_modules/@pm2/agent/node_modules/chalk": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@pm2/agent/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@pm2/agent/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/@pm2/agent/node_modules/dayjs": { - "version": "1.8.36", - "license": "MIT" - }, - "node_modules/@pm2/agent/node_modules/eventemitter2": { - "version": "5.0.1", - "license": "MIT" - }, - "node_modules/@pm2/agent/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@pm2/agent/node_modules/semver": { - "version": "7.2.3", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@pm2/agent/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@pm2/agent/node_modules/ws": { - "version": "7.4.6", - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@pm2/io": { - "version": "5.0.0", - "license": "Apache-2", - "dependencies": { - "@opencensus/core": "0.0.9", - "@opencensus/propagation-b3": "0.0.8", - "async": "~2.6.1", - "debug": "~4.3.1", - "eventemitter2": "^6.3.1", - "require-in-the-middle": "^5.0.0", - "semver": "6.3.0", - "shimmer": "^1.2.0", - "signal-exit": "^3.0.3", - "tslib": "1.9.3" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/@pm2/io/node_modules/tslib": { - "version": "1.9.3", - "license": "Apache-2.0" - }, - "node_modules/@pm2/js-api": { - "version": "0.6.7", - "license": "Apache-2", - "dependencies": { - "async": "^2.6.3", - "axios": "^0.21.0", - "debug": "~4.3.1", - "eventemitter2": "^6.3.1", - "ws": "^7.0.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/@pm2/js-api/node_modules/axios": { - "version": "0.21.4", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.14.0" - } - }, - "node_modules/@pm2/js-api/node_modules/ws": { - "version": "7.5.9", - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@pm2/pm2-version-check": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.4.tgz", - "integrity": "sha512-SXsM27SGH3yTWKc2fKR4SYNxsmnvuBQ9dd6QHtEWmiZ/VqaOYPAIlS8+vMcn27YLtAEBGvNRSh3TPNvtjZgfqA==", - "dependencies": { - "debug": "^4.3.1" - } - }, - "node_modules/@readme/better-ajv-errors": { - "version": "1.6.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@babel/code-frame": "^7.16.0", - "@babel/runtime": "^7.21.0", - "@humanwhocodes/momoa": "^2.0.3", - "chalk": "^4.1.2", - "json-to-ast": "^2.0.3", - "jsonpointer": "^5.0.0", - "leven": "^3.1.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "ajv": "4.11.8 - 8" - } - }, - "node_modules/@readme/better-ajv-errors/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@readme/better-ajv-errors/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@readme/better-ajv-errors/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@readme/better-ajv-errors/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@readme/better-ajv-errors/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@readme/better-ajv-errors/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@readme/json-schema-ref-parser": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.6", - "call-me-maybe": "^1.0.1", - "js-yaml": "^4.1.0" - } - }, - "node_modules/@readme/openapi-parser": { - "version": "2.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@apidevtools/openapi-schemas": "^2.1.0", - "@apidevtools/swagger-methods": "^3.0.2", - "@jsdevtools/ono": "^7.1.3", - "@readme/better-ajv-errors": "^1.6.0", - "@readme/json-schema-ref-parser": "^1.2.0", - "ajv": "^8.12.0", - "ajv-draft-04": "^1.0.0", - "call-me-maybe": "^1.0.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "openapi-types": ">=7" - } - }, - "node_modules/@sapphire/async-queue": { - "version": "1.5.0", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@sapphire/shapeshift": { - "version": "3.9.2", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "lodash": "^4.17.21" - }, - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@sapphire/snowflake": { - "version": "3.5.1", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@segment/loosely-validate-event": { - "version": "2.0.0", - "dependencies": { - "component-type": "^1.2.1", - "join-component": "^1.1.0" - } - }, - "node_modules/@selderee/plugin-htmlparser2": { - "version": "0.6.0", - "license": "MIT", - "dependencies": { - "domhandler": "^4.2.0", - "selderee": "^0.6.0" - }, - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, - "node_modules/@sendgrid/client": { - "version": "7.7.0", - "license": "MIT", - "dependencies": { - "@sendgrid/helpers": "^7.7.0", - "axios": "^0.26.0" - }, - "engines": { - "node": "6.* || 8.* || >=10.*" - } - }, - "node_modules/@sendgrid/client/node_modules/axios": { - "version": "0.26.1", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.14.8" - } - }, - "node_modules/@sendgrid/eventwebhook": { - "version": "7.7.0", - "license": "MIT", - "dependencies": { - "starkbank-ecdsa": "^1.1.1" - }, - "engines": { - "node": "6.* || 8.* || >=10.*" - } - }, - "node_modules/@sendgrid/helpers": { - "version": "7.7.0", - "license": "MIT", - "dependencies": { - "deepmerge": "^4.2.2" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/@sendgrid/mail": { - "version": "7.2.6", - "license": "MIT", - "dependencies": { - "@sendgrid/client": "^7.2.6", - "@sendgrid/helpers": "^7.2.6" - }, - "engines": { - "node": "6.* || 8.* || >=10.*" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.25.24", - "dev": true, - "license": "MIT" - }, - "node_modules/@sindresorhus/is": { - "version": "0.14.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "node_modules/@slack/logger": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "@types/node": ">=12.0.0" - }, - "engines": { - "node": ">= 12.13.0", - "npm": ">= 6.12.0" - } - }, - "node_modules/@slack/types": { - "version": "2.8.0", - "license": "MIT", - "engines": { - "node": ">= 12.13.0", - "npm": ">= 6.12.0" - } - }, - "node_modules/@slack/web-api": { - "version": "6.8.1", - "license": "MIT", - "dependencies": { - "@slack/logger": "^3.0.0", - "@slack/types": "^2.0.0", - "@types/is-stream": "^1.1.0", - "@types/node": ">=12.0.0", - "axios": "^0.27.2", - "eventemitter3": "^3.1.0", - "form-data": "^2.5.0", - "is-electron": "2.2.0", - "is-stream": "^1.1.0", - "p-queue": "^6.6.1", - "p-retry": "^4.0.0" - }, - "engines": { - "node": ">= 12.13.0", - "npm": ">= 6.12.0" - } - }, - "node_modules/@smithy/protocol-http": { - "version": "1.1.0", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^1.1.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@smithy/types": { - "version": "1.1.0", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.0", - "license": "MIT" - }, - "node_modules/@superfaceai/ast": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "ajv": "^8.8.2", - "ajv-formats": "^2.1.1" - } - }, - "node_modules/@superfaceai/one-sdk": { - "version": "1.5.2", - "dependencies": { - "@superfaceai/ast": "1.2.0", - "@superfaceai/parser": "1.2.0", - "abort-controller": "^3.0.0", - "cross-fetch": "^3.1.5", - "debug": "^4.3.2", - "isomorphic-form-data": "^2.0.0", - "vm2": "^3.9.7" - } - }, - "node_modules/@superfaceai/parser": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "@superfaceai/ast": "^1.2.0", - "@types/debug": "^4.1.5", - "debug": "^4.3.3", - "typescript": "^4" - } - }, - "node_modules/@superfaceai/passport-twitter-oauth2": { - "version": "1.2.3", - "license": "MIT", - "dependencies": { - "passport-oauth2": "^1.6.1" - }, - "optionalDependencies": { - "@types/passport": "1.x", - "@types/passport-oauth2": ">=1.4" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "defer-to-connect": "^1.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "license": "MIT" - }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.9", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.2", - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/btoa-lite": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/@types/bunyan": { - "version": "1.8.8", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/bunyan-format": { - "version": "0.2.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/config": { - "version": "3.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/connect": { - "version": "3.4.35", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cookie": { - "version": "0.4.1", - "license": "MIT" - }, - "node_modules/@types/cookiejar": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/cors": { - "version": "2.8.13", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cron": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/luxon": "*", - "@types/node": "*" - } - }, - "node_modules/@types/debug": { - "version": "4.1.8", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.17", - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.35", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/html-to-text": { - "version": "8.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/is-stream": { - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "29.5.2", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.12", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true - }, - "node_modules/@types/jsonwebtoken": { - "version": "9.0.2", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/lru-cache": { - "version": "5.1.1", - "license": "MIT" - }, - "node_modules/@types/luxon": { - "version": "3.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "1.3.2", - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "0.7.31", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "17.0.45", - "license": "MIT" - }, - "node_modules/@types/oauth": { - "version": "0.9.1", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/passport": { - "version": "1.0.12", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/passport-oauth2": { - "version": "1.4.12", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/express": "*", - "@types/oauth": "*", - "@types/passport": "*" - } - }, - "node_modules/@types/prettier": { - "version": "2.7.3", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/qs": { - "version": "6.9.7", - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.4", - "license": "MIT" - }, - "node_modules/@types/retry": { - "version": "0.12.0", - "license": "MIT" - }, - "node_modules/@types/sanitize-html": { - "version": "2.9.0", - "dev": true, - "license": "MIT", - "dependencies": { - "htmlparser2": "^8.0.0" - } - }, - "node_modules/@types/semver": { - "version": "7.5.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "0.17.1", - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.1", - "license": "MIT", - "dependencies": { - "@types/mime": "*", - "@types/node": "*" - } - }, - "node_modules/@types/stack-utils": { - "version": "2.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/superagent": { - "version": "4.1.18", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/cookiejar": "*", - "@types/node": "*" - } - }, - "node_modules/@types/uuid": { - "version": "9.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/validator": { - "version": "13.7.17", - "license": "MIT" - }, - "node_modules/@types/ws": { - "version": "8.5.5", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/yargs": { - "version": "17.0.24", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.60.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.60.0", - "@typescript-eslint/type-utils": "5.60.0", - "@typescript-eslint/utils": "5.60.0", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.5.2", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/@typescript-eslint/parser": { - "version": "5.60.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "5.60.0", - "@typescript-eslint/types": "5.60.0", - "@typescript-eslint/typescript-estree": "5.60.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "5.60.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.60.0", - "@typescript-eslint/visitor-keys": "5.60.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "5.60.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "5.60.0", - "@typescript-eslint/utils": "5.60.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "5.60.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.60.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "5.60.0", - "@typescript-eslint/visitor-keys": "5.60.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.5.2", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/@typescript-eslint/utils": { - "version": "5.60.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.60.0", - "@typescript-eslint/types": "5.60.0", - "@typescript-eslint/typescript-estree": "5.60.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.5.2", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.60.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.60.0", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@vladfrangu/async_event_emitter": { - "version": "2.2.2", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/abbrev": { - "version": "1.1.1", - "license": "ISC" - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.9.0", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.2.0", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/agentkeepalive": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "depd": "^2.0.0", - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ajv": { - "version": "8.12.0", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-draft-04": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^8.5.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/amp": { - "version": "0.3.1", - "license": "MIT" - }, - "node_modules/amp-message": { - "version": "0.1.2", - "license": "MIT", - "dependencies": { - "amp": "0.3.1" - } - }, - "node_modules/analytics-node": { - "version": "6.2.0", - "license": "MIT", - "dependencies": { - "@segment/loosely-validate-event": "^2.0.0", - "axios": "^0.27.2", - "axios-retry": "3.2.0", - "lodash.isstring": "^4.0.1", - "md5": "^2.2.1", - "ms": "^2.0.0", - "remove-trailing-slash": "^0.1.0", - "uuid": "^8.3.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/analytics-node/node_modules/uuid": { - "version": "8.3.2", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/ansi-align": { - "version": "3.0.1", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.1.0" - } - }, - "node_modules/ansi-align/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-align/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ansicolors": { - "version": "0.2.1", - "license": "MIT" - }, - "node_modules/ansistyles": { - "version": "0.1.3", - "license": "MIT" - }, - "node_modules/any-promise": { - "version": "1.3.0", - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/aproba": { - "version": "1.2.0", - "license": "ISC" - }, - "node_modules/are-we-there-yet": { - "version": "1.1.7", - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "node_modules/are-we-there-yet/node_modules/readable-stream": { - "version": "2.3.8", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/are-we-there-yet/node_modules/string_decoder": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-back": { - "version": "3.1.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "license": "MIT" - }, - "node_modules/array-includes": { - "version": "3.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arrify": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/asap": { - "version": "2.0.6", - "license": "MIT" - }, - "node_modules/ast-types": { - "version": "0.13.4", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/async": { - "version": "2.6.4", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.14" - } - }, - "node_modules/async-listener": { - "version": "0.6.10", - "license": "BSD-2-Clause", - "dependencies": { - "semver": "^5.3.0", - "shimmer": "^1.1.0" - }, - "engines": { - "node": "<=0.11.8 || >0.11.10" - } - }, - "node_modules/async-listener/node_modules/semver": { - "version": "5.7.1", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/async-retry": { - "version": "1.3.3", - "license": "MIT", - "dependencies": { - "retry": "0.13.1" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "license": "MIT" - }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/aws-sdk": { - "version": "2.814.0", - "license": "Apache-2.0", - "dependencies": { - "buffer": "4.9.2", - "events": "1.1.1", - "ieee754": "1.1.13", - "jmespath": "0.15.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "uuid": "3.3.2", - "xml2js": "0.4.19" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/aws-sdk/node_modules/uuid": { - "version": "3.3.2", - "license": "MIT", - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/aws4": { - "version": "1.12.0", - "license": "MIT" - }, - "node_modules/axios": { - "version": "0.27.2", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - } - }, - "node_modules/axios-retry": { - "version": "3.2.0", - "license": "Apache-2.0", - "dependencies": { - "is-retry-allowed": "^1.1.0" - } - }, - "node_modules/axios/node_modules/form-data": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/babel-jest": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "^29.5.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.5.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-jest/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/babel-jest/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/babel-jest/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/babel-jest/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/babel-jest/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-jest/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.4.0", - "semver": "^6.1.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.0", - "core-js-compat": "^3.30.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-jest": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "^29.5.0", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/base64id": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": "^4.5.0 || >= 5.9" - } - }, - "node_modules/base64url": { - "version": "3.0.1", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/bcrypt": { - "version": "5.0.0", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^3.0.0", - "node-pre-gyp": "0.15.0" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/big-integer": { - "version": "1.6.51", - "license": "Unlicense", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/bignumber.js": { - "version": "9.1.1", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/buffer": { - "version": "5.7.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/blessed": { - "version": "0.1.81", - "license": "MIT", - "bin": { - "blessed": "bin/tput.js" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/bluebird": { - "version": "2.11.0", - "license": "MIT" - }, - "node_modules/bodec": { - "version": "0.1.0", - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "1.20.2", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/boolbase": { - "version": "1.0.0", - "license": "ISC" - }, - "node_modules/bowser": { - "version": "2.11.0", - "license": "MIT" - }, - "node_modules/boxen": { - "version": "4.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-align": "^3.0.0", - "camelcase": "^5.3.1", - "chalk": "^3.0.0", - "cli-boxes": "^2.2.0", - "string-width": "^4.1.0", - "term-size": "^2.1.0", - "type-fest": "^0.8.1", - "widest-line": "^3.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/boxen/node_modules/chalk": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/boxen/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/boxen/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/boxen/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/boxen/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/boxen/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/boxen/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/boxen/node_modules/type-fest": { - "version": "0.8.1", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=8" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.21.9", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001503", - "electron-to-chromium": "^1.4.431", - "node-releases": "^2.0.12", - "update-browserslist-db": "^1.0.11" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bs-logger": { - "version": "0.2.6", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/btoa-lite": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/buffer": { - "version": "4.9.2", - "license": "MIT", - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "license": "BSD-3-Clause" - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "license": "MIT" - }, - "node_modules/buffer-writer": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/bufferutil": { - "version": "4.0.7", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, - "node_modules/bunyan": { - "version": "1.8.15", - "engines": [ - "node >=0.10.0" - ], - "license": "MIT", - "bin": { - "bunyan": "bin/bunyan" - }, - "optionalDependencies": { - "dtrace-provider": "~0.8", - "moment": "^2.19.3", - "mv": "~2", - "safe-json-stringify": "~1" - } - }, - "node_modules/bunyan-format": { - "version": "0.2.1", - "license": "MIT", - "dependencies": { - "ansicolors": "~0.2.1", - "ansistyles": "~0.1.1", - "xtend": "~2.1.1" - } - }, - "node_modules/bunyan-middleware": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "@types/bunyan": "^1.8.6", - "@types/express": "^4.0.35", - "uuid": "^8.3.2" - } - }, - "node_modules/bunyan-middleware/node_modules/uuid": { - "version": "8.3.2", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/busboy": { - "version": "1.6.0", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cacache": { - "version": "16.1.3", - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^2.1.0", - "@npmcli/move-file": "^2.0.0", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "glob": "^8.0.1", - "infer-owner": "^1.0.4", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^9.0.0", - "tar": "^6.1.11", - "unique-filename": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/cacache/node_modules/chownr": { - "version": "2.0.0", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/cacache/node_modules/fs-minipass": { - "version": "2.1.0", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "8.1.0", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "7.18.3", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/cacache/node_modules/minimatch": { - "version": "5.1.6", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cacache/node_modules/minipass": { - "version": "3.3.6", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cacache/node_modules/minizlib": { - "version": "2.1.2", - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cacache/node_modules/tar": { - "version": "6.1.15", - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cacache/node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/cacache/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/cacheable-request": { - "version": "6.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cacheable-request/node_modules/get-stream": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cacheable-request/node_modules/lowercase-keys": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-me-maybe": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/callsites": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001506", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "2.4.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/charenc": { - "version": "0.0.2", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/charm": { - "version": "0.1.2", - "license": "MIT/X11" - }, - "node_modules/chokidar": { - "version": "3.5.3", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "license": "ISC" - }, - "node_modules/ci-info": { - "version": "3.8.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.2.3", - "dev": true, - "license": "MIT" - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/clearbit": { - "version": "1.3.5", - "license": "MIT", - "dependencies": { - "bluebird": "2", - "create-error": "0.3", - "lodash": "4.x", - "needle": "clearbit/needle#84d28b5f2c3916db1e7eb84aeaa9d976cc40054b" - } - }, - "node_modules/cli-boxes": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-color": { - "version": "1.4.0", - "license": "ISC", - "dependencies": { - "ansi-regex": "^2.1.1", - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "memoizee": "^0.4.14", - "timers-ext": "^0.1.5" - } - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-highlight": { - "version": "2.1.6", - "license": "ISC", - "dependencies": { - "chalk": "^3.0.0", - "highlight.js": "^10.0.0", - "mz": "^2.4.0", - "parse5": "^5.1.1", - "parse5-htmlparser2-tree-adapter": "^5.1.1", - "yargs": "^15.0.0" - }, - "bin": { - "highlight": "bin/highlight" - }, - "engines": { - "node": ">=8.0.0", - "npm": ">=5.0.0" - } - }, - "node_modules/cli-highlight/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cli-highlight/node_modules/chalk": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-highlight/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/cli-highlight/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/cli-highlight/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-highlight/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-table": { - "version": "0.3.11", - "dev": true, - "dependencies": { - "colors": "1.0.3" - }, - "engines": { - "node": ">= 0.2.0" - } - }, - "node_modules/cli-tableau": { - "version": "2.0.1", - "dependencies": { - "chalk": "3.0.0" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/cli-tableau/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cli-tableau/node_modules/chalk": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-tableau/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/cli-tableau/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/cli-tableau/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-tableau/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/clone": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clone-response": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/co": { - "version": "4.6.0", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/code-error-fragment": { - "version": "0.0.230", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/code-point-at": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/color-convert": { - "version": "1.9.3", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "license": "MIT" - }, - "node_modules/colors": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/command-line-args": { - "version": "5.2.1", - "license": "MIT", - "dependencies": { - "array-back": "^3.1.0", - "find-replace": "^3.0.0", - "lodash.camelcase": "^4.3.0", - "typical": "^4.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/command-line-usage": { - "version": "6.1.3", - "license": "MIT", - "dependencies": { - "array-back": "^4.0.2", - "chalk": "^2.4.2", - "table-layout": "^1.0.2", - "typical": "^5.2.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/command-line-usage/node_modules/array-back": { - "version": "4.0.2", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/command-line-usage/node_modules/typical": { - "version": "5.2.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/commander": { - "version": "6.2.1", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/comment-parser": { - "version": "0.7.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/component-emitter": { - "version": "1.3.0", - "license": "MIT" - }, - "node_modules/component-type": { - "version": "1.2.1", - "license": "MIT" - }, - "node_modules/compressible": { - "version": "2.0.18", - "license": "MIT", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "license": "MIT" - }, - "node_modules/concat-stream": { - "version": "2.0.0", - "engines": [ - "node >= 6.0" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/config": { - "version": "3.3.9", - "license": "MIT", - "dependencies": { - "json5": "^2.2.3" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/config-chain": { - "version": "1.1.13", - "license": "MIT", - "dependencies": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "node_modules/configstore": { - "version": "5.0.1", - "license": "BSD-2-Clause", - "dependencies": { - "dot-prop": "^5.2.0", - "graceful-fs": "^4.1.2", - "make-dir": "^3.0.0", - "unique-string": "^2.0.0", - "write-file-atomic": "^3.0.0", - "xdg-basedir": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/confusing-browser-globals": { - "version": "1.0.11", - "dev": true, - "license": "MIT" - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "license": "ISC" - }, - "node_modules/content-disposition": { - "version": "0.5.3", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/continuation-local-storage": { - "version": "3.2.1", - "license": "BSD-2-Clause", - "dependencies": { - "async-listener": "^0.6.0", - "emitter-listener": "^1.1.1" - } - }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.4.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "license": "MIT" - }, - "node_modules/cookiejar": { - "version": "2.1.4", - "license": "MIT" - }, - "node_modules/copy-anything": { - "version": "3.0.5", - "license": "MIT", - "dependencies": { - "is-what": "^4.1.8" - }, - "engines": { - "node": ">=12.13" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, - "node_modules/copyfiles": { - "version": "2.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "^7.0.5", - "minimatch": "^3.0.3", - "mkdirp": "^1.0.4", - "noms": "0.0.0", - "through2": "^2.0.1", - "untildify": "^4.0.0", - "yargs": "^16.1.0" - }, - "bin": { - "copyfiles": "copyfiles", - "copyup": "copyfiles" - } - }, - "node_modules/copyfiles/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/copyfiles/node_modules/cliui": { - "version": "7.0.4", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/copyfiles/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/copyfiles/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/copyfiles/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/copyfiles/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/copyfiles/node_modules/wrap-ansi": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/copyfiles/node_modules/y18n": { - "version": "5.0.8", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/copyfiles/node_modules/yargs": { - "version": "16.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/copyfiles/node_modules/yargs-parser": { - "version": "20.2.9", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/core-js": { - "version": "3.31.0", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-compat": { - "version": "3.31.0", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.21.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/create-error": { - "version": "0.3.1", - "license": "MIT" - }, - "node_modules/create-require": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/cron": { - "version": "2.3.1", - "license": "MIT", - "dependencies": { - "luxon": "^3.2.1" - } - }, - "node_modules/cron-time-generator": { - "version": "1.3.2", - "license": "MIT" - }, - "node_modules/croner": { - "version": "4.1.97", - "license": "MIT" - }, - "node_modules/cross-env": { - "version": "7.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "bin": { - "cross-env": "src/bin/cross-env.js", - "cross-env-shell": "src/bin/cross-env-shell.js" - }, - "engines": { - "node": ">=10.14", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/cross-fetch": { - "version": "3.1.6", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.11" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crowd-sentiment": { - "version": "1.1.7", - "license": "Apache-2.0" - }, - "node_modules/crypt": { - "version": "0.0.2", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/crypto-js": { - "version": "4.1.1", - "license": "MIT" - }, - "node_modules/crypto-random-string": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/css-select": { - "version": "5.1.0", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-select/node_modules/domhandler": { - "version": "5.0.3", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/culvert": { - "version": "0.1.2", - "license": "MIT" - }, - "node_modules/d": { - "version": "1.0.1", - "license": "ISC", - "dependencies": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "3.0.1", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/date-and-time": { - "version": "0.14.2", - "license": "MIT" - }, - "node_modules/dayjs": { - "version": "1.11.8", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.3.4", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "license": "MIT" - }, - "node_modules/decamelize": { - "version": "1.2.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decompress-response": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/dedent": { - "version": "0.7.0", - "dev": true, - "license": "MIT" - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/defaults": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defer-to-connect": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/define-properties": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/degenerator": { - "version": "3.0.4", - "license": "MIT", - "dependencies": { - "ast-types": "^0.13.2", - "escodegen": "^1.8.1", - "esprima": "^4.0.0", - "vm2": "^3.9.17" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/depd": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/deprecation": { - "version": "2.3.1", - "license": "ISC" - }, - "node_modules/destroy": { - "version": "1.2.0", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "1.0.3", - "license": "Apache-2.0", - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/dezalgo": { - "version": "1.0.4", - "license": "ISC", - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "node_modules/diff": { - "version": "4.0.2", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/diff-sequences": { - "version": "29.4.3", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/discontinuous-range": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/discord-api-types": { - "version": "0.37.46", - "license": "MIT" - }, - "node_modules/discord.js": { - "version": "14.11.0", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/builders": "^1.6.3", - "@discordjs/collection": "^1.5.1", - "@discordjs/formatters": "^0.3.1", - "@discordjs/rest": "^1.7.1", - "@discordjs/util": "^0.3.1", - "@discordjs/ws": "^0.8.3", - "@sapphire/snowflake": "^3.4.2", - "@types/ws": "^8.5.4", - "discord-api-types": "^0.37.41", - "fast-deep-equal": "^3.1.3", - "lodash.snakecase": "^4.1.1", - "tslib": "^2.5.0", - "undici": "^5.22.0", - "ws": "^8.13.0" - }, - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/dom-serializer/node_modules/domhandler": { - "version": "5.0.3", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "4.3.1", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.1.0", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/domutils/node_modules/domhandler": { - "version": "5.0.3", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/dot-prop": { - "version": "5.3.0", - "license": "MIT", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dotenv": { - "version": "8.2.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/dotenv-expand": { - "version": "8.0.3", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/dottie": { - "version": "2.0.6", - "license": "MIT" - }, - "node_modules/dtrace-provider": { - "version": "0.8.8", - "hasInstallScript": true, - "license": "BSD-2-Clause", - "optional": true, - "dependencies": { - "nan": "^2.14.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/duplexer3": { - "version": "0.1.5", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/duplexify": { - "version": "3.7.1", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "node_modules/duplexify/node_modules/readable-stream": { - "version": "2.3.8", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/duplexify/node_modules/string_decoder": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/editor": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/editorconfig": { - "version": "0.15.3", - "license": "MIT", - "dependencies": { - "commander": "^2.19.0", - "lru-cache": "^4.1.5", - "semver": "^5.6.0", - "sigmund": "^1.0.1" - }, - "bin": { - "editorconfig": "bin/editorconfig" - } - }, - "node_modules/editorconfig/node_modules/commander": { - "version": "2.20.3", - "license": "MIT" - }, - "node_modules/editorconfig/node_modules/lru-cache": { - "version": "4.1.5", - "license": "ISC", - "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "node_modules/editorconfig/node_modules/semver": { - "version": "5.7.1", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/editorconfig/node_modules/yallist": { - "version": "2.1.2", - "license": "ISC" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.4.437", - "dev": true, - "license": "ISC" - }, - "node_modules/emitter-listener": { - "version": "1.1.2", - "license": "BSD-2-Clause", - "dependencies": { - "shimmer": "^1.2.0" - } - }, - "node_modules/emittery": { - "version": "0.13.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-chars": { - "version": "1.0.12", - "license": "MIT", - "dependencies": { - "emoji-unicode-map": "^1.0.0" - } - }, - "node_modules/emoji-dictionary": { - "version": "1.0.11", - "license": "MIT", - "dependencies": { - "emoji-chars": "^1.0.0", - "emoji-name-map": "^1.0.0", - "emoji-names": "^1.0.1", - "emoji-unicode-map": "^1.0.0", - "emojilib": "^2.0.2" - } - }, - "node_modules/emoji-name-map": { - "version": "1.2.9", - "license": "MIT", - "dependencies": { - "emojilib": "^2.0.2", - "iterate-object": "^1.3.1", - "map-o": "^2.0.1" - } - }, - "node_modules/emoji-names": { - "version": "1.0.12", - "license": "MIT", - "dependencies": { - "emoji-name-map": "^1.0.0" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "license": "MIT" - }, - "node_modules/emoji-unicode-map": { - "version": "1.1.11", - "license": "MIT", - "dependencies": { - "emoji-name-map": "^1.1.0", - "iterate-object": "^1.3.1" - } - }, - "node_modules/emojilib": { - "version": "2.4.0", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/encoding": { - "version": "0.1.13", - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/engine.io": { - "version": "6.4.2", - "license": "MIT", - "dependencies": { - "@types/cookie": "^0.4.1", - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.4.1", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", - "ws": "~8.11.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io-parser": { - "version": "5.0.7", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io/node_modules/cookie": { - "version": "0.4.2", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/ws": { - "version": "8.11.0", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/enquirer": { - "version": "2.3.6", - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/ent": { - "version": "2.2.0", - "license": "MIT" - }, - "node_modules/entities": { - "version": "4.5.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/erlpack": { - "version": "0.1.4", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "nan": "^2.15.0" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "license": "MIT" - }, - "node_modules/error-ex": { - "version": "1.3.2", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-abstract": { - "version": "1.21.2", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-set-tostringtag": "^2.0.1", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.2.0", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.7", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has": "^1.0.3" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es5-ext": { - "version": "0.10.62", - "hasInstallScript": true, - "license": "ISC", - "dependencies": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "next-tick": "^1.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "license": "MIT", - "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "node_modules/es6-promise": { - "version": "3.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/es6-symbol": { - "version": "3.1.3", - "license": "ISC", - "dependencies": { - "d": "^1.0.1", - "ext": "^1.1.2" - } - }, - "node_modules/es6-weak-map": { - "version": "2.0.3", - "license": "ISC", - "dependencies": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-goat": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/escodegen": { - "version": "1.14.3", - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=4.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" - }, - "node_modules/escodegen/node_modules/levn": { - "version": "0.3.0", - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/optionator": { - "version": "0.8.3", - "license": "MIT", - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/prelude-ls": { - "version": "1.1.2", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/type-check": { - "version": "0.3.2", - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/eslint": { - "version": "8.43.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.43.0", - "@humanwhocodes/config-array": "^0.11.10", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-airbnb-base": { - "version": "15.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "confusing-browser-globals": "^1.0.10", - "object.assign": "^4.1.2", - "object.entries": "^1.1.5", - "semver": "^6.3.0" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "peerDependencies": { - "eslint": "^7.32.0 || ^8.2.0", - "eslint-plugin-import": "^2.25.2" - } - }, - "node_modules/eslint-config-airbnb-typescript": { - "version": "16.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-config-airbnb-base": "^15.0.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^5.0.0", - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^7.32.0 || ^8.2.0", - "eslint-plugin-import": "^2.25.3" - } - }, - "node_modules/eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.7", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.11.0", - "resolve": "^1.22.1" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.8.0", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.27.5", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "array.prototype.flatmap": "^1.3.1", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.7", - "eslint-module-utils": "^2.7.4", - "has": "^1.0.3", - "is-core-module": "^2.11.0", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.values": "^1.1.6", - "resolve": "^1.22.1", - "semver": "^6.3.0", - "tsconfig-paths": "^3.14.1" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/eslint-plugin-import/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-plugin-import/node_modules/tsconfig-paths": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", - "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", - "dev": true, - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/eslint-plugin-openapi": { - "version": "0.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "comment-parser": "^0.7.4" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/eslint/node_modules/globals": { - "version": "13.20.0", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/espree": { - "version": "9.5.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esquery/node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-emitter": { - "version": "0.3.5", - "license": "MIT", - "dependencies": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/eventemitter2": { - "version": "6.4.9", - "license": "MIT" - }, - "node_modules/eventemitter3": { - "version": "3.1.2", - "license": "MIT" - }, - "node_modules/events": { - "version": "1.1.1", - "license": "MIT", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/node_modules/is-stream": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/express": { - "version": "4.17.1", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express-rate-limit": { - "version": "6.5.1", - "license": "MIT", - "engines": { - "node": ">= 12.9.0" - }, - "peerDependencies": { - "express": "^4 || ^5" - } - }, - "node_modules/express/node_modules/body-parser": { - "version": "1.19.0", - "license": "MIT", - "dependencies": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/bytes": { - "version": "3.1.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/depd": { - "version": "1.1.2", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/http-errors": { - "version": "1.7.2", - "license": "MIT", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/inherits": { - "version": "2.0.3", - "license": "ISC" - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/express/node_modules/on-finished": { - "version": "2.3.0", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/qs": { - "version": "6.7.0", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/express/node_modules/raw-body": { - "version": "2.4.0", - "license": "MIT", - "dependencies": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/toidentifier": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/ext": { - "version": "1.7.0", - "license": "ISC", - "dependencies": { - "type": "^2.7.2" - } - }, - "node_modules/ext/node_modules/type": { - "version": "2.7.2", - "license": "ISC" - }, - "node_modules/extend": { - "version": "3.0.2", - "license": "MIT" - }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.2.12", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-patch": { - "version": "3.1.1", - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", - "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", - "dependencies": { - "fastest-levenshtein": "^1.0.7" - } - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "license": "MIT" - }, - "node_modules/fast-text-encoding": { - "version": "1.0.6", - "license": "Apache-2.0" - }, - "node_modules/fast-xml-parser": { - "version": "4.2.4", - "funding": [ - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" - }, - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^1.0.5" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/fastq": { - "version": "1.15.0", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fclone": { - "version": "1.0.11", - "license": "MIT" - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/file-type": { - "version": "18.5.0", - "license": "MIT", - "dependencies": { - "readable-web-to-node-stream": "^3.0.2", - "strtok3": "^7.0.0", - "token-types": "^5.0.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/fill-range": { - "version": "7.0.1", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.1.2", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/finalhandler/node_modules/on-finished": { - "version": "2.3.0", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/find-replace": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "array-back": "^3.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.7", - "dev": true, - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.15.2", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/for-each": { - "version": "0.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/form-data": { - "version": "2.5.1", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/formidable": { - "version": "1.2.6", - "license": "MIT", - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, - "node_modules/formidable-serverless": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "formidable": "^1.2.2" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs-minipass": { - "version": "1.2.7", - "license": "ISC", - "dependencies": { - "minipass": "^2.6.0" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "license": "ISC" - }, - "node_modules/ftp": { - "version": "0.3.10", - "dependencies": { - "readable-stream": "1.1.x", - "xregexp": "2.0.0" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/ftp/node_modules/isarray": { - "version": "0.0.1", - "license": "MIT" - }, - "node_modules/ftp/node_modules/readable-stream": { - "version": "1.1.14", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/ftp/node_modules/string_decoder": { - "version": "0.10.31", - "license": "MIT" - }, - "node_modules/function-bind": { - "version": "1.1.1", - "license": "MIT" - }, - "node_modules/function.prototype.name": { - "version": "1.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gauge": { - "version": "2.7.4", - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "node_modules/gauge/node_modules/strip-ansi": { - "version": "3.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gaxios": { - "version": "3.2.0", - "license": "Apache-2.0", - "dependencies": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.3.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gaxios/node_modules/is-stream": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gcp-metadata": { - "version": "4.3.1", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^4.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gcp-metadata/node_modules/gaxios": { - "version": "4.3.3", - "license": "Apache-2.0", - "dependencies": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.7" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gcp-metadata/node_modules/is-stream": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gcs-resumable-upload": { - "version": "3.6.0", - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "async-retry": "^1.3.3", - "configstore": "^5.0.0", - "extend": "^3.0.2", - "gaxios": "^4.0.0", - "google-auth-library": "^7.0.0", - "pumpify": "^2.0.0", - "stream-events": "^1.0.4" - }, - "bin": { - "gcs-upload": "build/src/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gcs-resumable-upload/node_modules/gaxios": { - "version": "4.3.3", - "license": "Apache-2.0", - "dependencies": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.7" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gcs-resumable-upload/node_modules/is-stream": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.1", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-uri": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "@tootallnate/once": "1", - "data-uri-to-buffer": "3", - "debug": "4", - "file-uri-to-path": "2", - "fs-extra": "^8.1.0", - "ftp": "^0.3.10" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/get-uri/node_modules/file-uri-to-path": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/git-node-fs": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/git-sha1": { - "version": "0.1.2", - "license": "MIT" - }, - "node_modules/glob": { - "version": "7.2.3", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/global-dirs": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "1.3.7" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/global-dirs/node_modules/ini": { - "version": "1.3.7", - "dev": true, - "license": "ISC" - }, - "node_modules/globals": { - "version": "11.12.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/globalthis": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/google-auth-library": { - "version": "7.14.1", - "license": "Apache-2.0", - "dependencies": { - "arrify": "^2.0.0", - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "fast-text-encoding": "^1.0.0", - "gaxios": "^4.0.0", - "gcp-metadata": "^4.2.0", - "gtoken": "^5.0.4", - "jws": "^4.0.0", - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/google-auth-library/node_modules/gaxios": { - "version": "4.3.3", - "license": "Apache-2.0", - "dependencies": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.7" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/google-auth-library/node_modules/is-stream": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/google-auth-library/node_modules/lru-cache": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/google-auth-library/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/google-p12-pem": { - "version": "3.1.4", - "license": "MIT", - "dependencies": { - "node-forge": "^1.3.1" - }, - "bin": { - "gp12-pem": "build/src/bin/gp12-pem.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "9.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/got/node_modules/get-stream": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "license": "ISC" - }, - "node_modules/grapheme-splitter": { - "version": "1.0.4", - "dev": true, - "license": "MIT" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/gray-matter": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "js-yaml": "^3.13.1", - "kind-of": "^6.0.2", - "section-matter": "^1.0.0", - "strip-bom-string": "^1.0.0" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/gray-matter/node_modules/argparse": { - "version": "1.0.10", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/gray-matter/node_modules/js-yaml": { - "version": "3.14.1", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/gray-matter/node_modules/sprintf-js": { - "version": "1.0.3", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/gtoken": { - "version": "5.3.2", - "license": "MIT", - "dependencies": { - "gaxios": "^4.0.0", - "google-p12-pem": "^3.1.3", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gtoken/node_modules/gaxios": { - "version": "4.3.3", - "license": "Apache-2.0", - "dependencies": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.7" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gtoken/node_modules/is-stream": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/has": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "license": "ISC" - }, - "node_modules/has-yarn": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hash-stream-validation": { - "version": "0.2.4", - "license": "MIT" - }, - "node_modules/he": { - "version": "1.2.0", - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, - "node_modules/helmet": { - "version": "4.1.1", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/hexoid": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/highlight.js": { - "version": "10.7.3", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "license": "ISC" - }, - "node_modules/hpagent": { - "version": "0.1.2", - "license": "MIT" - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/html-to-mrkdwn-ts": { - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "node-html-markdown": "^1.1.3" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/html-to-text": { - "version": "8.2.1", - "license": "MIT", - "dependencies": { - "@selderee/plugin-htmlparser2": "^0.6.0", - "deepmerge": "^4.2.2", - "he": "^1.2.0", - "htmlparser2": "^6.1.0", - "minimist": "^1.2.6", - "selderee": "^0.6.0" - }, - "bin": { - "html-to-text": "bin/cli.js" - }, - "engines": { - "node": ">=10.23.2" - } - }, - "node_modules/html-to-text/node_modules/dom-serializer": { - "version": "1.4.1", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/html-to-text/node_modules/domutils": { - "version": "2.8.0", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/html-to-text/node_modules/entities": { - "version": "2.2.0", - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/html-to-text/node_modules/htmlparser2": { - "version": "6.1.0", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - }, - "node_modules/htmlparser2": { - "version": "8.0.2", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, - "node_modules/htmlparser2/node_modules/domhandler": { - "version": "5.0.3", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "license": "BSD-2-Clause" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/setprototypeof": { - "version": "1.2.0", - "license": "ISC" - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-proxy-agent": { - "version": "4.0.1", - "license": "MIT", - "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http2-client": { - "version": "1.3.5", - "dev": true, - "license": "MIT" - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "license": "MIT", - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.1.13", - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.2.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "dev": true, - "license": "ISC" - }, - "node_modules/ignore-walk": { - "version": "3.0.4", - "license": "ISC", - "dependencies": { - "minimatch": "^3.0.4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-lazy": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/import-local": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/infer-owner": { - "version": "1.0.4", - "license": "ISC" - }, - "node_modules/inflection": { - "version": "1.13.4", - "engines": [ - "node >= 0.4.0" - ], - "license": "MIT" - }, - "node_modules/inflight": { - "version": "1.0.6", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "license": "ISC" - }, - "node_modules/internal-slot": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/invert-kv": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ip": { - "version": "1.1.8", - "license": "MIT" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "license": "MIT" - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-buffer": { - "version": "1.1.6", - "license": "MIT" - }, - "node_modules/is-callable": { - "version": "1.2.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-ci": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ci-info": "^2.0.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/is-ci/node_modules/ci-info": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/is-core-module": { - "version": "2.12.1", - "license": "MIT", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-electron": { - "version": "2.2.0", - "license": "MIT" - }, - "node_modules/is-extendable": { - "version": "0.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-installed-globally": { - "version": "0.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "global-dirs": "^2.0.1", - "is-path-inside": "^3.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-interactive": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-lambda": { - "version": "1.0.1", - "license": "MIT" - }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-npm": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-obj": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-promise": { - "version": "2.2.2", - "license": "MIT" - }, - "node_modules/is-regex": { - "version": "1.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-retry-allowed": { - "version": "1.2.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.10", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-what": { - "version": "4.1.15", - "license": "MIT", - "engines": { - "node": ">=12.13" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-yarn-global": { - "version": "0.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/isarray": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/isemail": { - "version": "3.2.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "punycode": "2.x.x" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "license": "ISC" - }, - "node_modules/isomorphic-form-data": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "form-data": "^2.3.2" - } - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.5", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/iterate-object": { - "version": "1.3.4", - "license": "MIT" - }, - "node_modules/jest": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.5.0", - "@jest/types": "^29.5.0", - "import-local": "^3.0.2", - "jest-cli": "^29.5.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.0.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/expect": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.5.0", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.5.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-circus/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-circus/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-circus/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "import-local": "^3.0.2", - "jest-config": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", - "prompts": "^2.0.1", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-cli/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/cliui": { - "version": "8.0.1", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/jest-cli/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-cli/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-cli/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli/node_modules/wrap-ansi": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/y18n": { - "version": "5.0.8", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-cli/node_modules/yargs": { - "version": "17.7.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/jest-config": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.5.0", - "@jest/types": "^29.5.0", - "babel-jest": "^29.5.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.5.0", - "jest-environment-node": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-runner": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.5.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-config/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-config/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-config/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-config/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-diff": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.4.3", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-diff/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-diff/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-diff/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-docblock": { - "version": "29.4.3", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.5.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.4.3", - "jest-util": "^29.5.0", - "pretty-format": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-each/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-each/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-each/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-environment-node": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/fake-timers": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/node": "*", - "jest-mock": "^29.5.0", - "jest-util": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.4.3", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.5.0", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.5.0", - "jest-worker": "^29.5.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-leak-detector": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.5.0", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-matcher-utils/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-matcher-utils/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-message-util": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.5.0", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.5.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-message-util/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-message-util/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-message-util/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-mock": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.5.0", - "@types/node": "*", - "jest-util": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "29.4.3", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-regex-util": "^29.4.3", - "jest-snapshot": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-resolve/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-resolve/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-resolve/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-resolve/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-resolve/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runner": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.5.0", - "@jest/environment": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.4.3", - "jest-environment-node": "^29.5.0", - "jest-haste-map": "^29.5.0", - "jest-leak-detector": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-resolve": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-util": "^29.5.0", - "jest-watcher": "^29.5.0", - "jest-worker": "^29.5.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-runner/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-runner/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-runner/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-runner/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runner/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runtime": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/fake-timers": "^29.5.0", - "@jest/globals": "^29.5.0", - "@jest/source-map": "^29.4.3", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-mock": "^29.5.0", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runtime/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-runtime/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-runtime/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-runtime/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-runtime/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runtime/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-snapshot": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.5.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.5.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-snapshot/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-snapshot/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.5.2", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-snapshot/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-snapshot/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/jest-util": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.5.0", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-util/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-util/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-util/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-util/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-validate": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.5.0", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.4.3", - "leven": "^3.1.0", - "pretty-format": "^29.5.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-validate/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-validate/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-validate/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-validate/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-validate/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watcher": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.5.0", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-watcher/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-watcher/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-watcher/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-watcher/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-watcher/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watcher/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-util": "^29.5.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jmespath": { - "version": "0.15.0", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/join-component": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/jose": { - "version": "4.14.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", - "integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/js-beautify": { - "version": "1.14.8", - "license": "MIT", - "dependencies": { - "config-chain": "^1.1.13", - "editorconfig": "^0.15.3", - "glob": "^8.1.0", - "nopt": "^6.0.0" - }, - "bin": { - "css-beautify": "js/bin/css-beautify.js", - "html-beautify": "js/bin/html-beautify.js", - "js-beautify": "js/bin/js-beautify.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/js-beautify/node_modules/brace-expansion": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/js-beautify/node_modules/glob": { - "version": "8.1.0", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/js-beautify/node_modules/minimatch": { - "version": "5.1.6", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/js-beautify/node_modules/nopt": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "abbrev": "^1.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/js-git": { - "version": "0.7.8", - "license": "MIT", - "dependencies": { - "bodec": "^0.1.0", - "culvert": "^0.1.2", - "git-sha1": "^0.1.2", - "pako": "^0.2.5" - } - }, - "node_modules/js-sha256": { - "version": "0.9.0", - "license": "MIT" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "2.5.2", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json-bigint": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, - "node_modules/json-buffer": { - "version": "3.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "license": "ISC", - "optional": true - }, - "node_modules/json-to-ast": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "code-error-fragment": "0.0.230", - "grapheme-splitter": "^1.0.4" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/json2csv": { - "version": "5.0.7", - "license": "MIT", - "dependencies": { - "commander": "^6.1.0", - "jsonparse": "^1.3.1", - "lodash.get": "^4.4.2" - }, - "bin": { - "json2csv": "bin/json2csv.js" - }, - "engines": { - "node": ">= 10", - "npm": ">= 6.13.0" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonparse": { - "version": "1.3.1", - "engines": [ - "node >= 0.2.0" - ], - "license": "MIT" - }, - "node_modules/jsonpointer": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jsonwebtoken": { - "version": "8.5.1", - "license": "MIT", - "dependencies": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=4", - "npm": ">=1.4.28" - } - }, - "node_modules/jsonwebtoken/node_modules/jwa": { - "version": "1.4.1", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jsonwebtoken/node_modules/jws": { - "version": "3.2.2", - "license": "MIT", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jsonwebtoken/node_modules/semver": { - "version": "5.7.1", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/jwa": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jwks-rsa": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.0.1.tgz", - "integrity": "sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw==", - "dependencies": { - "@types/express": "^4.17.14", - "@types/jsonwebtoken": "^9.0.0", - "debug": "^4.3.4", - "jose": "^4.10.4", - "limiter": "^1.1.5", - "lru-memoizer": "^2.1.4" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/jws": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/keyv": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.0" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/latest-version": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "package-json": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lazy": { - "version": "1.0.11", - "license": "MIT", - "engines": { - "node": ">=0.2.0" - } - }, - "node_modules/lcid": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "invert-kv": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/limiter": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", - "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "dev": true, - "license": "MIT" - }, - "node_modules/load-json-file": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/load-json-file/node_modules/parse-json": { - "version": "2.2.0", - "license": "MIT", - "dependencies": { - "error-ex": "^1.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/load-json-file/node_modules/strip-bom": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "license": "MIT" - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "license": "MIT" - }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.get": { - "version": "4.4.2", - "license": "MIT" - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "license": "MIT" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "license": "MIT" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "license": "MIT" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "license": "MIT" - }, - "node_modules/lodash.snakecase": { - "version": "4.1.1", - "license": "MIT" - }, - "node_modules/log-driver": { - "version": "1.2.7", - "license": "ISC", - "engines": { - "node": ">=0.8.6" - } - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-symbols/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/log-symbols/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/log-symbols/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lowercase-keys": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lru-memoizer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.2.0.tgz", - "integrity": "sha512-QfOZ6jNkxCcM/BkIPnFsqDhtrazLRsghi9mBwFAzol5GCvj4EkFT899Za3+QwikCg5sRX8JstioBDwOxEyzaNw==", - "dependencies": { - "lodash.clonedeep": "^4.5.0", - "lru-cache": "~4.0.0" - } - }, - "node_modules/lru-memoizer/node_modules/lru-cache": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", - "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", - "dependencies": { - "pseudomap": "^1.0.1", - "yallist": "^2.0.0" - } - }, - "node_modules/lru-memoizer/node_modules/yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" - }, - "node_modules/lru-queue": { - "version": "0.1.0", - "license": "MIT", - "dependencies": { - "es5-ext": "~0.10.2" - } - }, - "node_modules/luxon": { - "version": "3.3.0", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "dev": true, - "license": "ISC" - }, - "node_modules/make-fetch-happen": { - "version": "10.2.1", - "license": "ISC", - "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^16.1.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^2.0.3", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^9.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/@tootallnate/once": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/make-fetch-happen/node_modules/lru-cache": { - "version": "7.18.3", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/make-fetch-happen/node_modules/minipass": { - "version": "3.3.6", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/make-fetch-happen/node_modules/socks-proxy-agent": { - "version": "7.0.0", - "license": "MIT", - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/make-fetch-happen/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/makeerror": { - "version": "1.0.12", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/map-o": { - "version": "2.0.10", - "license": "MIT", - "dependencies": { - "iterate-object": "^1.3.0" - } - }, - "node_modules/md5": { - "version": "2.3.0", - "license": "BSD-3-Clause", - "dependencies": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mem": { - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "mimic-fn": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mem/node_modules/mimic-fn": { - "version": "1.2.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/memoizee": { - "version": "0.4.15", - "license": "ISC", - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.53", - "es6-weak-map": "^2.0.3", - "event-emitter": "^0.3.5", - "is-promise": "^2.2.2", - "lru-queue": "^0.1.0", - "next-tick": "^1.1.0", - "timers-ext": "^0.1.7" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "license": "MIT" - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "2.6.0", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "2.9.0", - "license": "ISC", - "dependencies": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-collect/node_modules/minipass": { - "version": "3.3.6", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/minipass-fetch": { - "version": "2.1.2", - "license": "MIT", - "dependencies": { - "minipass": "^3.1.6", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/minipass-fetch/node_modules/minipass": { - "version": "3.3.6", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-fetch/node_modules/minizlib": { - "version": "2.1.2", - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-fetch/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-flush/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/minizlib": { - "version": "1.3.3", - "license": "MIT", - "dependencies": { - "minipass": "^2.9.0" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/module-details-from-path": { - "version": "1.0.3", - "license": "MIT" - }, - "node_modules/moment": { - "version": "2.29.4", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/moment-timezone": { - "version": "0.5.43", - "license": "MIT", - "dependencies": { - "moment": "^2.29.4" - }, - "engines": { - "node": "*" - } - }, - "node_modules/moo": { - "version": "0.5.2", - "license": "BSD-3-Clause" - }, - "node_modules/ms": { - "version": "2.1.3", - "license": "MIT" - }, - "node_modules/murmurhash3js": { - "version": "3.0.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mute-stream": { - "version": "0.0.8", - "license": "ISC" - }, - "node_modules/mv": { - "version": "2.1.1", - "license": "MIT", - "dependencies": { - "mkdirp": "~0.5.1", - "ncp": "~2.0.0", - "rimraf": "~2.4.0" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/mv/node_modules/glob": { - "version": "6.0.4", - "license": "ISC", - "dependencies": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mv/node_modules/mkdirp": { - "version": "0.5.6", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mv/node_modules/rimraf": { - "version": "2.4.5", - "license": "ISC", - "dependencies": { - "glob": "^6.0.1" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/mz": { - "version": "2.7.0", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nan": { - "version": "2.17.0", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.6", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ncp": { - "version": "2.0.0", - "license": "MIT", - "bin": { - "ncp": "bin/ncp" - } - }, - "node_modules/nearley": { - "version": "2.20.1", - "license": "MIT", - "dependencies": { - "commander": "^2.19.0", - "moo": "^0.5.0", - "railroad-diagrams": "^1.0.0", - "randexp": "0.4.6" - }, - "bin": { - "nearley-railroad": "bin/nearley-railroad.js", - "nearley-test": "bin/nearley-test.js", - "nearley-unparse": "bin/nearley-unparse.js", - "nearleyc": "bin/nearleyc.js" - }, - "funding": { - "type": "individual", - "url": "https://nearley.js.org/#give-to-nearley" - } - }, - "node_modules/nearley/node_modules/commander": { - "version": "2.20.3", - "license": "MIT" - }, - "node_modules/needle": { - "version": "0.7.10", - "resolved": "git+ssh://git@github.com/clearbit/needle.git#84d28b5f2c3916db1e7eb84aeaa9d976cc40054b", - "integrity": "sha512-9VnoxVBudfy+C5eIHHbb+SkkWugmACsefrBS+EkHTufUJeHUA5/xBeSquvw+Bj5NvQmieEStduiIDnFKP+Kbog==", - "dependencies": { - "iconv-lite": "^0.4.4" - }, - "bin": { - "needle": "bin/needle" - }, - "engines": { - "node": ">= 0.10.x" - } - }, - "node_modules/negotiator": { - "version": "0.6.3", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/netmask": { - "version": "2.0.2", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/next-tick": { - "version": "1.1.0", - "license": "ISC" - }, - "node_modules/node-addon-api": { - "version": "3.2.1", - "license": "MIT" - }, - "node_modules/node-fetch": { - "version": "2.6.11", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch-h2": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "http2-client": "^1.2.5" - }, - "engines": { - "node": "4.x || >=6.0.0" - } - }, - "node_modules/node-forge": { - "version": "1.3.1", - "license": "(BSD-3-Clause OR GPL-2.0)", - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/node-gyp-build": { - "version": "4.6.0", - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/node-html-markdown": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "node-html-parser": "^6.1.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/node-html-parser": { - "version": "6.1.5", - "license": "MIT", - "dependencies": { - "css-select": "^5.1.0", - "he": "1.2.0" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/node-mocks-http": { - "version": "1.9.0", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "^1.3.7", - "depd": "^1.1.0", - "fresh": "^0.5.2", - "merge-descriptors": "^1.0.1", - "methods": "^1.1.2", - "mime": "^1.3.4", - "parseurl": "^1.3.3", - "range-parser": "^1.2.0", - "type-is": "^1.6.18" - }, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/node-mocks-http/node_modules/depd": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-mocks-http/node_modules/mime": { - "version": "1.6.0", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/node-pre-gyp": { - "version": "0.15.0", - "license": "BSD-3-Clause", - "dependencies": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.3", - "needle": "^2.5.0", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4.4.2" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/node-pre-gyp/node_modules/debug": { - "version": "3.2.7", - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/node-pre-gyp/node_modules/mkdirp": { - "version": "0.5.6", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/node-pre-gyp/node_modules/needle": { - "version": "2.9.1", - "license": "MIT", - "dependencies": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "bin": { - "needle": "bin/needle" - }, - "engines": { - "node": ">= 4.4.x" - } - }, - "node_modules/node-pre-gyp/node_modules/rimraf": { - "version": "2.7.1", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/node-pre-gyp/node_modules/sax": { - "version": "1.2.4", - "license": "ISC" - }, - "node_modules/node-pre-gyp/node_modules/semver": { - "version": "5.7.1", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/node-readfiles": { - "version": "0.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "es6-promise": "^3.2.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.12", - "dev": true, - "license": "MIT" - }, - "node_modules/nodemon": { - "version": "2.0.4", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.2.2", - "debug": "^3.2.6", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.0.4", - "pstree.remy": "^1.1.7", - "semver": "^5.7.1", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.2", - "update-notifier": "^4.0.0" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=8.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/nodemon/node_modules/semver": { - "version": "5.7.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/noms": { - "version": "0.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "~1.0.31" - } - }, - "node_modules/noms/node_modules/isarray": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/noms/node_modules/readable-stream": { - "version": "1.0.34", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/noms/node_modules/string_decoder": { - "version": "0.10.31", - "dev": true, - "license": "MIT" - }, - "node_modules/nopt": { - "version": "4.0.3", - "license": "ISC", - "dependencies": { - "abbrev": "1", - "osenv": "^0.1.4" - }, - "bin": { - "nopt": "bin/nopt.js" - } - }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.1", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "4.5.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm-bundled": { - "version": "1.1.2", - "license": "ISC", - "dependencies": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "node_modules/npm-normalize-package-bin": { - "version": "1.0.1", - "license": "ISC" - }, - "node_modules/npm-packlist": { - "version": "1.4.8", - "license": "ISC", - "dependencies": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npmlog": { - "version": "4.1.2", - "license": "ISC", - "dependencies": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "node_modules/nssocket": { - "version": "0.6.0", - "license": "MIT", - "dependencies": { - "eventemitter2": "~0.4.14", - "lazy": "~1.0.11" - }, - "engines": { - "node": ">= 0.10.x" - } - }, - "node_modules/nssocket/node_modules/eventemitter2": { - "version": "0.4.14", - "license": "MIT" - }, - "node_modules/nth-check": { - "version": "2.1.1", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/oas-kit-common": { - "version": "1.0.8", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "fast-safe-stringify": "^2.0.7" - } - }, - "node_modules/oas-linter": { - "version": "3.2.2", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@exodus/schemasafe": "^1.0.0-rc.2", - "should": "^13.2.1", - "yaml": "^1.10.0" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-normalize": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@readme/openapi-parser": "^2.2.0", - "js-yaml": "^4.1.0", - "node-fetch": "^2.6.1", - "swagger2openapi": "^7.0.8" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/oas-resolver": { - "version": "2.5.6", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "node-fetch-h2": "^2.3.0", - "oas-kit-common": "^1.0.8", - "reftools": "^1.1.9", - "yaml": "^1.10.0", - "yargs": "^17.0.1" - }, - "bin": { - "resolve": "resolve.js" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-resolver/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/oas-resolver/node_modules/cliui": { - "version": "8.0.1", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/oas-resolver/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/oas-resolver/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/oas-resolver/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/oas-resolver/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/oas-resolver/node_modules/wrap-ansi": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/oas-resolver/node_modules/y18n": { - "version": "5.0.8", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/oas-resolver/node_modules/yargs": { - "version": "17.7.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/oas-schema-walker": { - "version": "1.1.5", - "dev": true, - "license": "BSD-3-Clause", - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-validator": { - "version": "5.0.8", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "call-me-maybe": "^1.0.1", - "oas-kit-common": "^1.0.8", - "oas-linter": "^3.2.2", - "oas-resolver": "^2.5.6", - "oas-schema-walker": "^1.1.5", - "reftools": "^1.1.9", - "should": "^13.2.1", - "yaml": "^1.10.0" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oauth": { - "version": "0.9.15", - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.12.3", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/omit-deep-by-values": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "lodash": "~4.17.11" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "8.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/openapi-comment-parser": { - "version": "1.0.0", - "license": "MIT", - "bin": { - "openapi-comment-parser": "bin/index.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/openapi-types": { - "version": "12.1.3", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/optionator": { - "version": "0.9.1", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/optionator/node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/ora": { - "version": "5.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ora/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/ora/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/ora/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/os-homedir": { - "version": "1.0.2", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/os-locale": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "execa": "^0.7.0", - "lcid": "^1.0.0", - "mem": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/os-locale/node_modules/cross-spawn": { - "version": "5.1.0", - "license": "MIT", - "dependencies": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "node_modules/os-locale/node_modules/execa": { - "version": "0.7.0", - "license": "MIT", - "dependencies": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/os-locale/node_modules/get-stream": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/os-locale/node_modules/lru-cache": { - "version": "4.1.5", - "license": "ISC", - "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "node_modules/os-locale/node_modules/npm-run-path": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/os-locale/node_modules/path-key": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/os-locale/node_modules/shebang-command": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/os-locale/node_modules/shebang-regex": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/os-locale/node_modules/which": { - "version": "1.3.1", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/os-locale/node_modules/yallist": { - "version": "2.1.2", - "license": "ISC" - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/osenv": { - "version": "0.1.5", - "license": "ISC", - "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "node_modules/p-cancelable": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-queue": { - "version": "6.6.2", - "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.4", - "p-timeout": "^3.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-queue/node_modules/eventemitter3": { - "version": "4.0.7", - "license": "MIT" - }, - "node_modules/p-retry": { - "version": "4.6.2", - "license": "MIT", - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-timeout": { - "version": "3.2.0", - "license": "MIT", - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pac-proxy-agent": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4", - "get-uri": "3", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "5", - "pac-resolver": "^5.0.0", - "raw-body": "^2.2.0", - "socks-proxy-agent": "5" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/pac-resolver": { - "version": "5.0.1", - "license": "MIT", - "dependencies": { - "degenerator": "^3.0.2", - "ip": "^1.1.5", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/package-json": { - "version": "6.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "got": "^9.6.0", - "registry-auth-token": "^4.0.0", - "registry-url": "^5.0.0", - "semver": "^6.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/packet-reader": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/pako": { - "version": "0.2.9", - "license": "MIT" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-link-header": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "xtend": "~4.0.1" - } - }, - "node_modules/parse-link-header/node_modules/xtend": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/parse-srcset": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/parse5": { - "version": "5.1.1", - "license": "MIT" - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "5.1.1", - "license": "MIT", - "dependencies": { - "parse5": "^5.1.1" - } - }, - "node_modules/parseley": { - "version": "0.7.0", - "license": "MIT", - "dependencies": { - "moo": "^0.5.1", - "nearley": "^2.20.1" - }, - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/passport": { - "version": "0.6.0", - "license": "MIT", - "dependencies": { - "passport-strategy": "1.x.x", - "pause": "0.0.1", - "utils-merge": "^1.0.1" - }, - "engines": { - "node": ">= 0.4.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jaredhanson" - } - }, - "node_modules/passport-facebook": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "passport-oauth2": "1.x.x" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/passport-github2": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz", - "integrity": "sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==", - "dependencies": { - "passport-oauth2": "1.x.x" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/passport-google-oauth": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "passport-google-oauth1": "1.x.x", - "passport-google-oauth20": "2.x.x" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/passport-google-oauth1": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "passport-oauth1": "1.x.x" - } - }, - "node_modules/passport-google-oauth20": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "passport-oauth2": "1.x.x" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/passport-oauth": { - "version": "0.1.15", - "dependencies": { - "oauth": "0.9.x", - "passport": "~0.1.1", - "pkginfo": "0.2.x" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/passport-oauth/node_modules/passport": { - "version": "0.1.18", - "dependencies": { - "pause": "0.0.1", - "pkginfo": "0.2.x" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/passport-oauth1": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "oauth": "0.9.x", - "passport-strategy": "1.x.x", - "utils-merge": "1.x.x" - }, - "engines": { - "node": ">= 0.4.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jaredhanson" - } - }, - "node_modules/passport-oauth2": { - "version": "1.7.0", - "license": "MIT", - "dependencies": { - "base64url": "3.x.x", - "oauth": "0.9.x", - "passport-strategy": "1.x.x", - "uid2": "0.0.x", - "utils-merge": "1.x.x" - }, - "engines": { - "node": ">= 0.4.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jaredhanson" - } - }, - "node_modules/passport-slack": { - "version": "0.0.7", - "dependencies": { - "passport-oauth": "~0.1.1", - "pkginfo": "0.2.x" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/passport-strategy": { - "version": "1.0.0", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "license": "MIT" - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pause": { - "version": "0.0.1" - }, - "node_modules/peek-readable": { - "version": "5.0.0", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/peopledatalabs": { - "version": "5.0.5", - "license": "MIT", - "dependencies": { - "axios": "^1.4.0", - "copy-anything": "^3.0.5" - } - }, - "node_modules/peopledatalabs/node_modules/axios": { - "version": "1.4.0", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/peopledatalabs/node_modules/form-data": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/pg": { - "version": "8.11.0", - "license": "MIT", - "dependencies": { - "buffer-writer": "2.0.0", - "packet-reader": "1.0.0", - "pg-connection-string": "^2.6.0", - "pg-pool": "^3.6.0", - "pg-protocol": "^1.6.0", - "pg-types": "^2.1.0", - "pgpass": "1.x" - }, - "engines": { - "node": ">= 8.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.1.0" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.1.0", - "license": "MIT", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.6.0", - "license": "MIT" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.6.0", - "license": "MIT", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.6.0", - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pgpass": { - "version": "1.0.5", - "license": "MIT", - "dependencies": { - "split2": "^4.1.0" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pidusage": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pidusage/node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/pify": { - "version": "2.3.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkginfo": { - "version": "0.2.3", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/pm2": { - "version": "5.3.0", - "license": "AGPL-3.0", - "dependencies": { - "@pm2/agent": "~2.0.0", - "@pm2/io": "~5.0.0", - "@pm2/js-api": "~0.6.7", - "@pm2/pm2-version-check": "latest", - "async": "~3.2.0", - "blessed": "0.1.81", - "chalk": "3.0.0", - "chokidar": "^3.5.3", - "cli-tableau": "^2.0.0", - "commander": "2.15.1", - "croner": "~4.1.92", - "dayjs": "~1.11.5", - "debug": "^4.3.1", - "enquirer": "2.3.6", - "eventemitter2": "5.0.1", - "fclone": "1.0.11", - "mkdirp": "1.0.4", - "needle": "2.4.0", - "pidusage": "~3.0", - "pm2-axon": "~4.0.1", - "pm2-axon-rpc": "~0.7.1", - "pm2-deploy": "~1.0.2", - "pm2-multimeter": "^0.1.2", - "promptly": "^2", - "semver": "^7.2", - "source-map-support": "0.5.21", - "sprintf-js": "1.1.2", - "vizion": "~2.2.1", - "yamljs": "0.3.0" - }, - "bin": { - "pm2": "bin/pm2", - "pm2-dev": "bin/pm2-dev", - "pm2-docker": "bin/pm2-docker", - "pm2-runtime": "bin/pm2-runtime" - }, - "engines": { - "node": ">=10.0.0" - }, - "optionalDependencies": { - "pm2-sysmonit": "^1.2.8" - } - }, - "node_modules/pm2-axon": { - "version": "4.0.1", - "license": "MIT", - "dependencies": { - "amp": "~0.3.1", - "amp-message": "~0.1.1", - "debug": "^4.3.1", - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=5" - } - }, - "node_modules/pm2-axon-rpc": { - "version": "0.7.1", - "license": "MIT", - "dependencies": { - "debug": "^4.3.1" - }, - "engines": { - "node": ">=5" - } - }, - "node_modules/pm2-axon/node_modules/escape-string-regexp": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pm2-deploy": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "run-series": "^1.1.8", - "tv4": "^1.3.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pm2-multimeter": { - "version": "0.1.2", - "license": "MIT/X11", - "dependencies": { - "charm": "~0.1.1" - } - }, - "node_modules/pm2-sysmonit": { - "version": "1.2.8", - "license": "Apache", - "optional": true, - "dependencies": { - "async": "^3.2.0", - "debug": "^4.3.1", - "pidusage": "^2.0.21", - "systeminformation": "^5.7", - "tx2": "~1.0.4" - } - }, - "node_modules/pm2-sysmonit/node_modules/async": { - "version": "3.2.4", - "license": "MIT", - "optional": true - }, - "node_modules/pm2-sysmonit/node_modules/pidusage": { - "version": "2.0.21", - "license": "MIT", - "optional": true, - "dependencies": { - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pm2-sysmonit/node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/pm2/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/pm2/node_modules/async": { - "version": "3.2.4", - "license": "MIT" - }, - "node_modules/pm2/node_modules/chalk": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pm2/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/pm2/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/pm2/node_modules/commander": { - "version": "2.15.1", - "license": "MIT" - }, - "node_modules/pm2/node_modules/eventemitter2": { - "version": "5.0.1", - "license": "MIT" - }, - "node_modules/pm2/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pm2/node_modules/lru-cache": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pm2/node_modules/needle": { - "version": "2.4.0", - "license": "MIT", - "dependencies": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "bin": { - "needle": "bin/needle" - }, - "engines": { - "node": ">= 4.4.x" - } - }, - "node_modules/pm2/node_modules/needle/node_modules/debug": { - "version": "3.2.7", - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/pm2/node_modules/sax": { - "version": "1.2.4", - "license": "ISC" - }, - "node_modules/pm2/node_modules/semver": { - "version": "7.5.2", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pm2/node_modules/source-map-support": { - "version": "0.5.21", - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/pm2/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pm2/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/postcss": { - "version": "8.4.24", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval/node_modules/xtend": { - "version": "4.0.2", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prepend-http": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/prettier": { - "version": "2.8.8", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty-format": { - "version": "29.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.4.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "license": "MIT" - }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "license": "ISC" - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/promise-retry/node_modules/retry": { - "version": "0.12.0", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/promptly": { - "version": "2.2.0", - "license": "MIT", - "dependencies": { - "read": "^1.0.4" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/proto-list": { - "version": "1.2.4", - "license": "ISC" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-agent": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "agent-base": "^6.0.0", - "debug": "4", - "http-proxy-agent": "^4.0.0", - "https-proxy-agent": "^5.0.0", - "lru-cache": "^5.1.1", - "pac-proxy-agent": "^5.0.0", - "proxy-from-env": "^1.0.0", - "socks-proxy-agent": "^5.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/pseudomap": { - "version": "1.0.2", - "license": "ISC" - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "dev": true, - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/pumpify": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "duplexify": "^4.1.1", - "inherits": "^2.0.3", - "pump": "^3.0.0" - } - }, - "node_modules/pumpify/node_modules/duplexify": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" - } - }, - "node_modules/punycode": { - "version": "2.3.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pupa": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-goat": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pure-rand": { - "version": "6.0.2", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/qs": { - "version": "6.11.0", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/querystring": { - "version": "0.2.0", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/railroad-diagrams": { - "version": "1.0.0", - "license": "CC0-1.0" - }, - "node_modules/ramda": { - "version": "0.27.2", - "license": "MIT" - }, - "node_modules/randexp": { - "version": "0.4.6", - "license": "MIT", - "dependencies": { - "discontinuous-range": "1.0.0", - "ret": "~0.1.10" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/rdme": { - "version": "7.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@actions/core": "^1.6.0", - "chalk": "^4.1.2", - "cli-table": "^0.3.1", - "command-line-args": "^5.2.0", - "command-line-usage": "^6.0.2", - "config": "^3.1.0", - "configstore": "^5.0.0", - "debug": "^4.3.3", - "editor": "^1.0.0", - "enquirer": "^2.3.0", - "form-data": "^4.0.0", - "gray-matter": "^4.0.1", - "isemail": "^3.1.3", - "mime-types": "^2.1.35", - "node-fetch": "^2.6.1", - "oas-normalize": "^6.0.0", - "open": "^8.2.1", - "ora": "^5.4.1", - "parse-link-header": "^2.0.0", - "read": "^1.0.7", - "semver": "^7.0.0", - "tmp-promise": "^3.0.2", - "update-notifier": "^5.1.0" - }, - "bin": { - "rdme": "bin/rdme" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/rdme/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/rdme/node_modules/boxen": { - "version": "5.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-align": "^3.0.0", - "camelcase": "^6.2.0", - "chalk": "^4.1.0", - "cli-boxes": "^2.2.1", - "string-width": "^4.2.2", - "type-fest": "^0.20.2", - "widest-line": "^3.1.0", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rdme/node_modules/camelcase": { - "version": "6.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rdme/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/rdme/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/rdme/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/rdme/node_modules/form-data": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/rdme/node_modules/global-dirs": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rdme/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/rdme/node_modules/ini": { - "version": "2.0.0", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/rdme/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/rdme/node_modules/is-installed-globally": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rdme/node_modules/is-npm": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rdme/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/rdme/node_modules/semver": { - "version": "7.5.2", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/rdme/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/rdme/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/rdme/node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rdme/node_modules/update-notifier": { - "version": "5.1.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boxen": "^5.0.0", - "chalk": "^4.1.0", - "configstore": "^5.0.1", - "has-yarn": "^2.1.0", - "import-lazy": "^2.1.0", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.4.0", - "is-npm": "^5.0.0", - "is-yarn-global": "^0.3.0", - "latest-version": "^5.1.0", - "pupa": "^2.1.1", - "semver": "^7.3.4", - "semver-diff": "^3.1.1", - "xdg-basedir": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/yeoman/update-notifier?sponsor=1" - } - }, - "node_modules/rdme/node_modules/wrap-ansi": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/rdme/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/react-is": { - "version": "18.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/read": { - "version": "1.0.7", - "license": "ISC", - "dependencies": { - "mute-stream": "~0.0.4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/read-pkg": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "load-json-file": "^2.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "find-up": "^2.0.0", - "read-pkg": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "locate-path": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up/node_modules/locate-path": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up/node_modules/p-limit": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "p-try": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up/node_modules/p-locate": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "p-limit": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up/node_modules/p-try": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg-up/node_modules/path-exists": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg/node_modules/path-type": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "pify": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readable-web-to-node-stream": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/reduce-flatten": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/reftools": { - "version": "1.1.9", - "dev": true, - "license": "BSD-3-Clause", - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/regenerate": { - "version": "1.4.2", - "dev": true, - "license": "MIT" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.13.11", - "dev": true, - "license": "MIT" - }, - "node_modules/regenerator-transform": { - "version": "0.15.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexpu-core": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/registry-auth-token": { - "version": "4.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "rc": "1.2.8" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/registry-url": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "rc": "^1.2.8" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/regjsparser": { - "version": "0.9.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "jsesc": "~0.5.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - } - }, - "node_modules/remove-trailing-slash": { - "version": "0.1.1", - "license": "MIT" - }, - "node_modules/require-directory": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-in-the-middle": { - "version": "5.2.0", - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "module-details-from-path": "^1.0.3", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "license": "ISC" - }, - "node_modules/resolve": { - "version": "1.22.2", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve.exports": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/responselike": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "lowercase-keys": "^1.0.0" - } - }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ret": { - "version": "0.1.15", - "license": "MIT", - "engines": { - "node": ">=0.12" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/retry-as-promised": { - "version": "5.0.0", - "license": "MIT" - }, - "node_modules/retry-request": { - "version": "4.2.2", - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "extend": "^3.0.2" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/run-series": { - "version": "1.1.9", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "license": "MIT" - }, - "node_modules/safe-json-stringify": { - "version": "1.2.0", - "license": "MIT", - "optional": true - }, - "node_modules/safe-regex-test": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "license": "MIT" - }, - "node_modules/sanitize-html": { - "version": "2.11.0", - "license": "MIT", - "dependencies": { - "deepmerge": "^4.2.2", - "escape-string-regexp": "^4.0.0", - "htmlparser2": "^8.0.0", - "is-plain-object": "^5.0.0", - "parse-srcset": "^1.0.2", - "postcss": "^8.3.11" - } - }, - "node_modules/sanitize-html/node_modules/escape-string-regexp": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sax": { - "version": "1.2.1", - "license": "ISC" - }, - "node_modules/section-matter": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/secure-json-parse": { - "version": "2.7.0", - "license": "BSD-3-Clause" - }, - "node_modules/selderee": { - "version": "0.6.0", - "license": "MIT", - "dependencies": { - "parseley": "^0.7.0" - }, - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, - "node_modules/semver": { - "version": "6.3.0", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/semver-diff": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/send": { - "version": "0.17.1", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/send/node_modules/depd": { - "version": "1.1.2", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/destroy": { - "version": "1.0.4", - "license": "MIT" - }, - "node_modules/send/node_modules/http-errors": { - "version": "1.7.3", - "license": "MIT", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime": { - "version": "1.6.0", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.1", - "license": "MIT" - }, - "node_modules/send/node_modules/on-finished": { - "version": "2.3.0", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/toidentifier": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/sequelize": { - "version": "6.21.2", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/sequelize" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.1.7", - "@types/validator": "^13.7.1", - "debug": "^4.3.3", - "dottie": "^2.0.2", - "inflection": "^1.13.2", - "lodash": "^4.17.21", - "moment": "^2.29.1", - "moment-timezone": "^0.5.34", - "pg-connection-string": "^2.5.0", - "retry-as-promised": "^5.0.0", - "semver": "^7.3.5", - "sequelize-pool": "^7.1.0", - "toposort-class": "^1.0.1", - "uuid": "^8.3.2", - "validator": "^13.7.0", - "wkx": "^0.5.0" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependenciesMeta": { - "ibm_db": { - "optional": true - }, - "mariadb": { - "optional": true - }, - "mysql2": { - "optional": true - }, - "pg": { - "optional": true - }, - "pg-hstore": { - "optional": true - }, - "snowflake-sdk": { - "optional": true - }, - "sqlite3": { - "optional": true - }, - "tedious": { - "optional": true - } - } - }, - "node_modules/sequelize-cli-typescript": { - "version": "3.2.0c", - "license": "MIT", - "dependencies": { - "bluebird": "^3.5.1", - "cli-color": "^1.2.0", - "fs-extra": "^4.0.2", - "js-beautify": "^1.7.4", - "lodash": "^4.17.4", - "resolve": "^1.5.0", - "umzug": "^2.1.0", - "yargs": "^8.0.2" - }, - "bin": { - "sequelize": "lib/sequelize" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/sequelize-cli-typescript/node_modules/bluebird": { - "version": "3.7.2", - "license": "MIT" - }, - "node_modules/sequelize-cli-typescript/node_modules/camelcase": { - "version": "4.1.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/sequelize-cli-typescript/node_modules/cliui": { - "version": "3.2.0", - "license": "ISC", - "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "node_modules/sequelize-cli-typescript/node_modules/fs-extra": { - "version": "4.0.3", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "node_modules/sequelize-cli-typescript/node_modules/get-caller-file": { - "version": "1.0.3", - "license": "ISC" - }, - "node_modules/sequelize-cli-typescript/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/sequelize-cli-typescript/node_modules/require-main-filename": { - "version": "1.0.1", - "license": "ISC" - }, - "node_modules/sequelize-cli-typescript/node_modules/strip-ansi": { - "version": "3.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sequelize-cli-typescript/node_modules/wrap-ansi": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sequelize-cli-typescript/node_modules/y18n": { - "version": "3.2.2", - "license": "ISC" - }, - "node_modules/sequelize-cli-typescript/node_modules/yargs": { - "version": "8.0.2", - "license": "MIT", - "dependencies": { - "camelcase": "^4.1.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^2.0.0", - "read-pkg-up": "^2.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^7.0.0" - } - }, - "node_modules/sequelize-cli-typescript/node_modules/yargs-parser": { - "version": "7.0.0", - "license": "ISC", - "dependencies": { - "camelcase": "^4.1.0" - } - }, - "node_modules/sequelize-cli-typescript/node_modules/yargs/node_modules/ansi-regex": { - "version": "3.0.1", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/sequelize-cli-typescript/node_modules/yargs/node_modules/string-width": { - "version": "2.1.1", - "license": "MIT", - "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/sequelize-cli-typescript/node_modules/yargs/node_modules/strip-ansi": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/sequelize-pool": { - "version": "7.1.0", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/sequelize/node_modules/lru-cache": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sequelize/node_modules/semver": { - "version": "7.5.2", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sequelize/node_modules/uuid": { - "version": "8.3.2", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/sequelize/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/serve-static": { - "version": "1.14.1", - "license": "MIT", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "license": "ISC" - }, - "node_modules/setprototypeof": { - "version": "1.1.1", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/shimmer": { - "version": "1.2.1", - "license": "BSD-2-Clause" - }, - "node_modules/should": { - "version": "13.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "should-equal": "^2.0.0", - "should-format": "^3.0.3", - "should-type": "^1.4.0", - "should-type-adaptors": "^1.0.1", - "should-util": "^1.0.0" - } - }, - "node_modules/should-equal": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "should-type": "^1.4.0" - } - }, - "node_modules/should-format": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "should-type": "^1.3.0", - "should-type-adaptors": "^1.0.1" - } - }, - "node_modules/should-type": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/should-type-adaptors": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "should-type": "^1.3.0", - "should-util": "^1.0.0" - } - }, - "node_modules/should-util": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/side-channel": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sigmund": { - "version": "1.0.1", - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "license": "ISC" - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/slack-block-builder": { - "version": "2.7.2", - "license": "MIT" - }, - "node_modules/slash": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/snakeize": { - "version": "0.1.0", - "license": "MIT" - }, - "node_modules/socket.io": { - "version": "4.6.2", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "debug": "~4.3.2", - "engine.io": "~6.4.2", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-adapter": { - "version": "2.5.2", - "license": "MIT", - "dependencies": { - "ws": "~8.11.0" - } - }, - "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.11.0", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socks": { - "version": "2.7.1", - "license": "MIT", - "dependencies": { - "ip": "^2.0.0", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.13.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "5.0.1", - "license": "MIT", - "dependencies": { - "agent-base": "^6.0.2", - "debug": "4", - "socks": "^2.3.3" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/socks/node_modules/ip": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/source-map": { - "version": "0.6.1", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.13", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.3.0", - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.13", - "license": "CC0-1.0" - }, - "node_modules/split2": { - "version": "4.2.0", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.2", - "license": "BSD-3-Clause" - }, - "node_modules/ssri": { - "version": "9.0.1", - "license": "ISC", - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/ssri/node_modules/minipass": { - "version": "3.3.6", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ssri/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/starkbank-ecdsa": { - "version": "1.1.5", - "license": "MIT License", - "dependencies": { - "big-integer": "^1.6.48", - "js-sha256": "^0.9.0" - } - }, - "node_modules/statuses": { - "version": "1.5.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/stream-events": { - "version": "1.0.5", - "license": "MIT", - "dependencies": { - "stubs": "^3.0.0" - } - }, - "node_modules/stream-shift": { - "version": "1.0.1", - "license": "MIT" - }, - "node_modules/streamsearch": { - "version": "1.1.0", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/string-length": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-width": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "3.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom-string": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-eof": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stripe": { - "version": "10.17.0", - "license": "MIT", - "dependencies": { - "@types/node": ">=8.1.0", - "qs": "^6.11.0" - }, - "engines": { - "node": "^8.1 || >=10.*" - } - }, - "node_modules/strnum": { - "version": "1.0.5", - "license": "MIT" - }, - "node_modules/strtok3": { - "version": "7.0.0", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^5.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/stubs": { - "version": "3.0.0", - "license": "MIT" - }, - "node_modules/superagent": { - "version": "8.0.9", - "license": "MIT", - "dependencies": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.4", - "debug": "^4.3.4", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^2.1.2", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.0", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=6.4.0 <13 || >=14" - } - }, - "node_modules/superagent/node_modules/form-data": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/superagent/node_modules/formidable": { - "version": "2.1.2", - "license": "MIT", - "dependencies": { - "dezalgo": "^1.0.4", - "hexoid": "^1.0.0", - "once": "^1.4.0", - "qs": "^6.11.0" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, - "node_modules/superagent/node_modules/lru-cache": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/superagent/node_modules/semver": { - "version": "7.5.2", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/superagent/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/supertest": { - "version": "6.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "methods": "^1.1.2", - "superagent": "^8.0.5" - }, - "engines": { - "node": ">=6.4.0" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/swagger-ui-dist": { - "version": "4.1.3", - "license": "Apache-2.0" - }, - "node_modules/swagger2openapi": { - "version": "7.0.8", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "call-me-maybe": "^1.0.1", - "node-fetch": "^2.6.1", - "node-fetch-h2": "^2.3.0", - "node-readfiles": "^0.2.0", - "oas-kit-common": "^1.0.8", - "oas-resolver": "^2.5.6", - "oas-schema-walker": "^1.1.5", - "oas-validator": "^5.0.8", - "reftools": "^1.1.9", - "yaml": "^1.10.0", - "yargs": "^17.0.1" - }, - "bin": { - "boast": "boast.js", - "oas-validate": "oas-validate.js", - "swagger2openapi": "swagger2openapi.js" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/swagger2openapi/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/swagger2openapi/node_modules/cliui": { - "version": "8.0.1", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/swagger2openapi/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/swagger2openapi/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/swagger2openapi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/swagger2openapi/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/swagger2openapi/node_modules/wrap-ansi": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/swagger2openapi/node_modules/y18n": { - "version": "5.0.8", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/swagger2openapi/node_modules/yargs": { - "version": "17.7.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/systeminformation": { - "version": "5.18.3", - "license": "MIT", - "optional": true, - "os": [ - "darwin", - "linux", - "win32", - "freebsd", - "openbsd", - "netbsd", - "sunos", - "android" - ], - "bin": { - "systeminformation": "lib/cli.js" - }, - "engines": { - "node": ">=8.0.0" - }, - "funding": { - "type": "Buy me a coffee", - "url": "https://www.buymeacoffee.com/systeminfo" - } - }, - "node_modules/table-layout": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "array-back": "^4.0.1", - "deep-extend": "~0.6.0", - "typical": "^5.2.0", - "wordwrapjs": "^4.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/table-layout/node_modules/array-back": { - "version": "4.0.2", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/table-layout/node_modules/typical": { - "version": "5.2.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/tar": { - "version": "4.4.19", - "license": "ISC", - "dependencies": { - "chownr": "^1.1.4", - "fs-minipass": "^1.2.7", - "minipass": "^2.9.0", - "minizlib": "^1.3.3", - "mkdirp": "^0.5.5", - "safe-buffer": "^5.2.1", - "yallist": "^3.1.1" - }, - "engines": { - "node": ">=4.5" - } - }, - "node_modules/tar/node_modules/mkdirp": { - "version": "0.5.6", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/tar/node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/teeny-request": { - "version": "7.2.0", - "license": "Apache-2.0", - "dependencies": { - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.1", - "stream-events": "^1.0.5", - "uuid": "^8.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/teeny-request/node_modules/@tootallnate/once": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/teeny-request/node_modules/http-proxy-agent": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/teeny-request/node_modules/uuid": { - "version": "8.3.2", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/term-size": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/thenify": { - "version": "3.3.1", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/through2": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/through2/node_modules/readable-stream": { - "version": "2.3.8", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/through2/node_modules/string_decoder": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/through2/node_modules/xtend": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/timers-ext": { - "version": "0.1.7", - "license": "ISC", - "dependencies": { - "es5-ext": "~0.10.46", - "next-tick": "1" - } - }, - "node_modules/tmp": { - "version": "0.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "rimraf": "^3.0.0" - }, - "engines": { - "node": ">=8.17.0" - } - }, - "node_modules/tmp-promise": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "tmp": "^0.2.0" - } - }, - "node_modules/tmpl": { - "version": "1.0.5", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/to-readable-stream": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/token-types": { - "version": "5.0.1", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/token-types/node_modules/ieee754": { - "version": "1.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/toposort-class": { - "version": "1.0.1", - "license": "MIT" - }, - "node_modules/touch": { - "version": "3.1.0", - "dev": true, - "license": "ISC", - "dependencies": { - "nopt": "~1.0.10" - }, - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, - "node_modules/touch/node_modules/nopt": { - "version": "1.0.10", - "dev": true, - "license": "MIT", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "license": "MIT" - }, - "node_modules/ts-jest": { - "version": "29.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "bs-logger": "0.x", - "fast-json-stable-stringify": "2.x", - "jest-util": "^29.0.0", - "json5": "^2.2.3", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "7.x", - "yargs-parser": "^21.0.1" - }, - "bin": { - "ts-jest": "cli.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/types": "^29.0.0", - "babel-jest": "^29.0.0", - "jest": "^29.0.0", - "typescript": ">=4.3 <6" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@jest/types": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - } - } - }, - "node_modules/ts-jest/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-jest/node_modules/semver": { - "version": "7.5.2", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-jest/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/ts-mixer": { - "version": "6.0.3", - "license": "MIT" - }, - "node_modules/ts-node": { - "version": "10.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "0.7.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.0", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tsconfig-paths/node_modules/strip-bom": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/tslib": { - "version": "2.5.3", - "license": "0BSD" - }, - "node_modules/tsutils": { - "version": "3.21.0", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "dev": true, - "license": "0BSD" - }, - "node_modules/tunnel": { - "version": "0.0.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.11 <=0.7.0 || >=0.7.3" - } - }, - "node_modules/tv4": { - "version": "1.3.0", - "license": [ - { - "type": "Public Domain", - "url": "http://geraintluff.github.io/tv4/LICENSE.txt" - }, - { - "type": "MIT", - "url": "http://jsonary.com/LICENSE.txt" - } - ], - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/tx2": { - "version": "1.0.5", - "license": "MIT", - "optional": true, - "dependencies": { - "json-stringify-safe": "^5.0.1" - } - }, - "node_modules/type": { - "version": "1.2.0", - "license": "ISC" - }, - "node_modules/type-check": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "license": "MIT" - }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "license": "MIT", - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, - "node_modules/typescript": { - "version": "4.9.5", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/typical": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/uid2": { - "version": "0.0.4", - "license": "MIT" - }, - "node_modules/umzug": { - "version": "2.3.0", - "license": "MIT", - "dependencies": { - "bluebird": "^3.7.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/umzug/node_modules/bluebird": { - "version": "3.7.2", - "license": "MIT" - }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undefsafe": { - "version": "2.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/undici": { - "version": "5.22.1", - "license": "MIT", - "dependencies": { - "busboy": "^1.6.0" - }, - "engines": { - "node": ">=14.0" - } - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unique-filename": { - "version": "2.0.1", - "license": "ISC", - "dependencies": { - "unique-slug": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/unique-slug": { - "version": "3.0.0", - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/unique-string": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "crypto-random-string": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/universal-github-app-jwt": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "@types/jsonwebtoken": "^9.0.0", - "jsonwebtoken": "^9.0.0" - } - }, - "node_modules/universal-github-app-jwt/node_modules/jsonwebtoken": { - "version": "9.0.0", - "license": "MIT", - "dependencies": { - "jws": "^3.2.2", - "lodash": "^4.17.21", - "ms": "^2.1.1", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/universal-github-app-jwt/node_modules/jwa": { - "version": "1.4.1", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/universal-github-app-jwt/node_modules/jws": { - "version": "3.2.2", - "license": "MIT", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/universal-github-app-jwt/node_modules/lru-cache": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/universal-github-app-jwt/node_modules/semver": { - "version": "7.5.2", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/universal-github-app-jwt/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/universal-user-agent": { - "version": "6.0.0", - "license": "ISC" - }, - "node_modules/universalify": { - "version": "0.1.2", - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/unleash-client": { - "version": "3.21.0", - "license": "Apache-2.0", - "dependencies": { - "ip": "^1.1.8", - "make-fetch-happen": "^10.2.1", - "murmurhash3js": "^3.0.1", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=10", - "npm": ">=4.0.0" - } - }, - "node_modules/unleash-client/node_modules/lru-cache": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/unleash-client/node_modules/semver": { - "version": "7.5.2", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/unleash-client/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/untildify": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.11", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/update-notifier": { - "version": "4.1.3", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boxen": "^4.2.0", - "chalk": "^3.0.0", - "configstore": "^5.0.1", - "has-yarn": "^2.1.0", - "import-lazy": "^2.1.0", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.3.1", - "is-npm": "^4.0.0", - "is-yarn-global": "^0.3.0", - "latest-version": "^5.0.0", - "pupa": "^2.0.1", - "semver-diff": "^3.1.1", - "xdg-basedir": "^4.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/yeoman/update-notifier?sponsor=1" - } - }, - "node_modules/update-notifier/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/update-notifier/node_modules/chalk": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/update-notifier/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/update-notifier/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/update-notifier/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/update-notifier/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/url": { - "version": "0.10.3", - "license": "MIT", - "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "node_modules/url-parse-lax": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "prepend-http": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/url-search-params-polyfill": { - "version": "7.0.1", - "license": "MIT" - }, - "node_modules/url/node_modules/punycode": { - "version": "1.3.2", - "license": "MIT" - }, - "node_modules/utf-8-validate": { - "version": "5.0.10", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "9.0.0", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/v8-to-istanbul": { - "version": "9.1.0", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/validator": { - "version": "13.9.0", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/verify-github-webhook": { - "version": "1.0.1", - "license": "MIT" - }, - "node_modules/vizion": { - "version": "2.2.1", - "license": "Apache-2.0", - "dependencies": { - "async": "^2.6.3", - "git-node-fs": "^1.0.0", - "ini": "^1.3.5", - "js-git": "^0.7.8" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/vm2": { - "version": "3.9.19", - "license": "MIT", - "dependencies": { - "acorn": "^8.7.0", - "acorn-walk": "^8.2.0" - }, - "bin": { - "vm2": "bin/vm2" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-module": { - "version": "2.0.1", - "license": "ISC" - }, - "node_modules/which-typed-array": { - "version": "1.1.9", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "license": "ISC", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/widest-line": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "string-width": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/widest-line/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/widest-line/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wkx": { - "version": "0.5.0", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/word-wrap": { - "version": "1.2.3", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wordwrapjs": { - "version": "4.0.1", - "license": "MIT", - "dependencies": { - "reduce-flatten": "^2.0.0", - "typical": "^5.2.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/wordwrapjs/node_modules/typical": { - "version": "5.2.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "3.0.3", - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "node_modules/ws": { - "version": "8.13.0", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xdg-basedir": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/xml2js": { - "version": "0.4.19", - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" - } - }, - "node_modules/xmlbuilder": { - "version": "9.0.7", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/xregexp": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/xtend": { - "version": "2.1.2", - "dependencies": { - "object-keys": "~0.4.0" - }, - "engines": { - "node": ">=0.4" - } - }, - "node_modules/xtend/node_modules/object-keys": { - "version": "0.4.0", - "license": "MIT" - }, - "node_modules/y18n": { - "version": "4.0.3", - "license": "ISC" - }, - "node_modules/yallist": { - "version": "3.1.1", - "license": "ISC" - }, - "node_modules/yaml": { - "version": "1.10.2", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/yamljs": { - "version": "0.3.0", - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "glob": "^7.0.5" - }, - "bin": { - "json2yaml": "bin/json2yaml", - "yaml2json": "bin/yaml2json" - } - }, - "node_modules/yamljs/node_modules/argparse": { - "version": "1.0.10", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/yamljs/node_modules/sprintf-js": { - "version": "1.0.3", - "license": "BSD-3-Clause" - }, - "node_modules/yargs": { - "version": "15.4.1", - "license": "MIT", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs/node_modules/find-up": { - "version": "4.1.0", - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/locate-path": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/p-limit": { - "version": "2.3.0", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs/node_modules/p-locate": { - "version": "4.1.0", - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/yargs-parser": { - "version": "18.1.3", - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zlib-sync": { - "version": "0.1.8", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "nan": "^2.17.0" - } - } - }, - "dependencies": { - "@actions/core": { - "version": "1.10.0", - "dev": true, - "requires": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" - }, - "dependencies": { - "uuid": { - "version": "8.3.2", - "dev": true - } - } - }, - "@actions/http-client": { - "version": "2.1.0", - "dev": true, - "requires": { - "tunnel": "^0.0.6" - } - }, - "@ampproject/remapping": { - "version": "2.2.1", - "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@apidevtools/openapi-schemas": { - "version": "2.1.0", - "dev": true - }, - "@apidevtools/swagger-methods": { - "version": "3.0.2", - "dev": true - }, - "@aws-crypto/crc32": { - "version": "3.0.0", - "requires": { - "@aws-crypto/util": "^3.0.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^1.11.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1" - } - } - }, - "@aws-crypto/ie11-detection": { - "version": "3.0.0", - "requires": { - "tslib": "^1.11.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1" - } - } - }, - "@aws-crypto/sha256-browser": { - "version": "3.0.0", - "requires": { - "@aws-crypto/ie11-detection": "^3.0.0", - "@aws-crypto/sha256-js": "^3.0.0", - "@aws-crypto/supports-web-crypto": "^3.0.0", - "@aws-crypto/util": "^3.0.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@aws-sdk/util-utf8-browser": "^3.0.0", - "tslib": "^1.11.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1" - } - } - }, - "@aws-crypto/sha256-js": { - "version": "3.0.0", - "requires": { - "@aws-crypto/util": "^3.0.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^1.11.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1" - } - } - }, - "@aws-crypto/supports-web-crypto": { - "version": "3.0.0", - "requires": { - "tslib": "^1.11.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1" - } - } - }, - "@aws-crypto/util": { - "version": "3.0.0", - "requires": { - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-utf8-browser": "^3.0.0", - "tslib": "^1.11.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1" - } - } - }, - "@aws-sdk/abort-controller": { - "version": "3.357.0", - "requires": { - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/client-comprehend": { - "version": "3.357.0", - "requires": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.357.0", - "@aws-sdk/config-resolver": "3.357.0", - "@aws-sdk/credential-provider-node": "3.357.0", - "@aws-sdk/fetch-http-handler": "3.357.0", - "@aws-sdk/hash-node": "3.357.0", - "@aws-sdk/invalid-dependency": "3.357.0", - "@aws-sdk/middleware-content-length": "3.357.0", - "@aws-sdk/middleware-endpoint": "3.357.0", - "@aws-sdk/middleware-host-header": "3.357.0", - "@aws-sdk/middleware-logger": "3.357.0", - "@aws-sdk/middleware-recursion-detection": "3.357.0", - "@aws-sdk/middleware-retry": "3.357.0", - "@aws-sdk/middleware-serde": "3.357.0", - "@aws-sdk/middleware-signing": "3.357.0", - "@aws-sdk/middleware-stack": "3.357.0", - "@aws-sdk/middleware-user-agent": "3.357.0", - "@aws-sdk/node-config-provider": "3.357.0", - "@aws-sdk/node-http-handler": "3.357.0", - "@aws-sdk/smithy-client": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/url-parser": "3.357.0", - "@aws-sdk/util-base64": "3.310.0", - "@aws-sdk/util-body-length-browser": "3.310.0", - "@aws-sdk/util-body-length-node": "3.310.0", - "@aws-sdk/util-defaults-mode-browser": "3.357.0", - "@aws-sdk/util-defaults-mode-node": "3.357.0", - "@aws-sdk/util-endpoints": "3.357.0", - "@aws-sdk/util-retry": "3.357.0", - "@aws-sdk/util-user-agent-browser": "3.357.0", - "@aws-sdk/util-user-agent-node": "3.357.0", - "@aws-sdk/util-utf8": "3.310.0", - "@smithy/protocol-http": "^1.0.1", - "@smithy/types": "^1.0.0", - "tslib": "^2.5.0", - "uuid": "^8.3.2" - }, - "dependencies": { - "uuid": { - "version": "8.3.2" - } - } - }, - "@aws-sdk/client-sso": { - "version": "3.357.0", - "requires": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/config-resolver": "3.357.0", - "@aws-sdk/fetch-http-handler": "3.357.0", - "@aws-sdk/hash-node": "3.357.0", - "@aws-sdk/invalid-dependency": "3.357.0", - "@aws-sdk/middleware-content-length": "3.357.0", - "@aws-sdk/middleware-endpoint": "3.357.0", - "@aws-sdk/middleware-host-header": "3.357.0", - "@aws-sdk/middleware-logger": "3.357.0", - "@aws-sdk/middleware-recursion-detection": "3.357.0", - "@aws-sdk/middleware-retry": "3.357.0", - "@aws-sdk/middleware-serde": "3.357.0", - "@aws-sdk/middleware-stack": "3.357.0", - "@aws-sdk/middleware-user-agent": "3.357.0", - "@aws-sdk/node-config-provider": "3.357.0", - "@aws-sdk/node-http-handler": "3.357.0", - "@aws-sdk/smithy-client": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/url-parser": "3.357.0", - "@aws-sdk/util-base64": "3.310.0", - "@aws-sdk/util-body-length-browser": "3.310.0", - "@aws-sdk/util-body-length-node": "3.310.0", - "@aws-sdk/util-defaults-mode-browser": "3.357.0", - "@aws-sdk/util-defaults-mode-node": "3.357.0", - "@aws-sdk/util-endpoints": "3.357.0", - "@aws-sdk/util-retry": "3.357.0", - "@aws-sdk/util-user-agent-browser": "3.357.0", - "@aws-sdk/util-user-agent-node": "3.357.0", - "@aws-sdk/util-utf8": "3.310.0", - "@smithy/protocol-http": "^1.0.1", - "@smithy/types": "^1.0.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/client-sso-oidc": { - "version": "3.357.0", - "requires": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/config-resolver": "3.357.0", - "@aws-sdk/fetch-http-handler": "3.357.0", - "@aws-sdk/hash-node": "3.357.0", - "@aws-sdk/invalid-dependency": "3.357.0", - "@aws-sdk/middleware-content-length": "3.357.0", - "@aws-sdk/middleware-endpoint": "3.357.0", - "@aws-sdk/middleware-host-header": "3.357.0", - "@aws-sdk/middleware-logger": "3.357.0", - "@aws-sdk/middleware-recursion-detection": "3.357.0", - "@aws-sdk/middleware-retry": "3.357.0", - "@aws-sdk/middleware-serde": "3.357.0", - "@aws-sdk/middleware-stack": "3.357.0", - "@aws-sdk/middleware-user-agent": "3.357.0", - "@aws-sdk/node-config-provider": "3.357.0", - "@aws-sdk/node-http-handler": "3.357.0", - "@aws-sdk/smithy-client": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/url-parser": "3.357.0", - "@aws-sdk/util-base64": "3.310.0", - "@aws-sdk/util-body-length-browser": "3.310.0", - "@aws-sdk/util-body-length-node": "3.310.0", - "@aws-sdk/util-defaults-mode-browser": "3.357.0", - "@aws-sdk/util-defaults-mode-node": "3.357.0", - "@aws-sdk/util-endpoints": "3.357.0", - "@aws-sdk/util-retry": "3.357.0", - "@aws-sdk/util-user-agent-browser": "3.357.0", - "@aws-sdk/util-user-agent-node": "3.357.0", - "@aws-sdk/util-utf8": "3.310.0", - "@smithy/protocol-http": "^1.0.1", - "@smithy/types": "^1.0.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/client-sts": { - "version": "3.357.0", - "requires": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/config-resolver": "3.357.0", - "@aws-sdk/credential-provider-node": "3.357.0", - "@aws-sdk/fetch-http-handler": "3.357.0", - "@aws-sdk/hash-node": "3.357.0", - "@aws-sdk/invalid-dependency": "3.357.0", - "@aws-sdk/middleware-content-length": "3.357.0", - "@aws-sdk/middleware-endpoint": "3.357.0", - "@aws-sdk/middleware-host-header": "3.357.0", - "@aws-sdk/middleware-logger": "3.357.0", - "@aws-sdk/middleware-recursion-detection": "3.357.0", - "@aws-sdk/middleware-retry": "3.357.0", - "@aws-sdk/middleware-sdk-sts": "3.357.0", - "@aws-sdk/middleware-serde": "3.357.0", - "@aws-sdk/middleware-signing": "3.357.0", - "@aws-sdk/middleware-stack": "3.357.0", - "@aws-sdk/middleware-user-agent": "3.357.0", - "@aws-sdk/node-config-provider": "3.357.0", - "@aws-sdk/node-http-handler": "3.357.0", - "@aws-sdk/smithy-client": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/url-parser": "3.357.0", - "@aws-sdk/util-base64": "3.310.0", - "@aws-sdk/util-body-length-browser": "3.310.0", - "@aws-sdk/util-body-length-node": "3.310.0", - "@aws-sdk/util-defaults-mode-browser": "3.357.0", - "@aws-sdk/util-defaults-mode-node": "3.357.0", - "@aws-sdk/util-endpoints": "3.357.0", - "@aws-sdk/util-retry": "3.357.0", - "@aws-sdk/util-user-agent-browser": "3.357.0", - "@aws-sdk/util-user-agent-node": "3.357.0", - "@aws-sdk/util-utf8": "3.310.0", - "@smithy/protocol-http": "^1.0.1", - "@smithy/types": "^1.0.0", - "fast-xml-parser": "4.2.4", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/config-resolver": { - "version": "3.357.0", - "requires": { - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-config-provider": "3.310.0", - "@aws-sdk/util-middleware": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/credential-provider-env": { - "version": "3.357.0", - "requires": { - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/credential-provider-imds": { - "version": "3.357.0", - "requires": { - "@aws-sdk/node-config-provider": "3.357.0", - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/url-parser": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/credential-provider-ini": { - "version": "3.357.0", - "requires": { - "@aws-sdk/credential-provider-env": "3.357.0", - "@aws-sdk/credential-provider-imds": "3.357.0", - "@aws-sdk/credential-provider-process": "3.357.0", - "@aws-sdk/credential-provider-sso": "3.357.0", - "@aws-sdk/credential-provider-web-identity": "3.357.0", - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/shared-ini-file-loader": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/credential-provider-node": { - "version": "3.357.0", - "requires": { - "@aws-sdk/credential-provider-env": "3.357.0", - "@aws-sdk/credential-provider-imds": "3.357.0", - "@aws-sdk/credential-provider-ini": "3.357.0", - "@aws-sdk/credential-provider-process": "3.357.0", - "@aws-sdk/credential-provider-sso": "3.357.0", - "@aws-sdk/credential-provider-web-identity": "3.357.0", - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/shared-ini-file-loader": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/credential-provider-process": { - "version": "3.357.0", - "requires": { - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/shared-ini-file-loader": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/credential-provider-sso": { - "version": "3.357.0", - "requires": { - "@aws-sdk/client-sso": "3.357.0", - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/shared-ini-file-loader": "3.357.0", - "@aws-sdk/token-providers": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/credential-provider-web-identity": { - "version": "3.357.0", - "requires": { - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/eventstream-codec": { - "version": "3.357.0", - "requires": { - "@aws-crypto/crc32": "3.0.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-hex-encoding": "3.310.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/fetch-http-handler": { - "version": "3.357.0", - "requires": { - "@aws-sdk/protocol-http": "3.357.0", - "@aws-sdk/querystring-builder": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-base64": "3.310.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/hash-node": { - "version": "3.357.0", - "requires": { - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-buffer-from": "3.310.0", - "@aws-sdk/util-utf8": "3.310.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/invalid-dependency": { - "version": "3.357.0", - "requires": { - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/is-array-buffer": { - "version": "3.310.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-content-length": { - "version": "3.357.0", - "requires": { - "@aws-sdk/protocol-http": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-endpoint": { - "version": "3.357.0", - "requires": { - "@aws-sdk/middleware-serde": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/url-parser": "3.357.0", - "@aws-sdk/util-middleware": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-host-header": { - "version": "3.357.0", - "requires": { - "@aws-sdk/protocol-http": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-logger": { - "version": "3.357.0", - "requires": { - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-recursion-detection": { - "version": "3.357.0", - "requires": { - "@aws-sdk/protocol-http": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-retry": { - "version": "3.357.0", - "requires": { - "@aws-sdk/protocol-http": "3.357.0", - "@aws-sdk/service-error-classification": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-middleware": "3.357.0", - "@aws-sdk/util-retry": "3.357.0", - "tslib": "^2.5.0", - "uuid": "^8.3.2" - }, - "dependencies": { - "uuid": { - "version": "8.3.2" - } - } - }, - "@aws-sdk/middleware-sdk-sts": { - "version": "3.357.0", - "requires": { - "@aws-sdk/middleware-signing": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-serde": { - "version": "3.357.0", - "requires": { - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-signing": { - "version": "3.357.0", - "requires": { - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/protocol-http": "3.357.0", - "@aws-sdk/signature-v4": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-middleware": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-stack": { - "version": "3.357.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-user-agent": { - "version": "3.357.0", - "requires": { - "@aws-sdk/protocol-http": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-endpoints": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/node-config-provider": { - "version": "3.357.0", - "requires": { - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/shared-ini-file-loader": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/node-http-handler": { - "version": "3.357.0", - "requires": { - "@aws-sdk/abort-controller": "3.357.0", - "@aws-sdk/protocol-http": "3.357.0", - "@aws-sdk/querystring-builder": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/property-provider": { - "version": "3.357.0", - "requires": { - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/protocol-http": { - "version": "3.357.0", - "requires": { - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/querystring-builder": { - "version": "3.357.0", - "requires": { - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-uri-escape": "3.310.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/querystring-parser": { - "version": "3.357.0", - "requires": { - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/s3-request-presigner": { - "version": "3.357.0", - "requires": { - "@aws-sdk/middleware-endpoint": "3.357.0", - "@aws-sdk/protocol-http": "3.357.0", - "@aws-sdk/signature-v4-multi-region": "3.357.0", - "@aws-sdk/smithy-client": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-format-url": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/service-error-classification": { - "version": "3.357.0" - }, - "@aws-sdk/shared-ini-file-loader": { - "version": "3.357.0", - "requires": { - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/signature-v4": { - "version": "3.357.0", - "requires": { - "@aws-sdk/eventstream-codec": "3.357.0", - "@aws-sdk/is-array-buffer": "3.310.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-hex-encoding": "3.310.0", - "@aws-sdk/util-middleware": "3.357.0", - "@aws-sdk/util-uri-escape": "3.310.0", - "@aws-sdk/util-utf8": "3.310.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/signature-v4-multi-region": { - "version": "3.357.0", - "requires": { - "@aws-sdk/protocol-http": "3.357.0", - "@aws-sdk/signature-v4": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/smithy-client": { - "version": "3.357.0", - "requires": { - "@aws-sdk/middleware-stack": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-stream": "3.357.0", - "@smithy/types": "^1.0.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/token-providers": { - "version": "3.357.0", - "requires": { - "@aws-sdk/client-sso-oidc": "3.357.0", - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/shared-ini-file-loader": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/types": { - "version": "3.357.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@aws-sdk/url-parser": { - "version": "3.357.0", - "requires": { - "@aws-sdk/querystring-parser": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-base64": { - "version": "3.310.0", - "requires": { - "@aws-sdk/util-buffer-from": "3.310.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-body-length-browser": { - "version": "3.310.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-body-length-node": { - "version": "3.310.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-buffer-from": { - "version": "3.310.0", - "requires": { - "@aws-sdk/is-array-buffer": "3.310.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-config-provider": { - "version": "3.310.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-defaults-mode-browser": { - "version": "3.357.0", - "requires": { - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/types": "3.357.0", - "bowser": "^2.11.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-defaults-mode-node": { - "version": "3.357.0", - "requires": { - "@aws-sdk/config-resolver": "3.357.0", - "@aws-sdk/credential-provider-imds": "3.357.0", - "@aws-sdk/node-config-provider": "3.357.0", - "@aws-sdk/property-provider": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-endpoints": { - "version": "3.357.0", - "requires": { - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-format-url": { - "version": "3.357.0", - "requires": { - "@aws-sdk/querystring-builder": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-hex-encoding": { - "version": "3.310.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-locate-window": { - "version": "3.310.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-middleware": { - "version": "3.357.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-retry": { - "version": "3.357.0", - "requires": { - "@aws-sdk/service-error-classification": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-stream": { - "version": "3.357.0", - "requires": { - "@aws-sdk/fetch-http-handler": "3.357.0", - "@aws-sdk/node-http-handler": "3.357.0", - "@aws-sdk/types": "3.357.0", - "@aws-sdk/util-base64": "3.310.0", - "@aws-sdk/util-buffer-from": "3.310.0", - "@aws-sdk/util-hex-encoding": "3.310.0", - "@aws-sdk/util-utf8": "3.310.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-uri-escape": { - "version": "3.310.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-user-agent-browser": { - "version": "3.357.0", - "requires": { - "@aws-sdk/types": "3.357.0", - "bowser": "^2.11.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-user-agent-node": { - "version": "3.357.0", - "requires": { - "@aws-sdk/node-config-provider": "3.357.0", - "@aws-sdk/types": "3.357.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-utf8": { - "version": "3.310.0", - "requires": { - "@aws-sdk/util-buffer-from": "3.310.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-utf8-browser": { - "version": "3.259.0", - "requires": { - "tslib": "^2.3.1" - } - }, - "@babel/code-frame": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/highlight": "^7.22.5" - } - }, - "@babel/compat-data": { - "version": "7.22.5", - "dev": true - }, - "@babel/core": { - "version": "7.22.5", - "dev": true, - "requires": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helpers": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" - } - }, - "@babel/generator": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/types": "^7.22.5", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/compat-data": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "browserslist": "^4.21.3", - "lru-cache": "^5.1.1", - "semver": "^6.3.0" - } - }, - "@babel/helper-create-class-features-plugin": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "semver": "^6.3.0" - } - }, - "@babel/helper-create-regexp-features-plugin": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "regexpu-core": "^5.3.1", - "semver": "^6.3.0" - } - }, - "@babel/helper-define-polyfill-provider": { - "version": "0.4.0", - "dev": true, - "requires": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - } - }, - "@babel/helper-environment-visitor": { - "version": "7.22.5", - "dev": true - }, - "@babel/helper-function-name": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-module-imports": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-module-transforms": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.22.5", - "dev": true - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-wrap-function": "^7.22.5", - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-replace-supers": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-simple-access": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-string-parser": { - "version": "7.22.5", - "dev": true - }, - "@babel/helper-validator-identifier": { - "version": "7.22.5", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.22.5", - "dev": true - }, - "@babel/helper-wrap-function": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" - } - }, - "@babel/helpers": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" - } - }, - "@babel/highlight": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.22.5", - "dev": true - }, - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.5" - } - }, - "@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "dev": true, - "requires": {} - }, - "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-syntax-import-assertions": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-syntax-import-attributes": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-jsx": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-typescript": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-async-generator-functions": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5", - "@babel/plugin-syntax-async-generators": "^7.8.4" - } - }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5" - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-class-properties": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-class-static-block": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "globals": "^11.1.0" - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.5" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-dynamic-import": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-export-namespace-from": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-json-strings": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-json-strings": "^7.8.3" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-logical-assignment-operators": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - } - }, - "@babel/plugin-transform-member-expression-literals": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5" - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-new-target": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - } - }, - "@babel/plugin-transform-numeric-separator": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - } - }, - "@babel/plugin-transform-object-rest-spread": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/compat-data": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.22.5" - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5" - } - }, - "@babel/plugin-transform-optional-catch-binding": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - } - }, - "@babel/plugin-transform-optional-chaining": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-private-methods": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-private-property-in-object": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - } - }, - "@babel/plugin-transform-property-literals": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "regenerator-transform": "^0.15.1" - } - }, - "@babel/plugin-transform-reserved-words": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-typescript": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-typescript": "^7.22.5" - } - }, - "@babel/plugin-transform-unicode-escapes": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-unicode-property-regex": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-unicode-sets-regex": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/preset-env": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/compat-data": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.22.5", - "@babel/plugin-syntax-import-attributes": "^7.22.5", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.22.5", - "@babel/plugin-transform-async-generator-functions": "^7.22.5", - "@babel/plugin-transform-async-to-generator": "^7.22.5", - "@babel/plugin-transform-block-scoped-functions": "^7.22.5", - "@babel/plugin-transform-block-scoping": "^7.22.5", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-class-static-block": "^7.22.5", - "@babel/plugin-transform-classes": "^7.22.5", - "@babel/plugin-transform-computed-properties": "^7.22.5", - "@babel/plugin-transform-destructuring": "^7.22.5", - "@babel/plugin-transform-dotall-regex": "^7.22.5", - "@babel/plugin-transform-duplicate-keys": "^7.22.5", - "@babel/plugin-transform-dynamic-import": "^7.22.5", - "@babel/plugin-transform-exponentiation-operator": "^7.22.5", - "@babel/plugin-transform-export-namespace-from": "^7.22.5", - "@babel/plugin-transform-for-of": "^7.22.5", - "@babel/plugin-transform-function-name": "^7.22.5", - "@babel/plugin-transform-json-strings": "^7.22.5", - "@babel/plugin-transform-literals": "^7.22.5", - "@babel/plugin-transform-logical-assignment-operators": "^7.22.5", - "@babel/plugin-transform-member-expression-literals": "^7.22.5", - "@babel/plugin-transform-modules-amd": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.22.5", - "@babel/plugin-transform-modules-systemjs": "^7.22.5", - "@babel/plugin-transform-modules-umd": "^7.22.5", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.22.5", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5", - "@babel/plugin-transform-numeric-separator": "^7.22.5", - "@babel/plugin-transform-object-rest-spread": "^7.22.5", - "@babel/plugin-transform-object-super": "^7.22.5", - "@babel/plugin-transform-optional-catch-binding": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.5", - "@babel/plugin-transform-parameters": "^7.22.5", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/plugin-transform-private-property-in-object": "^7.22.5", - "@babel/plugin-transform-property-literals": "^7.22.5", - "@babel/plugin-transform-regenerator": "^7.22.5", - "@babel/plugin-transform-reserved-words": "^7.22.5", - "@babel/plugin-transform-shorthand-properties": "^7.22.5", - "@babel/plugin-transform-spread": "^7.22.5", - "@babel/plugin-transform-sticky-regex": "^7.22.5", - "@babel/plugin-transform-template-literals": "^7.22.5", - "@babel/plugin-transform-typeof-symbol": "^7.22.5", - "@babel/plugin-transform-unicode-escapes": "^7.22.5", - "@babel/plugin-transform-unicode-property-regex": "^7.22.5", - "@babel/plugin-transform-unicode-regex": "^7.22.5", - "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.22.5", - "babel-plugin-polyfill-corejs2": "^0.4.3", - "babel-plugin-polyfill-corejs3": "^0.8.1", - "babel-plugin-polyfill-regenerator": "^0.5.0", - "core-js-compat": "^3.30.2", - "semver": "^6.3.0" - } - }, - "@babel/preset-modules": { - "version": "0.1.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - } - }, - "@babel/preset-typescript": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.22.5", - "@babel/plugin-transform-typescript": "^7.22.5" - } - }, - "@babel/regjsgen": { - "version": "0.8.0", - "dev": true - }, - "@babel/runtime": { - "version": "7.22.5", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.11" - } - }, - "@babel/template": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" - } - }, - "@babel/traverse": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - } - }, - "@bcoe/v8-coverage": { - "version": "0.2.3", - "dev": true - }, - "@crowd/alerting": { - "version": "file:../services/libs/alerting", - "requires": { - "@types/node": "^20.3.1", - "@typescript-eslint/eslint-plugin": "^5.59.11", - "@typescript-eslint/parser": "^5.59.11", - "axios": "^1.4.0", - "eslint": "^8.42.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.1.3" - }, - "dependencies": { - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1" - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.42.0" - }, - "@humanwhocodes/config-array": { - "version": "0.11.10", - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1" - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1" - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5" - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/json-schema": { - "version": "7.0.12" - }, - "@types/node": { - "version": "20.3.1" - }, - "@types/semver": { - "version": "7.5.0" - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.11", - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.11", - "@typescript-eslint/type-utils": "5.59.11", - "@typescript-eslint/utils": "5.59.11", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.11", - "requires": { - "@typescript-eslint/scope-manager": "5.59.11", - "@typescript-eslint/types": "5.59.11", - "@typescript-eslint/typescript-estree": "5.59.11", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.11", - "requires": { - "@typescript-eslint/types": "5.59.11", - "@typescript-eslint/visitor-keys": "5.59.11" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.11", - "requires": { - "@typescript-eslint/typescript-estree": "5.59.11", - "@typescript-eslint/utils": "5.59.11", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.11" - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.11", - "requires": { - "@typescript-eslint/types": "5.59.11", - "@typescript-eslint/visitor-keys": "5.59.11", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.11", - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.11", - "@typescript-eslint/types": "5.59.11", - "@typescript-eslint/typescript-estree": "5.59.11", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "dependencies": { - "eslint-scope": { - "version": "5.1.1", - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "estraverse": { - "version": "4.3.0" - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.11", - "requires": { - "@typescript-eslint/types": "5.59.11", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2" - }, - "acorn-jsx": { - "version": "5.3.2", - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1" - }, - "ansi-styles": { - "version": "4.3.0", - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1" - }, - "array-union": { - "version": "2.1.0" - }, - "asynckit": { - "version": "0.4.0" - }, - "axios": { - "version": "1.4.0", - "requires": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "balanced-match": { - "version": "1.0.2" - }, - "brace-expansion": { - "version": "1.1.11", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "requires": { - "fill-range": "^7.0.1" - } - }, - "callsites": { - "version": "3.1.0" - }, - "chalk": { - "version": "4.1.2", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4" - }, - "combined-stream": { - "version": "1.0.8", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "concat-map": { - "version": "0.0.1" - }, - "cross-spawn": { - "version": "7.0.3", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4" - }, - "delayed-stream": { - "version": "1.0.0" - }, - "dir-glob": { - "version": "3.0.1", - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "requires": { - "esutils": "^2.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0" - }, - "eslint": { - "version": "8.42.0", - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.42.0", - "@humanwhocodes/config-array": "^0.11.10", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "7.2.0", - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1" - }, - "espree": { - "version": "9.5.2", - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0" - }, - "esutils": { - "version": "2.0.3" - }, - "fast-deep-equal": { - "version": "3.1.3" - }, - "fast-diff": { - "version": "1.3.0" - }, - "fast-glob": { - "version": "3.2.12", - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0" - }, - "fast-levenshtein": { - "version": "2.0.6" - }, - "fastq": { - "version": "1.15.0", - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7" - }, - "follow-redirects": { - "version": "1.15.2" - }, - "form-data": { - "version": "4.0.0", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "fs.realpath": { - "version": "1.0.0" - }, - "glob": { - "version": "7.2.3", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4" - }, - "graphemer": { - "version": "1.4.0" - }, - "has-flag": { - "version": "4.0.0" - }, - "ignore": { - "version": "5.2.4" - }, - "import-fresh": { - "version": "3.3.0", - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4" - }, - "inflight": { - "version": "1.0.6", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4" - }, - "is-extglob": { - "version": "2.1.1" - }, - "is-glob": { - "version": "4.0.3", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0" - }, - "is-path-inside": { - "version": "3.0.3" - }, - "isexe": { - "version": "2.0.0" - }, - "js-yaml": { - "version": "4.1.0", - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1" - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1" - }, - "levn": { - "version": "0.4.1", - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2" - }, - "lru-cache": { - "version": "6.0.0", - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1" - }, - "micromatch": { - "version": "4.0.5", - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "mime-db": { - "version": "1.52.0" - }, - "mime-types": { - "version": "2.1.35", - "requires": { - "mime-db": "1.52.0" - } - }, - "minimatch": { - "version": "3.1.2", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2" - }, - "natural-compare": { - "version": "1.4.0" - }, - "natural-compare-lite": { - "version": "1.4.0" - }, - "once": { - "version": "1.4.0", - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0" - }, - "path-is-absolute": { - "version": "1.0.1" - }, - "path-key": { - "version": "3.1.1" - }, - "path-type": { - "version": "4.0.0" - }, - "picomatch": { - "version": "2.3.1" - }, - "prelude-ls": { - "version": "1.2.1" - }, - "prettier": { - "version": "2.8.8" - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "requires": { - "fast-diff": "^1.1.2" - } - }, - "proxy-from-env": { - "version": "1.1.0" - }, - "punycode": { - "version": "2.3.0" - }, - "queue-microtask": { - "version": "1.2.3" - }, - "resolve-from": { - "version": "4.0.0" - }, - "reusify": { - "version": "1.0.4" - }, - "rimraf": { - "version": "3.0.2", - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.5.1", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0" - }, - "slash": { - "version": "3.0.0" - }, - "strip-ansi": { - "version": "6.0.1", - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1" - }, - "supports-color": { - "version": "7.2.0", - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0" - }, - "to-regex-range": { - "version": "5.0.1", - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1" - }, - "tsutils": { - "version": "3.21.0", - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2" - }, - "typescript": { - "version": "5.1.3" - }, - "uri-js": { - "version": "4.4.1", - "requires": { - "punycode": "^2.1.0" - } - }, - "which": { - "version": "2.0.2", - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3" - }, - "wrappy": { - "version": "1.0.2" - }, - "yallist": { - "version": "4.0.0" - }, - "yocto-queue": { - "version": "0.1.0" - } - } - }, - "@crowd/common": { - "version": "file:../services/libs/common", - "requires": { - "@crowd/logging": "file:../logging", - "@crowd/types": "file:../types", - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "psl": "^1.9.0", - "typescript": "^5.0.4", - "uuid": "^9.0.0" - }, - "dependencies": { - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "dev": true - }, - "@types/node": { - "version": "18.16.7", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/type-utils": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.6", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "dependencies": { - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint": { - "version": "8.40.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "3.1.2", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "once": { - "version": "1.4.0", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.5.1", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.0.4", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "uuid": { - "version": "9.0.0" - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@crowd/integrations": { - "version": "file:../services/libs/integrations", - "requires": { - "@crowd/common": "file:../common", - "@crowd/logging": "file:../logging", - "@crowd/types": "file:../types", - "@octokit/auth-app": "^4.0.13", - "@octokit/graphql": "^5.0.6", - "@types/he": "^1.2.0", - "@types/node": "^18.16.3", - "@types/sanitize-html": "^2.9.0", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "axios": "^1.4.0", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "he": "^1.2.0", - "prettier": "^2.8.8", - "sanitize-html": "^2.10.0", - "typescript": "^5.0.4", - "verify-github-webhook": "^1.0.1" - }, - "dependencies": { - "@crowd/common": { - "version": "file:../services/libs/common", - "requires": { - "@crowd/logging": "file:../logging", - "@crowd/types": "file:../types", - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "psl": "^1.9.0", - "typescript": "^5.0.4", - "uuid": "^9.0.0" - }, - "dependencies": { - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "dev": true - }, - "@types/node": { - "version": "18.16.7", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/type-utils": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.6", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "dependencies": { - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint": { - "version": "8.40.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "3.1.2", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "once": { - "version": "1.4.0", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.5.1", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.0.4", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "uuid": { - "version": "9.0.0" - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@crowd/logging": { - "version": "file:../services/libs/logging", - "requires": { - "@crowd/common": "file:../common", - "@crowd/tracing": "file:../tracing", - "@types/bunyan": "^1.8.8", - "@types/bunyan-format": "^0.2.5", - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "bunyan": "^1.8.15", - "bunyan-format": "^0.2.1", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - }, - "dependencies": { - "@crowd/common": { - "version": "file:../services/libs/common", - "requires": { - "@crowd/logging": "file:../logging", - "@crowd/types": "file:../types", - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "psl": "^1.9.0", - "typescript": "^5.0.4", - "uuid": "^9.0.0" - }, - "dependencies": { - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "dev": true - }, - "@types/node": { - "version": "18.16.7", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/type-utils": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.6", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "dependencies": { - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint": { - "version": "8.40.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "3.1.2", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "once": { - "version": "1.4.0", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.5.1", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.0.4", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "uuid": { - "version": "9.0.0" - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/bunyan": { - "version": "1.8.8", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/bunyan-format": { - "version": "0.2.5", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "dev": true - }, - "@types/node": { - "version": "18.16.8", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/type-utils": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.6", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "dependencies": { - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "ansicolors": { - "version": "0.2.1" - }, - "ansistyles": { - "version": "0.1.3" - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "devOptional": true - }, - "brace-expansion": { - "version": "1.1.11", - "devOptional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "bunyan": { - "version": "1.8.15", - "requires": { - "dtrace-provider": "~0.8", - "moment": "^2.19.3", - "mv": "~2", - "safe-json-stringify": "~1" - } - }, - "bunyan-format": { - "version": "0.2.1", - "requires": { - "ansicolors": "~0.2.1", - "ansistyles": "~0.1.1", - "xtend": "~2.1.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "devOptional": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "dtrace-provider": { - "version": "0.8.8", - "optional": true, - "requires": { - "nan": "^2.14.0" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint": { - "version": "8.40.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "devOptional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "devOptional": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "3.1.2", - "devOptional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.8", - "optional": true - }, - "mkdirp": { - "version": "0.5.6", - "optional": true, - "requires": { - "minimist": "^1.2.6" - } - }, - "moment": { - "version": "2.29.4", - "optional": true - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "mv": { - "version": "2.1.1", - "optional": true, - "requires": { - "mkdirp": "~0.5.1", - "ncp": "~2.0.0", - "rimraf": "~2.4.0" - }, - "dependencies": { - "glob": { - "version": "6.0.4", - "optional": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "2.4.5", - "optional": true, - "requires": { - "glob": "^6.0.1" - } - } - } - }, - "nan": { - "version": "2.17.0", - "optional": true - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "ncp": { - "version": "2.0.0", - "optional": true - }, - "object-keys": { - "version": "0.4.0" - }, - "once": { - "version": "1.4.0", - "devOptional": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "devOptional": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "safe-json-stringify": { - "version": "1.2.0", - "optional": true - }, - "semver": { - "version": "7.5.1", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.0.4", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "devOptional": true - }, - "xtend": { - "version": "2.1.2", - "requires": { - "object-keys": "~0.4.0" - } - }, - "yallist": { - "version": "4.0.0", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@crowd/types": { - "version": "file:../services/libs/types", - "requires": { - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - }, - "dependencies": { - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "dev": true - }, - "@types/node": { - "version": "18.16.9", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.5", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/type-utils": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.5", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.5", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint": { - "version": "8.40.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "dependencies": { - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "3.1.2", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "once": { - "version": "1.4.0", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.5.1", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.0.4", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/he": { - "version": "1.2.0", - "dev": true - }, - "@types/json-schema": { - "version": "7.0.11", - "dev": true - }, - "@types/node": { - "version": "18.16.9", - "dev": true - }, - "@types/sanitize-html": { - "version": "2.9.0", - "dev": true, - "requires": { - "htmlparser2": "^8.0.0" - } - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.5", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/type-utils": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.5", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.5", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "asynckit": { - "version": "0.4.0" - }, - "axios": { - "version": "1.4.0", - "requires": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "balanced-match": { - "version": "1.0.2", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "deepmerge": { - "version": "4.3.1" - }, - "delayed-stream": { - "version": "1.0.0" - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "dom-serializer": { - "version": "2.0.0", - "requires": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - } - }, - "domelementtype": { - "version": "2.3.0" - }, - "domhandler": { - "version": "5.0.3", - "requires": { - "domelementtype": "^2.3.0" - } - }, - "domutils": { - "version": "3.1.0", - "requires": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - } - }, - "entities": { - "version": "4.5.0" - }, - "escape-string-regexp": { - "version": "4.0.0" - }, - "eslint": { - "version": "8.40.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "dependencies": { - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "follow-redirects": { - "version": "1.15.2" - }, - "form-data": { - "version": "4.0.0", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "he": { - "version": "1.2.0" - }, - "htmlparser2": { - "version": "8.0.2", - "requires": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "is-plain-object": { - "version": "5.0.0" - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "mime-db": { - "version": "1.52.0" - }, - "mime-types": { - "version": "2.1.35", - "requires": { - "mime-db": "1.52.0" - } - }, - "minimatch": { - "version": "3.1.2", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "nanoid": { - "version": "3.3.6" - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "once": { - "version": "1.4.0", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-srcset": { - "version": "1.0.2" - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picocolors": { - "version": "1.0.0" - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "postcss": { - "version": "8.4.23", - "requires": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "proxy-from-env": { - "version": "1.1.0" - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "sanitize-html": { - "version": "2.10.0", - "requires": { - "deepmerge": "^4.2.2", - "escape-string-regexp": "^4.0.0", - "htmlparser2": "^8.0.0", - "is-plain-object": "^5.0.0", - "parse-srcset": "^1.0.2", - "postcss": "^8.3.11" - } - }, - "semver": { - "version": "7.5.1", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "source-map-js": { - "version": "1.0.2" - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.0.4", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@crowd/logging": { - "version": "file:../services/libs/logging", - "requires": { - "@crowd/common": "file:../common", - "@crowd/tracing": "file:../tracing", - "@types/bunyan": "^1.8.8", - "@types/bunyan-format": "^0.2.5", - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "bunyan": "^1.8.15", - "bunyan-format": "^0.2.1", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - }, - "dependencies": { - "@crowd/common": { - "version": "file:../services/libs/common", - "requires": { - "@crowd/logging": "file:../logging", - "@crowd/types": "file:../types", - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "psl": "^1.9.0", - "typescript": "^5.0.4", - "uuid": "^9.0.0" - }, - "dependencies": { - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "dev": true - }, - "@types/node": { - "version": "18.16.7", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/type-utils": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.6", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "dependencies": { - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint": { - "version": "8.40.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "3.1.2", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "once": { - "version": "1.4.0", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.5.1", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.0.4", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "uuid": { - "version": "9.0.0" - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/bunyan": { - "version": "1.8.8", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/bunyan-format": { - "version": "0.2.5", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "dev": true - }, - "@types/node": { - "version": "18.16.8", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/type-utils": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.6", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "dependencies": { - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "ansicolors": { - "version": "0.2.1" - }, - "ansistyles": { - "version": "0.1.3" - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "devOptional": true - }, - "brace-expansion": { - "version": "1.1.11", - "devOptional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "bunyan": { - "version": "1.8.15", - "requires": { - "dtrace-provider": "~0.8", - "moment": "^2.19.3", - "mv": "~2", - "safe-json-stringify": "~1" - } - }, - "bunyan-format": { - "version": "0.2.1", - "requires": { - "ansicolors": "~0.2.1", - "ansistyles": "~0.1.1", - "xtend": "~2.1.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "devOptional": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "dtrace-provider": { - "version": "0.8.8", - "optional": true, - "requires": { - "nan": "^2.14.0" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint": { - "version": "8.40.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "devOptional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "devOptional": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "3.1.2", - "devOptional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.8", - "optional": true - }, - "mkdirp": { - "version": "0.5.6", - "optional": true, - "requires": { - "minimist": "^1.2.6" - } - }, - "moment": { - "version": "2.29.4", - "optional": true - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "mv": { - "version": "2.1.1", - "optional": true, - "requires": { - "mkdirp": "~0.5.1", - "ncp": "~2.0.0", - "rimraf": "~2.4.0" - }, - "dependencies": { - "glob": { - "version": "6.0.4", - "optional": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "2.4.5", - "optional": true, - "requires": { - "glob": "^6.0.1" - } - } - } - }, - "nan": { - "version": "2.17.0", - "optional": true - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "ncp": { - "version": "2.0.0", - "optional": true - }, - "object-keys": { - "version": "0.4.0" - }, - "once": { - "version": "1.4.0", - "devOptional": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "devOptional": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "safe-json-stringify": { - "version": "1.2.0", - "optional": true - }, - "semver": { - "version": "7.5.1", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.0.4", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "devOptional": true - }, - "xtend": { - "version": "2.1.2", - "requires": { - "object-keys": "~0.4.0" - } - }, - "yallist": { - "version": "4.0.0", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@crowd/opensearch": { - "version": "file:../services/libs/opensearch", - "requires": { - "@crowd/types": "file:../types", - "@opensearch-project/opensearch": "^1.2.0", - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - }, - "dependencies": { - "@crowd/types": { - "version": "file:../services/libs/types", - "requires": { - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - }, - "dependencies": { - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "dev": true - }, - "@types/node": { - "version": "18.16.9", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.5", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/type-utils": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.5", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.5", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint": { - "version": "8.40.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "dependencies": { - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "3.1.2", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "once": { - "version": "1.4.0", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.5.1", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.0.4", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.42.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.10", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@opensearch-project/opensearch": { - "version": "1.2.0", - "requires": { - "aws4": "^1.11.0", - "debug": "^4.3.1", - "hpagent": "^0.1.1", - "ms": "^2.1.3", - "secure-json-parse": "^2.4.0" - }, - "dependencies": { - "ms": { - "version": "2.1.3" - } - } - }, - "@types/json-schema": { - "version": "7.0.12", - "dev": true - }, - "@types/node": { - "version": "18.16.18", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.11", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.11", - "@typescript-eslint/type-utils": "5.59.11", - "@typescript-eslint/utils": "5.59.11", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.11", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.11", - "@typescript-eslint/types": "5.59.11", - "@typescript-eslint/typescript-estree": "5.59.11", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.11", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.11", - "@typescript-eslint/visitor-keys": "5.59.11" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.11", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.11", - "@typescript-eslint/utils": "5.59.11", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.11", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.11", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.11", - "@typescript-eslint/visitor-keys": "5.59.11", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.11", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.11", - "@typescript-eslint/types": "5.59.11", - "@typescript-eslint/typescript-estree": "5.59.11", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.11", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.11", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "aws4": { - "version": "1.12.0" - }, - "balanced-match": { - "version": "1.0.2", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint": { - "version": "8.42.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.42.0", - "@humanwhocodes/config-array": "^0.11.10", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "dependencies": { - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.3.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "graphemer": { - "version": "1.4.0", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "hpagent": { - "version": "0.1.2" - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "3.1.2", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2" - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "once": { - "version": "1.4.0", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "secure-json-parse": { - "version": "2.7.0" - }, - "semver": { - "version": "7.5.1", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.1.3", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@crowd/redis": { - "version": "file:../services/libs/redis", - "requires": { - "@crowd/common": "file:../common", - "@crowd/logging": "file:../logging", - "@crowd/types": "file:../types", - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "redis": "^4.6.6", - "typescript": "^5.0.4" - }, - "dependencies": { - "@crowd/common": { - "version": "file:../services/libs/common", - "requires": { - "@crowd/logging": "file:../logging", - "@crowd/types": "file:../types", - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "psl": "^1.9.0", - "typescript": "^5.0.4", - "uuid": "^9.0.0" - }, - "dependencies": { - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "dev": true - }, - "@types/node": { - "version": "18.16.7", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/type-utils": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.6", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "dependencies": { - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint": { - "version": "8.40.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "3.1.2", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "once": { - "version": "1.4.0", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.5.1", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.0.4", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "uuid": { - "version": "9.0.0" - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@crowd/logging": { - "version": "file:../services/libs/logging", - "requires": { - "@crowd/common": "file:../common", - "@crowd/tracing": "file:../tracing", - "@types/bunyan": "^1.8.8", - "@types/bunyan-format": "^0.2.5", - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "bunyan": "^1.8.15", - "bunyan-format": "^0.2.1", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - }, - "dependencies": { - "@crowd/common": { - "version": "file:../services/libs/common", - "requires": { - "@crowd/logging": "file:../logging", - "@crowd/types": "file:../types", - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "psl": "^1.9.0", - "typescript": "^5.0.4", - "uuid": "^9.0.0" - }, - "dependencies": { - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "dev": true - }, - "@types/node": { - "version": "18.16.7", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/type-utils": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.6", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "dependencies": { - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint": { - "version": "8.40.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "3.1.2", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "once": { - "version": "1.4.0", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.5.1", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.0.4", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "uuid": { - "version": "9.0.0" - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/bunyan": { - "version": "1.8.8", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/bunyan-format": { - "version": "0.2.5", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "dev": true - }, - "@types/node": { - "version": "18.16.8", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/type-utils": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.6", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "dependencies": { - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "ansicolors": { - "version": "0.2.1" - }, - "ansistyles": { - "version": "0.1.3" - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "devOptional": true - }, - "brace-expansion": { - "version": "1.1.11", - "devOptional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "bunyan": { - "version": "1.8.15", - "requires": { - "dtrace-provider": "~0.8", - "moment": "^2.19.3", - "mv": "~2", - "safe-json-stringify": "~1" - } - }, - "bunyan-format": { - "version": "0.2.1", - "requires": { - "ansicolors": "~0.2.1", - "ansistyles": "~0.1.1", - "xtend": "~2.1.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "devOptional": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "dtrace-provider": { - "version": "0.8.8", - "optional": true, - "requires": { - "nan": "^2.14.0" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint": { - "version": "8.40.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "devOptional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "devOptional": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "3.1.2", - "devOptional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.8", - "optional": true - }, - "mkdirp": { - "version": "0.5.6", - "optional": true, - "requires": { - "minimist": "^1.2.6" - } - }, - "moment": { - "version": "2.29.4", - "optional": true - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "mv": { - "version": "2.1.1", - "optional": true, - "requires": { - "mkdirp": "~0.5.1", - "ncp": "~2.0.0", - "rimraf": "~2.4.0" - }, - "dependencies": { - "glob": { - "version": "6.0.4", - "optional": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "2.4.5", - "optional": true, - "requires": { - "glob": "^6.0.1" - } - } - } - }, - "nan": { - "version": "2.17.0", - "optional": true - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "ncp": { - "version": "2.0.0", - "optional": true - }, - "object-keys": { - "version": "0.4.0" - }, - "once": { - "version": "1.4.0", - "devOptional": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "devOptional": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "safe-json-stringify": { - "version": "1.2.0", - "optional": true - }, - "semver": { - "version": "7.5.1", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.0.4", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "devOptional": true - }, - "xtend": { - "version": "2.1.2", - "requires": { - "object-keys": "~0.4.0" - } - }, - "yallist": { - "version": "4.0.0", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@crowd/types": { - "version": "file:../services/libs/types", - "requires": { - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - }, - "dependencies": { - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "dev": true - }, - "@types/node": { - "version": "18.16.9", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.5", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/type-utils": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.5", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.5", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint": { - "version": "8.40.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "dependencies": { - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "3.1.2", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "once": { - "version": "1.4.0", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.5.1", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.0.4", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@redis/bloom": { - "version": "1.2.0", - "requires": {} - }, - "@redis/client": { - "version": "1.5.7", - "requires": { - "cluster-key-slot": "1.1.2", - "generic-pool": "3.9.0", - "yallist": "4.0.0" - } - }, - "@redis/graph": { - "version": "1.1.0", - "requires": {} - }, - "@redis/json": { - "version": "1.0.4", - "requires": {} - }, - "@redis/search": { - "version": "1.1.2", - "requires": {} - }, - "@redis/time-series": { - "version": "1.0.4", - "requires": {} - }, - "@types/json-schema": { - "version": "7.0.11", - "dev": true - }, - "@types/node": { - "version": "18.16.9", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/type-utils": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.6", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "dependencies": { - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "cluster-key-slot": { - "version": "1.1.2" - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint": { - "version": "8.40.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "generic-pool": { - "version": "3.9.0" - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "3.1.2", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "once": { - "version": "1.4.0", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "redis": { - "version": "4.6.6", - "requires": { - "@redis/bloom": "1.2.0", - "@redis/client": "1.5.7", - "@redis/graph": "1.1.0", - "@redis/json": "1.0.4", - "@redis/search": "1.1.2", - "@redis/time-series": "1.0.4" - } - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.5.1", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.0.4", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "dev": true - }, - "yallist": { - "version": "4.0.0" - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@crowd/sqs": { - "version": "file:../services/libs/sqs", - "requires": { - "@aws-sdk/client-sqs": "^3.332.0", - "@aws-sdk/types": "^3.329.0", - "@crowd/common": "file:../common", - "@crowd/logging": "file:../logging", - "@crowd/tracing": "file:../tracing", - "@crowd/types": "file:../types", - "@smithy/util-retry": "^2.0.1", - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - }, - "dependencies": { - "@aws-crypto/ie11-detection": { - "version": "3.0.0", - "requires": { - "tslib": "^1.11.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1" - } - } - }, - "@aws-crypto/sha256-browser": { - "version": "3.0.0", - "requires": { - "@aws-crypto/ie11-detection": "^3.0.0", - "@aws-crypto/sha256-js": "^3.0.0", - "@aws-crypto/supports-web-crypto": "^3.0.0", - "@aws-crypto/util": "^3.0.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@aws-sdk/util-utf8-browser": "^3.0.0", - "tslib": "^1.11.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1" - } - } - }, - "@aws-crypto/sha256-js": { - "version": "3.0.0", - "requires": { - "@aws-crypto/util": "^3.0.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^1.11.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1" - } - } - }, - "@aws-crypto/supports-web-crypto": { - "version": "3.0.0", - "requires": { - "tslib": "^1.11.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1" - } - } - }, - "@aws-crypto/util": { - "version": "3.0.0", - "requires": { - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-utf8-browser": "^3.0.0", - "tslib": "^1.11.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1" - } - } - }, - "@aws-sdk/abort-controller": { - "version": "3.329.0", - "requires": { - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/client-sqs": { - "version": "3.332.0", - "requires": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.332.0", - "@aws-sdk/config-resolver": "3.329.0", - "@aws-sdk/credential-provider-node": "3.332.0", - "@aws-sdk/fetch-http-handler": "3.329.0", - "@aws-sdk/hash-node": "3.329.0", - "@aws-sdk/invalid-dependency": "3.329.0", - "@aws-sdk/md5-js": "3.329.0", - "@aws-sdk/middleware-content-length": "3.329.0", - "@aws-sdk/middleware-endpoint": "3.329.0", - "@aws-sdk/middleware-host-header": "3.329.0", - "@aws-sdk/middleware-logger": "3.329.0", - "@aws-sdk/middleware-recursion-detection": "3.329.0", - "@aws-sdk/middleware-retry": "3.329.0", - "@aws-sdk/middleware-sdk-sqs": "3.329.0", - "@aws-sdk/middleware-serde": "3.329.0", - "@aws-sdk/middleware-signing": "3.329.0", - "@aws-sdk/middleware-stack": "3.329.0", - "@aws-sdk/middleware-user-agent": "3.332.0", - "@aws-sdk/node-config-provider": "3.329.0", - "@aws-sdk/node-http-handler": "3.329.0", - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/smithy-client": "3.329.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/url-parser": "3.329.0", - "@aws-sdk/util-base64": "3.310.0", - "@aws-sdk/util-body-length-browser": "3.310.0", - "@aws-sdk/util-body-length-node": "3.310.0", - "@aws-sdk/util-defaults-mode-browser": "3.329.0", - "@aws-sdk/util-defaults-mode-node": "3.329.0", - "@aws-sdk/util-endpoints": "3.332.0", - "@aws-sdk/util-retry": "3.329.0", - "@aws-sdk/util-user-agent-browser": "3.329.0", - "@aws-sdk/util-user-agent-node": "3.329.0", - "@aws-sdk/util-utf8": "3.310.0", - "fast-xml-parser": "4.1.2", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/client-sso": { - "version": "3.332.0", - "requires": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/config-resolver": "3.329.0", - "@aws-sdk/fetch-http-handler": "3.329.0", - "@aws-sdk/hash-node": "3.329.0", - "@aws-sdk/invalid-dependency": "3.329.0", - "@aws-sdk/middleware-content-length": "3.329.0", - "@aws-sdk/middleware-endpoint": "3.329.0", - "@aws-sdk/middleware-host-header": "3.329.0", - "@aws-sdk/middleware-logger": "3.329.0", - "@aws-sdk/middleware-recursion-detection": "3.329.0", - "@aws-sdk/middleware-retry": "3.329.0", - "@aws-sdk/middleware-serde": "3.329.0", - "@aws-sdk/middleware-stack": "3.329.0", - "@aws-sdk/middleware-user-agent": "3.332.0", - "@aws-sdk/node-config-provider": "3.329.0", - "@aws-sdk/node-http-handler": "3.329.0", - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/smithy-client": "3.329.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/url-parser": "3.329.0", - "@aws-sdk/util-base64": "3.310.0", - "@aws-sdk/util-body-length-browser": "3.310.0", - "@aws-sdk/util-body-length-node": "3.310.0", - "@aws-sdk/util-defaults-mode-browser": "3.329.0", - "@aws-sdk/util-defaults-mode-node": "3.329.0", - "@aws-sdk/util-endpoints": "3.332.0", - "@aws-sdk/util-retry": "3.329.0", - "@aws-sdk/util-user-agent-browser": "3.329.0", - "@aws-sdk/util-user-agent-node": "3.329.0", - "@aws-sdk/util-utf8": "3.310.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/client-sso-oidc": { - "version": "3.332.0", - "requires": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/config-resolver": "3.329.0", - "@aws-sdk/fetch-http-handler": "3.329.0", - "@aws-sdk/hash-node": "3.329.0", - "@aws-sdk/invalid-dependency": "3.329.0", - "@aws-sdk/middleware-content-length": "3.329.0", - "@aws-sdk/middleware-endpoint": "3.329.0", - "@aws-sdk/middleware-host-header": "3.329.0", - "@aws-sdk/middleware-logger": "3.329.0", - "@aws-sdk/middleware-recursion-detection": "3.329.0", - "@aws-sdk/middleware-retry": "3.329.0", - "@aws-sdk/middleware-serde": "3.329.0", - "@aws-sdk/middleware-stack": "3.329.0", - "@aws-sdk/middleware-user-agent": "3.332.0", - "@aws-sdk/node-config-provider": "3.329.0", - "@aws-sdk/node-http-handler": "3.329.0", - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/smithy-client": "3.329.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/url-parser": "3.329.0", - "@aws-sdk/util-base64": "3.310.0", - "@aws-sdk/util-body-length-browser": "3.310.0", - "@aws-sdk/util-body-length-node": "3.310.0", - "@aws-sdk/util-defaults-mode-browser": "3.329.0", - "@aws-sdk/util-defaults-mode-node": "3.329.0", - "@aws-sdk/util-endpoints": "3.332.0", - "@aws-sdk/util-retry": "3.329.0", - "@aws-sdk/util-user-agent-browser": "3.329.0", - "@aws-sdk/util-user-agent-node": "3.329.0", - "@aws-sdk/util-utf8": "3.310.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/client-sts": { - "version": "3.332.0", - "requires": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/config-resolver": "3.329.0", - "@aws-sdk/credential-provider-node": "3.332.0", - "@aws-sdk/fetch-http-handler": "3.329.0", - "@aws-sdk/hash-node": "3.329.0", - "@aws-sdk/invalid-dependency": "3.329.0", - "@aws-sdk/middleware-content-length": "3.329.0", - "@aws-sdk/middleware-endpoint": "3.329.0", - "@aws-sdk/middleware-host-header": "3.329.0", - "@aws-sdk/middleware-logger": "3.329.0", - "@aws-sdk/middleware-recursion-detection": "3.329.0", - "@aws-sdk/middleware-retry": "3.329.0", - "@aws-sdk/middleware-sdk-sts": "3.329.0", - "@aws-sdk/middleware-serde": "3.329.0", - "@aws-sdk/middleware-signing": "3.329.0", - "@aws-sdk/middleware-stack": "3.329.0", - "@aws-sdk/middleware-user-agent": "3.332.0", - "@aws-sdk/node-config-provider": "3.329.0", - "@aws-sdk/node-http-handler": "3.329.0", - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/smithy-client": "3.329.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/url-parser": "3.329.0", - "@aws-sdk/util-base64": "3.310.0", - "@aws-sdk/util-body-length-browser": "3.310.0", - "@aws-sdk/util-body-length-node": "3.310.0", - "@aws-sdk/util-defaults-mode-browser": "3.329.0", - "@aws-sdk/util-defaults-mode-node": "3.329.0", - "@aws-sdk/util-endpoints": "3.332.0", - "@aws-sdk/util-retry": "3.329.0", - "@aws-sdk/util-user-agent-browser": "3.329.0", - "@aws-sdk/util-user-agent-node": "3.329.0", - "@aws-sdk/util-utf8": "3.310.0", - "fast-xml-parser": "4.1.2", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/config-resolver": { - "version": "3.329.0", - "requires": { - "@aws-sdk/types": "3.329.0", - "@aws-sdk/util-config-provider": "3.310.0", - "@aws-sdk/util-middleware": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/credential-provider-env": { - "version": "3.329.0", - "requires": { - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/credential-provider-imds": { - "version": "3.329.0", - "requires": { - "@aws-sdk/node-config-provider": "3.329.0", - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/url-parser": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/credential-provider-ini": { - "version": "3.332.0", - "requires": { - "@aws-sdk/credential-provider-env": "3.329.0", - "@aws-sdk/credential-provider-imds": "3.329.0", - "@aws-sdk/credential-provider-process": "3.329.0", - "@aws-sdk/credential-provider-sso": "3.332.0", - "@aws-sdk/credential-provider-web-identity": "3.329.0", - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/shared-ini-file-loader": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/credential-provider-node": { - "version": "3.332.0", - "requires": { - "@aws-sdk/credential-provider-env": "3.329.0", - "@aws-sdk/credential-provider-imds": "3.329.0", - "@aws-sdk/credential-provider-ini": "3.332.0", - "@aws-sdk/credential-provider-process": "3.329.0", - "@aws-sdk/credential-provider-sso": "3.332.0", - "@aws-sdk/credential-provider-web-identity": "3.329.0", - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/shared-ini-file-loader": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/credential-provider-process": { - "version": "3.329.0", - "requires": { - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/shared-ini-file-loader": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/credential-provider-sso": { - "version": "3.332.0", - "requires": { - "@aws-sdk/client-sso": "3.332.0", - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/shared-ini-file-loader": "3.329.0", - "@aws-sdk/token-providers": "3.332.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/credential-provider-web-identity": { - "version": "3.329.0", - "requires": { - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/fetch-http-handler": { - "version": "3.329.0", - "requires": { - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/querystring-builder": "3.329.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/util-base64": "3.310.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/hash-node": { - "version": "3.329.0", - "requires": { - "@aws-sdk/types": "3.329.0", - "@aws-sdk/util-buffer-from": "3.310.0", - "@aws-sdk/util-utf8": "3.310.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/invalid-dependency": { - "version": "3.329.0", - "requires": { - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/is-array-buffer": { - "version": "3.310.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@aws-sdk/md5-js": { - "version": "3.329.0", - "requires": { - "@aws-sdk/types": "3.329.0", - "@aws-sdk/util-utf8": "3.310.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-content-length": { - "version": "3.329.0", - "requires": { - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-endpoint": { - "version": "3.329.0", - "requires": { - "@aws-sdk/middleware-serde": "3.329.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/url-parser": "3.329.0", - "@aws-sdk/util-middleware": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-host-header": { - "version": "3.329.0", - "requires": { - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-logger": { - "version": "3.329.0", - "requires": { - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-recursion-detection": { - "version": "3.329.0", - "requires": { - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-retry": { - "version": "3.329.0", - "requires": { - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/service-error-classification": "3.329.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/util-middleware": "3.329.0", - "@aws-sdk/util-retry": "3.329.0", - "tslib": "^2.5.0", - "uuid": "^8.3.2" - } - }, - "@aws-sdk/middleware-sdk-sqs": { - "version": "3.329.0", - "requires": { - "@aws-sdk/types": "3.329.0", - "@aws-sdk/util-hex-encoding": "3.310.0", - "@aws-sdk/util-utf8": "3.310.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-sdk-sts": { - "version": "3.329.0", - "requires": { - "@aws-sdk/middleware-signing": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-serde": { - "version": "3.329.0", - "requires": { - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-signing": { - "version": "3.329.0", - "requires": { - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/signature-v4": "3.329.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/util-middleware": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-stack": { - "version": "3.329.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@aws-sdk/middleware-user-agent": { - "version": "3.332.0", - "requires": { - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/util-endpoints": "3.332.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/node-config-provider": { - "version": "3.329.0", - "requires": { - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/shared-ini-file-loader": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/node-http-handler": { - "version": "3.329.0", - "requires": { - "@aws-sdk/abort-controller": "3.329.0", - "@aws-sdk/protocol-http": "3.329.0", - "@aws-sdk/querystring-builder": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/property-provider": { - "version": "3.329.0", - "requires": { - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/protocol-http": { - "version": "3.329.0", - "requires": { - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/querystring-builder": { - "version": "3.329.0", - "requires": { - "@aws-sdk/types": "3.329.0", - "@aws-sdk/util-uri-escape": "3.310.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/querystring-parser": { - "version": "3.329.0", - "requires": { - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/service-error-classification": { - "version": "3.329.0" - }, - "@aws-sdk/shared-ini-file-loader": { - "version": "3.329.0", - "requires": { - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/signature-v4": { - "version": "3.329.0", - "requires": { - "@aws-sdk/is-array-buffer": "3.310.0", - "@aws-sdk/types": "3.329.0", - "@aws-sdk/util-hex-encoding": "3.310.0", - "@aws-sdk/util-middleware": "3.329.0", - "@aws-sdk/util-uri-escape": "3.310.0", - "@aws-sdk/util-utf8": "3.310.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/smithy-client": { - "version": "3.329.0", - "requires": { - "@aws-sdk/middleware-stack": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/token-providers": { - "version": "3.332.0", - "requires": { - "@aws-sdk/client-sso-oidc": "3.332.0", - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/shared-ini-file-loader": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/types": { - "version": "3.329.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@aws-sdk/url-parser": { - "version": "3.329.0", - "requires": { - "@aws-sdk/querystring-parser": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-base64": { - "version": "3.310.0", - "requires": { - "@aws-sdk/util-buffer-from": "3.310.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-body-length-browser": { - "version": "3.310.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-body-length-node": { - "version": "3.310.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-buffer-from": { - "version": "3.310.0", - "requires": { - "@aws-sdk/is-array-buffer": "3.310.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-config-provider": { - "version": "3.310.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-defaults-mode-browser": { - "version": "3.329.0", - "requires": { - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/types": "3.329.0", - "bowser": "^2.11.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-defaults-mode-node": { - "version": "3.329.0", - "requires": { - "@aws-sdk/config-resolver": "3.329.0", - "@aws-sdk/credential-provider-imds": "3.329.0", - "@aws-sdk/node-config-provider": "3.329.0", - "@aws-sdk/property-provider": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-endpoints": { - "version": "3.332.0", - "requires": { - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-hex-encoding": { - "version": "3.310.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-locate-window": { - "version": "3.310.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-middleware": { - "version": "3.329.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-retry": { - "version": "3.329.0", - "requires": { - "@aws-sdk/service-error-classification": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-uri-escape": { - "version": "3.310.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-user-agent-browser": { - "version": "3.329.0", - "requires": { - "@aws-sdk/types": "3.329.0", - "bowser": "^2.11.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-user-agent-node": { - "version": "3.329.0", - "requires": { - "@aws-sdk/node-config-provider": "3.329.0", - "@aws-sdk/types": "3.329.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-utf8": { - "version": "3.310.0", - "requires": { - "@aws-sdk/util-buffer-from": "3.310.0", - "tslib": "^2.5.0" - } - }, - "@aws-sdk/util-utf8-browser": { - "version": "3.259.0", - "requires": { - "tslib": "^2.3.1" - } - }, - "@crowd/common": { - "version": "file:../services/libs/common", - "requires": { - "@crowd/logging": "file:../logging", - "@crowd/types": "file:../types", - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "psl": "^1.9.0", - "typescript": "^5.0.4", - "uuid": "^9.0.0" - }, - "dependencies": { - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "dev": true - }, - "@types/node": { - "version": "18.16.7", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/type-utils": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.6", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "dependencies": { - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint": { - "version": "8.40.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "3.1.2", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "once": { - "version": "1.4.0", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.5.1", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.0.4", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "uuid": { - "version": "9.0.0" - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@crowd/logging": { - "version": "file:../services/libs/logging", - "requires": { - "@crowd/common": "file:../common", - "@crowd/tracing": "file:../tracing", - "@types/bunyan": "^1.8.8", - "@types/bunyan-format": "^0.2.5", - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "bunyan": "^1.8.15", - "bunyan-format": "^0.2.1", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - }, - "dependencies": { - "@crowd/common": { - "version": "file:../services/libs/common", - "requires": { - "@crowd/logging": "file:../logging", - "@crowd/types": "file:../types", - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "psl": "^1.9.0", - "typescript": "^5.0.4", - "uuid": "^9.0.0" - }, - "dependencies": { - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "dev": true - }, - "@types/node": { - "version": "18.16.7", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/type-utils": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.6", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "dependencies": { - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint": { - "version": "8.40.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "3.1.2", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "once": { - "version": "1.4.0", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.5.1", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.0.4", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "uuid": { - "version": "9.0.0" - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/bunyan": { - "version": "1.8.8", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/bunyan-format": { - "version": "0.2.5", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "dev": true - }, - "@types/node": { - "version": "18.16.8", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/type-utils": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.6", - "@typescript-eslint/utils": "5.59.6", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.6", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/visitor-keys": "5.59.6", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.6", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.6", - "@typescript-eslint/types": "5.59.6", - "@typescript-eslint/typescript-estree": "5.59.6", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "dependencies": { - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.6", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.6", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "ansicolors": { - "version": "0.2.1" - }, - "ansistyles": { - "version": "0.1.3" - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "devOptional": true - }, - "brace-expansion": { - "version": "1.1.11", - "devOptional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "bunyan": { - "version": "1.8.15", - "requires": { - "dtrace-provider": "~0.8", - "moment": "^2.19.3", - "mv": "~2", - "safe-json-stringify": "~1" - } - }, - "bunyan-format": { - "version": "0.2.1", - "requires": { - "ansicolors": "~0.2.1", - "ansistyles": "~0.1.1", - "xtend": "~2.1.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "devOptional": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "dtrace-provider": { - "version": "0.8.8", - "optional": true, - "requires": { - "nan": "^2.14.0" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint": { - "version": "8.40.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "devOptional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "devOptional": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "3.1.2", - "devOptional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.8", - "optional": true - }, - "mkdirp": { - "version": "0.5.6", - "optional": true, - "requires": { - "minimist": "^1.2.6" - } - }, - "moment": { - "version": "2.29.4", - "optional": true - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "mv": { - "version": "2.1.1", - "optional": true, - "requires": { - "mkdirp": "~0.5.1", - "ncp": "~2.0.0", - "rimraf": "~2.4.0" - }, - "dependencies": { - "glob": { - "version": "6.0.4", - "optional": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "2.4.5", - "optional": true, - "requires": { - "glob": "^6.0.1" - } - } - } - }, - "nan": { - "version": "2.17.0", - "optional": true - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "ncp": { - "version": "2.0.0", - "optional": true - }, - "object-keys": { - "version": "0.4.0" - }, - "once": { - "version": "1.4.0", - "devOptional": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "devOptional": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "safe-json-stringify": { - "version": "1.2.0", - "optional": true - }, - "semver": { - "version": "7.5.1", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.0.4", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "devOptional": true - }, - "xtend": { - "version": "2.1.2", - "requires": { - "object-keys": "~0.4.0" - } - }, - "yallist": { - "version": "4.0.0", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@crowd/types": { - "version": "file:../services/libs/types", - "requires": { - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - }, - "dependencies": { - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "dev": true - }, - "@types/node": { - "version": "18.16.9", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.5", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/type-utils": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.5", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.5", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint": { - "version": "8.40.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "dependencies": { - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "3.1.2", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "once": { - "version": "1.4.0", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.5.1", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.0.4", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "dev": true - }, - "@types/node": { - "version": "18.16.8", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.5", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/type-utils": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.5", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.5", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "dependencies": { - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "dev": true - }, - "bowser": { - "version": "2.11.0" - }, - "brace-expansion": { - "version": "1.1.11", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint": { - "version": "8.40.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fast-xml-parser": { - "version": "4.1.2", - "requires": { - "strnum": "^1.0.5" - } - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "3.1.2", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "once": { - "version": "1.4.0", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.5.0", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "strnum": { - "version": "1.0.5" - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "2.5.0" - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "dev": true - } - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.0.4", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "uuid": { - "version": "8.3.2" - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@crowd/tracing": { - "version": "file:../services/libs/tracing", - "requires": { - "@crowd/common": "file:../common", - "@opentelemetry/api": "~1.6.0", - "@opentelemetry/exporter-trace-otlp-grpc": "~0.43.0", - "@opentelemetry/instrumentation-aws-sdk": "~0.36.0", - "@opentelemetry/instrumentation-bunyan": "~0.32.1", - "@opentelemetry/instrumentation-express": "~0.33.1", - "@opentelemetry/instrumentation-http": "~0.43.0", - "@opentelemetry/instrumentation-redis": "~0.35.1", - "@opentelemetry/resource-detector-aws": "~1.3.1", - "@opentelemetry/resources": "~1.17.0", - "@opentelemetry/sdk-node": "~0.43.0", - "@opentelemetry/semantic-conventions": "~1.17.0", - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "opentelemetry-instrumentation-sequelize": "~0.39.1", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - } - }, - "@crowd/types": { - "version": "file:../services/libs/types", - "requires": { - "@types/node": "^18.16.3", - "@typescript-eslint/eslint-plugin": "^5.59.2", - "@typescript-eslint/parser": "^5.59.2", - "eslint": "^8.39.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.8", - "typescript": "^5.0.4" - }, - "dependencies": { - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.40.0", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.8", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "dev": true - }, - "@types/node": { - "version": "18.16.9", - "dev": true - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.59.5", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/type-utils": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.59.5", - "@typescript-eslint/utils": "5.59.5", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.59.5", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.59.5", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.59.5", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.59.5", - "eslint-visitor-keys": "^3.3.0" - } - }, - "acorn": { - "version": "8.8.2", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-is": { - "version": "0.1.4", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint": { - "version": "8.40.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "dependencies": { - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "dev": true - }, - "fast-diff": { - "version": "1.2.0", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "dev": true - }, - "glob": { - "version": "7.2.3", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "dev": true - }, - "js-sdsl": { - "version": "4.4.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "minimatch": { - "version": "3.1.2", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "once": { - "version": "1.4.0", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "p-limit": { - "version": "3.1.0", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "punycode": { - "version": "2.3.0", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.5.1", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "typescript": { - "version": "5.0.4", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "dev": true - } - } - }, - "@cspotcode/source-map-consumer": { - "version": "0.8.0", - "dev": true - }, - "@cspotcode/source-map-support": { - "version": "0.7.0", - "dev": true, - "requires": { - "@cspotcode/source-map-consumer": "0.8.0" - } - }, - "@cubejs-client/core": { - "version": "0.30.74", - "requires": { - "core-js": "^3.6.5", - "cross-fetch": "^3.0.2", - "dayjs": "^1.10.4", - "ramda": "^0.27.0", - "url-search-params-polyfill": "^7.0.0", - "uuid": "^8.3.2" - }, - "dependencies": { - "uuid": { - "version": "8.3.2" - } - } - }, - "@discordjs/builders": { - "version": "1.6.3", - "requires": { - "@discordjs/formatters": "^0.3.1", - "@discordjs/util": "^0.3.1", - "@sapphire/shapeshift": "^3.8.2", - "discord-api-types": "^0.37.41", - "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.3", - "tslib": "^2.5.0" - } - }, - "@discordjs/collection": { - "version": "1.5.1" - }, - "@discordjs/formatters": { - "version": "0.3.1", - "requires": { - "discord-api-types": "^0.37.41" - } - }, - "@discordjs/rest": { - "version": "1.7.1", - "requires": { - "@discordjs/collection": "^1.5.1", - "@discordjs/util": "^0.3.0", - "@sapphire/async-queue": "^1.5.0", - "@sapphire/snowflake": "^3.4.2", - "discord-api-types": "^0.37.41", - "file-type": "^18.3.0", - "tslib": "^2.5.0", - "undici": "^5.22.0" - } - }, - "@discordjs/util": { - "version": "0.3.1" - }, - "@discordjs/ws": { - "version": "0.8.3", - "requires": { - "@discordjs/collection": "^1.5.1", - "@discordjs/rest": "^1.7.1", - "@discordjs/util": "^0.3.1", - "@sapphire/async-queue": "^1.5.0", - "@types/ws": "^8.5.4", - "@vladfrangu/async_event_emitter": "^2.2.1", - "discord-api-types": "^0.37.41", - "tslib": "^2.5.0", - "ws": "^8.13.0" - } - }, - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "type-fest": { - "version": "0.20.2", - "dev": true - } - } - }, - "@eslint/js": { - "version": "8.43.0", - "dev": true - }, - "@exodus/schemasafe": { - "version": "1.0.1", - "dev": true - }, - "@gar/promisify": { - "version": "1.1.3" - }, - "@google-cloud/common": { - "version": "3.10.0", - "requires": { - "@google-cloud/projectify": "^2.0.0", - "@google-cloud/promisify": "^2.0.0", - "arrify": "^2.0.1", - "duplexify": "^4.1.1", - "ent": "^2.2.0", - "extend": "^3.0.2", - "google-auth-library": "^7.14.0", - "retry-request": "^4.2.2", - "teeny-request": "^7.0.0" - }, - "dependencies": { - "duplexify": { - "version": "4.1.2", - "requires": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" - } - } - } - }, - "@google-cloud/paginator": { - "version": "3.0.7", - "requires": { - "arrify": "^2.0.0", - "extend": "^3.0.2" - } - }, - "@google-cloud/projectify": { - "version": "2.1.1" - }, - "@google-cloud/promisify": { - "version": "2.0.4" - }, - "@google-cloud/storage": { - "version": "5.3.0", - "requires": { - "@google-cloud/common": "^3.3.0", - "@google-cloud/paginator": "^3.0.0", - "@google-cloud/promisify": "^2.0.0", - "arrify": "^2.0.0", - "compressible": "^2.0.12", - "concat-stream": "^2.0.0", - "date-and-time": "^0.14.0", - "duplexify": "^3.5.0", - "extend": "^3.0.2", - "gaxios": "^3.0.0", - "gcs-resumable-upload": "^3.1.0", - "hash-stream-validation": "^0.2.2", - "mime": "^2.2.0", - "mime-types": "^2.0.8", - "onetime": "^5.1.0", - "p-limit": "^3.0.1", - "pumpify": "^2.0.0", - "snakeize": "^0.1.0", - "stream-events": "^1.0.1", - "xdg-basedir": "^4.0.0" - } - }, - "@humanwhocodes/config-array": { - "version": "0.11.10", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true - }, - "@humanwhocodes/momoa": { - "version": "2.0.4", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - }, - "@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "dependencies": { - "argparse": { - "version": "1.0.10", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "find-up": { - "version": "4.1.0", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "js-yaml": { - "version": "3.14.1", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "dev": true - }, - "sprintf-js": { - "version": "1.0.3", - "dev": true - } - } - }, - "@istanbuljs/schema": { - "version": "0.1.3", - "dev": true - }, - "@jest/console": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/types": "^29.5.0", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", - "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/core": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/console": "^29.5.0", - "@jest/reporters": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.5.0", - "jest-config": "^29.5.0", - "jest-haste-map": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-resolve-dependencies": "^29.5.0", - "jest-runner": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", - "jest-watcher": "^29.5.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.5.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/environment": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/fake-timers": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/node": "*", - "jest-mock": "^29.5.0" - } - }, - "@jest/expect": { - "version": "29.5.0", - "dev": true, - "requires": { - "expect": "^29.5.0", - "jest-snapshot": "^29.5.0" - } - }, - "@jest/expect-utils": { - "version": "29.5.0", - "dev": true, - "requires": { - "jest-get-type": "^29.4.3" - } - }, - "@jest/fake-timers": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/types": "^29.5.0", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.5.0", - "jest-mock": "^29.5.0", - "jest-util": "^29.5.0" - } - }, - "@jest/globals": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/environment": "^29.5.0", - "@jest/expect": "^29.5.0", - "@jest/types": "^29.5.0", - "jest-mock": "^29.5.0" - } - }, - "@jest/reporters": { - "version": "29.5.0", - "dev": true, - "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@jridgewell/trace-mapping": "^0.3.15", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", - "jest-worker": "^29.5.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/schemas": { - "version": "29.4.3", - "dev": true, - "requires": { - "@sinclair/typebox": "^0.25.16" - } - }, - "@jest/source-map": { - "version": "29.4.3", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.15", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - } - }, - "@jest/test-result": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/console": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - } - }, - "@jest/test-sequencer": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/test-result": "^29.5.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", - "slash": "^3.0.0" - } - }, - "@jest/transform": { - "version": "29.5.0", - "dev": true, - "requires": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.5.0", - "@jridgewell/trace-mapping": "^0.3.15", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.5.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "convert-source-map": { - "version": "2.0.0", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "write-file-atomic": { - "version": "4.0.2", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - } - } - } - }, - "@jest/types": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/schemas": "^29.4.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jridgewell/gen-mapping": { - "version": "0.3.3", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "dev": true - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.18", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - }, - "dependencies": { - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "dev": true - } - } - }, - "@jsdevtools/ono": { - "version": "7.1.3", - "dev": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@npmcli/fs": { - "version": "2.1.2", - "requires": { - "@gar/promisify": "^1.1.3", - "semver": "^7.3.5" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.2", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0" - } - } - }, - "@npmcli/move-file": { - "version": "2.0.1", - "requires": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - } - }, - "@octokit/auth-app": { - "version": "3.6.1", - "requires": { - "@octokit/auth-oauth-app": "^4.3.0", - "@octokit/auth-oauth-user": "^1.2.3", - "@octokit/request": "^5.6.0", - "@octokit/request-error": "^2.1.0", - "@octokit/types": "^6.0.3", - "@types/lru-cache": "^5.1.0", - "deprecation": "^2.3.1", - "lru-cache": "^6.0.0", - "universal-github-app-jwt": "^1.0.1", - "universal-user-agent": "^6.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "requires": { - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0" - } - } - }, - "@octokit/auth-oauth-app": { - "version": "4.3.4", - "requires": { - "@octokit/auth-oauth-device": "^3.1.1", - "@octokit/auth-oauth-user": "^2.0.0", - "@octokit/request": "^5.6.3", - "@octokit/types": "^6.0.3", - "@types/btoa-lite": "^1.0.0", - "btoa-lite": "^1.0.0", - "universal-user-agent": "^6.0.0" - }, - "dependencies": { - "@octokit/auth-oauth-user": { - "version": "2.1.2", - "requires": { - "@octokit/auth-oauth-device": "^4.0.0", - "@octokit/oauth-methods": "^2.0.0", - "@octokit/request": "^6.0.0", - "@octokit/types": "^9.0.0", - "btoa-lite": "^1.0.0", - "universal-user-agent": "^6.0.0" - }, - "dependencies": { - "@octokit/auth-oauth-device": { - "version": "4.0.5", - "requires": { - "@octokit/oauth-methods": "^2.0.0", - "@octokit/request": "^6.0.0", - "@octokit/types": "^9.0.0", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/request": { - "version": "6.2.8", - "requires": { - "@octokit/endpoint": "^7.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/types": { - "version": "9.3.2", - "requires": { - "@octokit/openapi-types": "^18.0.0" - } - } - } - }, - "@octokit/endpoint": { - "version": "7.0.6", - "requires": { - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - }, - "dependencies": { - "@octokit/types": { - "version": "9.3.2", - "requires": { - "@octokit/openapi-types": "^18.0.0" - } - } - } - }, - "@octokit/openapi-types": { - "version": "18.0.0" - }, - "@octokit/request-error": { - "version": "3.0.3", - "requires": { - "@octokit/types": "^9.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - }, - "dependencies": { - "@octokit/types": { - "version": "9.3.2", - "requires": { - "@octokit/openapi-types": "^18.0.0" - } - } - } - } - } - }, - "@octokit/auth-oauth-device": { - "version": "3.1.4", - "requires": { - "@octokit/oauth-methods": "^2.0.0", - "@octokit/request": "^6.0.0", - "@octokit/types": "^6.10.0", - "universal-user-agent": "^6.0.0" - }, - "dependencies": { - "@octokit/endpoint": { - "version": "7.0.6", - "requires": { - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - }, - "dependencies": { - "@octokit/types": { - "version": "9.3.2", - "requires": { - "@octokit/openapi-types": "^18.0.0" - } - } - } - }, - "@octokit/openapi-types": { - "version": "18.0.0" - }, - "@octokit/request": { - "version": "6.2.8", - "requires": { - "@octokit/endpoint": "^7.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" - }, - "dependencies": { - "@octokit/types": { - "version": "9.3.2", - "requires": { - "@octokit/openapi-types": "^18.0.0" - } - } - } - }, - "@octokit/request-error": { - "version": "3.0.3", - "requires": { - "@octokit/types": "^9.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - }, - "dependencies": { - "@octokit/types": { - "version": "9.3.2", - "requires": { - "@octokit/openapi-types": "^18.0.0" - } - } - } - } - } - }, - "@octokit/auth-oauth-user": { - "version": "1.3.0", - "requires": { - "@octokit/auth-oauth-device": "^3.1.1", - "@octokit/oauth-methods": "^1.1.0", - "@octokit/request": "^5.4.14", - "@octokit/types": "^6.12.2", - "btoa-lite": "^1.0.0", - "universal-user-agent": "^6.0.0" - }, - "dependencies": { - "@octokit/oauth-authorization-url": { - "version": "4.3.3" - }, - "@octokit/oauth-methods": { - "version": "1.2.6", - "requires": { - "@octokit/oauth-authorization-url": "^4.3.1", - "@octokit/request": "^5.4.14", - "@octokit/request-error": "^2.0.5", - "@octokit/types": "^6.12.2", - "btoa-lite": "^1.0.0" - } - } - } - }, - "@octokit/endpoint": { - "version": "6.0.12", - "requires": { - "@octokit/types": "^6.0.3", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/graphql": { - "version": "4.8.0", - "requires": { - "@octokit/request": "^5.6.0", - "@octokit/types": "^6.0.3", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/oauth-authorization-url": { - "version": "5.0.0" - }, - "@octokit/oauth-methods": { - "version": "2.0.6", - "requires": { - "@octokit/oauth-authorization-url": "^5.0.0", - "@octokit/request": "^6.2.3", - "@octokit/request-error": "^3.0.3", - "@octokit/types": "^9.0.0", - "btoa-lite": "^1.0.0" - }, - "dependencies": { - "@octokit/endpoint": { - "version": "7.0.6", - "requires": { - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/openapi-types": { - "version": "18.0.0" - }, - "@octokit/request": { - "version": "6.2.8", - "requires": { - "@octokit/endpoint": "^7.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/request-error": { - "version": "3.0.3", - "requires": { - "@octokit/types": "^9.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - } - }, - "@octokit/types": { - "version": "9.3.2", - "requires": { - "@octokit/openapi-types": "^18.0.0" - } - } - } - }, - "@octokit/openapi-types": { - "version": "12.11.0" - }, - "@octokit/request": { - "version": "5.6.3", - "requires": { - "@octokit/endpoint": "^6.0.1", - "@octokit/request-error": "^2.1.0", - "@octokit/types": "^6.16.1", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/request-error": { - "version": "2.1.0", - "requires": { - "@octokit/types": "^6.0.3", - "deprecation": "^2.0.0", - "once": "^1.4.0" - } - }, - "@octokit/types": { - "version": "6.41.0", - "requires": { - "@octokit/openapi-types": "^12.11.0" - } - }, - "@opencensus/core": { - "version": "0.0.9", - "requires": { - "continuation-local-storage": "^3.2.1", - "log-driver": "^1.2.7", - "semver": "^5.5.0", - "shimmer": "^1.2.0", - "uuid": "^3.2.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1" - }, - "uuid": { - "version": "3.4.0" - } - } - }, - "@opencensus/propagation-b3": { - "version": "0.0.8", - "requires": { - "@opencensus/core": "^0.0.8", - "uuid": "^3.2.1" - }, - "dependencies": { - "@opencensus/core": { - "version": "0.0.8", - "requires": { - "continuation-local-storage": "^3.2.1", - "log-driver": "^1.2.7", - "semver": "^5.5.0", - "shimmer": "^1.2.0", - "uuid": "^3.2.1" - } - }, - "semver": { - "version": "5.7.1" - }, - "uuid": { - "version": "3.4.0" - } - } - }, - "@opensearch-project/opensearch": { - "version": "1.2.0", - "requires": { - "aws4": "^1.11.0", - "debug": "^4.3.1", - "hpagent": "^0.1.1", - "ms": "^2.1.3", - "secure-json-parse": "^2.4.0" - } - }, - "@pm2/agent": { - "version": "2.0.1", - "requires": { - "async": "~3.2.0", - "chalk": "~3.0.0", - "dayjs": "~1.8.24", - "debug": "~4.3.1", - "eventemitter2": "~5.0.1", - "fast-json-patch": "^3.0.0-1", - "fclone": "~1.0.11", - "nssocket": "0.6.0", - "pm2-axon": "~4.0.1", - "pm2-axon-rpc": "~0.7.0", - "proxy-agent": "~5.0.0", - "semver": "~7.2.0", - "ws": "~7.4.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "requires": { - "color-convert": "^2.0.1" - } - }, - "async": { - "version": "3.2.4" - }, - "chalk": { - "version": "3.0.0", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4" - }, - "dayjs": { - "version": "1.8.36" - }, - "eventemitter2": { - "version": "5.0.1" - }, - "has-flag": { - "version": "4.0.0" - }, - "semver": { - "version": "7.2.3" - }, - "supports-color": { - "version": "7.2.0", - "requires": { - "has-flag": "^4.0.0" - } - }, - "ws": { - "version": "7.4.6", - "requires": {} - } - } - }, - "@pm2/io": { - "version": "5.0.0", - "requires": { - "@opencensus/core": "0.0.9", - "@opencensus/propagation-b3": "0.0.8", - "async": "~2.6.1", - "debug": "~4.3.1", - "eventemitter2": "^6.3.1", - "require-in-the-middle": "^5.0.0", - "semver": "6.3.0", - "shimmer": "^1.2.0", - "signal-exit": "^3.0.3", - "tslib": "1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.9.3" - } - } - }, - "@pm2/js-api": { - "version": "0.6.7", - "requires": { - "async": "^2.6.3", - "axios": "^0.21.0", - "debug": "~4.3.1", - "eventemitter2": "^6.3.1", - "ws": "^7.0.0" - }, - "dependencies": { - "axios": { - "version": "0.21.4", - "requires": { - "follow-redirects": "^1.14.0" - } - }, - "ws": { - "version": "7.5.9", - "requires": {} - } - } - }, - "@pm2/pm2-version-check": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.4.tgz", - "integrity": "sha512-SXsM27SGH3yTWKc2fKR4SYNxsmnvuBQ9dd6QHtEWmiZ/VqaOYPAIlS8+vMcn27YLtAEBGvNRSh3TPNvtjZgfqA==", - "requires": { - "debug": "^4.3.1" - } - }, - "@readme/better-ajv-errors": { - "version": "1.6.0", - "dev": true, - "requires": { - "@babel/code-frame": "^7.16.0", - "@babel/runtime": "^7.21.0", - "@humanwhocodes/momoa": "^2.0.3", - "chalk": "^4.1.2", - "json-to-ast": "^2.0.3", - "jsonpointer": "^5.0.0", - "leven": "^3.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@readme/json-schema-ref-parser": { - "version": "1.2.0", - "dev": true, - "requires": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.6", - "call-me-maybe": "^1.0.1", - "js-yaml": "^4.1.0" - } - }, - "@readme/openapi-parser": { - "version": "2.5.0", - "dev": true, - "requires": { - "@apidevtools/openapi-schemas": "^2.1.0", - "@apidevtools/swagger-methods": "^3.0.2", - "@jsdevtools/ono": "^7.1.3", - "@readme/better-ajv-errors": "^1.6.0", - "@readme/json-schema-ref-parser": "^1.2.0", - "ajv": "^8.12.0", - "ajv-draft-04": "^1.0.0", - "call-me-maybe": "^1.0.1" - } - }, - "@sapphire/async-queue": { - "version": "1.5.0" - }, - "@sapphire/shapeshift": { - "version": "3.9.2", - "requires": { - "fast-deep-equal": "^3.1.3", - "lodash": "^4.17.21" - } - }, - "@sapphire/snowflake": { - "version": "3.5.1" - }, - "@segment/loosely-validate-event": { - "version": "2.0.0", - "requires": { - "component-type": "^1.2.1", - "join-component": "^1.1.0" - } - }, - "@selderee/plugin-htmlparser2": { - "version": "0.6.0", - "requires": { - "domhandler": "^4.2.0", - "selderee": "^0.6.0" - } - }, - "@sendgrid/client": { - "version": "7.7.0", - "requires": { - "@sendgrid/helpers": "^7.7.0", - "axios": "^0.26.0" - }, - "dependencies": { - "axios": { - "version": "0.26.1", - "requires": { - "follow-redirects": "^1.14.8" - } - } - } - }, - "@sendgrid/eventwebhook": { - "version": "7.7.0", - "requires": { - "starkbank-ecdsa": "^1.1.1" - } - }, - "@sendgrid/helpers": { - "version": "7.7.0", - "requires": { - "deepmerge": "^4.2.2" - } - }, - "@sendgrid/mail": { - "version": "7.2.6", - "requires": { - "@sendgrid/client": "^7.2.6", - "@sendgrid/helpers": "^7.2.6" - } - }, - "@sinclair/typebox": { - "version": "0.25.24", - "dev": true - }, - "@sindresorhus/is": { - "version": "0.14.0", - "dev": true - }, - "@sinonjs/commons": { - "version": "3.0.0", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/fake-timers": { - "version": "10.3.0", - "dev": true, - "requires": { - "@sinonjs/commons": "^3.0.0" - } - }, - "@slack/logger": { - "version": "3.0.0", - "requires": { - "@types/node": ">=12.0.0" - } - }, - "@slack/types": { - "version": "2.8.0" - }, - "@slack/web-api": { - "version": "6.8.1", - "requires": { - "@slack/logger": "^3.0.0", - "@slack/types": "^2.0.0", - "@types/is-stream": "^1.1.0", - "@types/node": ">=12.0.0", - "axios": "^0.27.2", - "eventemitter3": "^3.1.0", - "form-data": "^2.5.0", - "is-electron": "2.2.0", - "is-stream": "^1.1.0", - "p-queue": "^6.6.1", - "p-retry": "^4.0.0" - } - }, - "@smithy/protocol-http": { - "version": "1.1.0", - "requires": { - "@smithy/types": "^1.1.0", - "tslib": "^2.5.0" - } - }, - "@smithy/types": { - "version": "1.1.0", - "requires": { - "tslib": "^2.5.0" - } - }, - "@socket.io/component-emitter": { - "version": "3.1.0" - }, - "@superfaceai/ast": { - "version": "1.2.0", - "requires": { - "ajv": "^8.8.2", - "ajv-formats": "^2.1.1" - } - }, - "@superfaceai/one-sdk": { - "version": "1.5.2", - "requires": { - "@superfaceai/ast": "1.2.0", - "@superfaceai/parser": "1.2.0", - "abort-controller": "^3.0.0", - "cross-fetch": "^3.1.5", - "debug": "^4.3.2", - "isomorphic-form-data": "^2.0.0", - "vm2": "^3.9.7" - } - }, - "@superfaceai/parser": { - "version": "1.2.0", - "requires": { - "@superfaceai/ast": "^1.2.0", - "@types/debug": "^4.1.5", - "debug": "^4.3.3", - "typescript": "^4" - } - }, - "@superfaceai/passport-twitter-oauth2": { - "version": "1.2.3", - "requires": { - "@types/passport": "1.x", - "@types/passport-oauth2": ">=1.4", - "passport-oauth2": "^1.6.1" - } - }, - "@szmarczak/http-timer": { - "version": "1.1.2", - "dev": true, - "requires": { - "defer-to-connect": "^1.0.1" - } - }, - "@tokenizer/token": { - "version": "0.3.0" - }, - "@tootallnate/once": { - "version": "1.1.2" - }, - "@tsconfig/node10": { - "version": "1.0.9", - "dev": true - }, - "@tsconfig/node12": { - "version": "1.0.11", - "dev": true - }, - "@tsconfig/node14": { - "version": "1.0.3", - "dev": true - }, - "@tsconfig/node16": { - "version": "1.0.4", - "dev": true - }, - "@types/babel__core": { - "version": "7.20.1", - "dev": true, - "requires": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "@types/babel__generator": { - "version": "7.6.4", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@types/babel__template": { - "version": "7.4.1", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@types/babel__traverse": { - "version": "7.20.1", - "dev": true, - "requires": { - "@babel/types": "^7.20.7" - } - }, - "@types/body-parser": { - "version": "1.19.2", - "requires": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "@types/btoa-lite": { - "version": "1.0.0" - }, - "@types/bunyan": { - "version": "1.8.8", - "requires": { - "@types/node": "*" - } - }, - "@types/bunyan-format": { - "version": "0.2.5", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/config": { - "version": "3.3.0", - "dev": true - }, - "@types/connect": { - "version": "3.4.35", - "requires": { - "@types/node": "*" - } - }, - "@types/cookie": { - "version": "0.4.1" - }, - "@types/cookiejar": { - "version": "2.1.2", - "dev": true - }, - "@types/cors": { - "version": "2.8.13", - "requires": { - "@types/node": "*" - } - }, - "@types/cron": { - "version": "2.0.1", - "dev": true, - "requires": { - "@types/luxon": "*", - "@types/node": "*" - } - }, - "@types/debug": { - "version": "4.1.8", - "requires": { - "@types/ms": "*" - } - }, - "@types/express": { - "version": "4.17.17", - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.35", - "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "@types/graceful-fs": { - "version": "4.1.6", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/html-to-text": { - "version": "8.1.1", - "dev": true - }, - "@types/is-stream": { - "version": "1.1.0", - "requires": { - "@types/node": "*" - } - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.4", - "dev": true - }, - "@types/istanbul-lib-report": { - "version": "3.0.0", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*" - } - }, - "@types/istanbul-reports": { - "version": "3.0.1", - "dev": true, - "requires": { - "@types/istanbul-lib-report": "*" - } - }, - "@types/jest": { - "version": "29.5.2", - "dev": true, - "requires": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, - "@types/json-schema": { - "version": "7.0.12", - "dev": true - }, - "@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true - }, - "@types/jsonwebtoken": { - "version": "9.0.2", - "requires": { - "@types/node": "*" - } - }, - "@types/lru-cache": { - "version": "5.1.1" - }, - "@types/luxon": { - "version": "3.3.0", - "dev": true - }, - "@types/mime": { - "version": "1.3.2" - }, - "@types/ms": { - "version": "0.7.31" - }, - "@types/node": { - "version": "17.0.45" - }, - "@types/oauth": { - "version": "0.9.1", - "optional": true, - "requires": { - "@types/node": "*" - } - }, - "@types/passport": { - "version": "1.0.12", - "optional": true, - "requires": { - "@types/express": "*" - } - }, - "@types/passport-oauth2": { - "version": "1.4.12", - "optional": true, - "requires": { - "@types/express": "*", - "@types/oauth": "*", - "@types/passport": "*" - } - }, - "@types/prettier": { - "version": "2.7.3", - "dev": true - }, - "@types/qs": { - "version": "6.9.7" - }, - "@types/range-parser": { - "version": "1.2.4" - }, - "@types/retry": { - "version": "0.12.0" - }, - "@types/sanitize-html": { - "version": "2.9.0", - "dev": true, - "requires": { - "htmlparser2": "^8.0.0" - } - }, - "@types/semver": { - "version": "7.5.0", - "dev": true - }, - "@types/send": { - "version": "0.17.1", - "requires": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "@types/serve-static": { - "version": "1.15.1", - "requires": { - "@types/mime": "*", - "@types/node": "*" - } - }, - "@types/stack-utils": { - "version": "2.0.1", - "dev": true - }, - "@types/superagent": { - "version": "4.1.18", - "dev": true, - "requires": { - "@types/cookiejar": "*", - "@types/node": "*" - } - }, - "@types/uuid": { - "version": "9.0.2", - "dev": true - }, - "@types/validator": { - "version": "13.7.17" - }, - "@types/ws": { - "version": "8.5.5", - "requires": { - "@types/node": "*" - } - }, - "@types/yargs": { - "version": "17.0.24", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "@types/yargs-parser": { - "version": "21.0.0", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.60.0", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.60.0", - "@typescript-eslint/type-utils": "5.60.0", - "@typescript-eslint/utils": "5.60.0", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.2", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "dev": true - } - } - }, - "@typescript-eslint/parser": { - "version": "5.60.0", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.60.0", - "@typescript-eslint/types": "5.60.0", - "@typescript-eslint/typescript-estree": "5.60.0", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.60.0", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.60.0", - "@typescript-eslint/visitor-keys": "5.60.0" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.60.0", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.60.0", - "@typescript-eslint/utils": "5.60.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.60.0", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.60.0", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.60.0", - "@typescript-eslint/visitor-keys": "5.60.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.2", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "dev": true - } - } - }, - "@typescript-eslint/utils": { - "version": "5.60.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.60.0", - "@typescript-eslint/types": "5.60.0", - "@typescript-eslint/typescript-estree": "5.60.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.2", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "dev": true - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.60.0", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.60.0", - "eslint-visitor-keys": "^3.3.0" - } - }, - "@vladfrangu/async_event_emitter": { - "version": "2.2.2" - }, - "abbrev": { - "version": "1.1.1" - }, - "abort-controller": { - "version": "3.0.0", - "requires": { - "event-target-shim": "^5.0.0" - } - }, - "accepts": { - "version": "1.3.8", - "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - } - }, - "acorn": { - "version": "8.9.0" - }, - "acorn-jsx": { - "version": "5.3.2", - "dev": true, - "requires": {} - }, - "acorn-walk": { - "version": "8.2.0" - }, - "agent-base": { - "version": "6.0.2", - "requires": { - "debug": "4" - } - }, - "agentkeepalive": { - "version": "4.3.0", - "requires": { - "debug": "^4.1.0", - "depd": "^2.0.0", - "humanize-ms": "^1.2.1" - } - }, - "aggregate-error": { - "version": "3.1.0", - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - }, - "ajv": { - "version": "8.12.0", - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "ajv-draft-04": { - "version": "1.0.0", - "dev": true, - "requires": {} - }, - "ajv-formats": { - "version": "2.1.1", - "requires": { - "ajv": "^8.0.0" - } - }, - "amp": { - "version": "0.3.1" - }, - "amp-message": { - "version": "0.1.2", - "requires": { - "amp": "0.3.1" - } - }, - "analytics-node": { - "version": "6.2.0", - "requires": { - "@segment/loosely-validate-event": "^2.0.0", - "axios": "^0.27.2", - "axios-retry": "3.2.0", - "lodash.isstring": "^4.0.1", - "md5": "^2.2.1", - "ms": "^2.0.0", - "remove-trailing-slash": "^0.1.0", - "uuid": "^8.3.2" - }, - "dependencies": { - "uuid": { - "version": "8.3.2" - } - } - }, - "ansi-align": { - "version": "3.0.1", - "dev": true, - "requires": { - "string-width": "^4.1.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - } - } - }, - "ansi-colors": { - "version": "4.1.3" - }, - "ansi-escapes": { - "version": "4.3.2", - "dev": true, - "requires": { - "type-fest": "^0.21.3" - } - }, - "ansi-regex": { - "version": "2.1.1" - }, - "ansi-styles": { - "version": "3.2.1", - "requires": { - "color-convert": "^1.9.0" - } - }, - "ansicolors": { - "version": "0.2.1" - }, - "ansistyles": { - "version": "0.1.3" - }, - "any-promise": { - "version": "1.3.0" - }, - "anymatch": { - "version": "3.1.3", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "aproba": { - "version": "1.2.0" - }, - "are-we-there-yet": { - "version": "1.1.7", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.8", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "arg": { - "version": "4.1.3", - "dev": true - }, - "argparse": { - "version": "2.0.1", - "dev": true - }, - "array-back": { - "version": "3.1.0" - }, - "array-buffer-byte-length": { - "version": "1.0.0", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - } - }, - "array-flatten": { - "version": "1.1.1" - }, - "array-includes": { - "version": "3.1.6", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", - "is-string": "^1.0.7" - } - }, - "array-union": { - "version": "2.1.0", - "dev": true - }, - "array.prototype.flat": { - "version": "1.3.1", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-shim-unscopables": "^1.0.0" - } - }, - "array.prototype.flatmap": { - "version": "1.3.1", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-shim-unscopables": "^1.0.0" - } - }, - "arrify": { - "version": "2.0.1" - }, - "asap": { - "version": "2.0.6" - }, - "ast-types": { - "version": "0.13.4", - "requires": { - "tslib": "^2.0.1" - } - }, - "async": { - "version": "2.6.4", - "requires": { - "lodash": "^4.17.14" - } - }, - "async-listener": { - "version": "0.6.10", - "requires": { - "semver": "^5.3.0", - "shimmer": "^1.1.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1" - } - } - }, - "async-retry": { - "version": "1.3.3", - "requires": { - "retry": "0.13.1" - } - }, - "asynckit": { - "version": "0.4.0" - }, - "available-typed-arrays": { - "version": "1.0.5", - "dev": true - }, - "aws-sdk": { - "version": "2.814.0", - "requires": { - "buffer": "4.9.2", - "events": "1.1.1", - "ieee754": "1.1.13", - "jmespath": "0.15.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "uuid": "3.3.2", - "xml2js": "0.4.19" - }, - "dependencies": { - "uuid": { - "version": "3.3.2" - } - } - }, - "aws4": { - "version": "1.12.0" - }, - "axios": { - "version": "0.27.2", - "requires": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - }, - "dependencies": { - "form-data": { - "version": "4.0.0", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } - } - }, - "axios-retry": { - "version": "3.2.0", - "requires": { - "is-retry-allowed": "^1.1.0" - } - }, - "babel-jest": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/transform": "^29.5.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.5.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "babel-plugin-istanbul": { - "version": "6.1.1", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - } - }, - "babel-plugin-jest-hoist": { - "version": "29.5.0", - "dev": true, - "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - } - }, - "babel-plugin-polyfill-corejs2": { - "version": "0.4.3", - "dev": true, - "requires": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.4.0", - "semver": "^6.1.1" - } - }, - "babel-plugin-polyfill-corejs3": { - "version": "0.8.1", - "dev": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.4.0", - "core-js-compat": "^3.30.1" - } - }, - "babel-plugin-polyfill-regenerator": { - "version": "0.5.0", - "dev": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.4.0" - } - }, - "babel-preset-current-node-syntax": { - "version": "1.0.1", - "dev": true, - "requires": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" - } - }, - "babel-preset-jest": { - "version": "29.5.0", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "^29.5.0", - "babel-preset-current-node-syntax": "^1.0.0" - } - }, - "balanced-match": { - "version": "1.0.2" - }, - "base64-js": { - "version": "1.5.1" - }, - "base64id": { - "version": "2.0.0" - }, - "base64url": { - "version": "3.0.1" - }, - "bcrypt": { - "version": "5.0.0", - "requires": { - "node-addon-api": "^3.0.0", - "node-pre-gyp": "0.15.0" - } - }, - "big-integer": { - "version": "1.6.51" - }, - "bignumber.js": { - "version": "9.1.1" - }, - "binary-extensions": { - "version": "2.2.0" - }, - "bindings": { - "version": "1.5.0", - "requires": { - "file-uri-to-path": "1.0.0" - } - }, - "bl": { - "version": "4.1.0", - "dev": true, - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - }, - "dependencies": { - "buffer": { - "version": "5.7.1", - "dev": true, - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - } - } - }, - "blessed": { - "version": "0.1.81" - }, - "bluebird": { - "version": "2.11.0" - }, - "bodec": { - "version": "0.1.0" - }, - "body-parser": { - "version": "1.20.2", - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0" - } - } - }, - "boolbase": { - "version": "1.0.0" - }, - "bowser": { - "version": "2.11.0" - }, - "boxen": { - "version": "4.2.0", - "dev": true, - "requires": { - "ansi-align": "^3.0.0", - "camelcase": "^5.3.1", - "chalk": "^3.0.0", - "cli-boxes": "^2.2.0", - "string-width": "^4.1.0", - "term-size": "^2.1.0", - "type-fest": "^0.8.1", - "widest-line": "^3.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "type-fest": { - "version": "0.8.1", - "dev": true - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "requires": { - "fill-range": "^7.0.1" - } - }, - "browserslist": { - "version": "4.21.9", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001503", - "electron-to-chromium": "^1.4.431", - "node-releases": "^2.0.12", - "update-browserslist-db": "^1.0.11" - } - }, - "bs-logger": { - "version": "0.2.6", - "dev": true, - "requires": { - "fast-json-stable-stringify": "2.x" - } - }, - "bser": { - "version": "2.1.1", - "dev": true, - "requires": { - "node-int64": "^0.4.0" - } - }, - "btoa-lite": { - "version": "1.0.0" - }, - "buffer": { - "version": "4.9.2", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "buffer-equal-constant-time": { - "version": "1.0.1" - }, - "buffer-from": { - "version": "1.1.2" - }, - "buffer-writer": { - "version": "2.0.0" - }, - "bufferutil": { - "version": "4.0.7", - "requires": { - "node-gyp-build": "^4.3.0" - } - }, - "bunyan": { - "version": "1.8.15", - "requires": { - "dtrace-provider": "~0.8", - "moment": "^2.19.3", - "mv": "~2", - "safe-json-stringify": "~1" - } - }, - "bunyan-format": { - "version": "0.2.1", - "requires": { - "ansicolors": "~0.2.1", - "ansistyles": "~0.1.1", - "xtend": "~2.1.1" - } - }, - "bunyan-middleware": { - "version": "1.0.2", - "requires": { - "@types/bunyan": "^1.8.6", - "@types/express": "^4.0.35", - "uuid": "^8.3.2" - }, - "dependencies": { - "uuid": { - "version": "8.3.2" - } - } - }, - "busboy": { - "version": "1.6.0", - "requires": { - "streamsearch": "^1.1.0" - } - }, - "bytes": { - "version": "3.1.2" - }, - "cacache": { - "version": "16.1.3", - "requires": { - "@npmcli/fs": "^2.1.0", - "@npmcli/move-file": "^2.0.0", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "glob": "^8.0.1", - "infer-owner": "^1.0.4", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^9.0.0", - "tar": "^6.1.11", - "unique-filename": "^2.0.0" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "requires": { - "balanced-match": "^1.0.0" - } - }, - "chownr": { - "version": "2.0.0" - }, - "fs-minipass": { - "version": "2.1.0", - "requires": { - "minipass": "^3.0.0" - } - }, - "glob": { - "version": "8.1.0", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - } - }, - "lru-cache": { - "version": "7.18.3" - }, - "minimatch": { - "version": "5.1.6", - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "minipass": { - "version": "3.3.6", - "requires": { - "yallist": "^4.0.0" - } - }, - "minizlib": { - "version": "2.1.2", - "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - } - }, - "tar": { - "version": "6.1.15", - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "dependencies": { - "minipass": { - "version": "5.0.0" - } - } - }, - "yallist": { - "version": "4.0.0" - } - } - }, - "cacheable-request": { - "version": "6.1.0", - "dev": true, - "requires": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" - }, - "dependencies": { - "get-stream": { - "version": "5.2.0", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "lowercase-keys": { - "version": "2.0.0", - "dev": true - } - } - }, - "call-bind": { - "version": "1.0.2", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "call-me-maybe": { - "version": "1.0.2", - "dev": true - }, - "callsites": { - "version": "3.1.0", - "dev": true - }, - "camelcase": { - "version": "5.3.1" - }, - "caniuse-lite": { - "version": "1.0.30001506", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "char-regex": { - "version": "1.0.2", - "dev": true - }, - "charenc": { - "version": "0.0.2" - }, - "charm": { - "version": "0.1.2" - }, - "chokidar": { - "version": "3.5.3", - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "chownr": { - "version": "1.1.4" - }, - "ci-info": { - "version": "3.8.0", - "dev": true - }, - "cjs-module-lexer": { - "version": "1.2.3", - "dev": true - }, - "clean-stack": { - "version": "2.2.0" - }, - "clearbit": { - "version": "1.3.5", - "requires": { - "bluebird": "2", - "create-error": "0.3", - "lodash": "4.x", - "needle": "clearbit/needle#84d28b5f2c3916db1e7eb84aeaa9d976cc40054b" - } - }, - "cli-boxes": { - "version": "2.2.1", - "dev": true - }, - "cli-color": { - "version": "1.4.0", - "requires": { - "ansi-regex": "^2.1.1", - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "memoizee": "^0.4.14", - "timers-ext": "^0.1.5" - } - }, - "cli-cursor": { - "version": "3.1.0", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "cli-highlight": { - "version": "2.1.6", - "requires": { - "chalk": "^3.0.0", - "highlight.js": "^10.0.0", - "mz": "^2.4.0", - "parse5": "^5.1.1", - "parse5-htmlparser2-tree-adapter": "^5.1.1", - "yargs": "^15.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4" - }, - "has-flag": { - "version": "4.0.0" - }, - "supports-color": { - "version": "7.2.0", - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "cli-spinners": { - "version": "2.9.0", - "dev": true - }, - "cli-table": { - "version": "0.3.11", - "dev": true, - "requires": { - "colors": "1.0.3" - } - }, - "cli-tableau": { - "version": "2.0.1", - "requires": { - "chalk": "3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4" - }, - "has-flag": { - "version": "4.0.0" - }, - "supports-color": { - "version": "7.2.0", - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "cliui": { - "version": "6.0.0", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "3.0.0" - }, - "string-width": { - "version": "4.2.3", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - } - } - }, - "clone": { - "version": "1.0.4", - "dev": true - }, - "clone-response": { - "version": "1.0.3", - "dev": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, - "co": { - "version": "4.6.0", - "dev": true - }, - "code-error-fragment": { - "version": "0.0.230", - "dev": true - }, - "code-point-at": { - "version": "1.1.0" - }, - "collect-v8-coverage": { - "version": "1.0.1", - "dev": true - }, - "color-convert": { - "version": "1.9.3", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3" - }, - "colors": { - "version": "1.0.3", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "command-line-args": { - "version": "5.2.1", - "requires": { - "array-back": "^3.1.0", - "find-replace": "^3.0.0", - "lodash.camelcase": "^4.3.0", - "typical": "^4.0.0" - } - }, - "command-line-usage": { - "version": "6.1.3", - "requires": { - "array-back": "^4.0.2", - "chalk": "^2.4.2", - "table-layout": "^1.0.2", - "typical": "^5.2.0" - }, - "dependencies": { - "array-back": { - "version": "4.0.2" - }, - "typical": { - "version": "5.2.0" - } - } - }, - "commander": { - "version": "6.2.1" - }, - "comment-parser": { - "version": "0.7.6", - "dev": true - }, - "component-emitter": { - "version": "1.3.0" - }, - "component-type": { - "version": "1.2.1" - }, - "compressible": { - "version": "2.0.18", - "requires": { - "mime-db": ">= 1.43.0 < 2" - } - }, - "concat-map": { - "version": "0.0.1" - }, - "concat-stream": { - "version": "2.0.0", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, - "config": { - "version": "3.3.9", - "requires": { - "json5": "^2.2.3" - } - }, - "config-chain": { - "version": "1.1.13", - "requires": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "configstore": { - "version": "5.0.1", - "requires": { - "dot-prop": "^5.2.0", - "graceful-fs": "^4.1.2", - "make-dir": "^3.0.0", - "unique-string": "^2.0.0", - "write-file-atomic": "^3.0.0", - "xdg-basedir": "^4.0.0" - } - }, - "confusing-browser-globals": { - "version": "1.0.11", - "dev": true - }, - "console-control-strings": { - "version": "1.1.0" - }, - "content-disposition": { - "version": "0.5.3", - "requires": { - "safe-buffer": "5.1.2" - } - }, - "content-type": { - "version": "1.0.5" - }, - "continuation-local-storage": { - "version": "3.2.1", - "requires": { - "async-listener": "^0.6.0", - "emitter-listener": "^1.1.1" - } - }, - "convert-source-map": { - "version": "1.9.0", - "dev": true - }, - "cookie": { - "version": "0.4.0" - }, - "cookie-signature": { - "version": "1.0.6" - }, - "cookiejar": { - "version": "2.1.4" - }, - "copy-anything": { - "version": "3.0.5", - "requires": { - "is-what": "^4.1.8" - } - }, - "copyfiles": { - "version": "2.4.1", - "dev": true, - "requires": { - "glob": "^7.0.5", - "minimatch": "^3.0.3", - "mkdirp": "^1.0.4", - "noms": "0.0.0", - "through2": "^2.0.1", - "untildify": "^4.0.0", - "yargs": "^16.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "cliui": { - "version": "7.0.4", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "wrap-ansi": { - "version": "7.0.0", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "y18n": { - "version": "5.0.8", - "dev": true - }, - "yargs": { - "version": "16.2.0", - "dev": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.9", - "dev": true - } - } - }, - "core-js": { - "version": "3.31.0" - }, - "core-js-compat": { - "version": "3.31.0", - "dev": true, - "requires": { - "browserslist": "^4.21.5" - } - }, - "core-util-is": { - "version": "1.0.3" - }, - "cors": { - "version": "2.8.5", - "requires": { - "object-assign": "^4", - "vary": "^1" - } - }, - "create-error": { - "version": "0.3.1" - }, - "create-require": { - "version": "1.1.1", - "dev": true - }, - "cron": { - "version": "2.3.1", - "requires": { - "luxon": "^3.2.1" - } - }, - "cron-time-generator": { - "version": "1.3.2" - }, - "croner": { - "version": "4.1.97" - }, - "cross-env": { - "version": "7.0.2", - "dev": true, - "requires": { - "cross-spawn": "^7.0.1" - } - }, - "cross-fetch": { - "version": "3.1.6", - "requires": { - "node-fetch": "^2.6.11" - } - }, - "cross-spawn": { - "version": "7.0.3", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "crowd-sentiment": { - "version": "1.1.7" - }, - "crypt": { - "version": "0.0.2" - }, - "crypto-js": { - "version": "4.1.1" - }, - "crypto-random-string": { - "version": "2.0.0" - }, - "css-select": { - "version": "5.1.0", - "requires": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "dependencies": { - "domhandler": { - "version": "5.0.3", - "requires": { - "domelementtype": "^2.3.0" - } - } - } - }, - "css-what": { - "version": "6.1.0" - }, - "culvert": { - "version": "0.1.2" - }, - "d": { - "version": "1.0.1", - "requires": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, - "data-uri-to-buffer": { - "version": "3.0.1" - }, - "date-and-time": { - "version": "0.14.2" - }, - "dayjs": { - "version": "1.11.8" - }, - "debug": { - "version": "4.3.4", - "requires": { - "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2" - } - } - }, - "decamelize": { - "version": "1.2.0" - }, - "decompress-response": { - "version": "3.3.0", - "dev": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, - "dedent": { - "version": "0.7.0", - "dev": true - }, - "deep-extend": { - "version": "0.6.0" - }, - "deep-is": { - "version": "0.1.4" - }, - "deepmerge": { - "version": "4.3.1" - }, - "defaults": { - "version": "1.0.4", - "dev": true, - "requires": { - "clone": "^1.0.2" - } - }, - "defer-to-connect": { - "version": "1.1.3", - "dev": true - }, - "define-lazy-prop": { - "version": "2.0.0", - "dev": true - }, - "define-properties": { - "version": "1.2.0", - "dev": true, - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - }, - "degenerator": { - "version": "3.0.4", - "requires": { - "ast-types": "^0.13.2", - "escodegen": "^1.8.1", - "esprima": "^4.0.0", - "vm2": "^3.9.17" - } - }, - "delayed-stream": { - "version": "1.0.0" - }, - "delegates": { - "version": "1.0.0" - }, - "depd": { - "version": "2.0.0" - }, - "deprecation": { - "version": "2.3.1" - }, - "destroy": { - "version": "1.2.0" - }, - "detect-libc": { - "version": "1.0.3" - }, - "detect-newline": { - "version": "3.1.0", - "dev": true - }, - "dezalgo": { - "version": "1.0.4", - "requires": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "diff": { - "version": "4.0.2", - "dev": true - }, - "diff-sequences": { - "version": "29.4.3", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "discontinuous-range": { - "version": "1.0.0" - }, - "discord-api-types": { - "version": "0.37.46" - }, - "discord.js": { - "version": "14.11.0", - "requires": { - "@discordjs/builders": "^1.6.3", - "@discordjs/collection": "^1.5.1", - "@discordjs/formatters": "^0.3.1", - "@discordjs/rest": "^1.7.1", - "@discordjs/util": "^0.3.1", - "@discordjs/ws": "^0.8.3", - "@sapphire/snowflake": "^3.4.2", - "@types/ws": "^8.5.4", - "discord-api-types": "^0.37.41", - "fast-deep-equal": "^3.1.3", - "lodash.snakecase": "^4.1.1", - "tslib": "^2.5.0", - "undici": "^5.22.0", - "ws": "^8.13.0" - } - }, - "doctrine": { - "version": "3.0.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "dom-serializer": { - "version": "2.0.0", - "requires": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "dependencies": { - "domhandler": { - "version": "5.0.3", - "requires": { - "domelementtype": "^2.3.0" - } - } - } - }, - "domelementtype": { - "version": "2.3.0" - }, - "domhandler": { - "version": "4.3.1", - "requires": { - "domelementtype": "^2.2.0" - } - }, - "domutils": { - "version": "3.1.0", - "requires": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "dependencies": { - "domhandler": { - "version": "5.0.3", - "requires": { - "domelementtype": "^2.3.0" - } - } - } - }, - "dot-prop": { - "version": "5.3.0", - "requires": { - "is-obj": "^2.0.0" - } - }, - "dotenv": { - "version": "8.2.0" - }, - "dotenv-expand": { - "version": "8.0.3" - }, - "dottie": { - "version": "2.0.6" - }, - "dtrace-provider": { - "version": "0.8.8", - "optional": true, - "requires": { - "nan": "^2.14.0" - } - }, - "duplexer3": { - "version": "0.1.5", - "dev": true - }, - "duplexify": { - "version": "3.7.1", - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.8", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "editor": { - "version": "1.0.0", - "dev": true - }, - "editorconfig": { - "version": "0.15.3", - "requires": { - "commander": "^2.19.0", - "lru-cache": "^4.1.5", - "semver": "^5.6.0", - "sigmund": "^1.0.1" - }, - "dependencies": { - "commander": { - "version": "2.20.3" - }, - "lru-cache": { - "version": "4.1.5", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "semver": { - "version": "5.7.1" - }, - "yallist": { - "version": "2.1.2" - } - } - }, - "ee-first": { - "version": "1.1.1" - }, - "electron-to-chromium": { - "version": "1.4.437", - "dev": true - }, - "emitter-listener": { - "version": "1.1.2", - "requires": { - "shimmer": "^1.2.0" - } - }, - "emittery": { - "version": "0.13.1", - "dev": true - }, - "emoji-chars": { - "version": "1.0.12", - "requires": { - "emoji-unicode-map": "^1.0.0" - } - }, - "emoji-dictionary": { - "version": "1.0.11", - "requires": { - "emoji-chars": "^1.0.0", - "emoji-name-map": "^1.0.0", - "emoji-names": "^1.0.1", - "emoji-unicode-map": "^1.0.0", - "emojilib": "^2.0.2" - } - }, - "emoji-name-map": { - "version": "1.2.9", - "requires": { - "emojilib": "^2.0.2", - "iterate-object": "^1.3.1", - "map-o": "^2.0.1" - } - }, - "emoji-names": { - "version": "1.0.12", - "requires": { - "emoji-name-map": "^1.0.0" - } - }, - "emoji-regex": { - "version": "8.0.0" - }, - "emoji-unicode-map": { - "version": "1.1.11", - "requires": { - "emoji-name-map": "^1.1.0", - "iterate-object": "^1.3.1" - } - }, - "emojilib": { - "version": "2.4.0" - }, - "encodeurl": { - "version": "1.0.2" - }, - "encoding": { - "version": "0.1.13", - "optional": true, - "requires": { - "iconv-lite": "^0.6.2" - }, - "dependencies": { - "iconv-lite": { - "version": "0.6.3", - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - } - } - }, - "end-of-stream": { - "version": "1.4.4", - "requires": { - "once": "^1.4.0" - } - }, - "engine.io": { - "version": "6.4.2", - "requires": { - "@types/cookie": "^0.4.1", - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.4.1", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", - "ws": "~8.11.0" - }, - "dependencies": { - "cookie": { - "version": "0.4.2" - }, - "ws": { - "version": "8.11.0", - "requires": {} - } - } - }, - "engine.io-parser": { - "version": "5.0.7" - }, - "enquirer": { - "version": "2.3.6", - "requires": { - "ansi-colors": "^4.1.1" - } - }, - "ent": { - "version": "2.2.0" - }, - "entities": { - "version": "4.5.0" - }, - "erlpack": { - "version": "0.1.4", - "requires": { - "bindings": "^1.5.0", - "nan": "^2.15.0" - } - }, - "err-code": { - "version": "2.0.3" - }, - "error-ex": { - "version": "1.3.2", - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.21.2", - "dev": true, - "requires": { - "array-buffer-byte-length": "^1.0.0", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-set-tostringtag": "^2.0.1", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.2.0", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.7", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" - } - }, - "es-set-tostringtag": { - "version": "2.0.1", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" - } - }, - "es-shim-unscopables": { - "version": "1.0.0", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "es5-ext": { - "version": "0.10.62", - "requires": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "next-tick": "^1.1.0" - } - }, - "es6-iterator": { - "version": "2.0.3", - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "es6-promise": { - "version": "3.3.1", - "dev": true - }, - "es6-symbol": { - "version": "3.1.3", - "requires": { - "d": "^1.0.1", - "ext": "^1.1.2" - } - }, - "es6-weak-map": { - "version": "2.0.3", - "requires": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" - } - }, - "escalade": { - "version": "3.1.1", - "dev": true - }, - "escape-goat": { - "version": "2.1.1", - "dev": true - }, - "escape-html": { - "version": "1.0.3" - }, - "escape-string-regexp": { - "version": "1.0.5" - }, - "escodegen": { - "version": "1.14.3", - "requires": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" - }, - "levn": { - "version": "0.3.0", - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "optionator": { - "version": "0.8.3", - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "prelude-ls": { - "version": "1.1.2" - }, - "type-check": { - "version": "0.3.2", - "requires": { - "prelude-ls": "~1.1.2" - } - } - } - }, - "eslint": { - "version": "8.43.0", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.43.0", - "@humanwhocodes/config-array": "^0.11.10", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "escape-string-regexp": { - "version": "4.0.0", - "dev": true - }, - "eslint-scope": { - "version": "7.2.0", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "dev": true - }, - "globals": { - "version": "13.20.0", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - } - } - }, - "eslint-config-airbnb-base": { - "version": "15.0.0", - "dev": true, - "requires": { - "confusing-browser-globals": "^1.0.10", - "object.assign": "^4.1.2", - "object.entries": "^1.1.5", - "semver": "^6.3.0" - } - }, - "eslint-config-airbnb-typescript": { - "version": "16.2.0", - "dev": true, - "requires": { - "eslint-config-airbnb-base": "^15.0.0" - } - }, - "eslint-config-prettier": { - "version": "8.8.0", - "dev": true, - "requires": {} - }, - "eslint-import-resolver-node": { - "version": "0.3.7", - "dev": true, - "requires": { - "debug": "^3.2.7", - "is-core-module": "^2.11.0", - "resolve": "^1.22.1" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "eslint-module-utils": { - "version": "2.8.0", - "dev": true, - "requires": { - "debug": "^3.2.7" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "eslint-plugin-import": { - "version": "2.27.5", - "dev": true, - "requires": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "array.prototype.flatmap": "^1.3.1", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.7", - "eslint-module-utils": "^2.7.4", - "has": "^1.0.3", - "is-core-module": "^2.11.0", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.values": "^1.1.6", - "resolve": "^1.22.1", - "semver": "^6.3.0", - "tsconfig-paths": "^3.14.1" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "doctrine": { - "version": "2.1.0", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true - }, - "tsconfig-paths": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", - "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", - "dev": true, - "requires": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - } - } - }, - "eslint-plugin-openapi": { - "version": "0.0.4", - "dev": true, - "requires": { - "comment-parser": "^0.7.4" - } - }, - "eslint-scope": { - "version": "5.1.1", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-visitor-keys": { - "version": "3.4.1", - "dev": true - }, - "espree": { - "version": "9.5.2", - "dev": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esprima": { - "version": "4.0.1" - }, - "esquery": { - "version": "1.5.0", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0" - }, - "esutils": { - "version": "2.0.3" - }, - "etag": { - "version": "1.8.1" - }, - "event-emitter": { - "version": "0.3.5", - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "event-target-shim": { - "version": "5.0.1" - }, - "eventemitter2": { - "version": "6.4.9" - }, - "eventemitter3": { - "version": "3.1.2" - }, - "events": { - "version": "1.1.1" - }, - "execa": { - "version": "5.1.1", - "dev": true, - "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "dependencies": { - "is-stream": { - "version": "2.0.1", - "dev": true - } - } - }, - "exit": { - "version": "0.1.2", - "dev": true - }, - "expect": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/expect-utils": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0" - } - }, - "express": { - "version": "4.17.1", - "requires": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "body-parser": { - "version": "1.19.0", - "requires": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - } - }, - "bytes": { - "version": "3.1.0" - }, - "debug": { - "version": "2.6.9", - "requires": { - "ms": "2.0.0" - } - }, - "depd": { - "version": "1.1.2" - }, - "http-errors": { - "version": "1.7.2", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - }, - "inherits": { - "version": "2.0.3" - }, - "ms": { - "version": "2.0.0" - }, - "on-finished": { - "version": "2.3.0", - "requires": { - "ee-first": "1.1.1" - } - }, - "qs": { - "version": "6.7.0" - }, - "raw-body": { - "version": "2.4.0", - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "toidentifier": { - "version": "1.0.0" - } - } - }, - "express-rate-limit": { - "version": "6.5.1", - "requires": {} - }, - "ext": { - "version": "1.7.0", - "requires": { - "type": "^2.7.2" - }, - "dependencies": { - "type": { - "version": "2.7.2" - } - } - }, - "extend": { - "version": "3.0.2" - }, - "extend-shallow": { - "version": "2.0.1", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "fast-deep-equal": { - "version": "3.1.3" - }, - "fast-glob": { - "version": "3.2.12", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-patch": { - "version": "3.1.1" - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true - }, - "fast-levenshtein": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", - "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", - "requires": { - "fastest-levenshtein": "^1.0.7" - } - }, - "fast-safe-stringify": { - "version": "2.1.1" - }, - "fast-text-encoding": { - "version": "1.0.6" - }, - "fast-xml-parser": { - "version": "4.2.4", - "requires": { - "strnum": "^1.0.5" - } - }, - "fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==" - }, - "fastq": { - "version": "1.15.0", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "fb-watchman": { - "version": "2.0.2", - "dev": true, - "requires": { - "bser": "2.1.1" - } - }, - "fclone": { - "version": "1.0.11" - }, - "file-entry-cache": { - "version": "6.0.1", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "file-type": { - "version": "18.5.0", - "requires": { - "readable-web-to-node-stream": "^3.0.2", - "strtok3": "^7.0.0", - "token-types": "^5.0.1" - } - }, - "file-uri-to-path": { - "version": "1.0.0" - }, - "fill-range": { - "version": "7.0.1", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "finalhandler": { - "version": "1.1.2", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0" - }, - "on-finished": { - "version": "2.3.0", - "requires": { - "ee-first": "1.1.1" - } - } - } - }, - "find-replace": { - "version": "3.0.0", - "requires": { - "array-back": "^3.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "dev": true - }, - "follow-redirects": { - "version": "1.15.2" - }, - "for-each": { - "version": "0.3.3", - "dev": true, - "requires": { - "is-callable": "^1.1.3" - } - }, - "form-data": { - "version": "2.5.1", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "formidable": { - "version": "1.2.6" - }, - "formidable-serverless": { - "version": "1.1.1", - "requires": { - "formidable": "^1.2.2" - } - }, - "forwarded": { - "version": "0.2.0" - }, - "fresh": { - "version": "0.5.2" - }, - "fs-extra": { - "version": "8.1.0", - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fs-minipass": { - "version": "1.2.7", - "requires": { - "minipass": "^2.6.0" - } - }, - "fs.realpath": { - "version": "1.0.0" - }, - "ftp": { - "version": "0.3.10", - "requires": { - "readable-stream": "1.1.x", - "xregexp": "2.0.0" - }, - "dependencies": { - "isarray": { - "version": "0.0.1" - }, - "readable-stream": { - "version": "1.1.14", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31" - } - } - }, - "function-bind": { - "version": "1.1.1" - }, - "function.prototype.name": { - "version": "1.1.5", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - } - }, - "functions-have-names": { - "version": "1.2.3", - "dev": true - }, - "gauge": { - "version": "2.7.4", - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - }, - "dependencies": { - "strip-ansi": { - "version": "3.0.1", - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "gaxios": { - "version": "3.2.0", - "requires": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.3.0" - }, - "dependencies": { - "is-stream": { - "version": "2.0.1" - } - } - }, - "gcp-metadata": { - "version": "4.3.1", - "requires": { - "gaxios": "^4.0.0", - "json-bigint": "^1.0.0" - }, - "dependencies": { - "gaxios": { - "version": "4.3.3", - "requires": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.7" - } - }, - "is-stream": { - "version": "2.0.1" - } - } - }, - "gcs-resumable-upload": { - "version": "3.6.0", - "requires": { - "abort-controller": "^3.0.0", - "async-retry": "^1.3.3", - "configstore": "^5.0.0", - "extend": "^3.0.2", - "gaxios": "^4.0.0", - "google-auth-library": "^7.0.0", - "pumpify": "^2.0.0", - "stream-events": "^1.0.4" - }, - "dependencies": { - "gaxios": { - "version": "4.3.3", - "requires": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.7" - } - }, - "is-stream": { - "version": "2.0.1" - } - } - }, - "gensync": { - "version": "1.0.0-beta.2", - "dev": true - }, - "get-caller-file": { - "version": "2.0.5" - }, - "get-intrinsic": { - "version": "1.2.1", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - } - }, - "get-package-type": { - "version": "0.1.0", - "dev": true - }, - "get-stream": { - "version": "6.0.1", - "dev": true - }, - "get-symbol-description": { - "version": "1.0.0", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - } - }, - "get-uri": { - "version": "3.0.2", - "requires": { - "@tootallnate/once": "1", - "data-uri-to-buffer": "3", - "debug": "4", - "file-uri-to-path": "2", - "fs-extra": "^8.1.0", - "ftp": "^0.3.10" - }, - "dependencies": { - "file-uri-to-path": { - "version": "2.0.0" - } - } - }, - "git-node-fs": { - "version": "1.0.0" - }, - "git-sha1": { - "version": "0.1.2" - }, - "glob": { - "version": "7.2.3", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "global-dirs": { - "version": "2.1.0", - "dev": true, - "requires": { - "ini": "1.3.7" - }, - "dependencies": { - "ini": { - "version": "1.3.7", - "dev": true - } - } - }, - "globals": { - "version": "11.12.0", - "dev": true - }, - "globalthis": { - "version": "1.0.3", - "dev": true, - "requires": { - "define-properties": "^1.1.3" - } - }, - "globby": { - "version": "11.1.0", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "google-auth-library": { - "version": "7.14.1", - "requires": { - "arrify": "^2.0.0", - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "fast-text-encoding": "^1.0.0", - "gaxios": "^4.0.0", - "gcp-metadata": "^4.2.0", - "gtoken": "^5.0.4", - "jws": "^4.0.0", - "lru-cache": "^6.0.0" - }, - "dependencies": { - "gaxios": { - "version": "4.3.3", - "requires": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.7" - } - }, - "is-stream": { - "version": "2.0.1" - }, - "lru-cache": { - "version": "6.0.0", - "requires": { - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0" - } - } - }, - "google-p12-pem": { - "version": "3.1.4", - "requires": { - "node-forge": "^1.3.1" - } - }, - "gopd": { - "version": "1.0.1", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.3" - } - }, - "got": { - "version": "9.6.0", - "dev": true, - "requires": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - }, - "dependencies": { - "get-stream": { - "version": "4.1.0", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - } - } - }, - "graceful-fs": { - "version": "4.2.11" - }, - "grapheme-splitter": { - "version": "1.0.4", - "dev": true - }, - "graphemer": { - "version": "1.4.0", - "dev": true - }, - "gray-matter": { - "version": "4.0.3", - "dev": true, - "requires": { - "js-yaml": "^3.13.1", - "kind-of": "^6.0.2", - "section-matter": "^1.0.0", - "strip-bom-string": "^1.0.0" - }, - "dependencies": { - "argparse": { - "version": "1.0.10", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "js-yaml": { - "version": "3.14.1", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "dev": true - } - } - }, - "gtoken": { - "version": "5.3.2", - "requires": { - "gaxios": "^4.0.0", - "google-p12-pem": "^3.1.3", - "jws": "^4.0.0" - }, - "dependencies": { - "gaxios": { - "version": "4.3.3", - "requires": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.7" - } - }, - "is-stream": { - "version": "2.0.1" - } - } - }, - "has": { - "version": "1.0.3", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-bigints": { - "version": "1.0.2", - "dev": true - }, - "has-flag": { - "version": "3.0.0" - }, - "has-property-descriptors": { - "version": "1.0.0", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.1" - } - }, - "has-proto": { - "version": "1.0.1" - }, - "has-symbols": { - "version": "1.0.3" - }, - "has-tostringtag": { - "version": "1.0.0", - "dev": true, - "requires": { - "has-symbols": "^1.0.2" - } - }, - "has-unicode": { - "version": "2.0.1" - }, - "has-yarn": { - "version": "2.1.0", - "dev": true - }, - "hash-stream-validation": { - "version": "0.2.4" - }, - "he": { - "version": "1.2.0" - }, - "helmet": { - "version": "4.1.1" - }, - "hexoid": { - "version": "1.0.0" - }, - "highlight.js": { - "version": "10.7.3" - }, - "hosted-git-info": { - "version": "2.8.9" - }, - "hpagent": { - "version": "0.1.2" - }, - "html-escaper": { - "version": "2.0.2", - "dev": true - }, - "html-to-mrkdwn-ts": { - "version": "1.1.0", - "requires": { - "node-html-markdown": "^1.1.3" - } - }, - "html-to-text": { - "version": "8.2.1", - "requires": { - "@selderee/plugin-htmlparser2": "^0.6.0", - "deepmerge": "^4.2.2", - "he": "^1.2.0", - "htmlparser2": "^6.1.0", - "minimist": "^1.2.6", - "selderee": "^0.6.0" - }, - "dependencies": { - "dom-serializer": { - "version": "1.4.1", - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - } - }, - "domutils": { - "version": "2.8.0", - "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - } - }, - "entities": { - "version": "2.2.0" - }, - "htmlparser2": { - "version": "6.1.0", - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - } - } - }, - "htmlparser2": { - "version": "8.0.2", - "requires": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - }, - "dependencies": { - "domhandler": { - "version": "5.0.3", - "requires": { - "domelementtype": "^2.3.0" - } - } - } - }, - "http-cache-semantics": { - "version": "4.1.1" - }, - "http-errors": { - "version": "2.0.0", - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "dependencies": { - "setprototypeof": { - "version": "1.2.0" - }, - "statuses": { - "version": "2.0.1" - } - } - }, - "http-proxy-agent": { - "version": "4.0.1", - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - } - }, - "http2-client": { - "version": "1.3.5", - "dev": true - }, - "https-proxy-agent": { - "version": "5.0.1", - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "human-signals": { - "version": "2.1.0", - "dev": true - }, - "humanize-ms": { - "version": "1.2.1", - "requires": { - "ms": "^2.0.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ieee754": { - "version": "1.1.13" - }, - "ignore": { - "version": "5.2.4", - "dev": true - }, - "ignore-by-default": { - "version": "1.0.1", - "dev": true - }, - "ignore-walk": { - "version": "3.0.4", - "requires": { - "minimatch": "^3.0.4" - } - }, - "import-fresh": { - "version": "3.3.0", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "import-lazy": { - "version": "2.1.0", - "dev": true - }, - "import-local": { - "version": "3.1.0", - "dev": true, - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4" - }, - "indent-string": { - "version": "4.0.0" - }, - "infer-owner": { - "version": "1.0.4" - }, - "inflection": { - "version": "1.13.4" - }, - "inflight": { - "version": "1.0.6", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4" - }, - "ini": { - "version": "1.3.8" - }, - "internal-slot": { - "version": "1.0.5", - "dev": true, - "requires": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - } - }, - "invert-kv": { - "version": "1.0.0" - }, - "ip": { - "version": "1.1.8" - }, - "ipaddr.js": { - "version": "1.9.1" - }, - "is-array-buffer": { - "version": "3.0.2", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - } - }, - "is-arrayish": { - "version": "0.2.1" - }, - "is-bigint": { - "version": "1.0.4", - "dev": true, - "requires": { - "has-bigints": "^1.0.1" - } - }, - "is-binary-path": { - "version": "2.1.0", - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-boolean-object": { - "version": "1.1.2", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6" - }, - "is-callable": { - "version": "1.2.7", - "dev": true - }, - "is-ci": { - "version": "2.0.0", - "dev": true, - "requires": { - "ci-info": "^2.0.0" - }, - "dependencies": { - "ci-info": { - "version": "2.0.0", - "dev": true - } - } - }, - "is-core-module": { - "version": "2.12.1", - "requires": { - "has": "^1.0.3" - } - }, - "is-date-object": { - "version": "1.0.5", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-docker": { - "version": "2.2.1", - "dev": true - }, - "is-electron": { - "version": "2.2.0" - }, - "is-extendable": { - "version": "0.1.1", - "dev": true - }, - "is-extglob": { - "version": "2.1.1" - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-generator-fn": { - "version": "2.1.0", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-installed-globally": { - "version": "0.3.2", - "dev": true, - "requires": { - "global-dirs": "^2.0.1", - "is-path-inside": "^3.0.1" - } - }, - "is-interactive": { - "version": "1.0.0", - "dev": true - }, - "is-lambda": { - "version": "1.0.1" - }, - "is-negative-zero": { - "version": "2.0.2", - "dev": true - }, - "is-npm": { - "version": "4.0.0", - "dev": true - }, - "is-number": { - "version": "7.0.0" - }, - "is-number-object": { - "version": "1.0.7", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-obj": { - "version": "2.0.0" - }, - "is-path-inside": { - "version": "3.0.3", - "dev": true - }, - "is-plain-object": { - "version": "5.0.0" - }, - "is-promise": { - "version": "2.2.2" - }, - "is-regex": { - "version": "1.1.4", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-retry-allowed": { - "version": "1.2.0" - }, - "is-shared-array-buffer": { - "version": "1.0.2", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-stream": { - "version": "1.1.0" - }, - "is-string": { - "version": "1.0.7", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-symbol": { - "version": "1.0.4", - "dev": true, - "requires": { - "has-symbols": "^1.0.2" - } - }, - "is-typed-array": { - "version": "1.1.10", - "dev": true, - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0" - }, - "is-unicode-supported": { - "version": "0.1.0", - "dev": true - }, - "is-weakref": { - "version": "1.0.2", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-what": { - "version": "4.1.15" - }, - "is-wsl": { - "version": "2.2.0", - "dev": true, - "requires": { - "is-docker": "^2.0.0" - } - }, - "is-yarn-global": { - "version": "0.3.0", - "dev": true - }, - "isarray": { - "version": "1.0.0" - }, - "isemail": { - "version": "3.2.0", - "dev": true, - "requires": { - "punycode": "2.x.x" - } - }, - "isexe": { - "version": "2.0.0" - }, - "isomorphic-form-data": { - "version": "2.0.0", - "requires": { - "form-data": "^2.3.2" - } - }, - "istanbul-lib-coverage": { - "version": "3.2.0", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "5.2.1", - "dev": true, - "requires": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - } - }, - "istanbul-lib-report": { - "version": "3.0.0", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.1", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - } - }, - "istanbul-reports": { - "version": "3.1.5", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "iterate-object": { - "version": "1.3.4" - }, - "jest": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/core": "^29.5.0", - "@jest/types": "^29.5.0", - "import-local": "^3.0.2", - "jest-cli": "^29.5.0" - } - }, - "jest-changed-files": { - "version": "29.5.0", - "dev": true, - "requires": { - "execa": "^5.0.0", - "p-limit": "^3.1.0" - } - }, - "jest-circus": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/environment": "^29.5.0", - "@jest/expect": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.5.0", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.5.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-cli": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/core": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "import-local": "^3.0.2", - "jest-config": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", - "prompts": "^2.0.1", - "yargs": "^17.3.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "cliui": { - "version": "8.0.1", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "wrap-ansi": { - "version": "7.0.0", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "y18n": { - "version": "5.0.8", - "dev": true - }, - "yargs": { - "version": "17.7.2", - "dev": true, - "requires": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - } - } - } - }, - "jest-config": { - "version": "29.5.0", - "dev": true, - "requires": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.5.0", - "@jest/types": "^29.5.0", - "babel-jest": "^29.5.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.5.0", - "jest-environment-node": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-runner": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.5.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-diff": { - "version": "29.5.0", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^29.4.3", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-docblock": { - "version": "29.4.3", - "dev": true, - "requires": { - "detect-newline": "^3.0.0" - } - }, - "jest-each": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/types": "^29.5.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.4.3", - "jest-util": "^29.5.0", - "pretty-format": "^29.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-environment-node": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/environment": "^29.5.0", - "@jest/fake-timers": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/node": "*", - "jest-mock": "^29.5.0", - "jest-util": "^29.5.0" - } - }, - "jest-get-type": { - "version": "29.4.3", - "dev": true - }, - "jest-haste-map": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/types": "^29.5.0", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.3.2", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.5.0", - "jest-worker": "^29.5.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - } - }, - "jest-leak-detector": { - "version": "29.5.0", - "dev": true, - "requires": { - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" - } - }, - "jest-matcher-utils": { - "version": "29.5.0", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "jest-diff": "^29.5.0", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-message-util": { - "version": "29.5.0", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.5.0", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.5.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-mock": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/types": "^29.5.0", - "@types/node": "*", - "jest-util": "^29.5.0" - } - }, - "jest-pnp-resolver": { - "version": "1.2.3", - "dev": true, - "requires": {} - }, - "jest-regex-util": { - "version": "29.4.3", - "dev": true - }, - "jest-resolve": { - "version": "29.5.0", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-resolve-dependencies": { - "version": "29.5.0", - "dev": true, - "requires": { - "jest-regex-util": "^29.4.3", - "jest-snapshot": "^29.5.0" - } - }, - "jest-runner": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/console": "^29.5.0", - "@jest/environment": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.4.3", - "jest-environment-node": "^29.5.0", - "jest-haste-map": "^29.5.0", - "jest-leak-detector": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-resolve": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-util": "^29.5.0", - "jest-watcher": "^29.5.0", - "jest-worker": "^29.5.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-runtime": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/environment": "^29.5.0", - "@jest/fake-timers": "^29.5.0", - "@jest/globals": "^29.5.0", - "@jest/source-map": "^29.4.3", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-mock": "^29.5.0", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-snapshot": { - "version": "29.5.0", - "dev": true, - "requires": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.5.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.5.0", - "semver": "^7.3.5" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.2", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "dev": true - } - } - }, - "jest-util": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/types": "^29.5.0", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-validate": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/types": "^29.5.0", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.4.3", - "leven": "^3.1.0", - "pretty-format": "^29.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "camelcase": { - "version": "6.3.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-watcher": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.5.0", - "string-length": "^4.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-worker": { - "version": "29.5.0", - "dev": true, - "requires": { - "@types/node": "*", - "jest-util": "^29.5.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "8.1.1", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jmespath": { - "version": "0.15.0" - }, - "join-component": { - "version": "1.1.0" - }, - "jose": { - "version": "4.14.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", - "integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==" - }, - "js-beautify": { - "version": "1.14.8", - "requires": { - "config-chain": "^1.1.13", - "editorconfig": "^0.15.3", - "glob": "^8.1.0", - "nopt": "^6.0.0" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "requires": { - "balanced-match": "^1.0.0" - } - }, - "glob": { - "version": "8.1.0", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - } - }, - "minimatch": { - "version": "5.1.6", - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "nopt": { - "version": "6.0.0", - "requires": { - "abbrev": "^1.0.0" - } - } - } - }, - "js-git": { - "version": "0.7.8", - "requires": { - "bodec": "^0.1.0", - "culvert": "^0.1.2", - "git-sha1": "^0.1.2", - "pako": "^0.2.5" - } - }, - "js-sha256": { - "version": "0.9.0" - }, - "js-tokens": { - "version": "4.0.0", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "jsesc": { - "version": "2.5.2", - "dev": true - }, - "json-bigint": { - "version": "1.0.0", - "requires": { - "bignumber.js": "^9.0.0" - } - }, - "json-buffer": { - "version": "3.0.0", - "dev": true - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "dev": true - }, - "json-schema-traverse": { - "version": "1.0.0" - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "optional": true - }, - "json-to-ast": { - "version": "2.1.0", - "dev": true, - "requires": { - "code-error-fragment": "0.0.230", - "grapheme-splitter": "^1.0.4" - } - }, - "json2csv": { - "version": "5.0.7", - "requires": { - "commander": "^6.1.0", - "jsonparse": "^1.3.1", - "lodash.get": "^4.4.2" - } - }, - "json5": { - "version": "2.2.3" - }, - "jsonfile": { - "version": "4.0.0", - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsonparse": { - "version": "1.3.1" - }, - "jsonpointer": { - "version": "5.0.1", - "dev": true - }, - "jsonwebtoken": { - "version": "8.5.1", - "requires": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^5.6.0" - }, - "dependencies": { - "jwa": { - "version": "1.4.1", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "semver": { - "version": "5.7.1" - } - } - }, - "jwa": { - "version": "2.0.0", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jwks-rsa": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.0.1.tgz", - "integrity": "sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw==", - "requires": { - "@types/express": "^4.17.14", - "@types/jsonwebtoken": "^9.0.0", - "debug": "^4.3.4", - "jose": "^4.10.4", - "limiter": "^1.1.5", - "lru-memoizer": "^2.1.4" - } - }, - "jws": { - "version": "4.0.0", - "requires": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "keyv": { - "version": "3.1.0", - "dev": true, - "requires": { - "json-buffer": "3.0.0" - } - }, - "kind-of": { - "version": "6.0.3", - "dev": true - }, - "kleur": { - "version": "3.0.3", - "dev": true - }, - "latest-version": { - "version": "5.1.0", - "dev": true, - "requires": { - "package-json": "^6.3.0" - } - }, - "lazy": { - "version": "1.0.11" - }, - "lcid": { - "version": "1.0.0", - "requires": { - "invert-kv": "^1.0.0" - } - }, - "leven": { - "version": "3.1.0", - "dev": true - }, - "levn": { - "version": "0.4.1", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "limiter": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", - "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" - }, - "lines-and-columns": { - "version": "1.2.4", - "dev": true - }, - "load-json-file": { - "version": "2.0.0", - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "strip-bom": "^3.0.0" - }, - "dependencies": { - "parse-json": { - "version": "2.2.0", - "requires": { - "error-ex": "^1.2.0" - } - }, - "strip-bom": { - "version": "3.0.0" - } - } - }, - "locate-path": { - "version": "6.0.0", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash": { - "version": "4.17.21" - }, - "lodash.camelcase": { - "version": "4.3.0" - }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" - }, - "lodash.debounce": { - "version": "4.0.8", - "dev": true - }, - "lodash.get": { - "version": "4.4.2" - }, - "lodash.includes": { - "version": "4.3.0" - }, - "lodash.isboolean": { - "version": "3.0.3" - }, - "lodash.isinteger": { - "version": "4.0.4" - }, - "lodash.isnumber": { - "version": "3.0.3" - }, - "lodash.isplainobject": { - "version": "4.0.6" - }, - "lodash.isstring": { - "version": "4.0.1" - }, - "lodash.memoize": { - "version": "4.1.2", - "dev": true - }, - "lodash.merge": { - "version": "4.6.2", - "dev": true - }, - "lodash.once": { - "version": "4.1.1" - }, - "lodash.snakecase": { - "version": "4.1.1" - }, - "log-driver": { - "version": "1.2.7" - }, - "log-symbols": { - "version": "4.1.0", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "lowercase-keys": { - "version": "1.0.1", - "dev": true - }, - "lru-cache": { - "version": "5.1.1", - "requires": { - "yallist": "^3.0.2" - } - }, - "lru-memoizer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.2.0.tgz", - "integrity": "sha512-QfOZ6jNkxCcM/BkIPnFsqDhtrazLRsghi9mBwFAzol5GCvj4EkFT899Za3+QwikCg5sRX8JstioBDwOxEyzaNw==", - "requires": { - "lodash.clonedeep": "^4.5.0", - "lru-cache": "~4.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", - "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", - "requires": { - "pseudomap": "^1.0.1", - "yallist": "^2.0.0" - } - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" - } - } - }, - "lru-queue": { - "version": "0.1.0", - "requires": { - "es5-ext": "~0.10.2" - } - }, - "luxon": { - "version": "3.3.0" - }, - "make-dir": { - "version": "3.1.0", - "requires": { - "semver": "^6.0.0" - } - }, - "make-error": { - "version": "1.3.6", - "dev": true - }, - "make-fetch-happen": { - "version": "10.2.1", - "requires": { - "agentkeepalive": "^4.2.1", - "cacache": "^16.1.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^2.0.3", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^9.0.0" - }, - "dependencies": { - "@tootallnate/once": { - "version": "2.0.0" - }, - "http-proxy-agent": { - "version": "5.0.0", - "requires": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - } - }, - "lru-cache": { - "version": "7.18.3" - }, - "minipass": { - "version": "3.3.6", - "requires": { - "yallist": "^4.0.0" - } - }, - "socks-proxy-agent": { - "version": "7.0.0", - "requires": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - } - }, - "yallist": { - "version": "4.0.0" - } - } - }, - "makeerror": { - "version": "1.0.12", - "dev": true, - "requires": { - "tmpl": "1.0.5" - } - }, - "map-o": { - "version": "2.0.10", - "requires": { - "iterate-object": "^1.3.0" - } - }, - "md5": { - "version": "2.3.0", - "requires": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, - "media-typer": { - "version": "0.3.0" - }, - "mem": { - "version": "1.1.0", - "requires": { - "mimic-fn": "^1.0.0" - }, - "dependencies": { - "mimic-fn": { - "version": "1.2.0" - } - } - }, - "memoizee": { - "version": "0.4.15", - "requires": { - "d": "^1.0.1", - "es5-ext": "^0.10.53", - "es6-weak-map": "^2.0.3", - "event-emitter": "^0.3.5", - "is-promise": "^2.2.2", - "lru-queue": "^0.1.0", - "next-tick": "^1.1.0", - "timers-ext": "^0.1.7" - } - }, - "merge-descriptors": { - "version": "1.0.1" - }, - "merge-stream": { - "version": "2.0.0", - "dev": true - }, - "merge2": { - "version": "1.4.1", - "dev": true - }, - "methods": { - "version": "1.1.2" - }, - "micromatch": { - "version": "4.0.5", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "mime": { - "version": "2.6.0" - }, - "mime-db": { - "version": "1.52.0" - }, - "mime-types": { - "version": "2.1.35", - "requires": { - "mime-db": "1.52.0" - } - }, - "mimic-fn": { - "version": "2.1.0" - }, - "mimic-response": { - "version": "1.0.1", - "dev": true - }, - "minimatch": { - "version": "3.1.2", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.8" - }, - "minipass": { - "version": "2.9.0", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minipass-collect": { - "version": "1.0.2", - "requires": { - "minipass": "^3.0.0" - }, - "dependencies": { - "minipass": { - "version": "3.3.6", - "requires": { - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0" - } - } - }, - "minipass-fetch": { - "version": "2.1.2", - "requires": { - "encoding": "^0.1.13", - "minipass": "^3.1.6", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "dependencies": { - "minipass": { - "version": "3.3.6", - "requires": { - "yallist": "^4.0.0" - } - }, - "minizlib": { - "version": "2.1.2", - "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0" - } - } - }, - "minipass-flush": { - "version": "1.0.5", - "requires": { - "minipass": "^3.0.0" - }, - "dependencies": { - "minipass": { - "version": "3.3.6", - "requires": { - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0" - } - } - }, - "minipass-pipeline": { - "version": "1.2.4", - "requires": { - "minipass": "^3.0.0" - }, - "dependencies": { - "minipass": { - "version": "3.3.6", - "requires": { - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0" - } - } - }, - "minipass-sized": { - "version": "1.0.3", - "requires": { - "minipass": "^3.0.0" - }, - "dependencies": { - "minipass": { - "version": "3.3.6", - "requires": { - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0" - } - } - }, - "minizlib": { - "version": "1.3.3", - "requires": { - "minipass": "^2.9.0" - } - }, - "mkdirp": { - "version": "1.0.4" - }, - "module-details-from-path": { - "version": "1.0.3" - }, - "moment": { - "version": "2.29.4" - }, - "moment-timezone": { - "version": "0.5.43", - "requires": { - "moment": "^2.29.4" - } - }, - "moo": { - "version": "0.5.2" - }, - "ms": { - "version": "2.1.3" - }, - "murmurhash3js": { - "version": "3.0.1" - }, - "mute-stream": { - "version": "0.0.8" - }, - "mv": { - "version": "2.1.1", - "requires": { - "mkdirp": "~0.5.1", - "ncp": "~2.0.0", - "rimraf": "~2.4.0" - }, - "dependencies": { - "glob": { - "version": "6.0.4", - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "mkdirp": { - "version": "0.5.6", - "requires": { - "minimist": "^1.2.6" - } - }, - "rimraf": { - "version": "2.4.5", - "requires": { - "glob": "^6.0.1" - } - } - } - }, - "mz": { - "version": "2.7.0", - "requires": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "nan": { - "version": "2.17.0" - }, - "nanoid": { - "version": "3.3.6" - }, - "natural-compare": { - "version": "1.4.0", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "dev": true - }, - "ncp": { - "version": "2.0.0" - }, - "nearley": { - "version": "2.20.1", - "requires": { - "commander": "^2.19.0", - "moo": "^0.5.0", - "railroad-diagrams": "^1.0.0", - "randexp": "0.4.6" - }, - "dependencies": { - "commander": { - "version": "2.20.3" - } - } - }, - "needle": { - "version": "git+ssh://git@github.com/clearbit/needle.git#84d28b5f2c3916db1e7eb84aeaa9d976cc40054b", - "integrity": "sha512-9VnoxVBudfy+C5eIHHbb+SkkWugmACsefrBS+EkHTufUJeHUA5/xBeSquvw+Bj5NvQmieEStduiIDnFKP+Kbog==", - "from": "needle@clearbit/needle#84d28b5f2c3916db1e7eb84aeaa9d976cc40054b", - "requires": { - "iconv-lite": "^0.4.4" - } - }, - "negotiator": { - "version": "0.6.3" - }, - "netmask": { - "version": "2.0.2" - }, - "next-tick": { - "version": "1.1.0" - }, - "node-addon-api": { - "version": "3.2.1" - }, - "node-fetch": { - "version": "2.6.11", - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "node-fetch-h2": { - "version": "2.3.0", - "dev": true, - "requires": { - "http2-client": "^1.2.5" - } - }, - "node-forge": { - "version": "1.3.1" - }, - "node-gyp-build": { - "version": "4.6.0" - }, - "node-html-markdown": { - "version": "1.3.0", - "requires": { - "node-html-parser": "^6.1.1" - } - }, - "node-html-parser": { - "version": "6.1.5", - "requires": { - "css-select": "^5.1.0", - "he": "1.2.0" - } - }, - "node-int64": { - "version": "0.4.0", - "dev": true - }, - "node-mocks-http": { - "version": "1.9.0", - "dev": true, - "requires": { - "accepts": "^1.3.7", - "depd": "^1.1.0", - "fresh": "^0.5.2", - "merge-descriptors": "^1.0.1", - "methods": "^1.1.2", - "mime": "^1.3.4", - "parseurl": "^1.3.3", - "range-parser": "^1.2.0", - "type-is": "^1.6.18" - }, - "dependencies": { - "depd": { - "version": "1.1.2", - "dev": true - }, - "mime": { - "version": "1.6.0", - "dev": true - } - } - }, - "node-pre-gyp": { - "version": "0.15.0", - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.3", - "needle": "^2.5.0", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4.4.2" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "requires": { - "ms": "^2.1.1" - } - }, - "mkdirp": { - "version": "0.5.6", - "requires": { - "minimist": "^1.2.6" - } - }, - "needle": { - "version": "2.9.1", - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "rimraf": { - "version": "2.7.1", - "requires": { - "glob": "^7.1.3" - } - }, - "sax": { - "version": "1.2.4" - }, - "semver": { - "version": "5.7.1" - } - } - }, - "node-readfiles": { - "version": "0.2.0", - "dev": true, - "requires": { - "es6-promise": "^3.2.1" - } - }, - "node-releases": { - "version": "2.0.12", - "dev": true - }, - "nodemon": { - "version": "2.0.4", - "dev": true, - "requires": { - "chokidar": "^3.2.2", - "debug": "^3.2.6", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.0.4", - "pstree.remy": "^1.1.7", - "semver": "^5.7.1", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.2", - "update-notifier": "^4.0.0" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "semver": { - "version": "5.7.1", - "dev": true - } - } - }, - "noms": { - "version": "0.0.0", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "~1.0.31" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "dev": true - }, - "readable-stream": { - "version": "1.0.34", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "dev": true - } - } - }, - "nopt": { - "version": "4.0.3", - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "normalize-package-data": { - "version": "2.5.0", - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1" - } - } - }, - "normalize-path": { - "version": "3.0.0" - }, - "normalize-url": { - "version": "4.5.1", - "dev": true - }, - "npm-bundled": { - "version": "1.1.2", - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1" - }, - "npm-packlist": { - "version": "1.4.8", - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-run-path": { - "version": "4.0.1", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "npmlog": { - "version": "4.1.2", - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "nssocket": { - "version": "0.6.0", - "requires": { - "eventemitter2": "~0.4.14", - "lazy": "~1.0.11" - }, - "dependencies": { - "eventemitter2": { - "version": "0.4.14" - } - } - }, - "nth-check": { - "version": "2.1.1", - "requires": { - "boolbase": "^1.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1" - }, - "oas-kit-common": { - "version": "1.0.8", - "dev": true, - "requires": { - "fast-safe-stringify": "^2.0.7" - } - }, - "oas-linter": { - "version": "3.2.2", - "dev": true, - "requires": { - "@exodus/schemasafe": "^1.0.0-rc.2", - "should": "^13.2.1", - "yaml": "^1.10.0" - } - }, - "oas-normalize": { - "version": "6.0.0", - "dev": true, - "requires": { - "@readme/openapi-parser": "^2.2.0", - "js-yaml": "^4.1.0", - "node-fetch": "^2.6.1", - "swagger2openapi": "^7.0.8" - } - }, - "oas-resolver": { - "version": "2.5.6", - "dev": true, - "requires": { - "node-fetch-h2": "^2.3.0", - "oas-kit-common": "^1.0.8", - "reftools": "^1.1.9", - "yaml": "^1.10.0", - "yargs": "^17.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "cliui": { - "version": "8.0.1", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "wrap-ansi": { - "version": "7.0.0", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "y18n": { - "version": "5.0.8", - "dev": true - }, - "yargs": { - "version": "17.7.2", - "dev": true, - "requires": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - } - } - } - }, - "oas-schema-walker": { - "version": "1.1.5", - "dev": true - }, - "oas-validator": { - "version": "5.0.8", - "dev": true, - "requires": { - "call-me-maybe": "^1.0.1", - "oas-kit-common": "^1.0.8", - "oas-linter": "^3.2.2", - "oas-resolver": "^2.5.6", - "oas-schema-walker": "^1.1.5", - "reftools": "^1.1.9", - "should": "^13.2.1", - "yaml": "^1.10.0" - } - }, - "oauth": { - "version": "0.9.15" - }, - "object-assign": { - "version": "4.1.1" - }, - "object-inspect": { - "version": "1.12.3" - }, - "object-keys": { - "version": "1.1.1", - "dev": true - }, - "object.assign": { - "version": "4.1.4", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - } - }, - "object.entries": { - "version": "1.1.6", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "object.values": { - "version": "1.1.6", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "omit-deep-by-values": { - "version": "1.0.2", - "requires": { - "lodash": "~4.17.11" - } - }, - "on-finished": { - "version": "2.4.1", - "requires": { - "ee-first": "1.1.1" - } - }, - "once": { - "version": "1.4.0", - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "5.1.2", - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "open": { - "version": "8.4.2", - "dev": true, - "requires": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - } - }, - "openapi-comment-parser": { - "version": "1.0.0" - }, - "openapi-types": { - "version": "12.1.3", - "dev": true, - "peer": true - }, - "optionator": { - "version": "0.9.1", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "dependencies": { - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - } - } - }, - "ora": { - "version": "5.4.1", - "dev": true, - "requires": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "os-homedir": { - "version": "1.0.2" - }, - "os-locale": { - "version": "2.1.0", - "requires": { - "execa": "^0.7.0", - "lcid": "^1.0.0", - "mem": "^1.1.0" - }, - "dependencies": { - "cross-spawn": { - "version": "5.1.0", - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "0.7.0", - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "3.0.0" - }, - "lru-cache": { - "version": "4.1.5", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "npm-run-path": { - "version": "2.0.2", - "requires": { - "path-key": "^2.0.0" - } - }, - "path-key": { - "version": "2.0.1" - }, - "shebang-command": { - "version": "1.2.0", - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0" - }, - "which": { - "version": "1.3.1", - "requires": { - "isexe": "^2.0.0" - } - }, - "yallist": { - "version": "2.1.2" - } - } - }, - "os-tmpdir": { - "version": "1.0.2" - }, - "osenv": { - "version": "0.1.5", - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "p-cancelable": { - "version": "1.1.0", - "dev": true - }, - "p-finally": { - "version": "1.0.0" - }, - "p-limit": { - "version": "3.1.0", - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "p-map": { - "version": "4.0.0", - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "p-queue": { - "version": "6.6.2", - "requires": { - "eventemitter3": "^4.0.4", - "p-timeout": "^3.2.0" - }, - "dependencies": { - "eventemitter3": { - "version": "4.0.7" - } - } - }, - "p-retry": { - "version": "4.6.2", - "requires": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - } - }, - "p-timeout": { - "version": "3.2.0", - "requires": { - "p-finally": "^1.0.0" - } - }, - "p-try": { - "version": "2.2.0" - }, - "pac-proxy-agent": { - "version": "5.0.0", - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4", - "get-uri": "3", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "5", - "pac-resolver": "^5.0.0", - "raw-body": "^2.2.0", - "socks-proxy-agent": "5" - } - }, - "pac-resolver": { - "version": "5.0.1", - "requires": { - "degenerator": "^3.0.2", - "ip": "^1.1.5", - "netmask": "^2.0.2" - } - }, - "package-json": { - "version": "6.5.0", - "dev": true, - "requires": { - "got": "^9.6.0", - "registry-auth-token": "^4.0.0", - "registry-url": "^5.0.0", - "semver": "^6.2.0" - } - }, - "packet-reader": { - "version": "1.0.0" - }, - "pako": { - "version": "0.2.9" - }, - "parent-module": { - "version": "1.0.1", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-json": { - "version": "5.2.0", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "parse-link-header": { - "version": "2.0.0", - "dev": true, - "requires": { - "xtend": "~4.0.1" - }, - "dependencies": { - "xtend": { - "version": "4.0.2", - "dev": true - } - } - }, - "parse-srcset": { - "version": "1.0.2" - }, - "parse5": { - "version": "5.1.1" - }, - "parse5-htmlparser2-tree-adapter": { - "version": "5.1.1", - "requires": { - "parse5": "^5.1.1" - } - }, - "parseley": { - "version": "0.7.0", - "requires": { - "moo": "^0.5.1", - "nearley": "^2.20.1" - } - }, - "parseurl": { - "version": "1.3.3" - }, - "passport": { - "version": "0.6.0", - "requires": { - "passport-strategy": "1.x.x", - "pause": "0.0.1", - "utils-merge": "^1.0.1" - } - }, - "passport-facebook": { - "version": "3.0.0", - "requires": { - "passport-oauth2": "1.x.x" - } - }, - "passport-github2": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz", - "integrity": "sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==", - "requires": { - "passport-oauth2": "1.x.x" - } - }, - "passport-google-oauth": { - "version": "2.0.0", - "requires": { - "passport-google-oauth1": "1.x.x", - "passport-google-oauth20": "2.x.x" - } - }, - "passport-google-oauth1": { - "version": "1.0.0", - "requires": { - "passport-oauth1": "1.x.x" - } - }, - "passport-google-oauth20": { - "version": "2.0.0", - "requires": { - "passport-oauth2": "1.x.x" - } - }, - "passport-oauth": { - "version": "0.1.15", - "requires": { - "oauth": "0.9.x", - "passport": "~0.1.1", - "pkginfo": "0.2.x" - }, - "dependencies": { - "passport": { - "version": "0.1.18", - "requires": { - "pause": "0.0.1", - "pkginfo": "0.2.x" - } - } - } - }, - "passport-oauth1": { - "version": "1.3.0", - "requires": { - "oauth": "0.9.x", - "passport-strategy": "1.x.x", - "utils-merge": "1.x.x" - } - }, - "passport-oauth2": { - "version": "1.7.0", - "requires": { - "base64url": "3.x.x", - "oauth": "0.9.x", - "passport-strategy": "1.x.x", - "uid2": "0.0.x", - "utils-merge": "1.x.x" - } - }, - "passport-slack": { - "version": "0.0.7", - "requires": { - "passport-oauth": "~0.1.1", - "pkginfo": "0.2.x" - } - }, - "passport-strategy": { - "version": "1.0.0" - }, - "path-exists": { - "version": "4.0.0" - }, - "path-is-absolute": { - "version": "1.0.1" - }, - "path-key": { - "version": "3.1.1", - "dev": true - }, - "path-parse": { - "version": "1.0.7" - }, - "path-to-regexp": { - "version": "0.1.7" - }, - "path-type": { - "version": "4.0.0", - "dev": true - }, - "pause": { - "version": "0.0.1" - }, - "peek-readable": { - "version": "5.0.0" - }, - "peopledatalabs": { - "version": "5.0.5", - "requires": { - "axios": "^1.4.0", - "copy-anything": "^3.0.5" - }, - "dependencies": { - "axios": { - "version": "1.4.0", - "requires": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "form-data": { - "version": "4.0.0", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } - } - }, - "pg": { - "version": "8.11.0", - "requires": { - "buffer-writer": "2.0.0", - "packet-reader": "1.0.0", - "pg-cloudflare": "^1.1.0", - "pg-connection-string": "^2.6.0", - "pg-pool": "^3.6.0", - "pg-protocol": "^1.6.0", - "pg-types": "^2.1.0", - "pgpass": "1.x" - } - }, - "pg-cloudflare": { - "version": "1.1.0", - "optional": true - }, - "pg-connection-string": { - "version": "2.6.0" - }, - "pg-int8": { - "version": "1.0.1" - }, - "pg-pool": { - "version": "3.6.0", - "requires": {} - }, - "pg-protocol": { - "version": "1.6.0" - }, - "pg-types": { - "version": "2.2.0", - "requires": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - } - }, - "pgpass": { - "version": "1.0.5", - "requires": { - "split2": "^4.1.0" - } - }, - "picocolors": { - "version": "1.0.0" - }, - "picomatch": { - "version": "2.3.1" - }, - "pidusage": { - "version": "3.0.2", - "requires": { - "safe-buffer": "^5.2.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1" - } - } - }, - "pify": { - "version": "2.3.0" - }, - "pirates": { - "version": "4.0.6", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "dev": true, - "requires": { - "find-up": "^4.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - } - } - }, - "pkginfo": { - "version": "0.2.3" - }, - "pm2": { - "version": "5.3.0", - "requires": { - "@pm2/agent": "~2.0.0", - "@pm2/io": "~5.0.0", - "@pm2/js-api": "~0.6.7", - "@pm2/pm2-version-check": "latest", - "async": "~3.2.0", - "blessed": "0.1.81", - "chalk": "3.0.0", - "chokidar": "^3.5.3", - "cli-tableau": "^2.0.0", - "commander": "2.15.1", - "croner": "~4.1.92", - "dayjs": "~1.11.5", - "debug": "^4.3.1", - "enquirer": "2.3.6", - "eventemitter2": "5.0.1", - "fclone": "1.0.11", - "mkdirp": "1.0.4", - "needle": "2.4.0", - "pidusage": "~3.0", - "pm2-axon": "~4.0.1", - "pm2-axon-rpc": "~0.7.1", - "pm2-deploy": "~1.0.2", - "pm2-multimeter": "^0.1.2", - "pm2-sysmonit": "^1.2.8", - "promptly": "^2", - "semver": "^7.2", - "source-map-support": "0.5.21", - "sprintf-js": "1.1.2", - "vizion": "~2.2.1", - "yamljs": "0.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "requires": { - "color-convert": "^2.0.1" - } - }, - "async": { - "version": "3.2.4" - }, - "chalk": { - "version": "3.0.0", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4" - }, - "commander": { - "version": "2.15.1" - }, - "eventemitter2": { - "version": "5.0.1" - }, - "has-flag": { - "version": "4.0.0" - }, - "lru-cache": { - "version": "6.0.0", - "requires": { - "yallist": "^4.0.0" - } - }, - "needle": { - "version": "2.4.0", - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "sax": { - "version": "1.2.4" - }, - "semver": { - "version": "7.5.2", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "source-map-support": { - "version": "0.5.21", - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "supports-color": { - "version": "7.2.0", - "requires": { - "has-flag": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0" - } - } - }, - "pm2-axon": { - "version": "4.0.1", - "requires": { - "amp": "~0.3.1", - "amp-message": "~0.1.1", - "debug": "^4.3.1", - "escape-string-regexp": "^4.0.0" - }, - "dependencies": { - "escape-string-regexp": { - "version": "4.0.0" - } - } - }, - "pm2-axon-rpc": { - "version": "0.7.1", - "requires": { - "debug": "^4.3.1" - } - }, - "pm2-deploy": { - "version": "1.0.2", - "requires": { - "run-series": "^1.1.8", - "tv4": "^1.3.0" - } - }, - "pm2-multimeter": { - "version": "0.1.2", - "requires": { - "charm": "~0.1.1" - } - }, - "pm2-sysmonit": { - "version": "1.2.8", - "optional": true, - "requires": { - "async": "^3.2.0", - "debug": "^4.3.1", - "pidusage": "^2.0.21", - "systeminformation": "^5.7", - "tx2": "~1.0.4" - }, - "dependencies": { - "async": { - "version": "3.2.4", - "optional": true - }, - "pidusage": { - "version": "2.0.21", - "optional": true, - "requires": { - "safe-buffer": "^5.2.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "optional": true - } - } - }, - "postcss": { - "version": "8.4.24", - "requires": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "postgres-array": { - "version": "2.0.0" - }, - "postgres-bytea": { - "version": "1.0.0" - }, - "postgres-date": { - "version": "1.0.7" - }, - "postgres-interval": { - "version": "1.2.0", - "requires": { - "xtend": "^4.0.0" - }, - "dependencies": { - "xtend": { - "version": "4.0.2" - } - } - }, - "prelude-ls": { - "version": "1.2.1", - "dev": true - }, - "prepend-http": { - "version": "2.0.0", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "dev": true - }, - "pretty-format": { - "version": "29.5.0", - "dev": true, - "requires": { - "@jest/schemas": "^29.4.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "dev": true - } - } - }, - "process-nextick-args": { - "version": "2.0.1" - }, - "promise-inflight": { - "version": "1.0.1" - }, - "promise-retry": { - "version": "2.0.1", - "requires": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "dependencies": { - "retry": { - "version": "0.12.0" - } - } - }, - "promptly": { - "version": "2.2.0", - "requires": { - "read": "^1.0.4" - } - }, - "prompts": { - "version": "2.4.2", - "dev": true, - "requires": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - } - }, - "proto-list": { - "version": "1.2.4" - }, - "proxy-addr": { - "version": "2.0.7", - "requires": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - } - }, - "proxy-agent": { - "version": "5.0.0", - "requires": { - "agent-base": "^6.0.0", - "debug": "4", - "http-proxy-agent": "^4.0.0", - "https-proxy-agent": "^5.0.0", - "lru-cache": "^5.1.1", - "pac-proxy-agent": "^5.0.0", - "proxy-from-env": "^1.0.0", - "socks-proxy-agent": "^5.0.0" - } - }, - "proxy-from-env": { - "version": "1.1.0" - }, - "pseudomap": { - "version": "1.0.2" - }, - "pstree.remy": { - "version": "1.1.8", - "dev": true - }, - "pump": { - "version": "3.0.0", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "2.0.1", - "requires": { - "duplexify": "^4.1.1", - "inherits": "^2.0.3", - "pump": "^3.0.0" - }, - "dependencies": { - "duplexify": { - "version": "4.1.2", - "requires": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" - } - } - } - }, - "punycode": { - "version": "2.3.0" - }, - "pupa": { - "version": "2.1.1", - "dev": true, - "requires": { - "escape-goat": "^2.0.0" - } - }, - "pure-rand": { - "version": "6.0.2", - "dev": true - }, - "qs": { - "version": "6.11.0", - "requires": { - "side-channel": "^1.0.4" - } - }, - "querystring": { - "version": "0.2.0" - }, - "queue-microtask": { - "version": "1.2.3", - "dev": true - }, - "railroad-diagrams": { - "version": "1.0.0" - }, - "ramda": { - "version": "0.27.2" - }, - "randexp": { - "version": "0.4.6", - "requires": { - "discontinuous-range": "1.0.0", - "ret": "~0.1.10" - } - }, - "range-parser": { - "version": "1.2.1" - }, - "raw-body": { - "version": "2.5.2", - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "rc": { - "version": "1.2.8", - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "strip-json-comments": { - "version": "2.0.1" - } - } - }, - "rdme": { - "version": "7.5.0", - "dev": true, - "requires": { - "@actions/core": "^1.6.0", - "chalk": "^4.1.2", - "cli-table": "^0.3.1", - "command-line-args": "^5.2.0", - "command-line-usage": "^6.0.2", - "config": "^3.1.0", - "configstore": "^5.0.0", - "debug": "^4.3.3", - "editor": "^1.0.0", - "enquirer": "^2.3.0", - "form-data": "^4.0.0", - "gray-matter": "^4.0.1", - "isemail": "^3.1.3", - "mime-types": "^2.1.35", - "node-fetch": "^2.6.1", - "oas-normalize": "^6.0.0", - "open": "^8.2.1", - "ora": "^5.4.1", - "parse-link-header": "^2.0.0", - "read": "^1.0.7", - "semver": "^7.0.0", - "tmp-promise": "^3.0.2", - "update-notifier": "^5.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "boxen": { - "version": "5.1.2", - "dev": true, - "requires": { - "ansi-align": "^3.0.0", - "camelcase": "^6.2.0", - "chalk": "^4.1.0", - "cli-boxes": "^2.2.1", - "string-width": "^4.2.2", - "type-fest": "^0.20.2", - "widest-line": "^3.1.0", - "wrap-ansi": "^7.0.0" - } - }, - "camelcase": { - "version": "6.3.0", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "form-data": { - "version": "4.0.0", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "global-dirs": { - "version": "3.0.1", - "dev": true, - "requires": { - "ini": "2.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "ini": { - "version": "2.0.0", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true - }, - "is-installed-globally": { - "version": "0.4.0", - "dev": true, - "requires": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" - } - }, - "is-npm": { - "version": "5.0.0", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.2", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "string-width": { - "version": "4.2.3", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "type-fest": { - "version": "0.20.2", - "dev": true - }, - "update-notifier": { - "version": "5.1.0", - "dev": true, - "requires": { - "boxen": "^5.0.0", - "chalk": "^4.1.0", - "configstore": "^5.0.1", - "has-yarn": "^2.1.0", - "import-lazy": "^2.1.0", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.4.0", - "is-npm": "^5.0.0", - "is-yarn-global": "^0.3.0", - "latest-version": "^5.1.0", - "pupa": "^2.1.1", - "semver": "^7.3.4", - "semver-diff": "^3.1.1", - "xdg-basedir": "^4.0.0" - } - }, - "wrap-ansi": { - "version": "7.0.0", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "dev": true - } - } - }, - "react-is": { - "version": "18.2.0", - "dev": true - }, - "read": { - "version": "1.0.7", - "requires": { - "mute-stream": "~0.0.4" - } - }, - "read-pkg": { - "version": "2.0.0", - "requires": { - "load-json-file": "^2.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^2.0.0" - }, - "dependencies": { - "path-type": { - "version": "2.0.0", - "requires": { - "pify": "^2.0.0" - } - } - } - }, - "read-pkg-up": { - "version": "2.0.0", - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^2.0.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0" - }, - "path-exists": { - "version": "3.0.0" - } - } - }, - "readable-stream": { - "version": "3.6.2", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "readable-web-to-node-stream": { - "version": "3.0.2", - "requires": { - "readable-stream": "^3.6.0" - } - }, - "readdirp": { - "version": "3.6.0", - "requires": { - "picomatch": "^2.2.1" - } - }, - "reduce-flatten": { - "version": "2.0.0" - }, - "reftools": { - "version": "1.1.9", - "dev": true - }, - "regenerate": { - "version": "1.4.2", - "dev": true - }, - "regenerate-unicode-properties": { - "version": "10.1.0", - "dev": true, - "requires": { - "regenerate": "^1.4.2" - } - }, - "regenerator-runtime": { - "version": "0.13.11", - "dev": true - }, - "regenerator-transform": { - "version": "0.15.1", - "dev": true, - "requires": { - "@babel/runtime": "^7.8.4" - } - }, - "regexp.prototype.flags": { - "version": "1.5.0", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "functions-have-names": "^1.2.3" - } - }, - "regexpu-core": { - "version": "5.3.2", - "dev": true, - "requires": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - } - }, - "registry-auth-token": { - "version": "4.2.2", - "dev": true, - "requires": { - "rc": "1.2.8" - } - }, - "registry-url": { - "version": "5.1.0", - "dev": true, - "requires": { - "rc": "^1.2.8" - } - }, - "regjsparser": { - "version": "0.9.1", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "dev": true - } - } - }, - "remove-trailing-slash": { - "version": "0.1.1" - }, - "require-directory": { - "version": "2.1.1" - }, - "require-from-string": { - "version": "2.0.2" - }, - "require-in-the-middle": { - "version": "5.2.0", - "requires": { - "debug": "^4.1.1", - "module-details-from-path": "^1.0.3", - "resolve": "^1.22.1" - } - }, - "require-main-filename": { - "version": "2.0.0" - }, - "resolve": { - "version": "1.22.2", - "requires": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "resolve-cwd": { - "version": "3.0.0", - "dev": true, - "requires": { - "resolve-from": "^5.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "5.0.0", - "dev": true - } - } - }, - "resolve-from": { - "version": "4.0.0", - "dev": true - }, - "resolve.exports": { - "version": "2.0.2", - "dev": true - }, - "responselike": { - "version": "1.0.2", - "dev": true, - "requires": { - "lowercase-keys": "^1.0.0" - } - }, - "restore-cursor": { - "version": "3.1.0", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, - "ret": { - "version": "0.1.15" - }, - "retry": { - "version": "0.13.1" - }, - "retry-as-promised": { - "version": "5.0.0" - }, - "retry-request": { - "version": "4.2.2", - "requires": { - "debug": "^4.1.1", - "extend": "^3.0.2" - } - }, - "reusify": { - "version": "1.0.4", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "run-series": { - "version": "1.1.9" - }, - "safe-buffer": { - "version": "5.1.2" - }, - "safe-json-stringify": { - "version": "1.2.0", - "optional": true - }, - "safe-regex-test": { - "version": "1.0.0", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - } - }, - "safer-buffer": { - "version": "2.1.2" - }, - "sanitize-html": { - "version": "2.11.0", - "requires": { - "deepmerge": "^4.2.2", - "escape-string-regexp": "^4.0.0", - "htmlparser2": "^8.0.0", - "is-plain-object": "^5.0.0", - "parse-srcset": "^1.0.2", - "postcss": "^8.3.11" - }, - "dependencies": { - "escape-string-regexp": { - "version": "4.0.0" - } - } - }, - "sax": { - "version": "1.2.1" - }, - "section-matter": { - "version": "1.0.0", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "kind-of": "^6.0.0" - } - }, - "secure-json-parse": { - "version": "2.7.0" - }, - "selderee": { - "version": "0.6.0", - "requires": { - "parseley": "^0.7.0" - } - }, - "semver": { - "version": "6.3.0" - }, - "semver-diff": { - "version": "3.1.1", - "dev": true, - "requires": { - "semver": "^6.3.0" - } - }, - "send": { - "version": "0.17.1", - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0" - } - } - }, - "depd": { - "version": "1.1.2" - }, - "destroy": { - "version": "1.0.4" - }, - "http-errors": { - "version": "1.7.3", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - }, - "mime": { - "version": "1.6.0" - }, - "ms": { - "version": "2.1.1" - }, - "on-finished": { - "version": "2.3.0", - "requires": { - "ee-first": "1.1.1" - } - }, - "toidentifier": { - "version": "1.0.0" - } - } - }, - "sequelize": { - "version": "6.21.2", - "requires": { - "@types/debug": "^4.1.7", - "@types/validator": "^13.7.1", - "debug": "^4.3.3", - "dottie": "^2.0.2", - "inflection": "^1.13.2", - "lodash": "^4.17.21", - "moment": "^2.29.1", - "moment-timezone": "^0.5.34", - "pg-connection-string": "^2.5.0", - "retry-as-promised": "^5.0.0", - "semver": "^7.3.5", - "sequelize-pool": "^7.1.0", - "toposort-class": "^1.0.1", - "uuid": "^8.3.2", - "validator": "^13.7.0", - "wkx": "^0.5.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.2", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "uuid": { - "version": "8.3.2" - }, - "yallist": { - "version": "4.0.0" - } - } - }, - "sequelize-cli-typescript": { - "version": "3.2.0c", - "requires": { - "bluebird": "^3.5.1", - "cli-color": "^1.2.0", - "fs-extra": "^4.0.2", - "js-beautify": "^1.7.4", - "lodash": "^4.17.4", - "resolve": "^1.5.0", - "umzug": "^2.1.0", - "yargs": "^8.0.2" - }, - "dependencies": { - "bluebird": { - "version": "3.7.2" - }, - "camelcase": { - "version": "4.1.0" - }, - "cliui": { - "version": "3.2.0", - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "fs-extra": { - "version": "4.0.3", - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "get-caller-file": { - "version": "1.0.3" - }, - "is-fullwidth-code-point": { - "version": "2.0.0" - }, - "require-main-filename": { - "version": "1.0.1" - }, - "strip-ansi": { - "version": "3.0.1", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "wrap-ansi": { - "version": "2.1.0", - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - } - }, - "y18n": { - "version": "3.2.2" - }, - "yargs": { - "version": "8.0.2", - "requires": { - "camelcase": "^4.1.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^2.0.0", - "read-pkg-up": "^2.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^7.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.1" - }, - "string-width": { - "version": "2.1.1", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "yargs-parser": { - "version": "7.0.0", - "requires": { - "camelcase": "^4.1.0" - } - } - } - }, - "sequelize-pool": { - "version": "7.1.0" - }, - "serve-static": { - "version": "1.14.1", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - } - }, - "set-blocking": { - "version": "2.0.0" - }, - "setprototypeof": { - "version": "1.1.1" - }, - "shebang-command": { - "version": "2.0.0", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "dev": true - }, - "shimmer": { - "version": "1.2.1" - }, - "should": { - "version": "13.2.3", - "dev": true, - "requires": { - "should-equal": "^2.0.0", - "should-format": "^3.0.3", - "should-type": "^1.4.0", - "should-type-adaptors": "^1.0.1", - "should-util": "^1.0.0" - } - }, - "should-equal": { - "version": "2.0.0", - "dev": true, - "requires": { - "should-type": "^1.4.0" - } - }, - "should-format": { - "version": "3.0.3", - "dev": true, - "requires": { - "should-type": "^1.3.0", - "should-type-adaptors": "^1.0.1" - } - }, - "should-type": { - "version": "1.4.0", - "dev": true - }, - "should-type-adaptors": { - "version": "1.1.0", - "dev": true, - "requires": { - "should-type": "^1.3.0", - "should-util": "^1.0.0" - } - }, - "should-util": { - "version": "1.0.1", - "dev": true - }, - "side-channel": { - "version": "1.0.4", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "sigmund": { - "version": "1.0.1" - }, - "signal-exit": { - "version": "3.0.7" - }, - "sisteransi": { - "version": "1.0.5", - "dev": true - }, - "slack-block-builder": { - "version": "2.7.2" - }, - "slash": { - "version": "3.0.0", - "dev": true - }, - "smart-buffer": { - "version": "4.2.0" - }, - "snakeize": { - "version": "0.1.0" - }, - "socket.io": { - "version": "4.6.2", - "requires": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "debug": "~4.3.2", - "engine.io": "~6.4.2", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - } - }, - "socket.io-adapter": { - "version": "2.5.2", - "requires": { - "ws": "~8.11.0" - }, - "dependencies": { - "ws": { - "version": "8.11.0", - "requires": {} - } - } - }, - "socket.io-parser": { - "version": "4.2.4", - "requires": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - } - }, - "socks": { - "version": "2.7.1", - "requires": { - "ip": "^2.0.0", - "smart-buffer": "^4.2.0" - }, - "dependencies": { - "ip": { - "version": "2.0.0" - } - } - }, - "socks-proxy-agent": { - "version": "5.0.1", - "requires": { - "agent-base": "^6.0.2", - "debug": "4", - "socks": "^2.3.3" - } - }, - "source-map": { - "version": "0.6.1" - }, - "source-map-js": { - "version": "1.0.2" - }, - "source-map-support": { - "version": "0.5.13", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "spdx-correct": { - "version": "3.2.0", - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.3.0" - }, - "spdx-expression-parse": { - "version": "3.0.1", - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.13" - }, - "split2": { - "version": "4.2.0" - }, - "sprintf-js": { - "version": "1.1.2" - }, - "ssri": { - "version": "9.0.1", - "requires": { - "minipass": "^3.1.1" - }, - "dependencies": { - "minipass": { - "version": "3.3.6", - "requires": { - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0" - } - } - }, - "stack-utils": { - "version": "2.0.6", - "dev": true, - "requires": { - "escape-string-regexp": "^2.0.0" - }, - "dependencies": { - "escape-string-regexp": { - "version": "2.0.0", - "dev": true - } - } - }, - "starkbank-ecdsa": { - "version": "1.1.5", - "requires": { - "big-integer": "^1.6.48", - "js-sha256": "^0.9.0" - } - }, - "statuses": { - "version": "1.5.0" - }, - "stream-events": { - "version": "1.0.5", - "requires": { - "stubs": "^3.0.0" - } - }, - "stream-shift": { - "version": "1.0.1" - }, - "streamsearch": { - "version": "1.1.0" - }, - "string_decoder": { - "version": "1.3.0", - "requires": { - "safe-buffer": "~5.2.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1" - } - } - }, - "string-length": { - "version": "4.0.2", - "dev": true, - "requires": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "3.0.1", - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "string.prototype.trim": { - "version": "1.2.7", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "string.prototype.trimend": { - "version": "1.0.6", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "string.prototype.trimstart": { - "version": "1.0.6", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "strip-ansi": { - "version": "6.0.1", - "requires": { - "ansi-regex": "^5.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1" - } - } - }, - "strip-bom": { - "version": "4.0.0", - "dev": true - }, - "strip-bom-string": { - "version": "1.0.0", - "dev": true - }, - "strip-eof": { - "version": "1.0.0" - }, - "strip-final-newline": { - "version": "2.0.0", - "dev": true - }, - "strip-json-comments": { - "version": "3.1.1", - "dev": true - }, - "stripe": { - "version": "10.17.0", - "requires": { - "@types/node": ">=8.1.0", - "qs": "^6.11.0" - } - }, - "strnum": { - "version": "1.0.5" - }, - "strtok3": { - "version": "7.0.0", - "requires": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^5.0.0" - } - }, - "stubs": { - "version": "3.0.0" - }, - "superagent": { - "version": "8.0.9", - "requires": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.4", - "debug": "^4.3.4", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^2.1.2", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.0", - "semver": "^7.3.8" - }, - "dependencies": { - "form-data": { - "version": "4.0.0", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "formidable": { - "version": "2.1.2", - "requires": { - "dezalgo": "^1.0.4", - "hexoid": "^1.0.0", - "once": "^1.4.0", - "qs": "^6.11.0" - } - }, - "lru-cache": { - "version": "6.0.0", - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.2", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0" - } - } - }, - "supertest": { - "version": "6.3.3", - "dev": true, - "requires": { - "methods": "^1.1.2", - "superagent": "^8.0.5" - } - }, - "supports-color": { - "version": "5.5.0", - "requires": { - "has-flag": "^3.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0" - }, - "swagger-ui-dist": { - "version": "4.1.3" - }, - "swagger2openapi": { - "version": "7.0.8", - "dev": true, - "requires": { - "call-me-maybe": "^1.0.1", - "node-fetch": "^2.6.1", - "node-fetch-h2": "^2.3.0", - "node-readfiles": "^0.2.0", - "oas-kit-common": "^1.0.8", - "oas-resolver": "^2.5.6", - "oas-schema-walker": "^1.1.5", - "oas-validator": "^5.0.8", - "reftools": "^1.1.9", - "yaml": "^1.10.0", - "yargs": "^17.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "cliui": { - "version": "8.0.1", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "wrap-ansi": { - "version": "7.0.0", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "y18n": { - "version": "5.0.8", - "dev": true - }, - "yargs": { - "version": "17.7.2", - "dev": true, - "requires": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - } - } - } - }, - "systeminformation": { - "version": "5.18.3", - "optional": true - }, - "table-layout": { - "version": "1.0.2", - "requires": { - "array-back": "^4.0.1", - "deep-extend": "~0.6.0", - "typical": "^5.2.0", - "wordwrapjs": "^4.0.0" - }, - "dependencies": { - "array-back": { - "version": "4.0.2" - }, - "typical": { - "version": "5.2.0" - } - } - }, - "tar": { - "version": "4.4.19", - "requires": { - "chownr": "^1.1.4", - "fs-minipass": "^1.2.7", - "minipass": "^2.9.0", - "minizlib": "^1.3.3", - "mkdirp": "^0.5.5", - "safe-buffer": "^5.2.1", - "yallist": "^3.1.1" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.6", - "requires": { - "minimist": "^1.2.6" - } - }, - "safe-buffer": { - "version": "5.2.1" - } - } - }, - "teeny-request": { - "version": "7.2.0", - "requires": { - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.1", - "stream-events": "^1.0.5", - "uuid": "^8.0.0" - }, - "dependencies": { - "@tootallnate/once": { - "version": "2.0.0" - }, - "http-proxy-agent": { - "version": "5.0.0", - "requires": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - } - }, - "uuid": { - "version": "8.3.2" - } - } - }, - "term-size": { - "version": "2.2.1", - "dev": true - }, - "test-exclude": { - "version": "6.0.0", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, - "text-table": { - "version": "0.2.0", - "dev": true - }, - "thenify": { - "version": "3.3.1", - "requires": { - "any-promise": "^1.0.0" - } - }, - "thenify-all": { - "version": "1.6.0", - "requires": { - "thenify": ">= 3.1.0 < 4" - } - }, - "through2": { - "version": "2.0.5", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.8", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "xtend": { - "version": "4.0.2", - "dev": true - } - } - }, - "timers-ext": { - "version": "0.1.7", - "requires": { - "es5-ext": "~0.10.46", - "next-tick": "1" - } - }, - "tmp": { - "version": "0.2.1", - "dev": true, - "requires": { - "rimraf": "^3.0.0" - } - }, - "tmp-promise": { - "version": "3.0.3", - "dev": true, - "requires": { - "tmp": "^0.2.0" - } - }, - "tmpl": { - "version": "1.0.5", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "dev": true - }, - "to-readable-stream": { - "version": "1.0.0", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "requires": { - "is-number": "^7.0.0" - } - }, - "toidentifier": { - "version": "1.0.1" - }, - "token-types": { - "version": "5.0.1", - "requires": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "dependencies": { - "ieee754": { - "version": "1.2.1" - } - } - }, - "toposort-class": { - "version": "1.0.1" - }, - "touch": { - "version": "3.1.0", - "dev": true, - "requires": { - "nopt": "~1.0.10" - }, - "dependencies": { - "nopt": { - "version": "1.0.10", - "dev": true, - "requires": { - "abbrev": "1" - } - } - } - }, - "tr46": { - "version": "0.0.3" - }, - "ts-jest": { - "version": "29.1.0", - "dev": true, - "requires": { - "bs-logger": "0.x", - "fast-json-stable-stringify": "2.x", - "jest-util": "^29.0.0", - "json5": "^2.2.3", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "7.x", - "yargs-parser": "^21.0.1" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.2", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "dev": true - } - } - }, - "ts-mixer": { - "version": "6.0.3" - }, - "ts-node": { - "version": "10.6.0", - "dev": true, - "requires": { - "@cspotcode/source-map-support": "0.7.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.0", - "yn": "3.1.1" - } - }, - "tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "requires": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "3.0.0" - } - } - }, - "tslib": { - "version": "2.5.3" - }, - "tsutils": { - "version": "3.21.0", - "dev": true, - "requires": { - "tslib": "^1.8.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "dev": true - } - } - }, - "tunnel": { - "version": "0.0.6", - "dev": true - }, - "tv4": { - "version": "1.3.0" - }, - "tx2": { - "version": "1.0.5", - "optional": true, - "requires": { - "json-stringify-safe": "^5.0.1" - } - }, - "type": { - "version": "1.2.0" - }, - "type-check": { - "version": "0.4.0", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-detect": { - "version": "4.0.8", - "dev": true - }, - "type-fest": { - "version": "0.21.3", - "dev": true - }, - "type-is": { - "version": "1.6.18", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typed-array-length": { - "version": "1.0.4", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" - } - }, - "typedarray": { - "version": "0.0.6" - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "typescript": { - "version": "4.9.5" - }, - "typical": { - "version": "4.0.0" - }, - "uid2": { - "version": "0.0.4" - }, - "umzug": { - "version": "2.3.0", - "requires": { - "bluebird": "^3.7.2" - }, - "dependencies": { - "bluebird": { - "version": "3.7.2" - } - } - }, - "unbox-primitive": { - "version": "1.0.2", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - } - }, - "undefsafe": { - "version": "2.0.5", - "dev": true - }, - "undici": { - "version": "5.22.1", - "requires": { - "busboy": "^1.6.0" - } - }, - "unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "dev": true - }, - "unicode-match-property-ecmascript": { - "version": "2.0.0", - "dev": true, - "requires": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - } - }, - "unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "dev": true - }, - "unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "dev": true - }, - "unique-filename": { - "version": "2.0.1", - "requires": { - "unique-slug": "^3.0.0" - } - }, - "unique-slug": { - "version": "3.0.0", - "requires": { - "imurmurhash": "^0.1.4" - } - }, - "unique-string": { - "version": "2.0.0", - "requires": { - "crypto-random-string": "^2.0.0" - } - }, - "universal-github-app-jwt": { - "version": "1.1.1", - "requires": { - "@types/jsonwebtoken": "^9.0.0", - "jsonwebtoken": "^9.0.0" - }, - "dependencies": { - "jsonwebtoken": { - "version": "9.0.0", - "requires": { - "jws": "^3.2.2", - "lodash": "^4.17.21", - "ms": "^2.1.1", - "semver": "^7.3.8" - } - }, - "jwa": { - "version": "1.4.1", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "lru-cache": { - "version": "6.0.0", - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.2", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0" - } - } - }, - "universal-user-agent": { - "version": "6.0.0" - }, - "universalify": { - "version": "0.1.2" - }, - "unleash-client": { - "version": "3.21.0", - "requires": { - "ip": "^1.1.8", - "make-fetch-happen": "^10.2.1", - "murmurhash3js": "^3.0.1", - "semver": "^7.3.8" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.2", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0" - } - } - }, - "unpipe": { - "version": "1.0.0" - }, - "untildify": { - "version": "4.0.0", - "dev": true - }, - "update-browserslist-db": { - "version": "1.0.11", - "dev": true, - "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - } - }, - "update-notifier": { - "version": "4.1.3", - "dev": true, - "requires": { - "boxen": "^4.2.0", - "chalk": "^3.0.0", - "configstore": "^5.0.1", - "has-yarn": "^2.1.0", - "import-lazy": "^2.1.0", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.3.1", - "is-npm": "^4.0.0", - "is-yarn-global": "^0.3.0", - "latest-version": "^5.0.0", - "pupa": "^2.0.1", - "semver-diff": "^3.1.1", - "xdg-basedir": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "uri-js": { - "version": "4.4.1", - "requires": { - "punycode": "^2.1.0" - } - }, - "url": { - "version": "0.10.3", - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2" - } - } - }, - "url-parse-lax": { - "version": "3.0.0", - "dev": true, - "requires": { - "prepend-http": "^2.0.0" - } - }, - "url-search-params-polyfill": { - "version": "7.0.1" - }, - "utf-8-validate": { - "version": "5.0.10", - "requires": { - "node-gyp-build": "^4.3.0" - } - }, - "util-deprecate": { - "version": "1.0.2" - }, - "utils-merge": { - "version": "1.0.1" - }, - "uuid": { - "version": "9.0.0" - }, - "v8-compile-cache-lib": { - "version": "3.0.1", - "dev": true - }, - "v8-to-istanbul": { - "version": "9.1.0", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" - } - }, - "validate-npm-package-license": { - "version": "3.0.4", - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "validator": { - "version": "13.9.0" - }, - "vary": { - "version": "1.1.2" - }, - "verify-github-webhook": { - "version": "1.0.1" - }, - "vizion": { - "version": "2.2.1", - "requires": { - "async": "^2.6.3", - "git-node-fs": "^1.0.0", - "ini": "^1.3.5", - "js-git": "^0.7.8" - } - }, - "vm2": { - "version": "3.9.19", - "requires": { - "acorn": "^8.7.0", - "acorn-walk": "^8.2.0" - } - }, - "walker": { - "version": "1.0.8", - "dev": true, - "requires": { - "makeerror": "1.0.12" - } - }, - "wcwidth": { - "version": "1.0.1", - "dev": true, - "requires": { - "defaults": "^1.0.3" - } - }, - "webidl-conversions": { - "version": "3.0.1" - }, - "whatwg-url": { - "version": "5.0.0", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "which": { - "version": "2.0.2", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-boxed-primitive": { - "version": "1.0.2", - "dev": true, - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - } - }, - "which-module": { - "version": "2.0.1" - }, - "which-typed-array": { - "version": "1.1.9", - "dev": true, - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" - } - }, - "wide-align": { - "version": "1.1.5", - "requires": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "widest-line": { - "version": "3.1.0", - "dev": true, - "requires": { - "string-width": "^4.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - } - } - }, - "wkx": { - "version": "0.5.0", - "requires": { - "@types/node": "*" - } - }, - "word-wrap": { - "version": "1.2.3" - }, - "wordwrapjs": { - "version": "4.0.1", - "requires": { - "reduce-flatten": "^2.0.0", - "typical": "^5.2.0" - }, - "dependencies": { - "typical": { - "version": "5.2.0" - } - } - }, - "wrap-ansi": { - "version": "6.2.0", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4" - }, - "is-fullwidth-code-point": { - "version": "3.0.0" - }, - "string-width": { - "version": "4.2.3", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - } - } - }, - "wrappy": { - "version": "1.0.2" - }, - "write-file-atomic": { - "version": "3.0.3", - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "ws": { - "version": "8.13.0", - "requires": {} - }, - "xdg-basedir": { - "version": "4.0.0" - }, - "xml2js": { - "version": "0.4.19", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" - } - }, - "xmlbuilder": { - "version": "9.0.7" - }, - "xregexp": { - "version": "2.0.0" - }, - "xtend": { - "version": "2.1.2", - "requires": { - "object-keys": "~0.4.0" - }, - "dependencies": { - "object-keys": { - "version": "0.4.0" - } - } - }, - "y18n": { - "version": "4.0.3" - }, - "yallist": { - "version": "3.1.1" - }, - "yaml": { - "version": "1.10.2", - "dev": true - }, - "yamljs": { - "version": "0.3.0", - "requires": { - "argparse": "^1.0.7", - "glob": "^7.0.5" - }, - "dependencies": { - "argparse": { - "version": "1.0.10", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "sprintf-js": { - "version": "1.0.3" - } - } - }, - "yargs": { - "version": "15.4.1", - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0" - }, - "locate-path": { - "version": "5.0.0", - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "requires": { - "p-limit": "^2.2.0" - } - }, - "string-width": { - "version": "4.2.3", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "yargs-parser": { - "version": "18.1.3", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "yargs-parser": { - "version": "21.1.1", - "dev": true - }, - "yn": { - "version": "3.1.1", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0" - }, - "zlib-sync": { - "version": "0.1.8", - "requires": { - "nan": "^2.17.0" - } - } - } -} diff --git a/backend/package.json b/backend/package.json index f7852e3f3b..a90c9770e8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -2,47 +2,43 @@ "name": "app-backend", "description": "Backend", "scripts": { - "start:api": "SERVICE=api TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/api.ts", - "start:api:dev": "nodemon --watch \"src/**/*.ts\" --watch ../services/libs -e ts,json --exec \"npm run start:api\"", - "start:api:dev:local": "set -a && . ./.env.dist.local && . ./.env.override.local && set +a && npm run start:api:dev", - "start:job-generator": "SERVICE=job-generator TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/job-generator.ts", - "start:job-generator:dev": "nodemon --watch \"src/**/*.ts\" --watch ../services/libs -e ts,json --exec \"npm run start:job-generator\"", - "start:job-generator:dev:local": "set -a && . ./.env.dist.local && . ./.env.override.local && set +a && npm run start:job-generator:dev", - "start:nodejs-worker": "SERVICE=nodejs-worker TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/nodejs-worker", - "start:nodejs-worker:dev": "nodemon --watch \"src/**/*.ts\" --watch ../services/libs -e ts,json --exec \"npm run start:nodejs-worker\"", - "start:nodejs-worker:dev:local": "set -a && . ./.env.dist.local && . ./.env.override.local && set +a && npm run start:nodejs-worker:dev", - "start:discord-ws": "SERVICE=discord-ws TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/discord-ws.ts", - "start:discord-ws:dev": "nodemon --watch \"src/**/*.ts\" --watch ../services/libs -e ts,json --exec \"npm run start:discord-ws\"", - "start:discord-ws:dev:local": "set -a && . ./.env.dist.local && . ./.env.override.local && set +a && npm run start:discord-ws:dev", - "build": "tsc && npm run build:documentation && cp package*json dist/ && cp .sequelizerc dist/.sequelizerc ", - "test": "../scripts/cli scaffold up-test && jest --clearCache && set -a && . ./.env.dist.local && . ./.env.test && set +a && NODE_ENV=test SERVICE=test jest --runInBand --verbose --forceExit", - "build:documentation": "copyfiles --flat ./src/documentation/openapi.json ./dist/documentation/", - "db:create:test": "npx ts-node ./src/database/initializers/create test", - "db:create:dev:source": "ts-node ./src/database/initializers/create dev", - "db:seed:test": "npx ts-node ./src/database/initializers/seed test", - "db:seed:dev": "npx ts-node ./src/database/initializers/seed dev", - "db:publish": "bash ./util/publish-db.sh", - "docs": "bash ./util/publish-docs.sh", - "sequelize-cli:source": "npm run build && npx sequelize --config src/database/sequelize-cli-config.ts --migrations-source-path src/database/migrations", + "start:api": "SERVICE=api TS_NODE_TRANSPILE_ONLY=true tsx src/bin/api.ts", + "start:api:dev": "nodemon --watch \"src/**/*.ts\" --watch ../services/libs -e ts,json --exec \"pnpm run start:api\"", + "start:api:dev:local": "set -a && . ./.env.dist.local && . ./.env.override.local && set +a && pnpm run start:api:dev", + "start:job-generator": "SERVICE=job-generator TS_NODE_TRANSPILE_ONLY=true tsx src/bin/job-generator.ts", + "start:job-generator:dev": "nodemon --watch \"src/**/*.ts\" --watch ../services/libs -e ts,json --exec \"pnpm run start:job-generator\"", + "start:job-generator:dev:local": "set -a && . ./.env.dist.local && . ./.env.override.local && set +a && pnpm run start:job-generator:dev", + "build": "tsc && cp package*json dist/ && cp .sequelizerc dist/.sequelizerc ", + "sequelize-cli:source": "pnpm run build && npx sequelize --config src/database/sequelize-cli-config.ts --migrations-source-path src/database/migrations", "sequelize-cli:build": "npx sequelize --config database/sequelize-cli-config.js --migrations-compiled-path database/migrations", - "stripe:login": "stripe login", - "stripe:start": "stripe listen --forward-to localhost:8080/api/plan/stripe/webhook", "lint": "eslint .", "format": "prettier --write .", "format-check": "prettier --check .", "tsc-check": "tsc --noEmit", - "script:process-integration": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/process-integration.ts", - "script:process-stream": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/process-stream.ts", - "script:continue-run": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/continue-run.ts", - "script:change-tenant-plan": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/change-tenant-plan.ts", - "script:process-webhook": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/process-webhook.ts", - "script:trigger-webhook": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/trigger-webhook.ts", - "script:send-weekly-analytics-email": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/send-weekly-analytics-email.ts", - "script:unleash-init": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/unleash-init.ts", - "script:enrich-members-organizations": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/enrich-members-and-organizations.ts", - "script:enrich-organizations": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/enrich-organizations-synchronous.ts", - "script:generate-merge-suggestions": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/generate-merge-suggestions.ts", - "script:merge-organizations": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/merge-organizations.ts" + "script": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx", + "script:continue-run": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/continue-run.ts", + "script:trigger-webhook": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/trigger-webhook.ts", + "script:send-weekly-analytics-email": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/send-weekly-analytics-email.ts", + "script:merge-organizations": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/merge-organizations.ts", + "script:refresh-materialized-views": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/refresh-materialized-views.ts", + "script:unmerge-members": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/unmerge-members.ts", + "script:member-unmerge-testing": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/member-unmerge-testing.ts", + "script:merge-similar-organizations": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/merge-similar-organizations.ts", + "script:cache-dashboard": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/cache-dashboard.ts", + "script:purge-tenants-and-data": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/purge-tenants-and-data.ts", + "script:import-lfx-memberships": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/import-lfx-memberships.ts", + "script:fix-missing-org-displayName": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/fix-missing-org-displayName.ts", + "script:refreshGithubRepoSettings": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/refresh-github-repo-settings.ts", + "script:fix-duplicate-members": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/fix-duplicate-members.ts", + "script:fix-members-activities-after-unaffilation": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/fix-members-activities-after-unaffilation.ts", + "script:process-bot-members": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/process-bot-members.ts", + "script:backfill-email-domain-member-organization-dates": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true tsx src/bin/scripts/backfill-email-domain-member-organization-dates.ts" + }, + "lint-staged": { + "**/*.ts": [ + "eslint", + "prettier --write" + ] }, "dependencies": { "@aws-sdk/client-comprehend": "^3.159.0", @@ -51,27 +47,29 @@ "@aws-sdk/s3-request-presigner": "^3.229.0", "@aws-sdk/url-parser": "^3.226.0", "@aws-sdk/util-format-url": "^3.226.0", - "@crowd/alerting": "file:../services/libs/alerting", - "@crowd/common": "file:../services/libs/common", - "@crowd/integrations": "file:../services/libs/integrations", - "@crowd/logging": "file:../services/libs/logging", - "@crowd/opensearch": "file:../services/libs/opensearch", - "@crowd/redis": "file:../services/libs/redis", - "@crowd/sqs": "file:../services/libs/sqs", - "@crowd/tracing": "file:../services/libs/tracing", - "@crowd/types": "file:../services/libs/types", - "@cubejs-client/core": "^0.30.4", + "@crowd/audit-logs": "workspace:*", + "@crowd/common": "workspace:*", + "@crowd/common_services": "workspace:*", + "@crowd/data-access-layer": "workspace:*", + "@crowd/integrations": "workspace:*", + "@crowd/logging": "workspace:*", + "@crowd/nango": "workspace:*", + "@crowd/opensearch": "workspace:*", + "@crowd/queue": "workspace:*", + "@crowd/redis": "workspace:*", + "@crowd/slack": "workspace:*", + "@crowd/snowflake": "workspace:*", + "@crowd/telemetry": "workspace:*", + "@crowd/temporal": "workspace:*", + "@crowd/types": "workspace:*", "@google-cloud/storage": "5.3.0", "@octokit/auth-app": "^3.6.1", + "@octokit/core": "^6.1.2", "@octokit/graphql": "^4.8.0", "@octokit/request": "^5.6.3", - "@opensearch-project/opensearch": "^1.2.0", - "@pm2/io": "^5.0.0", - "@sendgrid/eventwebhook": "^7.7.0", - "@sendgrid/mail": "7.2.6", + "@octokit/rest": "^22.0.0", + "@opensearch-project/opensearch": "^2.11.0", "@slack/web-api": "^6.7.2", - "@superfaceai/one-sdk": "^1.3.0", - "@superfaceai/passport-twitter-oauth2": "^1.0.0", "analytics-node": "^6.2.0", "aws-sdk": "2.814.0", "axios": "^0.27.2", @@ -91,12 +89,13 @@ "cron-time-generator": "^1.3.0", "crowd-sentiment": "^1.1.7", "crypto-js": "^4.1.1", - "discord.js": "^14.7.1", + "csv-parse": "^5.5.6", "dotenv": "8.2.0", "dotenv-expand": "^8.0.3", "emoji-dictionary": "^1.0.11", "erlpack": "^0.1.4", "express": "4.17.1", + "express-oauth2-jwt-bearer": "^1.7.4", "express-rate-limit": "6.5.1", "fast-levenshtein": "^3.0.0", "formidable-serverless": "1.1.1", @@ -113,44 +112,39 @@ "mv": "2.1.1", "node-fetch": "^2.6.7", "omit-deep-by-values": "^1.0.2", - "openapi-comment-parser": "^1.0.0", "passport": "0.6.0", "passport-facebook": "3.0.0", "passport-github2": "^0.1.12", "passport-google-oauth": "2.0.0", "passport-google-oauth20": "^2.0.0", "passport-slack": "0.0.7", - "peopledatalabs": "^5.0.3", + "peopledatalabs": "~6.1.5", "pg": "^8.7.3", - "pm2": "^5.2.0", + "pg-promise": "^11.4.3", "sanitize-html": "^2.7.1", - "sequelize": "6.21.2", + "sequelize": "6.37.8", "sequelize-cli-typescript": "^3.2.0-c", "slack-block-builder": "^2.7.2", "socket.io": "^4.5.4", - "stripe": "^10.0.0", "superagent": "^8.0.0", - "swagger-ui-dist": "4.1.3", "tsconfig-paths": "^4.2.0", - "unleash-client": "^3.18.1", "utf-8-validate": "^5.0.10", "uuid": "^9.0.0", "validator": "^13.7.0", "verify-github-webhook": "^1.0.1", - "zlib-sync": "^0.1.8" + "zlib-sync": "^0.1.8", + "zod": "^4.3.6" }, "private": true, "devDependencies": { - "@babel/core": "^7.21.8", - "@babel/preset-env": "^7.21.5", - "@babel/preset-typescript": "^7.21.5", + "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/bunyan": "^1.8.8", "@types/bunyan-format": "^0.2.5", "@types/config": "^3.3.0", "@types/cron": "^2.0.0", + "@types/express": "^4.17.17", "@types/html-to-text": "^8.1.1", - "@types/jest": "^29.5.1", - "@types/node": "^17.0.21", + "@types/node": "^20.8.2", "@types/sanitize-html": "^2.6.2", "@types/superagent": "^4.1.15", "@types/uuid": "^9.0.2", @@ -158,20 +152,18 @@ "@typescript-eslint/parser": "^5.17.0", "copyfiles": "2.4.1", "cross-env": "7.0.2", + "deep-object-diff": "^1.1.9", "eslint": "^8.12.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^16.1.4", "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.25.4", - "eslint-plugin-openapi": "^0.0.4", - "jest": "^29.5.0", - "node-mocks-http": "1.9.0", + "lint-staged": "^15.4.3", "nodemon": "2.0.4", - "prettier": "^2.5.1", + "prettier": "^3.3.3", "rdme": "^7.2.0", - "supertest": "^6.2.2", - "ts-jest": "^29.1.0", - "ts-node": "10.6.0", - "typescript": "^4.7.4" - } + "tsx": "^4.7.1", + "typescript": "^5.6.3" + }, + "packageManager": "pnpm@9.15.0" } diff --git a/backend/schema.sql b/backend/schema.sql deleted file mode 100644 index b82a2e3164..0000000000 --- a/backend/schema.sql +++ /dev/null @@ -1,1381 +0,0 @@ --- --- PostgreSQL database dump --- - --- Dumped from database version 13.6 --- Dumped by pg_dump version 13.6 - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - -SET default_tablespace = ''; - -SET default_table_access_method = heap; - --- --- Name: activities; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.activities ( - id uuid NOT NULL, - type text NOT NULL, - "timestamp" timestamp with time zone NOT NULL, - platform text NOT NULL, - info jsonb DEFAULT '{}'::jsonb, - "crowdInfo" jsonb DEFAULT '{}'::jsonb, - "isKeyAction" boolean DEFAULT false NOT NULL, - score integer DEFAULT 2, - "sourceId" text NOT NULL, - "sourceParentId" character varying(255), - "importHash" character varying(255), - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "deletedAt" timestamp with time zone, - "communityMemberId" uuid NOT NULL, - "conversationId" uuid, - "parentId" uuid, - "tenantId" uuid NOT NULL, - "createdById" uuid, - "updatedById" uuid -); - - --- --- Name: auditLogs; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public."auditLogs" ( - id uuid NOT NULL, - "entityName" character varying(255) NOT NULL, - "entityId" character varying(255) NOT NULL, - "tenantId" uuid, - action character varying(32) NOT NULL, - "createdById" uuid, - "createdByEmail" character varying(255), - "timestamp" timestamp with time zone NOT NULL, - "values" json NOT NULL -); - - --- --- Name: automationExecutions; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public."automationExecutions" ( - id uuid NOT NULL, - "automationId" uuid NOT NULL, - type character varying(80) NOT NULL, - "tenantId" uuid NOT NULL, - trigger character varying(80) NOT NULL, - state character varying(80) NOT NULL, - error json, - "executedAt" timestamp with time zone NOT NULL, - "eventId" character varying(255) NOT NULL, - payload json NOT NULL -); - - --- --- Name: automations; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.automations ( - id uuid NOT NULL, - type character varying(80) NOT NULL, - "tenantId" uuid NOT NULL, - trigger character varying(80) NOT NULL, - settings jsonb NOT NULL, - state character varying(80) NOT NULL, - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "createdById" uuid, - "updatedById" uuid -); - - --- --- Name: communityMemberNoMerge; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public."communityMemberNoMerge" ( - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "communityMemberId" uuid NOT NULL, - "noMergeId" uuid NOT NULL -); - - --- --- Name: communityMemberTags; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public."communityMemberTags" ( - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "communityMemberId" uuid NOT NULL, - "tagId" uuid NOT NULL -); - - --- --- Name: communityMemberToMerge; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public."communityMemberToMerge" ( - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "communityMemberId" uuid NOT NULL, - "toMergeId" uuid NOT NULL -); - - --- --- Name: communityMembers; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public."communityMembers" ( - id uuid NOT NULL, - username jsonb NOT NULL, - type character varying(255) DEFAULT 'member'::character varying NOT NULL, - info jsonb DEFAULT '{}'::jsonb, - "crowdInfo" jsonb DEFAULT '{}'::jsonb, - email text, - score integer DEFAULT '-1'::integer, - bio text, - organisation text, - location text, - signals text, - "joinedAt" timestamp with time zone NOT NULL, - "importHash" character varying(255), - reach jsonb DEFAULT '{"total": -1}'::jsonb, - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "deletedAt" timestamp with time zone, - "tenantId" uuid NOT NULL, - "createdById" uuid, - "updatedById" uuid -); - - --- --- Name: conversationSettings; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public."conversationSettings" ( - id uuid NOT NULL, - "customUrl" text, - "logoUrl" text, - "faviconUrl" text, - theme jsonb, - "autoPublish" jsonb, - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "tenantId" uuid, - "createdById" uuid, - "updatedById" uuid -); - - --- --- Name: conversations; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.conversations ( - id uuid NOT NULL, - title text NOT NULL, - slug text NOT NULL, - published boolean DEFAULT false NOT NULL, - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "tenantId" uuid NOT NULL, - "createdById" uuid, - "updatedById" uuid -); - - --- --- Name: eagleEyeContents; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public."eagleEyeContents" ( - id uuid NOT NULL, - "sourceId" text NOT NULL, - "vectorId" text NOT NULL, - status character varying(255) DEFAULT NULL::character varying, - title text NOT NULL, - username text NOT NULL, - url text NOT NULL, - text text, - "timestamp" timestamp with time zone NOT NULL, - platform text NOT NULL, - keywords text[], - "similarityScore" double precision, - "userAttributes" jsonb, - "postAttributes" jsonb, - "importHash" character varying(255), - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "deletedAt" timestamp with time zone, - "tenantId" uuid NOT NULL, - "createdById" uuid, - "updatedById" uuid -); - - --- --- Name: files; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.files ( - id uuid NOT NULL, - "belongsTo" character varying(255) NOT NULL, - "belongsToId" character varying(255) NOT NULL, - "belongsToColumn" character varying(255) NOT NULL, - name character varying(2083) NOT NULL, - "sizeInBytes" integer, - "privateUrl" character varying(2083), - "publicUrl" character varying(2083), - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "deletedAt" timestamp with time zone, - "tenantId" uuid, - "createdById" uuid, - "updatedById" uuid -); - - --- --- Name: integrations; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.integrations ( - id uuid NOT NULL, - platform text, - status text, - "limitCount" integer, - "limitLastResetAt" timestamp with time zone, - token text, - "refreshToken" text, - settings jsonb DEFAULT '{}'::jsonb, - "integrationIdentifier" text, - "importHash" character varying(255), - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "deletedAt" timestamp with time zone, - "tenantId" uuid NOT NULL, - "createdById" uuid, - "updatedById" uuid -); - - --- --- Name: microservices; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.microservices ( - id uuid NOT NULL, - init boolean DEFAULT false NOT NULL, - running boolean DEFAULT false NOT NULL, - type text NOT NULL, - variant text DEFAULT 'default'::text, - settings jsonb DEFAULT '{}'::jsonb NOT NULL, - "importHash" character varying(255), - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "tenantId" uuid NOT NULL, - "createdById" uuid, - "updatedById" uuid -); - - --- --- Name: reports; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.reports ( - id uuid NOT NULL, - public boolean DEFAULT false NOT NULL, - name text NOT NULL, - "importHash" character varying(255), - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "deletedAt" timestamp with time zone, - "tenantId" uuid NOT NULL, - "createdById" uuid, - "updatedById" uuid -); - - --- --- Name: settings; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.settings ( - id character varying(255) DEFAULT 'default'::character varying NOT NULL, - website character varying(255), - "backgroundImageUrl" character varying(1024), - "logoUrl" character varying(1024), - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "deletedAt" timestamp with time zone, - "tenantId" uuid NOT NULL, - "createdById" uuid, - "updatedById" uuid -); - - --- --- Name: tags; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.tags ( - id uuid NOT NULL, - name text NOT NULL, - "importHash" character varying(255), - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "deletedAt" timestamp with time zone, - "tenantId" uuid NOT NULL, - "createdById" uuid, - "updatedById" uuid -); - - --- --- Name: tenantUsers; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public."tenantUsers" ( - id uuid NOT NULL, - roles text[], - "invitationToken" character varying(255), - status character varying(255) NOT NULL, - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "deletedAt" timestamp with time zone, - "tenantId" uuid, - "userId" uuid NOT NULL, - "createdById" uuid, - "updatedById" uuid -); - - --- --- Name: tenants; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.tenants ( - id uuid NOT NULL, - name character varying(255) NOT NULL, - url character varying(50) NOT NULL, - plan character varying(255) DEFAULT 'free'::character varying NOT NULL, - "planStatus" character varying(255) DEFAULT 'active'::character varying NOT NULL, - "planStripeCustomerId" character varying(255), - "planUserId" uuid, - "onboardedAt" timestamp with time zone, - "hasSampleData" boolean DEFAULT false NOT NULL, - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "deletedAt" timestamp with time zone, - "createdById" uuid, - "updatedById" uuid -); - - --- --- Name: users; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.users ( - id uuid NOT NULL, - "fullName" character varying(255), - "firstName" character varying(80), - password character varying(255), - "emailVerified" boolean DEFAULT false NOT NULL, - "emailVerificationToken" character varying(255), - "emailVerificationTokenExpiresAt" timestamp with time zone, - provider character varying(255), - "providerId" character varying(2024), - "passwordResetToken" character varying(255), - "passwordResetTokenExpiresAt" timestamp with time zone, - "lastName" character varying(175), - "phoneNumber" character varying(24), - email character varying(255) NOT NULL, - "jwtTokenInvalidBefore" timestamp with time zone, - "importHash" character varying(255), - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "deletedAt" timestamp with time zone, - "createdById" uuid, - "updatedById" uuid -); - - --- --- Name: widgets; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.widgets ( - id uuid NOT NULL, - type text NOT NULL, - title text, - settings jsonb, - cache jsonb, - "importHash" character varying(255), - "createdAt" timestamp with time zone NOT NULL, - "updatedAt" timestamp with time zone NOT NULL, - "deletedAt" timestamp with time zone, - "reportId" uuid, - "tenantId" uuid NOT NULL, - "createdById" uuid, - "updatedById" uuid -); - - --- --- Name: activities activities_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.activities - ADD CONSTRAINT activities_pkey PRIMARY KEY (id); - - --- --- Name: auditLogs auditLogs_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."auditLogs" - ADD CONSTRAINT "auditLogs_pkey" PRIMARY KEY (id); - - --- --- Name: automationExecutions automationExecutions_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."automationExecutions" - ADD CONSTRAINT "automationExecutions_pkey" PRIMARY KEY (id); - - --- --- Name: automations automations_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.automations - ADD CONSTRAINT automations_pkey PRIMARY KEY (id); - - --- --- Name: communityMemberNoMerge communityMemberNoMerge_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."communityMemberNoMerge" - ADD CONSTRAINT "communityMemberNoMerge_pkey" PRIMARY KEY ("communityMemberId", "noMergeId"); - - --- --- Name: communityMemberTags communityMemberTags_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."communityMemberTags" - ADD CONSTRAINT "communityMemberTags_pkey" PRIMARY KEY ("communityMemberId", "tagId"); - - --- --- Name: communityMemberToMerge communityMemberToMerge_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."communityMemberToMerge" - ADD CONSTRAINT "communityMemberToMerge_pkey" PRIMARY KEY ("communityMemberId", "toMergeId"); - - --- --- Name: communityMembers communityMembers_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."communityMembers" - ADD CONSTRAINT "communityMembers_pkey" PRIMARY KEY (id); - - --- --- Name: conversationSettings conversationSettings_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."conversationSettings" - ADD CONSTRAINT "conversationSettings_pkey" PRIMARY KEY (id); - - --- --- Name: conversations conversations_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.conversations - ADD CONSTRAINT conversations_pkey PRIMARY KEY (id); - - --- --- Name: eagleEyeContents eagleEyeContents_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."eagleEyeContents" - ADD CONSTRAINT "eagleEyeContents_pkey" PRIMARY KEY (id); - - --- --- Name: files files_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.files - ADD CONSTRAINT files_pkey PRIMARY KEY (id); - - --- --- Name: integrations integrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.integrations - ADD CONSTRAINT integrations_pkey PRIMARY KEY (id); - - --- --- Name: microservices microservices_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.microservices - ADD CONSTRAINT microservices_pkey PRIMARY KEY (id); - - --- --- Name: reports reports_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.reports - ADD CONSTRAINT reports_pkey PRIMARY KEY (id); - - --- --- Name: settings settings_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settings - ADD CONSTRAINT settings_pkey PRIMARY KEY (id); - - --- --- Name: tags tags_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.tags - ADD CONSTRAINT tags_pkey PRIMARY KEY (id); - - --- --- Name: tenantUsers tenantUsers_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."tenantUsers" - ADD CONSTRAINT "tenantUsers_pkey" PRIMARY KEY (id); - - --- --- Name: tenants tenants_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.tenants - ADD CONSTRAINT tenants_pkey PRIMARY KEY (id); - - --- --- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users - ADD CONSTRAINT users_pkey PRIMARY KEY (id); - - --- --- Name: widgets widgets_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.widgets - ADD CONSTRAINT widgets_pkey PRIMARY KEY (id); - - --- --- Name: activities_community_member_id_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX activities_community_member_id_tenant_id ON public.activities USING btree ("communityMemberId", "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: activities_conversation_id_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX activities_conversation_id_tenant_id ON public.activities USING btree ("conversationId", "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: activities_deleted_at; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX activities_deleted_at ON public.activities USING btree ("deletedAt"); - - --- --- Name: activities_import_hash_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX activities_import_hash_tenant_id ON public.activities USING btree ("importHash", "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: activities_parent_id_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX activities_parent_id_tenant_id ON public.activities USING btree ("parentId", "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: activities_platform_tenant_id_type_timestamp; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX activities_platform_tenant_id_type_timestamp ON public.activities USING btree (platform, "tenantId", type, "timestamp") WHERE ("deletedAt" IS NULL); - - --- --- Name: activities_source_id_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX activities_source_id_tenant_id ON public.activities USING btree ("sourceId", "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: activities_source_parent_id_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX activities_source_parent_id_tenant_id ON public.activities USING btree ("sourceParentId", "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: activities_timestamp_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX activities_timestamp_tenant_id ON public.activities USING btree ("timestamp", "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: automation_executions_automation_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX automation_executions_automation_id ON public."automationExecutions" USING btree ("automationId"); - - --- --- Name: automations_type_tenant_id_trigger_state; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX automations_type_tenant_id_trigger_state ON public.automations USING btree (type, "tenantId", trigger, state); - - --- --- Name: community_members_created_at_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX community_members_created_at_tenant_id ON public."communityMembers" USING btree ("createdAt", "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: community_members_email_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX community_members_email_tenant_id ON public."communityMembers" USING btree (email, "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: community_members_import_hash_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX community_members_import_hash_tenant_id ON public."communityMembers" USING btree ("importHash", "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: community_members_joined_at_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX community_members_joined_at_tenant_id ON public."communityMembers" USING btree ("joinedAt", "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: community_members_location_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX community_members_location_tenant_id ON public."communityMembers" USING btree (location, "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: community_members_organisation_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX community_members_organisation_tenant_id ON public."communityMembers" USING btree (organisation, "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: community_members_score_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX community_members_score_tenant_id ON public."communityMembers" USING btree (score, "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: community_members_signals_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX community_members_signals_tenant_id ON public."communityMembers" USING btree (signals, "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: community_members_type_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX community_members_type_tenant_id ON public."communityMembers" USING btree (type, "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: community_members_username; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX community_members_username ON public."communityMembers" USING gin (username jsonb_path_ops); - - --- --- Name: conversations_slug_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX conversations_slug_tenant_id ON public.conversations USING btree (slug, "tenantId"); - - --- --- Name: discord; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX discord ON public."communityMembers" USING btree (((username ->> 'discord'::text))); - - --- --- Name: eagle_eye_contents_import_hash_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX eagle_eye_contents_import_hash_tenant_id ON public."eagleEyeContents" USING btree ("importHash", "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: eagle_eye_contents_platform_tenant_id_timestamp; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX eagle_eye_contents_platform_tenant_id_timestamp ON public."eagleEyeContents" USING btree (platform, "tenantId", "timestamp") WHERE ("deletedAt" IS NULL); - - --- --- Name: eagle_eye_contents_status_tenant_id_timestamp; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX eagle_eye_contents_status_tenant_id_timestamp ON public."eagleEyeContents" USING btree (status, "tenantId", "timestamp") WHERE ("deletedAt" IS NULL); - - --- --- Name: eagle_eye_contents_tenant_id_timestamp; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX eagle_eye_contents_tenant_id_timestamp ON public."eagleEyeContents" USING btree ("tenantId", "timestamp") WHERE ("deletedAt" IS NULL); - - --- --- Name: github; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX github ON public."communityMembers" USING btree (((username ->> 'github'::text))); - - --- --- Name: integrations_import_hash_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX integrations_import_hash_tenant_id ON public.integrations USING btree ("importHash", "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: integrations_integration_identifier; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX integrations_integration_identifier ON public.integrations USING btree ("integrationIdentifier"); - - --- --- Name: microservices_import_hash_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX microservices_import_hash_tenant_id ON public.microservices USING btree ("importHash", "tenantId"); - - --- --- Name: microservices_type_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX microservices_type_tenant_id ON public.microservices USING btree (type, "tenantId"); - - --- --- Name: reports_import_hash_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX reports_import_hash_tenant_id ON public.reports USING btree ("importHash", "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: slack; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX slack ON public."communityMembers" USING btree (((username ->> 'slack'::text))); - - --- --- Name: tags_import_hash_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX tags_import_hash_tenant_id ON public.tags USING btree ("importHash", "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: tags_name_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX tags_name_tenant_id ON public.tags USING btree (name, "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: tenants_url; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX tenants_url ON public.tenants USING btree (url) WHERE ("deletedAt" IS NULL); - - --- --- Name: twitter; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX twitter ON public."communityMembers" USING btree (((username ->> 'twitter'::text))); - - --- --- Name: users_email; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX users_email ON public.users USING btree (email) WHERE ("deletedAt" IS NULL); - - --- --- Name: users_import_hash; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX users_import_hash ON public.users USING btree ("importHash") WHERE ("deletedAt" IS NULL); - - --- --- Name: widgets_import_hash_tenant_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX widgets_import_hash_tenant_id ON public.widgets USING btree ("importHash", "tenantId") WHERE ("deletedAt" IS NULL); - - --- --- Name: activities activities_communityMemberId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.activities - ADD CONSTRAINT "activities_communityMemberId_fkey" FOREIGN KEY ("communityMemberId") REFERENCES public."communityMembers"(id) ON UPDATE CASCADE ON DELETE CASCADE; - - --- --- Name: activities activities_conversationId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.activities - ADD CONSTRAINT "activities_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES public.conversations(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: activities activities_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.activities - ADD CONSTRAINT "activities_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: activities activities_parentId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.activities - ADD CONSTRAINT "activities_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES public.activities(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: activities activities_tenantId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.activities - ADD CONSTRAINT "activities_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES public.tenants(id) ON UPDATE CASCADE; - - --- --- Name: activities activities_updatedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.activities - ADD CONSTRAINT "activities_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: automationExecutions automationExecutions_automationId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."automationExecutions" - ADD CONSTRAINT "automationExecutions_automationId_fkey" FOREIGN KEY ("automationId") REFERENCES public.automations(id) ON UPDATE CASCADE; - - --- --- Name: automationExecutions automationExecutions_tenantId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."automationExecutions" - ADD CONSTRAINT "automationExecutions_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES public.tenants(id) ON UPDATE CASCADE; - - --- --- Name: automations automations_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.automations - ADD CONSTRAINT "automations_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: automations automations_tenantId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.automations - ADD CONSTRAINT "automations_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES public.tenants(id) ON UPDATE CASCADE; - - --- --- Name: automations automations_updatedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.automations - ADD CONSTRAINT "automations_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: communityMemberNoMerge communityMemberNoMerge_communityMemberId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."communityMemberNoMerge" - ADD CONSTRAINT "communityMemberNoMerge_communityMemberId_fkey" FOREIGN KEY ("communityMemberId") REFERENCES public."communityMembers"(id) ON UPDATE CASCADE ON DELETE CASCADE; - - --- --- Name: communityMemberNoMerge communityMemberNoMerge_noMergeId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."communityMemberNoMerge" - ADD CONSTRAINT "communityMemberNoMerge_noMergeId_fkey" FOREIGN KEY ("noMergeId") REFERENCES public."communityMembers"(id) ON UPDATE CASCADE ON DELETE CASCADE; - - --- --- Name: communityMemberTags communityMemberTags_communityMemberId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."communityMemberTags" - ADD CONSTRAINT "communityMemberTags_communityMemberId_fkey" FOREIGN KEY ("communityMemberId") REFERENCES public."communityMembers"(id) ON UPDATE CASCADE ON DELETE CASCADE; - - --- --- Name: communityMemberTags communityMemberTags_tagId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."communityMemberTags" - ADD CONSTRAINT "communityMemberTags_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES public.tags(id) ON UPDATE CASCADE ON DELETE CASCADE; - - --- --- Name: communityMemberToMerge communityMemberToMerge_communityMemberId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."communityMemberToMerge" - ADD CONSTRAINT "communityMemberToMerge_communityMemberId_fkey" FOREIGN KEY ("communityMemberId") REFERENCES public."communityMembers"(id) ON UPDATE CASCADE ON DELETE CASCADE; - - --- --- Name: communityMemberToMerge communityMemberToMerge_toMergeId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."communityMemberToMerge" - ADD CONSTRAINT "communityMemberToMerge_toMergeId_fkey" FOREIGN KEY ("toMergeId") REFERENCES public."communityMembers"(id) ON UPDATE CASCADE ON DELETE CASCADE; - - --- --- Name: communityMembers communityMembers_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."communityMembers" - ADD CONSTRAINT "communityMembers_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: communityMembers communityMembers_tenantId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."communityMembers" - ADD CONSTRAINT "communityMembers_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES public.tenants(id) ON UPDATE CASCADE; - - --- --- Name: communityMembers communityMembers_updatedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."communityMembers" - ADD CONSTRAINT "communityMembers_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: conversationSettings conversationSettings_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."conversationSettings" - ADD CONSTRAINT "conversationSettings_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: conversationSettings conversationSettings_tenantId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."conversationSettings" - ADD CONSTRAINT "conversationSettings_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES public.tenants(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: conversationSettings conversationSettings_updatedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."conversationSettings" - ADD CONSTRAINT "conversationSettings_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: conversations conversations_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.conversations - ADD CONSTRAINT "conversations_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: conversations conversations_tenantId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.conversations - ADD CONSTRAINT "conversations_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES public.tenants(id) ON UPDATE CASCADE; - - --- --- Name: conversations conversations_updatedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.conversations - ADD CONSTRAINT "conversations_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: eagleEyeContents eagleEyeContents_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."eagleEyeContents" - ADD CONSTRAINT "eagleEyeContents_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: eagleEyeContents eagleEyeContents_tenantId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."eagleEyeContents" - ADD CONSTRAINT "eagleEyeContents_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES public.tenants(id) ON UPDATE CASCADE; - - --- --- Name: eagleEyeContents eagleEyeContents_updatedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."eagleEyeContents" - ADD CONSTRAINT "eagleEyeContents_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: files files_belongsToId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.files - ADD CONSTRAINT "files_belongsToId_fkey" FOREIGN KEY ("belongsToId") REFERENCES public.settings(id) ON UPDATE CASCADE ON DELETE CASCADE; - - --- --- Name: files files_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.files - ADD CONSTRAINT "files_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: files files_tenantId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.files - ADD CONSTRAINT "files_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES public.tenants(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: files files_updatedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.files - ADD CONSTRAINT "files_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: integrations integrations_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.integrations - ADD CONSTRAINT "integrations_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: integrations integrations_tenantId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.integrations - ADD CONSTRAINT "integrations_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES public.tenants(id) ON UPDATE CASCADE; - - --- --- Name: integrations integrations_updatedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.integrations - ADD CONSTRAINT "integrations_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: microservices microservices_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.microservices - ADD CONSTRAINT "microservices_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: microservices microservices_tenantId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.microservices - ADD CONSTRAINT "microservices_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES public.tenants(id) ON UPDATE CASCADE; - - --- --- Name: microservices microservices_updatedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.microservices - ADD CONSTRAINT "microservices_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: reports reports_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.reports - ADD CONSTRAINT "reports_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: reports reports_tenantId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.reports - ADD CONSTRAINT "reports_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES public.tenants(id) ON UPDATE CASCADE; - - --- --- Name: reports reports_updatedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.reports - ADD CONSTRAINT "reports_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: settings settings_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settings - ADD CONSTRAINT "settings_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: settings settings_tenantId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settings - ADD CONSTRAINT "settings_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES public.tenants(id) ON UPDATE CASCADE; - - --- --- Name: settings settings_updatedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.settings - ADD CONSTRAINT "settings_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: tags tags_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.tags - ADD CONSTRAINT "tags_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: tags tags_tenantId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.tags - ADD CONSTRAINT "tags_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES public.tenants(id) ON UPDATE CASCADE; - - --- --- Name: tags tags_updatedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.tags - ADD CONSTRAINT "tags_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: tenantUsers tenantUsers_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."tenantUsers" - ADD CONSTRAINT "tenantUsers_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: tenantUsers tenantUsers_tenantId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."tenantUsers" - ADD CONSTRAINT "tenantUsers_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES public.tenants(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: tenantUsers tenantUsers_updatedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."tenantUsers" - ADD CONSTRAINT "tenantUsers_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: tenantUsers tenantUsers_userId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."tenantUsers" - ADD CONSTRAINT "tenantUsers_userId_fkey" FOREIGN KEY ("userId") REFERENCES public.users(id) ON UPDATE CASCADE; - - --- --- Name: tenants tenants_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.tenants - ADD CONSTRAINT "tenants_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: tenants tenants_updatedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.tenants - ADD CONSTRAINT "tenants_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: users users_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users - ADD CONSTRAINT "users_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: users users_updatedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users - ADD CONSTRAINT "users_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: widgets widgets_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.widgets - ADD CONSTRAINT "widgets_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- Name: widgets widgets_reportId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.widgets - ADD CONSTRAINT "widgets_reportId_fkey" FOREIGN KEY ("reportId") REFERENCES public.reports(id) ON UPDATE CASCADE ON DELETE CASCADE; - - --- --- Name: widgets widgets_tenantId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.widgets - ADD CONSTRAINT "widgets_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES public.tenants(id) ON UPDATE CASCADE; - - --- --- Name: widgets widgets_updatedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.widgets - ADD CONSTRAINT "widgets_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL; - - --- --- PostgreSQL database dump complete --- - diff --git a/backend/server-config/nginx/anton-prod.crowd.dev b/backend/server-config/nginx/anton-prod.crowd.dev deleted file mode 100644 index f72800b627..0000000000 --- a/backend/server-config/nginx/anton-prod.crowd.dev +++ /dev/null @@ -1,20 +0,0 @@ -server { - listen 80; - server_name anton-prod.crowd.dev; - - client_max_body_size 200M; - access_log /var/log/nginx/anton-prod.access.log; - error_log /var/log/nginx/anton-prod.error.log; - - location / { - proxy_pass http://localhost:8080; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; - # kill cache - expires -1; - } -} - diff --git a/backend/server-config/nginx/anton.crowd.dev b/backend/server-config/nginx/anton.crowd.dev deleted file mode 100644 index ffe0cf66d7..0000000000 --- a/backend/server-config/nginx/anton.crowd.dev +++ /dev/null @@ -1,20 +0,0 @@ -server { - listen 80; - server_name anton.crowd.dev; - - client_max_body_size 200M; - access_log /var/log/nginx/anton.access.log; - error_log /var/log/nginx/anton.error.log; - - location / { - proxy_pass http://localhost:8081; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; - # kill cache - expires -1; - } -} - diff --git a/backend/server-config/nginx/default b/backend/server-config/nginx/default deleted file mode 100644 index 0527f8e154..0000000000 --- a/backend/server-config/nginx/default +++ /dev/null @@ -1,100 +0,0 @@ -## -# You should look at the following URL's in order to grasp a solid understanding -# of Nginx configuration files in order to fully unleash the power of Nginx. -# https://www.nginx.com/resources/wiki/start/ -# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/ -# https://wiki.debian.org/Nginx/DirectoryStructure -# -# In most cases, administrators will remove this file from sites-enabled/ and -# leave it as reference inside of sites-available where it will continue to be -# updated by the nginx packaging team. -# -# This file will automatically load configuration files provided by other -# applications, such as Drupal or Wordpress. These applications will be made -# available underneath a path with that package name, such as /drupal8. -# -# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples. -## - -# Default server configuration -# -server { - listen 80 default_server; - listen [::]:80 default_server; - - # SSL configuration - # - # listen 443 ssl default_server; - # listen [::]:443 ssl default_server; - # - # Note: You should disable gzip for SSL traffic. - # See: https://bugs.debian.org/773332 - # - # Read up on ssl_ciphers to ensure a secure configuration. - # See: https://bugs.debian.org/765782 - # - # Self signed certs generated by the ssl-cert package - # Don't use them in a production server! - # - # include snippets/snakeoil.conf; - - root /var/www/html; - - # Add index.php to the list if you are using PHP - index index.html index.htm index.nginx-debian.html; - - server_name _; - - location /health { - try_files $uri $uri/ =404; - } - location / { - ## First attempt to serve request as file, then - ## as directory, then fall back to displaying a 404. - #try_files $uri $uri/ =404; - #proxy_pass http://localhost:8080; - #proxy_http_version 1.1; - #proxy_set_header Upgrade $http_upgrade; - #proxy_set_header Connection 'upgrade'; - #proxy_set_header Host $host; - #proxy_cache_bypass $http_upgrade; - } - - # pass PHP scripts to FastCGI server - # - #location ~ \.php$ { - # include snippets/fastcgi-php.conf; - # - # # With php-fpm (or other unix sockets): - # fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; - # # With php-cgi (or other tcp sockets): - # fastcgi_pass 127.0.0.1:9000; - #} - - # deny access to .htaccess files, if Apache's document root - # concurs with nginx's one - # - #location ~ /\.ht { - # deny all; - #} -} - - -# Virtual Host configuration for example.com -# -# You can move that to a different file under sites-available/ and symlink that -# to sites-enabled/ to enable it. -# -#server { -# listen 80; -# listen [::]:80; -# -# server_name example.com; -# -# root /var/www/example.com; -# index index.html; -# -# location / { -# try_files $uri $uri/ =404; -# } -#} diff --git a/backend/server-config/nginx/setup.sh b/backend/server-config/nginx/setup.sh deleted file mode 100755 index 047b69a88d..0000000000 --- a/backend/server-config/nginx/setup.sh +++ /dev/null @@ -1,14 +0,0 @@ -# Install nginx configuration and restart the service if test passes - -cp anton* default /etc/nginx/sites-available/ - -nginx -t -if [ $? -eq 0 ]; then - echo "Passed nginx test, restarting" -else - echo "Failed nginx test, not restarting" - exit 1 -fi - -systemctl restart nginx - diff --git a/backend/server-config/pm2.config.js b/backend/server-config/pm2.config.js deleted file mode 100644 index 8c740ae690..0000000000 --- a/backend/server-config/pm2.config.js +++ /dev/null @@ -1,28 +0,0 @@ -module.exports = { - apps: [ - { - name: 'anton-prod', - script: 'server.js', - watch: true, - env: { - PORT: 8080, - NODE_ENV: 'production', - }, - merge_logs: true, - cwd: '/home/ubuntu/deploy/dist-prod/', - interpreter: 'node@16.13.1', - }, - { - name: 'anton', - script: 'server.js', - watch: true, - env: { - PORT: 8081, - NODE_ENV: 'staging', - }, - merge_logs: true, - cwd: '/home/ubuntu/deploy/dist/', - interpreter: 'node@16.13.1', - }, - ], -} diff --git a/backend/src/api/activity/activityAddWithMember.ts b/backend/src/api/activity/activityAddWithMember.ts index f65663a7ec..f3a96657fa 100644 --- a/backend/src/api/activity/activityAddWithMember.ts +++ b/backend/src/api/activity/activityAddWithMember.ts @@ -1,16 +1,15 @@ -import PermissionChecker from '../../services/user/permissionChecker' import Permissions from '../../security/permissions' import ActivityService from '../../services/activityService' +import PermissionChecker from '../../services/user/permissionChecker' /** - * POST /tenant/{tenantId}/activity/with-member + * POST /activity/with-member * @summary Create or update an activity with a member * @tag Activities * @security Bearer * @description Create or update an activity with a member - * Activity existence is checked by sourceId and tenantId + * Activity existence is checked by sourceId * Member existence is checked by platform and username - * @pathParam {string} tenantId - Your workspace/tenant ID * @bodyContent {ActivityUpsertWithMemberInput} application/json * @response 200 - Ok * @responseContent {Activity} 200.application/json @@ -20,10 +19,7 @@ import ActivityService from '../../services/activityService' * @response 429 - Too many requests */ export default async (req, res) => { - // Check we have the Create permissions new PermissionChecker(req).validateHas(Permissions.values.activityCreate) - // Call the createWithMember function in activity service - // to create the activity. const payload = await new ActivityService(req).createWithMember(req.body) await req.responseHandler.success(req, res, payload) diff --git a/backend/src/api/activity/activityAutocomplete.ts b/backend/src/api/activity/activityAutocomplete.ts deleted file mode 100644 index df173580ff..0000000000 --- a/backend/src/api/activity/activityAutocomplete.ts +++ /dev/null @@ -1,14 +0,0 @@ -import PermissionChecker from '../../services/user/permissionChecker' -import Permissions from '../../security/permissions' -import ActivityService from '../../services/activityService' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.activityAutocomplete) - - const payload = await new ActivityService(req).findAllAutocomplete( - req.query.query, - req.query.limit, - ) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/activity/activityChannels.ts b/backend/src/api/activity/activityChannels.ts new file mode 100644 index 0000000000..f1b0c33208 --- /dev/null +++ b/backend/src/api/activity/activityChannels.ts @@ -0,0 +1,24 @@ +import Permissions from '../../security/permissions' +import ActivityService from '../../services/activityService' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * GET /activity/channel + * @summary List activity channels + * @tag Activities + * @security Bearer + * @description Find all activity channels + * @response 200 - Ok + * @responseContent {ActivityResponse} 200.application/json + * @responseExample {ActivityFind} 200.application/json.Activity + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.activityRead) + + const payload = await new ActivityService(req).findActivityChannels() + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/activity/activityCreate.ts b/backend/src/api/activity/activityCreate.ts deleted file mode 100644 index b909904c75..0000000000 --- a/backend/src/api/activity/activityCreate.ts +++ /dev/null @@ -1,29 +0,0 @@ -import PermissionChecker from '../../services/user/permissionChecker' -import Permissions from '../../security/permissions' -import ActivityService from '../../services/activityService' -import track from '../../segment/track' - -/** - * POST /tenant/{tenantId}/activity - * @summary Create or update an activity - * @tag Activities - * @security Bearer - * @description Create or update an activity. Existence is checked by sourceId and tenantId - * @pathParam {string} tenantId - Your workspace/tenant ID - * @bodyContent {ActivityUpsertInput} application/json - * @response 200 - Ok - * @responseContent {Activity} 200.application/json - * @responseExample {ActivityUpsert} 200.application/json.Activity - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.activityCreate) - - const payload = await new ActivityService(req).upsert(req.body) - - track('Activity Manually Created', { ...payload }, { ...req }) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/activity/activityDestroy.ts b/backend/src/api/activity/activityDestroy.ts deleted file mode 100644 index f042b97394..0000000000 --- a/backend/src/api/activity/activityDestroy.ts +++ /dev/null @@ -1,26 +0,0 @@ -import PermissionChecker from '../../services/user/permissionChecker' -import Permissions from '../../security/permissions' -import ActivityService from '../../services/activityService' - -/** - * DELETE /tenant/{tenantId}/activity/{id} - * @summary Delete an activity - * @tag Activities - * @security Bearer - * @description Delete a activity given an ID - * @pathParam {string} tenantId - Your workspace/tenant ID - * @pathParam {string} id - The ID of the activity - * @response 200 - Ok - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.activityDestroy) - - await new ActivityService(req).destroyAll(req.query.ids) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/activity/activityFind.ts b/backend/src/api/activity/activityFind.ts deleted file mode 100644 index d95d20bd77..0000000000 --- a/backend/src/api/activity/activityFind.ts +++ /dev/null @@ -1,26 +0,0 @@ -import PermissionChecker from '../../services/user/permissionChecker' -import Permissions from '../../security/permissions' -import ActivityService from '../../services/activityService' - -/** - * GET /tenant/{tenantId}/activity/{id} - * @summary Find an activity - * @tag Activities - * @security Bearer - * @description Find a single activity by ID - * @pathParam {string} tenantId - Your workspace/tenant ID - * @pathParam {string} id - The ID of the activity - * @response 200 - Ok - * @responseContent {ActivityResponse} 200.application/json - * @responseExample {ActivityFind} 200.application/json.Activity - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.activityRead) - - const payload = await new ActivityService(req).findById(req.params.id) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/activity/activityImport.ts b/backend/src/api/activity/activityImport.ts deleted file mode 100644 index 02bdf6aba8..0000000000 --- a/backend/src/api/activity/activityImport.ts +++ /dev/null @@ -1,13 +0,0 @@ -import PermissionChecker from '../../services/user/permissionChecker' -import Permissions from '../../security/permissions' -import ActivityService from '../../services/activityService' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.activityImport) - - await new ActivityService(req).import(req.body, req.body.importHash) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/activity/activityList.ts b/backend/src/api/activity/activityList.ts deleted file mode 100644 index 33cc328e46..0000000000 --- a/backend/src/api/activity/activityList.ts +++ /dev/null @@ -1,16 +0,0 @@ -import PermissionChecker from '../../services/user/permissionChecker' -import Permissions from '../../security/permissions' -import ActivityService from '../../services/activityService' -import track from '../../segment/track' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.activityRead) - - const payload = await new ActivityService(req).findAndCountAll(req.query) - - if (req.query.filter && Object.keys(req.query.filter).length > 0) { - track('Activities Filtered', { filter: req.query.filter }, { ...req }) - } - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/activity/activityQuery.ts b/backend/src/api/activity/activityQuery.ts index cb07cdd102..3482dfc53f 100644 --- a/backend/src/api/activity/activityQuery.ts +++ b/backend/src/api/activity/activityQuery.ts @@ -1,15 +1,14 @@ -import PermissionChecker from '../../services/user/permissionChecker' import Permissions from '../../security/permissions' -import ActivityService from '../../services/activityService' import track from '../../segment/track' +import ActivityService from '../../services/activityService' +import PermissionChecker from '../../services/user/permissionChecker' /** - * POST /tenant/{tenantId}/activity/query + * POST /activity/query * @summary Query activities * @tag Activities * @security Bearer * @description Query activities. It accepts filters, sorting options and pagination. - * @pathParam {string} tenantId - Your workspace/tenant ID * @bodyContent {ActivityQuery} application/json * @response 200 - Ok * @responseContent {ActivityList} 200.application/json @@ -21,10 +20,11 @@ import track from '../../segment/track' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.activityRead) - const payload = await new ActivityService(req).query(req.body) + const service = new ActivityService(req) + const payload = await service.query(req.body) - if (req.query.filter && Object.keys(req.query.filter).length > 0) { - track('Activities Advanced Filter', { ...payload }, { ...req }) + if (req.body?.filter && Object.keys(req.body.filter).length > 0) { + track('Activities Advanced Filter', { ...req.body }, { ...req }) } await req.responseHandler.success(req, res, payload) diff --git a/backend/src/api/activity/activityTypes.ts b/backend/src/api/activity/activityTypes.ts new file mode 100644 index 0000000000..9f8b2b64f0 --- /dev/null +++ b/backend/src/api/activity/activityTypes.ts @@ -0,0 +1,11 @@ +import Permissions from '../../security/permissions' +import ActivityService from '../../services/activityService' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.activityRead) + + const payload = await new ActivityService(req).findActivityTypes() + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/activity/activityUpdate.ts b/backend/src/api/activity/activityUpdate.ts deleted file mode 100644 index 97ae23c799..0000000000 --- a/backend/src/api/activity/activityUpdate.ts +++ /dev/null @@ -1,27 +0,0 @@ -import PermissionChecker from '../../services/user/permissionChecker' -import Permissions from '../../security/permissions' -import ActivityService from '../../services/activityService' - -/** - * PUT /tenant/{tenantId}/activity/{id} - * @summary Update an activity - * @tag Activities - * @security Bearer - * @description Update an activity given an ID. - * @pathParam {string} tenantId - Your workspace/tenant ID - * @pathParam {string} id - The ID of the activity - * @bodyContent {ActivityUpsertInput} application/json - * @response 200 - Ok - * @responseContent {Activity} 200.application/json - * @responseExample {ActivityFind} 200.application/json.Activity - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.activityEdit) - - const payload = await new ActivityService(req).update(req.params.id, req.body) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/activity/index.ts b/backend/src/api/activity/index.ts index b00bb99583..ef58cafbb3 100644 --- a/backend/src/api/activity/index.ts +++ b/backend/src/api/activity/index.ts @@ -1,20 +1,8 @@ import { safeWrap } from '../../middlewares/errorMiddleware' export default (app) => { - app.post(`/tenant/:tenantId/activity`, safeWrap(require('./activityCreate').default)) - app.post(`/tenant/:tenantId/activity/query`, safeWrap(require('./activityQuery').default)) - app.put(`/tenant/:tenantId/activity/:id`, safeWrap(require('./activityUpdate').default)) - app.post(`/tenant/:tenantId/activity/import`, safeWrap(require('./activityImport').default)) - app.delete(`/tenant/:tenantId/activity`, safeWrap(require('./activityDestroy').default)) - app.get( - `/tenant/:tenantId/activity/autocomplete`, - safeWrap(require('./activityAutocomplete').default), - ) - app.get(`/tenant/:tenantId/activity`, safeWrap(require('./activityList').default)) - app.get(`/tenant/:tenantId/activity/:id`, safeWrap(require('./activityFind').default)) - app.post( - '/tenant/:tenantId/activity/with-member', - // Call the addActivityWithMember file in this dir - safeWrap(require('./activityAddWithMember').default), - ) + app.post(`/activity/query`, safeWrap(require('./activityQuery').default)) + app.get(`/activity/type`, safeWrap(require('./activityTypes').default)) + app.get(`/activity/channel`, safeWrap(require('./activityChannels').default)) + app.post('/activity/with-member', safeWrap(require('./activityAddWithMember').default)) } diff --git a/backend/src/api/apiDocumentation.ts b/backend/src/api/apiDocumentation.ts deleted file mode 100644 index f6b50d6219..0000000000 --- a/backend/src/api/apiDocumentation.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { API_CONFIG } from '../conf' - -const express = require('express') -const fs = require('fs') -const path = require('path') - -export default function setupSwaggerUI(app) { - if (API_CONFIG.documentation) { - return - } - - const serveSwaggerDef = function serveSwaggerDef(req, res) { - res.sendFile(path.resolve(`${__dirname}/../documentation/openapi.json`)) - } - app.get('/documentation-config', serveSwaggerDef) - - const swaggerUiAssetPath = require('swagger-ui-dist').getAbsoluteFSPath() - const swaggerFiles = express.static(swaggerUiAssetPath) - - const urlRegex = /url: "[^"]*",/ - - const patchIndex = function patchIndex(req, res) { - const indexContent = fs - .readFileSync(`${swaggerUiAssetPath}/index.html`) - .toString() - .replace(urlRegex, 'url: "../documentation-config",') - res.send(indexContent) - } - - app.get('/documentation', (req, res) => { - let targetUrl = req.originalUrl - if (!targetUrl.endsWith('/')) { - targetUrl += '/' - } - targetUrl += 'index.html' - res.redirect(targetUrl) - }) - app.get('/documentation/index.html', patchIndex) - - app.use('/documentation', swaggerFiles) -} diff --git a/backend/src/api/apiRateLimiter.ts b/backend/src/api/apiRateLimiter.ts index 57776e3bb5..af6d7670d3 100644 --- a/backend/src/api/apiRateLimiter.ts +++ b/backend/src/api/apiRateLimiter.ts @@ -1,28 +1,15 @@ import rateLimit from 'express-rate-limit' -export function createRateLimiter({ - max, - windowMs, - message, -}: { - max: number - windowMs: number - message: string -}) { +import { RateLimitError } from '@crowd/common' + +export function createRateLimiter({ max, windowMs }: { max: number; windowMs: number }) { return rateLimit({ max, windowMs, - message, - skip: (req) => { - if (req.method === 'OPTIONS') { - return true - } - - if (req.originalUrl.endsWith('/import')) { - return true - } - - return false + handler: (_req, res) => { + const err = new RateLimitError() + res.status(err.status).json(err.toJSON()) }, + skip: (req) => req.method === 'OPTIONS' || req.originalUrl.endsWith('/import'), }) } diff --git a/backend/src/api/apiResponseHandler.ts b/backend/src/api/apiResponseHandler.ts index f03704a57a..debe217a58 100644 --- a/backend/src/api/apiResponseHandler.ts +++ b/backend/src/api/apiResponseHandler.ts @@ -1,7 +1,15 @@ import { LoggerBase } from '@crowd/logging' +import { + SlackChannel, + SlackMessageSection, + SlackPersona, + sendSlackNotification, +} from '@crowd/slack' + import { IServiceOptions } from '../services/IServiceOptions' -const io = require('@pm2/io') +// Slack section text limit is 3000 chars. Keep conservative to account for title + formatting. +const SLACK_SECTION_TEXT_LIMIT = 2800 /* eslint-disable class-methods-use-this */ export default class ApiResponseHandler extends LoggerBase { @@ -22,6 +30,103 @@ export default class ApiResponseHandler extends LoggerBase { } } + private truncateForSlack(text: string, maxLength: number = SLACK_SECTION_TEXT_LIMIT): string { + if (text.length <= maxLength) { + return text + } + return `${text.substring(0, maxLength - 3)}...` + } + + private sendServerErrorToSlack( + req, + error, + code: number, + options?: { sql?: string; dbErrorMessage?: string }, + ): void { + // NOTE: Temporarily ignore specific query timeout errors for these endpoints + // Do not remove this until https://linuxfoundation.atlassian.net/browse/CM-872, and https://linuxfoundation.atlassian.net/browse/CM-822 are completed or we have a better solution. + const ignoredEndpoints = ['/member/autocomplete', '/member/query'] + + if ( + req.method === 'POST' && + code === 500 && + ignoredEndpoints.includes(req.url) && + error?.name === 'SequelizeDatabaseError' && + error?.message?.includes('Query read timeout') + ) { + return + } + + const sections: SlackMessageSection[] = [] + + // Request info section + sections.push({ + title: 'Request', + text: `*Method:* \`${req.method}\`\n*URL:* \`${req.url}\`\n*Status Code:* \`${code}\``, + }) + + // Error info section + const errorName = error?.name || 'Unknown' + const errorMessage = this.truncateForSlack(error?.message || 'No message', 2000) + sections.push({ + title: 'Error', + text: `*Name:* \`${errorName}\`\n*Message:* ${errorMessage}`, + }) + + // SQL query section (for Sequelize errors) + if (options?.sql) { + const truncatedSql = this.truncateForSlack(options.sql, 2700) + sections.push({ + title: 'SQL Query', + text: `\`\`\`${truncatedSql}\`\`\``, + }) + } + + // DB error message (for Sequelize errors) + if (options?.dbErrorMessage) { + sections.push({ + title: 'Database Error', + text: this.truncateForSlack(options.dbErrorMessage, 2700), + }) + } + + // Request body section (if present) + if (req.body && Object.keys(req.body).length > 0) { + const bodyStr = JSON.stringify(req.body, null, 2) + const truncatedBody = this.truncateForSlack(bodyStr, 2700) + sections.push({ + title: 'Request Body', + text: `\`\`\`${truncatedBody}\`\`\``, + }) + } + + // Query params section (if present) + if (req.query && Object.keys(req.query).length > 0) { + const queryStr = JSON.stringify(req.query, null, 2) + const truncatedQuery = this.truncateForSlack(queryStr, 2700) + sections.push({ + title: 'Query Params', + text: `\`\`\`${truncatedQuery}\`\`\``, + }) + } + + // Stack trace section + if (error?.stack) { + const truncatedStack = this.truncateForSlack(error.stack, 2700) + sections.push({ + title: 'Stack Trace', + text: `\`\`\`${truncatedStack}\`\`\``, + }) + } + + sendSlackNotification( + SlackChannel.CDP_ALERTS, + SlackPersona.ERROR_REPORTER, + `API Error ${code}: ${req.method} ${req.url}`, + sections, + ) + } + async error(req, res, error) { if (error && error.name && error.name.includes('Sequelize')) { req.log.error( @@ -36,7 +141,10 @@ export default class ApiResponseHandler extends LoggerBase { }, 'Database error while processing REST API request!', ) - io.notifyError(error) + this.sendServerErrorToSlack(req, error, 500, { + sql: error.sql, + dbErrorMessage: error.original?.message, + }) res.status(500).send('Internal Server Error') } else if (error && [400, 401, 403, 404].includes(error.code)) { req.log.error( @@ -45,16 +153,26 @@ export default class ApiResponseHandler extends LoggerBase { 'Client error while processing REST API request!', ) res.status(error.code).send(error.message) + } else if (error && error.message === 'stream is not readable') { + res.status(400).send('Request interrupted') } else { - if (!error.code) { + if (error.code && (!Number.isInteger(error.code) || error.code < 100 || error.code > 599)) { + error.code = 500 + } else if (!error.code) { error.code = 500 } + req.log.error( error, { code: error.code, url: req.url, method: req.method, query: req.query, body: req.body }, 'Error while processing REST API request!', ) - io.notifyError(error) + + // Send Slack notification for server errors (500-599) + if (error.code >= 500 && error.code <= 599) { + this.sendServerErrorToSlack(req, error, error.code) + } + res.status(error.code).send(error.message) } } diff --git a/backend/src/api/auditLog/auditLogList.ts b/backend/src/api/auditLog/auditLogList.ts deleted file mode 100644 index 113b70b743..0000000000 --- a/backend/src/api/auditLog/auditLogList.ts +++ /dev/null @@ -1,11 +0,0 @@ -import PermissionChecker from '../../services/user/permissionChecker' -import AuditLogRepository from '../../database/repositories/auditLogRepository' -import Permissions from '../../security/permissions' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.auditLogRead) - - const payload = await AuditLogRepository.findAndCountAll(req.query, req) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/auditLog/auditLogsQuery.ts b/backend/src/api/auditLog/auditLogsQuery.ts new file mode 100644 index 0000000000..718387447b --- /dev/null +++ b/backend/src/api/auditLog/auditLogsQuery.ts @@ -0,0 +1,13 @@ +import AuditLogsService from '@/services/auditLogsService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.auditLogRead) + + const auditLogsService = new AuditLogsService(req) + const payload = await auditLogsService.query(req.body) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/auditLog/index.ts b/backend/src/api/auditLog/index.ts index 4cf7174642..8c2ef84601 100644 --- a/backend/src/api/auditLog/index.ts +++ b/backend/src/api/auditLog/index.ts @@ -1,5 +1,5 @@ import { safeWrap } from '../../middlewares/errorMiddleware' export default (app) => { - app.get(`/tenant/:tenantId/audit-log`, safeWrap(require('./auditLogList').default)) + app.post(`/audit-logs/query`, safeWrap(require('./auditLogsQuery').default)) } diff --git a/backend/src/api/auth/authMe.ts b/backend/src/api/auth/authMe.ts index 7007c09204..7046d6dcf6 100644 --- a/backend/src/api/auth/authMe.ts +++ b/backend/src/api/auth/authMe.ts @@ -1,8 +1,4 @@ -import { RedisCache } from '@crowd/redis' -import AutomationRepository from '../../database/repositories/automationRepository' -import Error403 from '../../errors/Error403' -import { FeatureFlagRedisKey } from '../../types/common' -import SegmentService from '../../services/segmentService' +import { Error403 } from '@crowd/common' export default async (req, res) => { if (!req.currentUser || !req.currentUser.id) { @@ -12,48 +8,5 @@ export default async (req, res) => { const payload = req.currentUser - const csvExportCountCache = new RedisCache( - FeatureFlagRedisKey.CSV_EXPORT_COUNT, - req.redis, - req.log, - ) - const memberEnrichmentCountCache = new RedisCache( - FeatureFlagRedisKey.MEMBER_ENRICHMENT_COUNT, - req.redis, - req.log, - ) - - payload.tenants = await Promise.all( - payload.tenants.map(async (tenantUser) => { - tenantUser.tenant.dataValues = { - ...tenantUser.tenant.dataValues, - csvExportCount: Number(await csvExportCountCache.get(tenantUser.tenant.id)) || 0, - automationCount: - Number(await AutomationRepository.countAllActive(req.database, tenantUser.tenant.id)) || - 0, - memberEnrichmentCount: - Number(await memberEnrichmentCountCache.get(tenantUser.tenant.id)) || 0, - } - - const segmentService = new SegmentService(req) - const tenantSubprojects = await segmentService.getTenantSubprojects(tenantUser.tenant) - const activityTypes = await SegmentService.getTenantActivityTypes(tenantSubprojects) - const activityChannels = await SegmentService.getTenantActivityChannels( - tenantUser.tenant, - req, - ) - - // TODO: return actual activityTypes using segment information - tenantUser.tenant.dataValues.settings[0].dataValues = { - ...tenantUser.tenant.dataValues.settings[0].dataValues, - activityTypes, - activityChannels, - slackWebHook: !!tenantUser.tenant.settings[0].dataValues.slackWebHook, - } - - return tenantUser - }), - ) - await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/auth/authPasswordReset.ts b/backend/src/api/auth/authPasswordReset.ts deleted file mode 100644 index cb6e557741..0000000000 --- a/backend/src/api/auth/authPasswordReset.ts +++ /dev/null @@ -1,7 +0,0 @@ -import AuthService from '../../services/auth/authService' - -export default async (req, res) => { - const payload = await AuthService.passwordReset(req.body.token, req.body.password, req) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/auth/authSendEmailAddressVerificationEmail.ts b/backend/src/api/auth/authSendEmailAddressVerificationEmail.ts deleted file mode 100644 index af60ff102d..0000000000 --- a/backend/src/api/auth/authSendEmailAddressVerificationEmail.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Error403 from '../../errors/Error403' - -import AuthService from '../../services/auth/authService' - -export default async (req, res) => { - if (!req.currentUser) { - throw new Error403(req.language) - } - - await AuthService.sendEmailAddressVerificationEmail( - req.language, - req.currentUser.email, - req.body.tenantId, - req, - ) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/auth/authSendPasswordResetEmail.ts b/backend/src/api/auth/authSendPasswordResetEmail.ts deleted file mode 100644 index 3ff4a96e7e..0000000000 --- a/backend/src/api/auth/authSendPasswordResetEmail.ts +++ /dev/null @@ -1,9 +0,0 @@ -import AuthService from '../../services/auth/authService' - -export default async (req, res) => { - await AuthService.sendPasswordResetEmail(req.language, req.body.email, req.body.tenantId, req) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/auth/authSignIn.ts b/backend/src/api/auth/authSignIn.ts index 36b6d87c95..e33e918fe4 100644 --- a/backend/src/api/auth/authSignIn.ts +++ b/backend/src/api/auth/authSignIn.ts @@ -1,3 +1,5 @@ +import { DEFAULT_TENANT_ID } from '@crowd/common' + import AuthService from '../../services/auth/authService' export default async (req, res) => { @@ -5,7 +7,7 @@ export default async (req, res) => { req.body.email, req.body.password, req.body.invitationToken, - req.body.tenantId, + DEFAULT_TENANT_ID, req, ) diff --git a/backend/src/api/auth/authSignUp.ts b/backend/src/api/auth/authSignUp.ts index 51faac3a3d..7cfdccc8ab 100644 --- a/backend/src/api/auth/authSignUp.ts +++ b/backend/src/api/auth/authSignUp.ts @@ -1,3 +1,5 @@ +import { DEFAULT_TENANT_ID } from '@crowd/common' + import AuthService from '../../services/auth/authService' export default async (req, res) => { @@ -8,7 +10,7 @@ export default async (req, res) => { req.body.email, req.body.password, req.body.invitationToken, - req.body.tenantId, + DEFAULT_TENANT_ID, req.body.firstName, req.body.lastName, req.body.acceptedTermsAndPrivacy, diff --git a/backend/src/api/auth/authSocial.ts b/backend/src/api/auth/authSocial.ts index 97d9b29f0f..d27693ad62 100644 --- a/backend/src/api/auth/authSocial.ts +++ b/backend/src/api/auth/authSocial.ts @@ -1,5 +1,8 @@ import passport from 'passport' + +import { DEFAULT_TENANT_ID } from '@crowd/common' import { getServiceChildLogger } from '@crowd/logging' + import { API_CONFIG, GITHUB_CONFIG, GOOGLE_CONFIG } from '../../conf' import AuthService from '../../services/auth/authService' @@ -19,8 +22,7 @@ export default (app, routes) => { routes.post('/auth/social/onboard', async (req, res) => { const payload = await AuthService.handleOnboard( req.currentUser, - req.body.invitationToken, - req.body.tenantId, + { invitationToken: req.body.invitationToken, tenantId: DEFAULT_TENANT_ID }, req, ) diff --git a/backend/src/api/auth/authUpdateProfile.ts b/backend/src/api/auth/authUpdateProfile.ts index 00983df288..c118295096 100644 --- a/backend/src/api/auth/authUpdateProfile.ts +++ b/backend/src/api/auth/authUpdateProfile.ts @@ -1,5 +1,6 @@ +import { Error403 } from '@crowd/common' + import AuthProfileEditor from '../../services/auth/authProfileEditor' -import Error403 from '../../errors/Error403' export default async (req, res) => { if (!req.currentUser || !req.currentUser.id) { diff --git a/backend/src/api/auth/authVerifyEmail.ts b/backend/src/api/auth/authVerifyEmail.ts deleted file mode 100644 index 1f8fce1f1e..0000000000 --- a/backend/src/api/auth/authVerifyEmail.ts +++ /dev/null @@ -1,7 +0,0 @@ -import AuthService from '../../services/auth/authService' - -export default async (req, res) => { - const payload = await AuthService.verifyEmail(req.body.token, req) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/auth/index.ts b/backend/src/api/auth/index.ts index 6579bc6cb2..1914fd26d0 100644 --- a/backend/src/api/auth/index.ts +++ b/backend/src/api/auth/index.ts @@ -1,31 +1,10 @@ -import { createRateLimiter } from '../apiRateLimiter' import { safeWrap } from '../../middlewares/errorMiddleware' +import { createRateLimiter } from '../apiRateLimiter' export default (app) => { - app.put(`/auth/password-reset`, safeWrap(require('./authPasswordReset').default)) - - const emailRateLimiter = createRateLimiter({ - max: 6, - windowMs: 15 * 60 * 1000, - message: 'errors.429', - }) - - app.post( - `/auth/send-email-address-verification-email`, - emailRateLimiter, - safeWrap(require('./authSendEmailAddressVerificationEmail').default), - ) - - app.post( - `/auth/send-password-reset-email`, - emailRateLimiter, - safeWrap(require('./authSendPasswordResetEmail').default), - ) - const signInRateLimiter = createRateLimiter({ max: 100, windowMs: 15 * 60 * 1000, - message: 'errors.429', }) app.post(`/auth/sign-in`, signInRateLimiter, safeWrap(require('./authSignIn').default)) @@ -33,7 +12,6 @@ export default (app) => { const signUpRateLimiter = createRateLimiter({ max: 20, windowMs: 60 * 60 * 1000, - message: 'errors.429', }) app.post(`/auth/sign-up`, signUpRateLimiter, safeWrap(require('./authSignUp').default)) @@ -42,8 +20,6 @@ export default (app) => { app.put(`/auth/change-password`, safeWrap(require('./authPasswordChange').default)) - app.put(`/auth/verify-email`, safeWrap(require('./authVerifyEmail').default)) - app.get(`/auth/me`, safeWrap(require('./authMe').default)) app.post(`/auth/sso/callback`, safeWrap(require('./ssoCallback').default)) diff --git a/backend/src/api/auth/ssoCallback.ts b/backend/src/api/auth/ssoCallback.ts index a8b68e5172..17caecebe5 100644 --- a/backend/src/api/auth/ssoCallback.ts +++ b/backend/src/api/auth/ssoCallback.ts @@ -1,8 +1,10 @@ import jwt from 'jsonwebtoken' import jwksClient from 'jwks-rsa' -import AuthService from '../../services/auth/authService' + +import { DEFAULT_TENANT_ID, Error401 } from '@crowd/common' + import { AUTH0_CONFIG } from '../../conf' -import Error401 from '../../errors/Error401' +import AuthService from '../../services/auth/authService' const jwks = jwksClient({ jwksUri: AUTH0_CONFIG.jwks, @@ -19,18 +21,21 @@ async function getKey(header, callback) { } export default async (req, res) => { - const { idToken, invitationToken, tenantId } = req.body + const { idToken, invitationToken } = req.body + const tenantId = DEFAULT_TENANT_ID try { const verifyToken = new Promise((resolve, reject) => { jwt.verify(idToken, getKey, { algorithms: ['RS256'] }, (err, decoded) => { if (err) { + req.log.error('Error verifying token', err) reject(new Error401()) } const { aud } = decoded as any if (aud !== AUTH0_CONFIG.clientId) { + req.log.error(`Invalid audience: ${aud}`) reject(new Error401()) } @@ -50,10 +55,12 @@ export default async (req, res) => { data.name, invitationToken, tenantId, + data['http://lfx.dev/claims/roles'], req, ) return res.send(token) } catch (err) { + req.log.error('Error verifying token', err) return res.status(401).send({ error: err }) } } diff --git a/backend/src/api/automation/automationCreate.ts b/backend/src/api/automation/automationCreate.ts deleted file mode 100644 index e5cd8d49ab..0000000000 --- a/backend/src/api/automation/automationCreate.ts +++ /dev/null @@ -1,31 +0,0 @@ -import PermissionChecker from '../../services/user/permissionChecker' -import Permissions from '../../security/permissions' -import AutomationService from '../../services/automationService' -import track from '../../segment/track' -import identifyTenant from '../../segment/identifyTenant' - -/** - * POST /tenant/{tenantId}/automation - * @summary Create an automation - * @tag Automations - * @security Bearer - * @description Create a new automation for the tenant. - * @pathParam {string} tenantId - Your workspace/tenant ID - * @bodyContent {AutomationCreateInput} application/json - * @response 200 - Ok - * @responseContent {Automation} 200.application/json - * @responseExample {Automation} 200.application/json.Automation - * @response 401 - Unauthorized - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.automationCreate) - - const payload = await new AutomationService(req).create(req.body.data) - - track('Automation Created', { ...payload }, { ...req }) - - identifyTenant(req) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/automation/automationDestroy.ts b/backend/src/api/automation/automationDestroy.ts deleted file mode 100644 index 16f9271cc4..0000000000 --- a/backend/src/api/automation/automationDestroy.ts +++ /dev/null @@ -1,27 +0,0 @@ -import PermissionChecker from '../../services/user/permissionChecker' -import Permissions from '../../security/permissions' -import AutomationService from '../../services/automationService' -import track from '../../segment/track' -import identifyTenant from '../../segment/identifyTenant' - -/** - * DELETE /tenant/{tenantId}/automation/{automationId} - * @summary Destroy an automation - * @tag Automations - * @security Bearer - * @description Destroys an existing automation in the tenant. - * @pathParam {string} tenantId - Your workspace/tenant ID - * @pathParam {string} automationId - Automation ID that you want to update - * @response 204 - Ok - No content - * @response 401 - Unauthorized - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.automationDestroy) - await new AutomationService(req).destroy(req.params.automationId) - - track('Automation Destroyed', { id: req.params.automationId }, { ...req }) - identifyTenant(req) - - await req.responseHandler.success(req, res, true, 204) -} diff --git a/backend/src/api/automation/automationExecutionFind.ts b/backend/src/api/automation/automationExecutionFind.ts deleted file mode 100644 index 8d4eba88ea..0000000000 --- a/backend/src/api/automation/automationExecutionFind.ts +++ /dev/null @@ -1,40 +0,0 @@ -import PermissionChecker from '../../services/user/permissionChecker' -import Permissions from '../../security/permissions' -import AutomationExecutionService from '../../services/automationExecutionService' - -/** - * GET /tenant/{tenantId}/automation/{automationId}/executions - * @summary Get automation history - * @tag Automations - * @security Bearer - * @description Get all automation execution history for tenant and automation - * @pathParam {string} tenantId - Your workspace/tenant ID - * @pathParam {string} automationId - Your workspace/tenant ID - * @queryParam {integer} [offset=0] - How many elements from the beginning would you like to skip - * @queryParam {integer} [limit=10] - How many elements would you like to fetch - * @response 200 - Ok - * @responseContent {AutomationExecutionPage} 200.application/json - * @responseExample {AutomationExecutionPage} 200.application/json.AutomationExecutionPage - * @response 401 - Unauthorized - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.automationRead) - - let offset = 0 - if (req.query.offset) { - offset = parseInt(req.query.offset, 10) - } - let limit = 10 - if (req.query.limit) { - limit = parseInt(req.query.limit, 10) - } - - const payload = await new AutomationExecutionService(req).findAndCountAll({ - automationId: req.params.automationId, - offset, - limit, - }) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/automation/automationFind.ts b/backend/src/api/automation/automationFind.ts deleted file mode 100644 index 4feaa9e623..0000000000 --- a/backend/src/api/automation/automationFind.ts +++ /dev/null @@ -1,24 +0,0 @@ -import PermissionChecker from '../../services/user/permissionChecker' -import Permissions from '../../security/permissions' -import AutomationService from '../../services/automationService' - -/** - * GET /tenant/{tenantId}/automation/{automationId} - * @summary Find an automation - * @tag Automations - * @security Bearer - * @description Get an existing automation data in the tenant. - * @pathParam {string} tenantId - Your workspace/tenant ID - * @pathParam {string} automationId - Automation ID that you want to find - * @response 200 - Ok - * @responseContent {Automation} 200.application/json - * @responseExample {Automation} 200.application/json.Automation - * @response 401 - Unauthorized - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.automationRead) - const payload = await new AutomationService(req).findById(req.params.automationId) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/automation/automationList.ts b/backend/src/api/automation/automationList.ts deleted file mode 100644 index f9a19cef85..0000000000 --- a/backend/src/api/automation/automationList.ts +++ /dev/null @@ -1,54 +0,0 @@ -import Permissions from '../../security/permissions' -import AutomationService from '../../services/automationService' -import PermissionChecker from '../../services/user/permissionChecker' -import { - AutomationCriteria, - AutomationState, - AutomationTrigger, - AutomationType, -} from '../../types/automationTypes' - -/** - * GET /tenant/{tenantId}/automation - * @summary List automations - * @tag Automations - * @security Bearer - * @description Get all existing automation data for tenant. - * @pathParam {string} tenantId - Your workspace/tenant ID - * @queryParam {string} [filter[type]] - Filter by type of automation - * @queryParam {string} [filter[trigger]] - Filter by trigger type of automation - * @queryParam {string} [filter[state]] - Filter by state of automation - * @queryParam {number} [offset] - Skip the first n results. Default 0. - * @queryParam {number} [limit] - Limit the number of results. Default 50. - * @response 200 - Ok - * @responseContent {AutomationPage} 200.application/json - * @responseExample {AutomationPage} 200.application/json.AutomationPage - * @response 401 - Unauthorized - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.automationRead) - - let offset = 0 - if (req.query.offset) { - offset = parseInt(req.query.offset, 10) - } - let limit = 50 - if (req.query.limit) { - limit = parseInt(req.query.limit, 10) - } - - const criteria: AutomationCriteria = { - type: req.query.filter?.type ? (req.query.filter.type as AutomationType) : undefined, - trigger: req.query.filter?.trigger - ? (req.query.filter?.trigger as AutomationTrigger) - : undefined, - state: req.query.filter?.state ? (req.query.filter.state as AutomationState) : undefined, - limit, - offset, - } - - const payload = await new AutomationService(req).findAndCountAll(criteria) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/automation/automationSlackCallback.ts b/backend/src/api/automation/automationSlackCallback.ts deleted file mode 100644 index b8b7d84615..0000000000 --- a/backend/src/api/automation/automationSlackCallback.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Axios from 'axios' -import Permissions from '../../security/permissions' -import PermissionChecker from '../../services/user/permissionChecker' -import SettingsService from '../../services/settingsService' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.automationCreate) - const { redirectUrl } = JSON.parse(Buffer.from(req.query.state, 'base64').toString()) - - const { url } = req.account - - await SettingsService.save({ slackWebHook: url }, req) - await Axios.post(url, { - text: 'crowd.dev notifier has been successfully connected.', - }) - - res.redirect(redirectUrl) -} diff --git a/backend/src/api/automation/automationSlackConnect.ts b/backend/src/api/automation/automationSlackConnect.ts deleted file mode 100644 index 77dfd596f1..0000000000 --- a/backend/src/api/automation/automationSlackConnect.ts +++ /dev/null @@ -1,20 +0,0 @@ -import passport from 'passport' -import Permissions from '../../security/permissions' -import PermissionChecker from '../../services/user/permissionChecker' -import { getSlackNotifierStrategy } from '../../services/auth/passportStrategies/slackStrategy' - -export default async (req, res, next) => { - new PermissionChecker(req).validateHas(Permissions.values.automationCreate) - const state = { - tenantId: req.params.tenantId, - redirectUrl: req.query.redirectUrl, - crowdToken: req.query.crowdToken, - } - - const authenticator = passport.authenticate(getSlackNotifierStrategy(), { - scope: ['incoming-webhook'], - state: Buffer.from(JSON.stringify(state)).toString('base64'), - }) - - authenticator(req, res, next) -} diff --git a/backend/src/api/automation/automationUpdate.ts b/backend/src/api/automation/automationUpdate.ts deleted file mode 100644 index 8d53212469..0000000000 --- a/backend/src/api/automation/automationUpdate.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Permissions from '../../security/permissions' -import track from '../../segment/track' -import AutomationService from '../../services/automationService' -import PermissionChecker from '../../services/user/permissionChecker' - -/** - * PUT /tenant/{tenantId}/automation/{automationId} - * @summary Update an automation - * @tag Automations - * @security Bearer - * @description Updates an existing automation in the tenant. - * @pathParam {string} tenantId - Your workspace/tenant ID - * @pathParam {string} automationId - Automation ID that you want to update - * @bodyContent {AutomationUpdateInput} application/json - * @response 200 - Ok - * @responseContent {Automation} 200.application/json - * @responseExample {Automation} 200.application/json.Automation - * @response 401 - Unauthorized - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.automationUpdate) - const payload = await new AutomationService(req).update(req.params.automationId, req.body.data) - - track('Automation Updated', { ...payload }, { ...req }) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/automation/index.ts b/backend/src/api/automation/index.ts deleted file mode 100644 index 9230d784df..0000000000 --- a/backend/src/api/automation/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -import passport from 'passport' -import { safeWrap } from '../../middlewares/errorMiddleware' -import { API_CONFIG } from '../../conf' -import { authMiddleware } from '../../middlewares/authMiddleware' -import TenantService from '../../services/tenantService' -import { getSlackNotifierStrategy } from '../../services/auth/passportStrategies/slackStrategy' - -export default (app) => { - app.get( - '/tenant/:tenantId/automation/slack', - safeWrap(require('./automationSlackConnect').default), - ) - app.get( - '/tenant/automation/slack/callback', - passport.authorize(getSlackNotifierStrategy(), { - session: false, - failureRedirect: `${API_CONFIG.frontendUrl}/settings?activeTab=automations&error=true`, - }), - (req, _res, next) => { - const { crowdToken } = JSON.parse(Buffer.from(req.query.state, 'base64').toString()) - req.headers.authorization = `Bearer ${crowdToken}` - next() - }, - authMiddleware, - async (req, _res, next) => { - const { tenantId } = JSON.parse(Buffer.from(req.query.state, 'base64').toString()) - req.currentTenant = await new TenantService(req).findById(tenantId) - next() - }, - safeWrap(require('./automationSlackCallback').default), - ) - app.post('/tenant/:tenantId/automation', safeWrap(require('./automationCreate').default)) - app.put( - '/tenant/:tenantId/automation/:automationId', - safeWrap(require('./automationUpdate').default), - ) - app.delete( - '/tenant/:tenantId/automation/:automationId', - safeWrap(require('./automationDestroy').default), - ) - app.get( - '/tenant/:tenantId/automation/:automationId/executions', - safeWrap(require('./automationExecutionFind').default), - ) - app.get( - '/tenant/:tenantId/automation/:automationId', - safeWrap(require('./automationFind').default), - ) - app.get('/tenant/:tenantId/automation', safeWrap(require('./automationList').default)) -} diff --git a/backend/src/api/categories/categoryBulkDelete.ts b/backend/src/api/categories/categoryBulkDelete.ts new file mode 100644 index 0000000000..e361c2c948 --- /dev/null +++ b/backend/src/api/categories/categoryBulkDelete.ts @@ -0,0 +1,24 @@ +import { CategoryService } from '@/services/categoryService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * DELETE /category/ + * @summary Delete multiple categories + * @tag Categories + * @security Bearer + * @description Delete multiple categories by ID + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.categoryEdit) + + const service = new CategoryService(req) + await service.deleteCategories(req.body.ids) + + await req.responseHandler.success(req, res, true) +} diff --git a/backend/src/api/categories/categoryCreate.ts b/backend/src/api/categories/categoryCreate.ts new file mode 100644 index 0000000000..4ff8028e65 --- /dev/null +++ b/backend/src/api/categories/categoryCreate.ts @@ -0,0 +1,25 @@ +import { CategoryService } from '@/services/categoryService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * POST /category + * @summary Create a category + * @tag Category + * @security Bearer + * @description Create a new category + * @bodyContent {CollectionCreateInput} application/json + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.categoryEdit) + + const service = new CategoryService(req) + const payload = await service.createCategory(req.body) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/categories/categoryDelete.ts b/backend/src/api/categories/categoryDelete.ts new file mode 100644 index 0000000000..896410730a --- /dev/null +++ b/backend/src/api/categories/categoryDelete.ts @@ -0,0 +1,25 @@ +import { CategoryService } from '@/services/categoryService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * DELETE /category/{id} + * @summary Delete a category + * @tag Categories + * @security Bearer + * @description Delete a category by ID + * @pathParam {string} id - The ID of the category + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.categoryEdit) + + const service = new CategoryService(req) + await service.deleteCategory(req.params.id) + + await req.responseHandler.success(req, res, true) +} diff --git a/backend/src/api/categories/categoryGroupCreate.ts b/backend/src/api/categories/categoryGroupCreate.ts new file mode 100644 index 0000000000..2b28f36657 --- /dev/null +++ b/backend/src/api/categories/categoryGroupCreate.ts @@ -0,0 +1,25 @@ +import { CategoryService } from '@/services/categoryService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * POST /category-group + * @summary Create a category group + * @tag CategoryGroup + * @security Bearer + * @description Create a new categroy group + * @bodyContent {CollectionCreateInput} application/json + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.categoryEdit) + + const service = new CategoryService(req) + const payload = await service.createCategoryGroup(req.body) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/categories/categoryGroupDelete.ts b/backend/src/api/categories/categoryGroupDelete.ts new file mode 100644 index 0000000000..439bed0dac --- /dev/null +++ b/backend/src/api/categories/categoryGroupDelete.ts @@ -0,0 +1,25 @@ +import { CategoryService } from '@/services/categoryService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * DELETE /category-groups/{id} + * @summary Delete a category group + * @tag Category Groups + * @security Bearer + * @description Delete a category group by ID + * @pathParam {string} id - The ID of the category group + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.categoryEdit) + + const service = new CategoryService(req) + await service.deleteCategoryGroup(req.params.id) + + await req.responseHandler.success(req, res, true) +} diff --git a/backend/src/api/categories/categoryGroupList.ts b/backend/src/api/categories/categoryGroupList.ts new file mode 100644 index 0000000000..1d564d9d9d --- /dev/null +++ b/backend/src/api/categories/categoryGroupList.ts @@ -0,0 +1,25 @@ +import { CategoryService } from '@/services/categoryService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * Get /category-group + * @summary List Category groups + * @tag CategoryGroups + * @security Bearer + * @description Query category groups with filters and pagination + * @bodyContent {CategoryGroupsQuery} application/json + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.categoryRead) + + const service = new CategoryService(req) + const payload = await service.listCategoryGroups(req.query) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/categories/categoryGroupUpdate.ts b/backend/src/api/categories/categoryGroupUpdate.ts new file mode 100644 index 0000000000..6cf3e7f4d6 --- /dev/null +++ b/backend/src/api/categories/categoryGroupUpdate.ts @@ -0,0 +1,25 @@ +import { CategoryService } from '@/services/categoryService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * PATCH /category-group/:id + * @summary Update a category group + * @tag Category Groups + * @security Bearer + * @description Update a category group + * @bodyContent {CategoryGroupUpdateInput} application/json + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.categoryEdit) + + const service = new CategoryService(req) + const payload = await service.updateCategoryGroup(req.params.id, req.body) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/categories/categoryList.ts b/backend/src/api/categories/categoryList.ts new file mode 100644 index 0000000000..ccf815b5fa --- /dev/null +++ b/backend/src/api/categories/categoryList.ts @@ -0,0 +1,24 @@ +import { CategoryService } from '@/services/categoryService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * Get /category + * @summary List Category + * @tag Category + * @security Bearer + * @description Query category with filters + * @bodyContent {CategoryQuery} application/json + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.categoryRead) + + const service = new CategoryService(req) + const payload = await service.listCategories(req.query) + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/categories/categoryUpdate.ts b/backend/src/api/categories/categoryUpdate.ts new file mode 100644 index 0000000000..dec39e37e7 --- /dev/null +++ b/backend/src/api/categories/categoryUpdate.ts @@ -0,0 +1,25 @@ +import { CategoryService } from '@/services/categoryService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * PATCH /category/:id + * @summary Update a category + * @tag Categories + * @security Bearer + * @description Update a category + * @bodyContent {CategoryUpdateInput} application/json + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.categoryEdit) + + const service = new CategoryService(req) + const payload = await service.updateCategory(req.params.id, req.body) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/categories/index.ts b/backend/src/api/categories/index.ts new file mode 100644 index 0000000000..0f4c397a4f --- /dev/null +++ b/backend/src/api/categories/index.ts @@ -0,0 +1,16 @@ +import { safeWrap } from '@/middlewares/errorMiddleware' + +export default (app) => { + // Category groups routes + app.post('/category-group', safeWrap(require('./categoryGroupCreate').default)) + app.get('/category-group', safeWrap(require('./categoryGroupList').default)) + app.patch('/category-group/:id', safeWrap(require('./categoryGroupUpdate').default)) + app.delete('/category-group/:id', safeWrap(require('./categoryGroupDelete').default)) + + // Categories routes + app.post('/category', safeWrap(require('./categoryCreate').default)) + app.get('/category', safeWrap(require('./categoryList').default)) + app.patch('/category/:id', safeWrap(require('./categoryUpdate').default)) + app.delete('/category/:id', safeWrap(require('./categoryDelete').default)) + app.delete('/category', safeWrap(require('./categoryBulkDelete').default)) +} diff --git a/backend/src/api/collections/collectionsCreate.ts b/backend/src/api/collections/collectionsCreate.ts new file mode 100644 index 0000000000..e073884870 --- /dev/null +++ b/backend/src/api/collections/collectionsCreate.ts @@ -0,0 +1,25 @@ +import { CollectionService } from '@/services/collectionService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * POST /collections + * @summary Create a collection + * @tag Collections + * @security Bearer + * @description Create a new collection + * @bodyContent {CollectionCreateInput} application/json + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.collectionEdit) + + const service = new CollectionService(req) + const payload = await service.createCollection(req.body) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/collections/collectionsDestroy.ts b/backend/src/api/collections/collectionsDestroy.ts new file mode 100644 index 0000000000..8abf531036 --- /dev/null +++ b/backend/src/api/collections/collectionsDestroy.ts @@ -0,0 +1,25 @@ +import { CollectionService } from '@/services/collectionService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * DELETE /collections/{id} + * @summary Delete a collection + * @tag Collections + * @security Bearer + * @description Delete a collection by ID + * @pathParam {string} id - The ID of the collection + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.collectionEdit) + + const service = new CollectionService(req) + await service.destroy(req.params.id) + + await req.responseHandler.success(req, res, true) +} diff --git a/backend/src/api/collections/collectionsGet.ts b/backend/src/api/collections/collectionsGet.ts new file mode 100644 index 0000000000..c1eb27ed68 --- /dev/null +++ b/backend/src/api/collections/collectionsGet.ts @@ -0,0 +1,25 @@ +import { CollectionService } from '@/services/collectionService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * GET /collections/{id} + * @summary Get a collection + * @tag Collections + * @security Bearer + * @description Get a collection by ID + * @pathParam {string} id - The ID of the collection + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.collectionRead) + + const service = new CollectionService(req) + const payload = await service.findById(req.params.id) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/collections/collectionsQuery.ts b/backend/src/api/collections/collectionsQuery.ts new file mode 100644 index 0000000000..a5b1fd44e1 --- /dev/null +++ b/backend/src/api/collections/collectionsQuery.ts @@ -0,0 +1,25 @@ +import { CollectionService } from '@/services/collectionService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * POST /collections/query + * @summary Query collections + * @tag Collections + * @security Bearer + * @description Query collections with filters and pagination + * @bodyContent {CollectionsQuery} application/json + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.collectionRead) + + const service = new CollectionService(req) + const payload = await service.query(req.body) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/collections/collectionsUpdate.ts b/backend/src/api/collections/collectionsUpdate.ts new file mode 100644 index 0000000000..79e632ac76 --- /dev/null +++ b/backend/src/api/collections/collectionsUpdate.ts @@ -0,0 +1,25 @@ +import { CollectionService } from '@/services/collectionService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * POST /collection/:id + * @summary Update a collection + * @tag Collections + * @security Bearer + * @description Update a collection + * @bodyContent {CollectionUpdateInput} application/json + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.collectionEdit) + + const service = new CollectionService(req) + const payload = await service.updateCollection(req.params.id, req.body) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/collections/index.ts b/backend/src/api/collections/index.ts new file mode 100644 index 0000000000..329fc1791e --- /dev/null +++ b/backend/src/api/collections/index.ts @@ -0,0 +1,36 @@ +import { safeWrap } from '../../middlewares/errorMiddleware' + +export default (app) => { + // Insights projects routes + app.post( + '/collections/insights-projects/query', + safeWrap(require('./insightsProjects/insightsProjectsQuery').default), + ) + app.post( + '/collections/insights-projects', + safeWrap(require('./insightsProjects/insightsProjectsCreate').default), + ) + app.delete( + '/collections/insights-projects/:id', + safeWrap(require('./insightsProjects/insightsProjectsDestroy').default), + ) + app.post( + '/collections/insights-projects/:id', + safeWrap(require('./insightsProjects/insightsProjectsUpdate').default), + ) + app.get( + '/collections/insights-projects/:id', + safeWrap(require('./insightsProjects/insightsProjectsGet').default), + ) + + // Collections routes + app.post('/collections/query', safeWrap(require('./collectionsQuery').default)) + app.post('/collections', safeWrap(require('./collectionsCreate').default)) + app.get('/collections/:id', safeWrap(require('./collectionsGet').default)) + app.post('/collections/:id', safeWrap(require('./collectionsUpdate').default)) + app.delete('/collections/:id', safeWrap(require('./collectionsDestroy').default)) + + app.get('/segments/:id/repositories', safeWrap(require('./segmentsRepositoriesGet').default)) + app.get('/segments/:id/github-insights', safeWrap(require('./segmentsGithubInsightsGet').default)) + app.get('/segments/:id/widgets', safeWrap(require('./segmentsWidgetsGet').default)) +} diff --git a/backend/src/api/collections/insightsProjects/insightsProjectsCreate.ts b/backend/src/api/collections/insightsProjects/insightsProjectsCreate.ts new file mode 100644 index 0000000000..c36f99b97e --- /dev/null +++ b/backend/src/api/collections/insightsProjects/insightsProjectsCreate.ts @@ -0,0 +1,25 @@ +import { CollectionService } from '@/services/collectionService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * POST /collections/insights-projects + * @summary Create an insights project + * @tag Collections + * @security Bearer + * @description Create a new insights project + * @bodyContent {InsightsProjectCreateInput} application/json + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.collectionEdit) + + const service = new CollectionService(req) + const payload = await service.createInsightsProject(req.body) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/collections/insightsProjects/insightsProjectsDestroy.ts b/backend/src/api/collections/insightsProjects/insightsProjectsDestroy.ts new file mode 100644 index 0000000000..7fdb45efa2 --- /dev/null +++ b/backend/src/api/collections/insightsProjects/insightsProjectsDestroy.ts @@ -0,0 +1,25 @@ +import { CollectionService } from '@/services/collectionService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * DELETE /collections/insights-projects/{id} + * @summary Delete an insights project + * @tag Collections + * @security Bearer + * @description Delete an insights project by ID + * @pathParam {string} id - The ID of the insights project + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.collectionEdit) + + const service = new CollectionService(req) + await service.destroyInsightsProject(req.params.id) + + await req.responseHandler.success(req, res, true) +} diff --git a/backend/src/api/collections/insightsProjects/insightsProjectsGet.ts b/backend/src/api/collections/insightsProjects/insightsProjectsGet.ts new file mode 100644 index 0000000000..bd87d3bd7b --- /dev/null +++ b/backend/src/api/collections/insightsProjects/insightsProjectsGet.ts @@ -0,0 +1,24 @@ +import { CollectionService } from '@/services/collectionService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * GET /collections/insights-projects/:id + * @summary Get an insights project + * @tag Collections + * @security Bearer + * @description Get an insights project by ID + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.collectionEdit) + + const service = new CollectionService(req) + const payload = await service.findInsightsProjectById(req.params.id) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/collections/insightsProjects/insightsProjectsQuery.ts b/backend/src/api/collections/insightsProjects/insightsProjectsQuery.ts new file mode 100644 index 0000000000..16c76f85d4 --- /dev/null +++ b/backend/src/api/collections/insightsProjects/insightsProjectsQuery.ts @@ -0,0 +1,25 @@ +import { CollectionService } from '@/services/collectionService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * POST /collections/insights-projects/query + * @summary Query insights projects + * @tag Collections + * @security Bearer + * @description Query insights projects with filters and pagination + * @bodyContent {InsightsProjectsQuery} application/json + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.collectionRead) + + const service = new CollectionService(req) + const payload = await service.queryInsightsProjects(req.body) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/collections/insightsProjects/insightsProjectsUpdate.ts b/backend/src/api/collections/insightsProjects/insightsProjectsUpdate.ts new file mode 100644 index 0000000000..14a2a0c8a5 --- /dev/null +++ b/backend/src/api/collections/insightsProjects/insightsProjectsUpdate.ts @@ -0,0 +1,25 @@ +import { CollectionService } from '@/services/collectionService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * POST /collections/insights-projects/{id} + * @summary Update an insights project + * @tag Collections + * @security Bearer + * @description Update an insights project + * @bodyContent {InsightsProjectUpdateInput} application/json + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.collectionEdit) + + const service = new CollectionService(req) + const payload = await service.updateInsightsProject(req.params.id, req.body) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/collections/segmentsGithubInsightsGet.ts b/backend/src/api/collections/segmentsGithubInsightsGet.ts new file mode 100644 index 0000000000..f4359e3599 --- /dev/null +++ b/backend/src/api/collections/segmentsGithubInsightsGet.ts @@ -0,0 +1,25 @@ +import { CollectionService } from '@/services/collectionService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * GET /segments/{id}/github-insights + * @summary Get github insights for a segment + * @tag Segments + * @security Bearer + * @description Get github insights for a segment + * @pathParam {string} id - The ID of the segment + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.collectionRead) + + const service = new CollectionService(req) + const payload = await service.findGithubInsightsForSegment(req.params.id) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/collections/segmentsRepositoriesGet.ts b/backend/src/api/collections/segmentsRepositoriesGet.ts new file mode 100644 index 0000000000..107f2e15fd --- /dev/null +++ b/backend/src/api/collections/segmentsRepositoriesGet.ts @@ -0,0 +1,27 @@ +import { findRepositoriesForSegment } from '@crowd/data-access-layer/src/integrations' + +import SequelizeRepository from '@/database/repositories/sequelizeRepository' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * GET /segments/{id}/repositories + * @summary Get repositories for a segment + * @tag Segments + * @security Bearer + * @description Get repositories for a segment + * @pathParam {string} id - The ID of the segment + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.collectionRead) + + const qx = SequelizeRepository.getQueryExecutor(req) + const payload = await findRepositoriesForSegment(qx, req.params.id) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/collections/segmentsWidgetsGet.ts b/backend/src/api/collections/segmentsWidgetsGet.ts new file mode 100644 index 0000000000..1c94d56eb7 --- /dev/null +++ b/backend/src/api/collections/segmentsWidgetsGet.ts @@ -0,0 +1,23 @@ +import { CollectionService } from '@/services/collectionService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * GET /segments/:id/widgets + * @summary Get needed widgets for a segmentId + * @tag Collections + * @security Bearer + * @description Get an insights project by ID + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.collectionEdit) + + const service = new CollectionService(req) + const payload = await service.findSegmentsWidgetsById(req.params.id) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/components/activity/activityTypes/examples.yaml b/backend/src/api/components/activity/activityTypes/examples.yaml deleted file mode 100644 index 1462d69dbf..0000000000 --- a/backend/src/api/components/activity/activityTypes/examples.yaml +++ /dev/null @@ -1,148 +0,0 @@ -components: - examples: - ActivityTypes: - value: - default: - github: - discussion-started: - default: started a discussion in {channel} - short: started a discussion - channel: '{channel}' - discussion-comment: - default: commented on a discussion in {channel} - short: commented on a discussion - channel: '{channel}' - fork: - default: forked {channel} - short: forked - channel: '{channel}' - issues-closed: - default: closed an issue in {channel} - short: closed an issue - channel: '{channel}' - issues-opened: - default: opened a new issue in {channel} - short: opened an issue - channel: '{channel}' - issue-comment: - default: commented on an issue in {channel} - short: commented on an issue - channel: '{channel}' - pull_request-closed: - default: closed a pull request in {channel} - short: closed a pull request - channel: '{channel}' - pull_request-opened: - default: opened a new pull request in {channel} - short: opened a pull request - channel: '{channel}' - pull_request-comment: - default: commented on a pull request in {channel} - short: commented on a pull request - channel: '{channel}' - star: - default: starred {channel} - short: starred - channel: '{channel}' - unstar: - default: unstarred {channel} - short: unstarred - channel: '{channel}' - devto: - comment: - default: commented on {attributes.articleTitle} - short: commented - channel: {attributes.articleTitle} - discord: - joined_guild: - default: joined server - short: joined server - channel: '' - message: - default: sent a message in #{channel} - short: sent a message - channel: #{channel} - thread_started: - default: started a new thread - short: started a new thread - channel: '' - thread_message: - default: replied to a message in thread #{channel} -> {attributes.parentChannel} - short: replied to a message - channel: 'thread #{channel} - -> #{attributes.parentChannel}' - hackernews: - comment: - default: commented on {attributes.parentTitle} - short: commented - channel: '{channel}' - post: - default: posted mentioning {channel} - short: posted - channel: '{channel}' - linkedin: - comment: - default: commented on a post {attributes.postBody} - short: commented - channel: {attributes.postBody} - message: - default: sent a message - short: sent a message - channel: '' - reaction: - default: - reacted with - on a post {attributes.postBody} - short: reacted - channel: {attributes.postBody} - reddit: - comment: - default: commented in subreddit r/{channel} - short: commented on a post - channel: r/{channel} - post: - default: posted in subreddit r/{channel} - short: posted in subreddit - channel: r/{channel} - slack: - channel_joined: - default: joined channel {channel} - short: joined channel - channel: '{channel}' - message: - default: sent a message in {channel} - short: sent a message - channel: '{channel}' - twitter: - hashtag: - default: posted a tweet - short: posted a tweet - channel: '' - follow: - default: followed you - short: followed you - channel: '' - mention: - default: mentioned you in a tweet - short: mentioned you - channel: '' - stackoverflow: - question: - default: Asked a question {self} - short: asked a question - channel: '' - answer: - default: Answered a question {self} - short: answered a question - channel: '' - custom: - other: - attended-a-meeting: - short: Attended a meeting - channel: '' - default: Attended a meeting - asked-question-in-webinar: - default: Asked question in webinar - short: Asked question in webinar - channel: '' diff --git a/backend/src/api/components/activity/activityTypes/inputs.yaml b/backend/src/api/components/activity/activityTypes/inputs.yaml deleted file mode 100644 index 86160b3ebb..0000000000 --- a/backend/src/api/components/activity/activityTypes/inputs.yaml +++ /dev/null @@ -1,17 +0,0 @@ -components: - schemas: - ActivityTypesCreateInput: - description: >- - An activity type. - properties: - type: - description: >- - Human-friendly type of the activity. Default and short displays will set to this and key will be generated using this value. - - ActivityTypesUpdateInput: - description: >- - An activity type. - properties: - type: - description: >- - Human-friendly type of the activity. Default and short displays will set to this and key will be generated using this value. diff --git a/backend/src/api/components/activity/activityTypes/models.yaml b/backend/src/api/components/activity/activityTypes/models.yaml deleted file mode 100644 index 6e4b1e62a6..0000000000 --- a/backend/src/api/components/activity/activityTypes/models.yaml +++ /dev/null @@ -1,39 +0,0 @@ -components: - schemas: - # defines the display options of a single activity type - ActivityTypeDisplayOptions: - type: object - required: - - default - - short - - channel - - description: Activity type display options. - properties: - default: - description: Default display of an activity type. Used in the activity module in the app. - type: string - short: - description: Short display version of an activity type. Used in the member list -> last activity. - type: string - channel: - description: Channel display of an activity type. Used in Dashboard -> trending conversations. - type: string - - xml: - name: ActivityTypeDisplayOptions - - # defines the custom and default activity type settings - ActivityTypes: - type: object - properties: - custom: - type: object - description: Custom activity types defined by the user. - additionalProperties: - $ref: '#/components/schemas/ActivityTypeDisplayOptions' - default: - type: object - description: Default activity types used by the integrations. - additionalProperties: - $ref: '#/components/schemas/ActivityTypeDisplayOptions' diff --git a/backend/src/api/components/activity/examples.yaml b/backend/src/api/components/activity/examples.yaml deleted file mode 100644 index 48fc826041..0000000000 --- a/backend/src/api/components/activity/examples.yaml +++ /dev/null @@ -1,343 +0,0 @@ -components: - examples: - ActivityUpsert: - value: - id: 782b426d-adc8-4fb4-a4ee-ab0bb07ffca0 - type: message - timestamp: '2020-05-27T15:13:30.000Z' - platform: discord - isContribution: true - score: 1 - sourceId: '1234' - sourceParentId: null - attributes: - reactions: 43 - channel: dev - body: It's not magic. It's talend and sweat. - title: null - url: discord.gg/1234 - sentiment: - label: negative - mixed: 1.1410574428737164 - neutral: 11.00325882434845 - negative: 85.99738478660583 - positive: 1.8582981079816818 - sentiment: 2 - importHash: null - createdAt: '2022-10-03T15:18:11.294Z' - updatedAt: '2022-10-03T15:21:49.402Z' - deletedAt: null - memberId: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - conversationId: null - parentId: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - member: - id: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - username: - github: gilfoyle - twitter: gilfoyle - attributes: - bio: - github: Systems engineer at Pied Piper - default: It's not magic. It's talent and sweat - twitter: It's not magic. It's talent and sweat - url: - github: https://github.com/gilfoyle - default: https://t.co/g - twitter: https://t.co/g - location: - custom: Erlich's house - github: Palo alto - default: Erlich's house - displayName: Gilfoyle - email: gilfoyle@piedpiper.io - score: -1 - joinedAt: '2022-10-03T15:17:03.540Z' - importHash: null - reach: - total: 10000 - github: 5000 - twitter: 5000 - createdAt: '2022-10-03T15:17:03.547Z' - updatedAt: '2022-10-03T15:17:27.073Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - parent: null - tasks: [] - - ActivityFind: - value: - id: 462ddc6b-5672-43b2-9018-4e3fd7332228 - type: pull_request-closed - timestamp: '2021-07-27T20:20:30.000Z' - platform: github - isContribution: true - score: 10 - sourceId: gh_1 - sourceParentId: null - attributes: {} - channel: piedpiper - body: Last one to finish the code sprint! But I will have fewer bugs than Gilfoyle. - title: Code sprint over! - url: github.com/piedpiper/piedpier - sentiment: - label: positive - mixed: 0.7594161201268435 - neutral: 39.13898766040802 - negative: 12.336093187332153 - positive: 47.76550233364105 - sentiment: 79 - createdAt: '2022-10-03T15:36:43.775Z' - updatedAt: '2022-10-03T15:39:38.199Z' - deletedAt: null - memberId: 2effc566-1932-44f3-a821-2d692933a953 - conversationId: 291af008-7717-457e-9242-f5c507c8987b - parentId: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - member: - id: 2effc566-1932-44f3-a821-2d692933a953 - username: - github: dinesh - twitter: dinesh.chugtai - attributes: - bio: - github: Lead developer at Pied Piper - default: Pakistani Denzel. Tesla and gold chain owner. - twitter: Pakistani Denzel. Tesla and gold chain owner. - url: - github: https://github.com/dinesh - default: https://t.co/d - twitter: https://t.co/d - location: - custom: Silicon Valley - github: Palo alto - default: Silicon Valley - displayName: Dinesh - email: dinesh@piedpiper.io - score: 9 - joinedAt: '2022-10-03T15:30:55.672Z' - importHash: null - reach: - total: 100 - github: 60 - twitter: 40 - createdAt: '2022-10-03T15:30:55.679Z' - updatedAt: '2022-10-03T15:30:55.679Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - parent: null - tasks: [] - - ActivityFind2: - value: - id: 73aa13b7-1ef9-4987-a273-e560edff94ca - type: pull_request-comment - timestamp: '2021-07-27T20:22:30.000Z' - platform: github - isContribution: true - score: 3 - sourceId: gh_2 - sourceParentId: gh_1 - attributes: {} - channel: piedpiper - body: I will never underestimate my talents again. - title: null - url: github.com/piedpiper/piedpier - sentiment: - label: positive - mixed: 14.308956265449524 - neutral: 14.437079429626465 - negative: 9.826807677745819 - positive: 61.42715811729431 - sentiment: 86 - importHash: null - createdAt: '2022-10-03T15:38:05.847Z' - updatedAt: '2022-10-03T15:46:34.610Z' - deletedAt: null - memberId: 2effc566-1932-44f3-a821-2d692933a953 - conversationId: 291af008-7717-457e-9242-f5c507c8987b - parentId: 462ddc6b-5672-43b2-9018-4e3fd7332228 - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - member: - id: 2effc566-1932-44f3-a821-2d692933a953 - username: - github: dinesh - twitter: dinesh.chugtai - attributes: - bio: - github: Lead developer at Pied Piper - default: Pakistani Denzel. Tesla and gold chain owner. - twitter: Pakistani Denzel. Tesla and gold chain owner. - url: - github: https://github.com/dinesh - default: https://t.co/d - twitter: https://t.co/d - location: - custom: Silicon Valley - github: Palo alto - default: Silicon Valley - displayName: Dinesh - email: dinesh@piedpiper.io - score: -1 - joinedAt: '2022-10-03T15:30:55.672Z' - importHash: null - reach: - total: 100 - github: 60 - twitter: 40 - createdAt: '2022-10-03T15:30:55.679Z' - updatedAt: '2022-10-03T15:30:55.679Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - parent: - id: 462ddc6b-5672-43b2-9018-4e3fd7332228 - type: pull_request-closed - timestamp: '2021-07-27T20:22:30.000Z' - platform: github - isContribution: true - score: 10 - sourceId: gh_1 - sourceParentId: null - attributes: {} - channel: piedpiper - body: Last one to finish the code sprint! But I will have less bugs than Gilfoyle. - title: Code sprint over! - url: github.com/piedpiper/piedpier - sentiment: - label: positive - mixed: 0.7594161201268435 - neutral: 39.13898766040802 - negative: 12.336093187332153 - positive: 47.76550233364105 - sentiment: 79 - importHash: null - createdAt: '2022-10-03T15:36:43.775Z' - updatedAt: '2022-10-03T15:39:38.199Z' - deletedAt: null - memberId: 2effc566-1932-44f3-a821-2d692933a953 - conversationId: 291af008-7717-457e-9242-f5c507c8987b - parentId: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - tasks: [] - - ActivityFind3: - value: - id: 2dcbe40e-36e0-4929-ab21-a30467fd9a65 - type: pull_request-comment - timestamp: '2021-07-27T20:23:30.000Z' - platform: github - isContribution: true - score: 3 - sourceId: gh_3 - sourceParentId: gh_1 - attributes: {} - channel: piedpiper - body: Don't worry. I will continue to do it for you. - title: null - url: github.com/piedpiper/piedpier - sentiment: - label: positive - mixed: 2.9098065569996834 - neutral: 25.578168034553528 - negative: 2.241993509232998 - positive: 69.27002668380737 - sentiment: 97 - importHash: null - createdAt: '2022-10-03T15:47:20.151Z' - updatedAt: '2022-10-03T15:47:20.220Z' - deletedAt: null - memberId: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - conversationId: 291af008-7717-457e-9242-f5c507c8987b - parentId: 462ddc6b-5672-43b2-9018-4e3fd7332228 - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - member: - id: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - username: - github: gilfoyle - twitter: gilfoyle - attributes: - bio: - github: Systems engineer at Pied Piper - default: It's not magic. It's talent and sweat - twitter: It's not magic. It's talent and sweat - url: - github: https://github.com/gilfoyle - default: https://t.co/g - twitter: https://t.co/g - location: - custom: Erlich's house - github: Palo alto - default: Erlich's house - displayName: Gilfoyle - email: gilfoyle@piedpiper.io - score: -1 - joinedAt: '2022-10-03T15:17:03.540Z' - importHash: null - reach: - total: 10000 - github: 5000 - twitter: 5000 - createdAt: '2022-10-03T15:17:03.547Z' - updatedAt: '2022-10-03T15:17:27.073Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - parent: - id: 462ddc6b-5672-43b2-9018-4e3fd7332228 - type: pull_request-closed - timestamp: '2021-07-27T20:22:30.000Z' - platform: github - isContribution: true - score: 10 - sourceId: gh_1 - sourceParentId: null - attributes: {} - channel: piedpiper - body: Last one to finish the code sprint! But I will have less bugs than Gilfoyle. - title: Code sprint over! - url: github.com/piedpiper/piedpier - sentiment: - label: positive - mixed: 0.7594161201268435 - neutral: 39.13898766040802 - negative: 12.336093187332153 - positive: 47.76550233364105 - sentiment: 79 - importHash: null - createdAt: '2022-10-03T15:36:43.775Z' - updatedAt: '2022-10-03T15:39:38.199Z' - deletedAt: null - memberId: 2effc566-1932-44f3-a821-2d692933a953 - conversationId: 291af008-7717-457e-9242-f5c507c8987b - parentId: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - tasks: [] - - ActivityList: - value: - rows: - - $ref: '#/components/examples/ActivityFind' - - $ref: '#/components/examples/ActivityFind2' - - $ref: '#/components/examples/ActivityFind3' - count: 3 - limit: 10 - offset: 0 diff --git a/backend/src/api/components/activity/inputs.yaml b/backend/src/api/components/activity/inputs.yaml deleted file mode 100644 index 4b631c8ad1..0000000000 --- a/backend/src/api/components/activity/inputs.yaml +++ /dev/null @@ -1,33 +0,0 @@ -components: - schemas: - ActivityRelationsInput: - description: Relations of an activity. - type: object - properties: - tasks: - description: Tasks associated with the activity - type: array - items: - $ref: '#/components/schemas/TaskNoId' - - ActivityUpsertInput: - required: [memberId] - description: >- - An activity performed by a member of your community. The member is sent as an ID. - allOf: - - $ref: '#/components/schemas/ActivityNoId' - - $ref: '#/components/schemas/ActivityRelationsInput' - properties: - memberId: - description: The ID of the member that performed the activity - - ActivityUpsertWithMemberInput: - type: object - description: >- - An activity performed by a member of your community. The member is sent as a whole object. - allOf: - - $ref: '#/components/schemas/ActivityNoId' - - $ref: '#/components/schemas/ActivityRelationsInput' - properties: - member: - $ref: '#/components/schemas/MemberNoId' diff --git a/backend/src/api/components/activity/models.yaml b/backend/src/api/components/activity/models.yaml deleted file mode 100644 index fd357f108e..0000000000 --- a/backend/src/api/components/activity/models.yaml +++ /dev/null @@ -1,113 +0,0 @@ -components: - schemas: - # defines the attributes of an activity, excluding the ID - ActivityNoId: - description: An activity performed by a member of your community. - type: object - - required: - - type - - platform - - timestamp - - sourceId - - properties: - type: - description: Type of activity - type: string - - timestamp: - description: Date and time when the activity took place - type: string - format: date-time - - platform: - description: Platform on which the activity took place - type: string - - title: - description: Title of the activity - type: string - - body: - description: Body of the activity - type: string - - channel: - description: Channel of the activity - type: string - - sentiment: - description: Sentiment of the activity - type: object - properties: - sentiment: - description: >- - Default sentiment score. -
Computed by mapping (positive - negative) from 0 to 100 - type: number - minimum: 0 - maximum: 100 - label: - description: Sentiment label - type: string - enum: [positive, negative, neutral, mixed] - positive: - description: Positive sentiment score - type: number - minimum: 0 - maximum: 1 - negative: - description: Negative sentiment score - type: number - minimum: 0 - maximum: 1 - neutral: - description: Neutral sentiment score - type: number - minimum: 0 - maximum: 1 - mixed: - description: Mixed sentiment score. Mixed contains both positive and negative sentiments - type: number - minimum: 0 - maximum: 1 - - sourceId: - description: The id of the activity in the platform (e.g. the id of the message in Discord) - type: string - - sourceParentId: - description: The id of the parent activity in the platform (e.g. the id of the parent message in Discord) - type: string - - parentId: - description: Id of the parent activity, if the activity has a parent - type: string - format: uuid - - score: - description: Score associated with the activity - type: number - - isContribution: - description: Whether the activity was a contribution - type: boolean - - attributes: - description: Extra attributes of the activity - type: object - additionalProperties: true - - createdAt: - description: Date the activity was created - type: string - format: date-time - - updatedAt: - description: Date the activity was last updated - type: string - format: date-time - - xml: - name: ActivityNoId diff --git a/backend/src/api/components/activity/query.yaml b/backend/src/api/components/activity/query.yaml deleted file mode 100644 index 20a83f3919..0000000000 --- a/backend/src/api/components/activity/query.yaml +++ /dev/null @@ -1,48 +0,0 @@ -components: - schemas: - FilterType: - type: object - additionalProperties: - oneOf: - - type: string - - $ref: '#/components/schemas/FilterType' - - ActivityQuery: - description: >- - All the parameters you can use to query activitys. - - properties: - filter: - description: >- - Filter. Please refer to filter docs. - type: string - format: blob - - orderBy: - type: string - enum: - - activitiesCount_DESC - - score_ASC - - score_ASC - - joinedAt_ASC - - joinedAt_DESC - - createdAt_ASC - - createdAt_DESC - - organisation_ASC - - organisation_DESC - - location_ASC - - location_DESC - - limit: - description: >- - Limit the number of records returned. Default is 10. - type: integer - minimum: 1 - maximum: 200 - default: 10 - offset: - description: >- - Offset the number of records returned. Default is 0. - type: integer - minimum: 0 - default: 0 diff --git a/backend/src/api/components/activity/responses.yaml b/backend/src/api/components/activity/responses.yaml deleted file mode 100644 index 65fc16f6cf..0000000000 --- a/backend/src/api/components/activity/responses.yaml +++ /dev/null @@ -1,51 +0,0 @@ -components: - schemas: - # defines a complete activity, including the ID - Activity: - type: object - allOf: - - $ref: '#/components/schemas/ActivityNoId' - properties: - id: - description: The unique identifier for an activity. - - ActivityRelationsResponse: - description: Relations of an activity. - type: object - properties: - member: - description: Member that performed the activity - $ref: '#/components/schemas/Member' - tasks: - description: Tasks associated with the activity. - type: array - items: - $ref: '#/components/schemas/Task' - - ActivityResponse: - description: An activity performed by a member. - type: object - allOf: - - $ref: '#/components/schemas/Activity' - - $ref: '#/components/schemas/ActivityRelationsResponse' - - ActivityList: - description: List and count of activities. - type: object - properties: - rows: - description: List of activities - type: array - items: - $ref: '#/components/schemas/ActivityResponse' - count: - description: Count - type: integer - limit: - description: Limit of records returned - type: integer - offset: - description: Offset, for pagination - type: integer - xml: - name: ActivitiesList diff --git a/backend/src/api/components/auth.yaml b/backend/src/api/components/auth.yaml deleted file mode 100644 index f16a980eaa..0000000000 --- a/backend/src/api/components/auth.yaml +++ /dev/null @@ -1,5 +0,0 @@ -components: - securitySchemes: - Bearer: - type: http - scheme: bearer diff --git a/backend/src/api/components/automation/examples.yaml b/backend/src/api/components/automation/examples.yaml deleted file mode 100644 index 5d7163e5d4..0000000000 --- a/backend/src/api/components/automation/examples.yaml +++ /dev/null @@ -1,42 +0,0 @@ -components: - examples: - Automation: - value: - id: b3297f3b-6924-4e92-80e7-ef2e0d87a120 - type: 'webhook' - tenantId: a3297f3b-6924-4e92-80e7-ef2e0d87a120 - trigger: 'new_activity' - settings: - url: 'https://webhook.url/new_activities' - createdAt: '2022-03-29T09:22:31.989Z' - - AutomationPage: - value: - count: 1 - offset: 0 - limit: 10 - rows: - - id: b3297f3b-6924-4e92-80e7-ef2e0d87a120 - type: 'webhook' - tenantId: a3297f3b-6924-4e92-80e7-ef2e0d87a120 - trigger: 'new_activity' - settings: - url: 'https://webhook.url/new_activities' - createdAt: '2022-03-29T09:22:31.989Z' - - AutomationExecutionPage: - value: - count: 1 - offset: 0 - limit: 10 - rows: - - id: 'b3297f3b-6924-4e92-80e7-ef2e0d87a120' - automationId: 'a3297f3b-6924-4e92-80e7-ef2e0d87a120' - state: success - executedAt: '2022-03-29T09:22:31.989Z' - eventId: 'a3297f3b-6924-4e92-80e7-ef2e0d87a121' - payload: - - id: 'a3297f3b-6924-4e92-80e7-ef2e0d87a121' - type: 'comment' - timestamp: '2022-03-29T09:22:31.989Z' - platform: 'twitter' diff --git a/backend/src/api/components/automation/inputs.yaml b/backend/src/api/components/automation/inputs.yaml deleted file mode 100644 index 6d1d508499..0000000000 --- a/backend/src/api/components/automation/inputs.yaml +++ /dev/null @@ -1,33 +0,0 @@ -components: - schemas: - AutomationCreateInput: - type: object - description: >- - Data to create a new automation. - required: - - type - - trigger - - settings - properties: - type: - $ref: '#/components/schemas/AutomationType' - trigger: - $ref: '#/components/schemas/AutomationTrigger' - settings: - $ref: '#/components/schemas/AutomationSettings' - - AutomationUpdateInput: - type: object - description: >- - Data to update an existing automation. - required: - - trigger - - settings - - state - properties: - trigger: - $ref: '#/components/schemas/AutomationTrigger' - settings: - $ref: '#/components/schemas/AutomationSettings' - state: - $ref: '#/components/schemas/AutomationState' diff --git a/backend/src/api/components/automation/models.yaml b/backend/src/api/components/automation/models.yaml deleted file mode 100644 index 428bf3d888..0000000000 --- a/backend/src/api/components/automation/models.yaml +++ /dev/null @@ -1,199 +0,0 @@ -components: - schemas: - # defines automation type enum - AutomationType: - description: Automation type - type: string - enum: ['webhook'] - - # defines automation state enum - AutomationState: - description: Automation state - type: string - enum: ['active', 'disabled'] - - # defines automation triggers - AutomationTrigger: - description: What will trigger an automation - type: string - enum: ['new_activity', 'new_member'] - - # defines automation execution state - AutomationExecutionState: - description: What was the state of the automation execution - type: string - enum: ['success', 'error'] - - # defines webhook automation settings - WebhookAutomationSettings: - description: Settings used by automation with type webhook - type: object - required: - - url - properties: - url: - description: URL to POST webhook data to - type: string - format: uri - - # defines new activity triggered automation settings - NewActivityAutomationSettings: - description: Settings used by automation that is triggered by new activities - type: object - required: - - types - - platforms - - keywords - - teamMemberActivities - properties: - types: - description: 'If activity type matches any of these we should trigger this automation' - type: array - items: - type: string - platforms: - description: 'If activity came from any of these platforms we should trigger this automation' - type: array - items: - type: string - keywords: - description: 'If activity content contains any of these keywords we should trigger this automation' - type: array - items: - type: string - teamMemberActivities: - description: 'If activity came from any of our team members - should we trigger automation or not?' - type: boolean - - # defines automation settings object - AutomationSettings: - description: Settings based on automation type and trigger - you need to provide union object of both automation type based settings and trigger based settings - type: object - anyOf: - - $ref: '#/components/schemas/WebhookAutomationSettings' - - $ref: '#/components/schemas/NewActivityAutomationSettings' - - # Responses - Automation: - type: object - required: - - id - - type - - tenantId - - trigger - - settings - - state - - createdAt - properties: - id: - description: Automation unique ID - type: string - format: uuid - type: - $ref: '#/components/schemas/AutomationType' - tenantId: - description: Automation tenant unique ID - type: string - format: uuid - trigger: - $ref: '#/components/schemas/AutomationTrigger' - settings: - $ref: '#/components/schemas/AutomationSettings' - state: - $ref: '#/components/schemas/AutomationState' - createdAt: - description: When was automation created - type: string - format: date-time - lastExecutionAt: - description: When was automation last executed - type: string - format: date-time - lastExecutionState: - description: State of the last automation execution - $ref: '#/components/schemas/AutomationExecutionState' - lastExecutionError: - description: Error information if last automation execution failed - type: object - - # Responses - AutomationPage: - type: object - required: - - rows - - count - - offset - - limit - properties: - rows: - description: Array of automations that were fetched - type: array - items: - $ref: '#/components/schemas/Automation' - count: - description: How many total automations there are - type: integer - offset: - description: What offset was used when preparing this response - type: integer - limit: - description: What limit was used when preparing this response - type: integer - - AutomationExecution: - type: object - required: - - id - - automationId - - state - - executedAt - - eventId - - payload - properties: - id: - description: Automation execution unique ID - type: string - format: uuid - automationId: - description: Automation unique ID - type: string - format: uuid - state: - description: Automation execution state - $ref: '#/components/schemas/AutomationExecutionState' - error: - description: If execution was not successful this object will contain error information - type: object - executedAt: - description: Automation execution timestamp - type: string - format: date-time - eventId: - description: Unique ID of the event that triggered this automation execution. - type: string - payload: - description: Payload that was sent when this execution was processed - type: object - - AutomationExecutionPage: - type: object - required: - - rows - - count - - offset - - limit - properties: - rows: - description: Automation Execution List - type: array - items: - $ref: '#/components/schemas/AutomationExecution' - count: - description: How many items are there in total - type: integer - offset: - description: What offset was used when preparing this response - type: integer - limit: - description: What limit was used when preparing this response - type: integer diff --git a/backend/src/api/components/conversation/examples.yaml b/backend/src/api/components/conversation/examples.yaml deleted file mode 100644 index cc1139bf61..0000000000 --- a/backend/src/api/components/conversation/examples.yaml +++ /dev/null @@ -1,503 +0,0 @@ -components: - examples: - Conversation: - value: - id: 24bdea79-3125-4950-bb38-07fa4a555012 - title: Best of dinesh and Gilfoyle - slug: best-of-dinesh-and-gilfoyle - published: true - createdAt: '2022-10-05T12:21:53.271Z' - updatedAt: '2022-10-05T12:21:53.271Z' - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - activities: - - id: 89a136ed-336d-4586-8842-790775465212 - type: message - timestamp: '2020-06-27T14:13:30.000Z' - platform: discord - isContribution: true - score: 1 - sourceId: d42 - sourceParentId: null - attributes: {} - channel: piedpiper - body: Sooner or later Gilfoyle's servers are going to fail and then it's all done - title: null - url: github.com/piedpiper/piedpier - sentiment: - label: negative - mixed: 3.6482997238636017 - neutral: 19.5749893784523 - negative: 75.36468505859375 - positive: 1.4120269566774368 - sentiment: 2 - importHash: null - createdAt: '2022-10-05T12:09:44.414Z' - updatedAt: '2022-10-05T12:21:53.279Z' - deletedAt: null - memberId: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - conversationId: 24bdea79-3125-4950-bb38-07fa4a555012 - parentId: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - member: - id: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - username: - github: gilfoyle - twitter: gilfoyle - attributes: - bio: - github: Systems engineer at Pied Piper - default: It's not magic. It's talent and sweat - twitter: It's not magic. It's talent and sweat - url: - github: https://github.com/gilfoyle - default: https://t.co/g - twitter: https://t.co/g - location: - custom: Erlich's house - github: Palo alto - default: Erlich's house - displayName: Gilfoyle - email: gilfoyle@piedpiper.io - score: 8 - joinedAt: '2022-10-03T15:17:03.540Z' - importHash: null - reach: - total: 10000 - github: 5000 - twitter: 5000 - createdAt: '2022-10-03T15:17:03.547Z' - updatedAt: '2022-10-05T11:40:32.560Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - - id: c39dc046-da1d-4a25-8624-6b78aad00f30 - type: message - timestamp: '2020-06-27T15:13:30.000Z' - platform: discord - isContribution: true - score: 1 - sourceId: '2345' - sourceParentId: '1234' - attributes: - reactions: 68 - channel: dev - body: My servers could handle 10x the traffic, if they weren't busy apologizing for your sh*t codebase. - title: null - url: discord.gg/2345 - sentiment: - label: negative - mixed: 5.963129922747612 - neutral: 20.673033595085144 - negative: 69.99874711036682 - positive: 3.365083411335945 - sentiment: 5 - importHash: null - createdAt: '2022-10-03T15:19:30.415Z' - updatedAt: '2022-10-05T12:21:53.279Z' - deletedAt: null - memberId: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - conversationId: 24bdea79-3125-4950-bb38-07fa4a555012 - parentId: 782b426d-adc8-4fb4-a4ee-ab0bb07ffca0 - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - member: - id: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - username: - github: gilfoyle - twitter: gilfoyle - attributes: - bio: - github: Systems engineer at Pied Piper - default: It's not magic. It's talent and sweat - twitter: It's not magic. It's talent and sweat - url: - github: https://github.com/gilfoyle - default: https://t.co/g - twitter: https://t.co/g - location: - custom: Erlich's house - github: Palo alto - default: Erlich's house - displayName: Gilfoyle - email: gilfoyle@piedpiper.io - score: 8 - joinedAt: '2022-10-03T15:17:03.540Z' - importHash: null - reach: - total: 10000 - github: 5000 - twitter: 5000 - createdAt: '2022-10-03T15:17:03.547Z' - updatedAt: '2022-10-05T11:40:32.560Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - conversationStarter: - id: 89a136ed-336d-4586-8842-790775465212 - type: message - timestamp: '2020-06-27T14:13:30.000Z' - platform: discord - isContribution: true - score: 1 - sourceId: d42 - sourceParentId: null - attributes: {} - channel: piedpiper - body: Sooner or later Gilfoyle's servers are going to fail and then it's all done - title: null - url: github.com/piedpiper/piedpier - sentiment: - label: negative - mixed: 3.6482997238636017 - neutral: 19.5749893784523 - negative: 75.36468505859375 - positive: 1.4120269566774368 - sentiment: 2 - importHash: null - createdAt: '2022-10-05T12:09:44.414Z' - updatedAt: '2022-10-05T12:21:53.279Z' - deletedAt: null - memberId: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - conversationId: 24bdea79-3125-4950-bb38-07fa4a555012 - parentId: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - member: - id: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - username: - github: gilfoyle - twitter: gilfoyle - attributes: - bio: - github: Systems engineer at Pied Piper - default: It's not magic. It's talent and sweat - twitter: It's not magic. It's talent and sweat - url: - github: https://github.com/gilfoyle - default: https://t.co/g - twitter: https://t.co/g - location: - custom: Erlich's house - github: Palo alto - default: Erlich's house - displayName: Gilfoyle - email: gilfoyle@piedpiper.io - score: 8 - joinedAt: '2022-10-03T15:17:03.540Z' - importHash: null - reach: - total: 10000 - github: 5000 - twitter: 5000 - createdAt: '2022-10-03T15:17:03.547Z' - updatedAt: '2022-10-05T11:40:32.560Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - activityCount: 2 - memberCount: 2 - platform: discord - channel: piedpiper - lastActive: '2020-06-27T15:13:30.000Z' - - ConversationList: - value: - rows: - - id: 291af008-7717-457e-9242-f5c507c8987b - title: Code sprint over! - slug: code-sprint-over - published: false - createdAt: '2022-10-03T15:38:05.900Z' - updatedAt: '2022-10-03T15:38:05.900Z' - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - platform: github - activityCount: 3 - lastActive: '2021-07-27T20:23:30.000Z' - conversationStarter: - id: 89a136ed-336d-4586-8842-790775465212 - type: message - timestamp: '2020-06-27T14:13:30.000Z' - platform: discord - isContribution: true - score: 1 - sourceId: d42 - sourceParentId: null - attributes: {} - channel: piedpiper - body: Sooner or later Gilfoyle's servers are going to fail and then it's all done - title: null - url: github.com/piedpiper/piedpier - sentiment: - label: negative - mixed: 3.6482997238636017 - neutral: 19.5749893784523 - negative: 75.36468505859375 - positive: 1.4120269566774368 - sentiment: 2 - importHash: null - createdAt: '2022-10-05T12:09:44.414Z' - updatedAt: '2022-10-05T12:21:53.279Z' - deletedAt: null - memberId: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - conversationId: 24bdea79-3125-4950-bb38-07fa4a555012 - parentId: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - member: - id: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - username: - github: gilfoyle - twitter: gilfoyle - attributes: - bio: - github: Systems engineer at Pied Piper - default: It's not magic. It's talent and sweat - twitter: It's not magic. It's talent and sweat - url: - github: https://github.com/gilfoyle - default: https://t.co/g - twitter: https://t.co/g - location: - custom: Erlich's house - github: Palo alto - default: Erlich's house - displayName: Gilfoyle - email: gilfoyle@piedpiper.io - score: 8 - joinedAt: '2022-10-03T15:17:03.540Z' - importHash: null - reach: - total: 10000 - github: 5000 - twitter: 5000 - createdAt: '2022-10-03T15:17:03.547Z' - updatedAt: '2022-10-05T11:40:32.560Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - lastReplies: - - id: c39dc046-da1d-4a25-8624-6b78aad00f30 - type: message - timestamp: '2020-06-27T15:13:30.000Z' - platform: discord - isContribution: true - score: 1 - sourceId: '2345' - sourceParentId: '1234' - attributes: - reactions: 68 - channel: dev - body: My servers could handle 10x the traffic, if they weren't busy apologizing for your sh*t codebase. - title: null - url: discord.gg/2345 - sentiment: - label: negative - mixed: 5.963129922747612 - neutral: 20.673033595085144 - negative: 69.99874711036682 - positive: 3.365083411335945 - sentiment: 5 - importHash: null - createdAt: '2022-10-03T15:19:30.415Z' - updatedAt: '2022-10-05T12:21:53.279Z' - deletedAt: null - memberId: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - conversationId: 24bdea79-3125-4950-bb38-07fa4a555012 - parentId: 782b426d-adc8-4fb4-a4ee-ab0bb07ffca0 - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - member: - id: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - username: - github: gilfoyle - twitter: gilfoyle - attributes: - bio: - github: Systems engineer at Pied Piper - default: It's not magic. It's talent and sweat - twitter: It's not magic. It's talent and sweat - url: - github: https://github.com/gilfoyle - default: https://t.co/g - twitter: https://t.co/g - location: - custom: Erlich's house - github: Palo alto - default: Erlich's house - displayName: Gilfoyle - email: gilfoyle@piedpiper.io - score: 8 - joinedAt: '2022-10-03T15:17:03.540Z' - importHash: null - reach: - total: 10000 - github: 5000 - twitter: 5000 - createdAt: '2022-10-03T15:17:03.547Z' - updatedAt: '2022-10-05T11:40:32.560Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - memberCount: 2 - channel: null - - id: 24bdea79-3125-4950-bb38-07fa4a555012 - title: Best of dinesh and Gilfoyle - slug: best-of-dinesh-and-gilfoyle - published: true - createdAt: '2022-10-05T12:21:53.271Z' - updatedAt: '2022-10-05T12:21:53.271Z' - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - platform: discord - activityCount: 1 - lastActive: '2020-06-29T15:13:30.000Z' - conversationStarter: - id: 89a136ed-336d-4586-8842-790775465212 - type: message - timestamp: '2020-05-27T14:13:30.000Z' - platform: discord - isContribution: true - score: 1 - sourceId: d42 - sourceParentId: null - attributes: {} - channel: piedpiper - body: Best of Dinesh and gilfoyle - title: null - url: github.com/piedpiper/piedpier - sentiment: - label: negative - mixed: 1.6482997238636017 - neutral: 12.5749893784523 - negative: 62.36468505859375 - positive: 1.4120269566774368 - sentiment: 2 - importHash: null - createdAt: '2022-10-05T12:09:44.414Z' - updatedAt: '2022-10-05T12:21:53.279Z' - deletedAt: null - memberId: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - conversationId: 24bdea79-3125-4950-bb38-07fa4a555012 - parentId: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - member: - id: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - username: - github: gilfoyle - twitter: gilfoyle - attributes: - bio: - github: Systems engineer at Pied Piper - default: It's not magic. It's talent and sweat - twitter: It's not magic. It's talent and sweat - url: - github: https://github.com/gilfoyle - default: https://t.co/g - twitter: https://t.co/g - location: - custom: Erlich's house - github: Palo alto - default: Erlich's house - displayName: Gilfoyle - email: gilfoyle@piedpiper.io - score: 8 - joinedAt: '2022-10-03T15:17:03.540Z' - importHash: null - reach: - total: 10000 - github: 5000 - twitter: 5000 - createdAt: '2022-10-03T15:17:03.547Z' - updatedAt: '2022-10-05T11:40:32.560Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - lastReplies: - - id: c39dc046-da1d-4a25-8624-6b78aad00f30 - type: message - timestamp: '2020-06-29T15:13:30.000Z' - platform: discord - isContribution: true - score: 1 - sourceId: '2345' - sourceParentId: '1234' - attributes: - reactions: 68 - channel: dev - body: A very last reply to the conversation. - title: null - url: discord.gg/2345 - sentiment: - label: negative - mixed: 5.963129922747612 - neutral: 20.673033595085144 - negative: 69.99874711036682 - positive: 3.365083411335945 - sentiment: 5 - importHash: null - createdAt: '2022-10-03T15:19:30.415Z' - updatedAt: '2022-10-05T12:21:53.279Z' - deletedAt: null - memberId: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - conversationId: 24bdea79-3125-4950-bb38-07fa4a555012 - parentId: 782b426d-adc8-4fb4-a4ee-ab0bb07ffca0 - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - member: - id: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - username: - github: gilfoyle - twitter: gilfoyle - attributes: - bio: - github: Systems engineer at Pied Piper - default: It's not magic. It's talent and sweat - twitter: It's not magic. It's talent and sweat - url: - github: https://github.com/gilfoyle - default: https://t.co/g - twitter: https://t.co/g - location: - custom: Erlich's house - github: Palo alto - default: Erlich's house - displayName: Gilfoyle - email: gilfoyle@piedpiper.io - score: 8 - joinedAt: '2022-10-03T15:17:03.540Z' - importHash: null - reach: - total: 10000 - github: 5000 - twitter: 5000 - createdAt: '2022-10-03T15:17:03.547Z' - updatedAt: '2022-10-05T11:40:32.560Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - memberCount: 2 - channel: dev - count: 2 - limit: 10 - offset: 0 diff --git a/backend/src/api/components/conversation/inputs.yaml b/backend/src/api/components/conversation/inputs.yaml deleted file mode 100644 index 8e3819f7f0..0000000000 --- a/backend/src/api/components/conversation/inputs.yaml +++ /dev/null @@ -1,2 +0,0 @@ -components: - schemas: diff --git a/backend/src/api/components/conversation/models.yaml b/backend/src/api/components/conversation/models.yaml deleted file mode 100644 index 99432ac467..0000000000 --- a/backend/src/api/components/conversation/models.yaml +++ /dev/null @@ -1,81 +0,0 @@ -components: - schemas: - # defines the attributes of a conversation, excluding the ID - ConversationNoId: - type: object - required: - - platform - - slug - - tenantId - - description: A conversation is a group of activities. Some attributes, like slug, are mostly used in public pages. - properties: - title: - description: Title of the conversation - type: string - slug: - description: Unique slug of the conversation - type: string - published: - description: Whether the conversation is publicaly visible from open pages. - type: boolean - default: false - conversationStarter: - description: The conversation starter activity - type: object - additionalProperties: - $ref: '#/components/schemas/Activity' - memberCount: - description: Number of participating members in the conversation. - type: integer - lastActive: - description: Last activity time in the conversation - type: string - format: date-time - createdAt: - description: Date the conversation was created - type: string - format: date-time - updatedAt: - description: Date the conversation was last updated - type: string - format: date-time - tenantId: - description: Your workspace/tenant id - type: string - format: uuid - - xml: - name: Conversation - - # defines a complete conversation, including the ID - Conversation: - allOf: - - $ref: '#/components/schemas/ConversationNoId' - properties: - id: - description: Unique identifier of the conversation - type: string - activities: - description: List of IDs of the activities in the conversation - type: array - items: - type: string - - # Responses - ConversationList: - type: object - properties: - rows: - type: array - items: - $ref: '#/components/schemas/Conversation' - count: - description: Count - type: integer - limit: - description: Limit of records returned - type: integer - offset: - description: Offset, for pagination - type: integer diff --git a/backend/src/api/components/member/examples.yaml b/backend/src/api/components/member/examples.yaml deleted file mode 100644 index fc7ce57435..0000000000 --- a/backend/src/api/components/member/examples.yaml +++ /dev/null @@ -1,503 +0,0 @@ -components: - examples: - MemberUpsert: - value: - id: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - username: - github: gilfoyle - twitter: gilfoyle - attributes: - bio: - github: Systems engineer at Pied Piper - default: It's not magic. It's talent and sweat - twitter: It's not magic. It's talent and sweat - url: - github: https://github.com/gilfoyle - default: https://t.co/g - twitter: https://t.co/g - location: - custom: Erlich's house - github: Palo alto - default: Erlich's house - displayName: Gilfoyle - email: gilfoyle@piedpiper.io - score: -1 - joinedAt: '2022-10-03T15:17:03.540Z' - importHash: null - reach: - total: 10000 - github: 5000 - twitter: 5000 - createdAt: '2022-10-03T15:17:03.547Z' - updatedAt: '2022-10-03T15:17:27.073Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - - MemberFind: - value: - id: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - username: - github: gilfoyle - twitter: gilfoyle - attributes: - bio: - github: Systems engineer at Pied Piper - default: It's not magic. It's talent and sweat - twitter: It's not magic. It's talent and sweat - url: - github: https://github.com/gilfoyle - default: https://t.co/g - twitter: https://t.co/g - location: - custom: Erlich's house - github: Palo alto - default: Erlich's house - displayName: Gilfoyle - email: gilfoyle@piedpiper.io - score: 8 - joinedAt: '2022-10-03T15:17:03.540Z' - importHash: null - reach: - total: 10000 - github: 5000 - twitter: 5000 - createdAt: '2022-10-03T15:17:03.547Z' - updatedAt: '2022-10-05T11:40:32.560Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - activities: - - id: 2dcbe40e-36e0-4929-ab21-a30467fd9a65 - type: pull_request-comment - timestamp: '2021-07-27T20:23:30.000Z' - platform: github - isContribution: true - score: 3 - sourceId: gh_3 - sourceParentId: gh_1 - attributes: {} - channel: piedpiper - body: Don't worry. I will continue to do it for you. - title: null - url: github.com/piedpiper/piedpier - sentiment: - label: positive - mixed: 2.9098065569996834 - neutral: 25.578168034553528 - negative: 2.241993509232998 - positive: 69.27002668380737 - sentiment: 97 - importHash: null - createdAt: '2022-10-03T15:47:20.151Z' - updatedAt: '2022-10-03T15:47:20.220Z' - deletedAt: null - memberId: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - conversationId: 291af008-7717-457e-9242-f5c507c8987b - parentId: 462ddc6b-5672-43b2-9018-4e3fd7332228 - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - - id: c39dc046-da1d-4a25-8624-6b78aad00f30 - type: message - timestamp: '2020-06-27T15:13:30.000Z' - platform: discord - isContribution: true - score: 1 - sourceId: '2345' - sourceParentId: '1234' - attributes: - reactions: 68 - channel: dev - body: My servers could handle 10x the traffic, if they weren't busy apologizing for your sh*t codebase. - title: null - url: discord.gg/2345 - sentiment: - label: negative - mixed: 5.963129922747612 - neutral: 20.673033595085144 - negative: 69.99874711036682 - positive: 3.365083411335945 - sentiment: 5 - importHash: null - createdAt: '2022-10-03T15:19:30.415Z' - updatedAt: '2022-10-03T15:26:02.599Z' - deletedAt: null - memberId: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - conversationId: null - parentId: 782b426d-adc8-4fb4-a4ee-ab0bb07ffca0 - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - - id: 782b426d-adc8-4fb4-a4ee-ab0bb07ffca0 - type: message - timestamp: '2020-05-27T15:13:30.000Z' - platform: discord - isContribution: true - score: 1 - sourceId: '1234' - sourceParentId: null - attributes: - reactions: 43 - channel: dev - body: It's not magic. It's talend and sweat. - title: null - url: discord.gg/1234 - sentiment: - label: negative - mixed: 1.1410574428737164 - neutral: 11.00325882434845 - negative: 85.99738478660583 - positive: 1.8582981079816818 - sentiment: 2 - importHash: null - createdAt: '2022-10-03T15:18:11.294Z' - updatedAt: '2022-10-03T15:21:49.402Z' - deletedAt: null - memberId: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - conversationId: null - parentId: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - lastActivity: - id: 2dcbe40e-36e0-4929-ab21-a30467fd9a65 - type: pull_request-comment - timestamp: '2021-07-27T20:23:30.000Z' - platform: github - isContribution: true - score: 3 - sourceId: gh_3 - sourceParentId: gh_1 - attributes: {} - channel: piedpiper - body: Don't worry. I will continue to do it for you. - title: null - url: github.com/piedpiper/piedpier - sentiment: - label: positive - mixed: 2.9098065569996834 - neutral: 25.578168034553528 - negative: 2.241993509232998 - positive: 69.27002668380737 - sentiment: 97 - importHash: null - createdAt: '2022-10-03T15:47:20.151Z' - updatedAt: '2022-10-03T15:47:20.220Z' - deletedAt: null - memberId: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - conversationId: 291af008-7717-457e-9242-f5c507c8987b - parentId: 462ddc6b-5672-43b2-9018-4e3fd7332228 - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - lastActive: '2021-07-27T20:23:30.000Z' - activityCount: 3 - averageSentiment: 34.67 - tags: - - id: 38807625-6302-47b5-9f35-58566ddec83b - name: developer - importHash: null - createdAt: '2022-10-05T11:41:20.162Z' - updatedAt: '2022-10-05T11:41:20.162Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - - id: dca36c33-38cd-4e68-8ba8-515167e00971 - name: attended-hooli-con - importHash: null - createdAt: '2022-10-05T11:42:17.414Z' - updatedAt: '2022-10-05T11:42:17.414Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - organizations: - - id: 31bff99a-2eac-49f5-b015-cba95aa6e530 - name: Pied Piper - url: https://piedpiper.io - description: The new internet - emails: - - richard@piedpiper.io - - hello@piedpiper.io - phoneNumbers: null - logo: null - tags: - - new-internet - - making-the-world-a-better-place - - not-like-hooli - twitter: - bio: The internet we deserve - handle: PiedPiper - location: The valley - followers: 5000 - following: 20 - linkedin: - handle: company/PiedPiper - crunchbase: - handle: company/PiedPiper - employees: 50 - revenueRange: - max: 50 - min: 10 - importHash: null - createdAt: '2022-10-03T16:15:21.812Z' - updatedAt: '2022-10-03T16:15:21.812Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - tasks: [] - notes: [] - noMerge: [] - toMerge: [] - - MemberList: - value: - rows: - - id: 2effc566-1932-44f3-a821-2d692933a953 - username: - github: dinesh - twitter: dinesh.chugtai - attributes: - bio: - github: Lead developer at Pied Piper - default: Pakistani Denzel. Tesla and gold chain owner. - twitter: Pakistani Denzel. Tesla and gold chain owner. - url: - github: https://github.com/dinesh - default: https://t.co/d - twitter: https://t.co/d - location: - custom: Silicon Valley - github: Palo alto - default: Silicon Valley - displayName: Dinesh - email: dinesh@piedpiper.io - score: 9 - joinedAt: '2022-10-03T15:30:55.672Z' - importHash: null - reach: - total: 100 - github: 60 - twitter: 40 - createdAt: '2022-10-03T15:30:55.679Z' - updatedAt: '2022-10-05T11:39:58.095Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - identities: - - github - - twitter - activeOn: - - github - activityCount: '2' - lastActive: '2021-07-27T20:22:30.000Z' - averageSentiment: '82.50' - noMerge: [] - toMerge: [] - lastActivity: - id: 73aa13b7-1ef9-4987-a273-e560edff94ca - type: pull_request-comment - timestamp: '2021-07-27T20:22:30.000Z' - platform: github - isContribution: true - score: 3 - sourceId: gh_2 - sourceParentId: gh_1 - attributes: {} - channel: piedpiper - body: I will never underestimate my talents again. - title: null - url: github.com/piedpiper/piedpier - sentiment: - label: positive - mixed: 14.308956265449524 - neutral: 14.437079429626465 - negative: 9.826807677745819 - positive: 61.42715811729431 - sentiment: 86 - importHash: null - createdAt: '2022-10-03T15:38:05.847Z' - updatedAt: '2022-10-03T15:46:34.610Z' - deletedAt: null - memberId: 2effc566-1932-44f3-a821-2d692933a953 - conversationId: 291af008-7717-457e-9242-f5c507c8987b - parentId: 462ddc6b-5672-43b2-9018-4e3fd7332228 - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - organizations: - - id: 31bff99a-2eac-49f5-b015-cba95aa6e530 - name: Pied Piper - url: https://piedpiper.io - description: The new internet - emails: - - richard@piedpiper.io - - hello@piedpiper.io - phoneNumbers: null - logo: null - tags: - - new-internet - - making-the-world-a-better-place - - not-like-hooli - twitter: - bio: The internet we deserve - handle: PiedPiper - location: The valley - followers: 5000 - following: 20 - linkedin: - handle: company/PiedPiper - crunchbase: - handle: company/PiedPiper - employees: 50 - revenueRange: - max: 50 - min: 10 - importHash: null - createdAt: '2022-10-03T16:15:21.812Z' - updatedAt: '2022-10-03T16:15:21.812Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - tags: - - id: 38807625-6302-47b5-9f35-58566ddec83b - name: developer - importHash: null - createdAt: '2022-10-05T11:41:20.162Z' - updatedAt: '2022-10-05T11:41:20.162Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - - id: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - username: - github: gilfoyle - twitter: gilfoyle - attributes: - bio: - github: Systems engineer at Pied Piper - default: It's not magic. It's talent and sweat - twitter: It's not magic. It's talent and sweat - url: - github: https://github.com/gilfoyle - default: https://t.co/g - twitter: https://t.co/g - location: - custom: Erlich's house - github: Palo alto - default: Erlich's house - displayName: Gilfoyle - email: gilfoyle@piedpiper.io - score: 8 - joinedAt: '2022-10-03T15:17:03.540Z' - importHash: null - reach: - total: 10000 - github: 5000 - twitter: 5000 - createdAt: '2022-10-03T15:17:03.547Z' - updatedAt: '2022-10-05T11:40:32.560Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - activityCount: '3' - lastActive: '2021-07-27T20:23:30.000Z' - averageSentiment: '34.67' - noMerge: [] - toMerge: [] - lastActivity: - id: 2dcbe40e-36e0-4929-ab21-a30467fd9a65 - type: pull_request-comment - timestamp: '2021-07-27T20:23:30.000Z' - platform: github - isContribution: true - score: 3 - sourceId: gh_3 - sourceParentId: gh_1 - attributes: {} - channel: piedpiper - body: Don't worry. I will continue to do it for you. - title: null - url: github.com/piedpiper/piedpier - sentiment: - label: positive - mixed: 2.9098065569996834 - neutral: 25.578168034553528 - negative: 2.241993509232998 - positive: 69.27002668380737 - sentiment: 97 - importHash: null - createdAt: '2022-10-03T15:47:20.151Z' - updatedAt: '2022-10-03T15:47:20.220Z' - deletedAt: null - memberId: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - conversationId: 291af008-7717-457e-9242-f5c507c8987b - parentId: 462ddc6b-5672-43b2-9018-4e3fd7332228 - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - organizations: - - id: 31bff99a-2eac-49f5-b015-cba95aa6e530 - name: Pied Piper - url: https://piedpiper.io - description: The new internet - emails: - - richard@piedpiper.io - - hello@piedpiper.io - phoneNumbers: null - logo: null - tags: - - new-internet - - making-the-world-a-better-place - - not-like-hooli - twitter: - bio: The internet we deserve - handle: PiedPiper - location: The valley - followers: 5000 - following: 20 - linkedin: - handle: company/PiedPiper - crunchbase: - handle: company/PiedPiper - employees: 50 - revenueRange: - max: 50 - min: 10 - importHash: null - createdAt: '2022-10-03T16:15:21.812Z' - updatedAt: '2022-10-03T16:15:21.812Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - tags: - - id: 38807625-6302-47b5-9f35-58566ddec83b - name: developer - importHash: null - createdAt: '2022-10-05T11:41:20.162Z' - updatedAt: '2022-10-05T11:41:20.162Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - - id: dca36c33-38cd-4e68-8ba8-515167e00971 - name: attended-hooli-con - importHash: null - createdAt: '2022-10-05T11:42:17.414Z' - updatedAt: '2022-10-05T11:42:17.414Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - count: 2 - offset: 0 - limit: 10 diff --git a/backend/src/api/components/member/inputs.yaml b/backend/src/api/components/member/inputs.yaml deleted file mode 100644 index 451e881980..0000000000 --- a/backend/src/api/components/member/inputs.yaml +++ /dev/null @@ -1,70 +0,0 @@ -components: - schemas: - MemberPlatformHelper: - type: object - required: - - platform - properties: - platform: - type: string - description: Platform for which to check member existence. - - MemberOrganizations: - type: object - properties: - organizations: - description: >- - Organizations associated with the member. Each element in the array is the name of the organization, or an organization object. - If the organization does not exist, it will be created. - type: array - items: - $ref: '#/components/schemas/OrganizationNoId' - - MemberOrganizationsUpdate: - type: object - properties: - organizations: - description: >- - Organizations associated with the member. Each element in the array is the name of the organization, or an organization object. - If the organization does not exist, it will be created. - type: array - items: - type: string - - MemberInputRelations: - type: object - properties: - tags: - description: Tags associated with the member. Each element in the array is the ID of the tag. - type: array - items: - type: string - tasks: - description: Tasks associated with the member. Each element in the array is the ID of the task. - type: array - items: - type: string - notes: - description: Notes associated with the member. Each element in the array is the ID of the note. - type: array - items: - type: string - activities: - description: Activities associated with the member. Each element in the array is the ID of the activity. - type: array - items: - type: string - - MemberUpsertInput: - allOf: - - $ref: '#/components/schemas/MemberPlatformHelper' - - $ref: '#/components/schemas/MemberNoId' - - $ref: '#/components/schemas/MemberOrganizations' - - $ref: '#/components/schemas/MemberInputRelations' - - MemberUpdateInput: - allOf: - - $ref: '#/components/schemas/MemberPlatformHelper' - - $ref: '#/components/schemas/MemberNoId' - - $ref: '#/components/schemas/MemberInputRelations' - - $ref: '#/components/schemas/MemberOrganizationsUpdate' diff --git a/backend/src/api/components/member/memberAttributeSettings/examples.yaml b/backend/src/api/components/member/memberAttributeSettings/examples.yaml deleted file mode 100644 index 8de609b41f..0000000000 --- a/backend/src/api/components/member/memberAttributeSettings/examples.yaml +++ /dev/null @@ -1,32 +0,0 @@ -components: - examples: - MemberAttributeSettings: - value: - id: 9eaedce9-1f3a-4a75-adc8-e475cbc47553' - type: 'string' - canDelete: false - show: true - label: 'Url' - name: 'url' - createdAt: '2022-09-07' - updatedAt: '2022-09-07' - tenantId: 'fcd5b9cc-144b-4687-8fd9-34818f35e70d' - - MemberAttributeSettings2: - value: - id: '13bb9e12-c371-44ad-8806-0678c2f53dd1' - type: 'boolean' - canDelete: false - show: true - label: 'is Hireable' - name: 'isHireable' - createdAt: '2022-09-07' - updatedAt: '2022-09-07' - tenantId: 'fcd5b9cc-144b-4687-8fd9-34818f35e70d' - - MemberAttributeSettingsList: - value: - rows: - - $ref: '#/components/examples/MemberAttributeSettings' - - $ref: '#/components/examples/MemberAttributeSettings2' - count: 2 diff --git a/backend/src/api/components/member/memberAttributeSettings/inputs.yaml b/backend/src/api/components/member/memberAttributeSettings/inputs.yaml deleted file mode 100644 index a78edca3d6..0000000000 --- a/backend/src/api/components/member/memberAttributeSettings/inputs.yaml +++ /dev/null @@ -1,21 +0,0 @@ -components: - schemas: - MemberAttributeSettingsCreateInput: - description: >- - A member attribute. - allOf: - - $ref: '#/components/schemas/MemberAttributeSettingsNoId' - - MemberAttributeSettingsUpdateInput: - description: >- - A member attribute. - properties: - label: - description: >- - Human-friendly name of the attribute. Label is unique in workspaces. - type: string - show: - description: >- - Whether to show the member attribute in the web app or not. - type: boolean - default: true diff --git a/backend/src/api/components/member/memberAttributeSettings/models.yaml b/backend/src/api/components/member/memberAttributeSettings/models.yaml deleted file mode 100644 index d32f1d72c4..0000000000 --- a/backend/src/api/components/member/memberAttributeSettings/models.yaml +++ /dev/null @@ -1,52 +0,0 @@ -components: - schemas: - # defines the settings of a member attribute, excluding the ID - MemberAttributeSettingsNoId: - type: object - required: - - label - - type - - description: A member attribute that can be created dynamically. - properties: - label: - description: Human-friendly name of the attribute. Label is unique in workspaces. - type: string - name: - description: Camel-case code friendly name of the attribute. If ommited, name will be generated from the label. Name is unique in workspaces. - type: string - type: - description: Type of the attribute's value - type: string - enum: ['boolean', 'number', 'email', 'string', 'url', 'date'] - - canDelete: - description: If set to false, member attribute can not be deleted in future requests. - type: boolean - default: false - - show: - description: Whether to show the member attribute in the web app or not. - type: boolean - default: true - - createdAt: - description: Date the member attribute was created. - type: string - format: date-time - updatedAt: - description: Date the member attribute was last updated. - type: string - format: date-time - - xml: - name: MemberAttributeSettings - - # defines the settings of a member attribute, including the ID - MemberAttributeSettings: - type: object - allOf: - - $ref: '#/components/schemas/MemberAttributeSettingsNoId' - properties: - id: - description: The attribute settings ID. diff --git a/backend/src/api/components/member/memberAttributeSettings/responses.yaml b/backend/src/api/components/member/memberAttributeSettings/responses.yaml deleted file mode 100644 index d980fe97cf..0000000000 --- a/backend/src/api/components/member/memberAttributeSettings/responses.yaml +++ /dev/null @@ -1,23 +0,0 @@ -components: - schemas: - # defines the response for a list of member attribute settings - MemberAttributeSettingsList: - description: List and count member attribute settings. - type: object - properties: - rows: - description: List of member attribute settings - type: array - items: - $ref: '#/components/schemas/MemberAttributeSettings' - count: - description: Count - type: integer - limit: - description: Limit of records returned - type: integer - offset: - description: Offset, for pagination - type: integer - xml: - name: MemberAttributeSettingsList diff --git a/backend/src/api/components/member/models.yaml b/backend/src/api/components/member/models.yaml deleted file mode 100644 index caaaf71f4d..0000000000 --- a/backend/src/api/components/member/models.yaml +++ /dev/null @@ -1,75 +0,0 @@ -components: - schemas: - # defines a member, excluding the ID - MemberNoId: - description: A member of your community. - type: object - required: - - username - - properties: - username: - description: >- - Usernames of the member in each platform. Exactly one for each platform in which the member is active. -
Example: ```{ github: 'iamgilfoyle', discord: 'gilfoyle '}``` - type: object - additionalProperties: true - displayName: - description: UI friendly name of the member - type: string - emails: - description: Email addresses of the member - type: array - items: - type: string - joinedAt: - description: Date of joining the community - type: string - format: date-time - score: - description: Engagement score of the member. From 0 to 10. Set -1 for not yet calculated. - type: number - reach: - description: >- - Reach of the member in each platform. At most one for each platform in which the member is active. -
Example: ```{ github: 10, twitter: 250, total: 260 }``` - type: object - properties: - total: - description: Sum of all the platform reaches. - type: number - additionalProperties: true - attributes: - description: >- - Attributes associated to the member. Each attribute must be an object with it's value for each platform, and a default. -
For example: ```{"location": {"github": "San Francisco", "twitter": "California", "default": "San Francisco"}}``` - type: object - additionalProperties: - $ref: '#/components/schemas/MemberAttribute' - createdAt: - description: Date the member was created - type: string - format: date-time - updatedAt: - description: Date the member was last updated - type: string - format: date-time - - xml: - name: Member - - # Member attribute - MemberAttribute: - description: >- - A key for each platform. -
- ```default``` is the value that will be displayed by default in the app -
- ```custom``` is the value that will be displayed if the user has set a custom value for the attribute - type: object - properties: - default: - description: Default value for the attribute. This is set automatically according to crowd.dev rules. - type: string - custom: - description: Custom value for the attribute. This is optionally set by the user. It will always be picked as the default when sent. - type: string - additionalProperties: true diff --git a/backend/src/api/components/member/query.yaml b/backend/src/api/components/member/query.yaml deleted file mode 100644 index 3edef18a02..0000000000 --- a/backend/src/api/components/member/query.yaml +++ /dev/null @@ -1,49 +0,0 @@ -components: - schemas: - FilterType: - type: object - additionalProperties: - oneOf: - - type: string - - $ref: '#/components/schemas/FilterType' - - MemberQuery: - description: >- - All the parameters you can use to query members. - - properties: - filter: - description: >- - Filter. Please refer to filter docs. - type: string - format: blob - - orderBy: - type: string - enum: - - activityCount_ASC - - activityCount_DESC - - score_ASC - - score_DESC - - joinedAt_ASC - - joinedAt_DESC - - createdAt_ASC - - createdAt_DESC - - organisation_ASC - - organisation_DESC - - location_ASC - - location_DESC - - limit: - description: >- - Limit the number of records returned. Default is 10. - type: integer - minimum: 1 - maximum: 200 - default: 10 - offset: - description: >- - Offset the number of records returned. Default is 0. - type: integer - minimum: 0 - default: 0 diff --git a/backend/src/api/components/member/responses.yaml b/backend/src/api/components/member/responses.yaml deleted file mode 100644 index 0a658ffb91..0000000000 --- a/backend/src/api/components/member/responses.yaml +++ /dev/null @@ -1,94 +0,0 @@ -components: - schemas: - # Single member - Member: - type: object - allOf: - - $ref: '#/components/schemas/MemberNoId' - properties: - id: - description: The unique identifier for a member of your community. - activityCount: - description: Number of activities performed by the member. - type: integer - lastActivity: - description: Timestamp, type and platform of the last activity performed by the member. - type: object - properties: - type: - description: Type of the last activity - type: string - timestamp: - description: Date and time of the last activity - type: string - format: date-time - platform: - description: Platform of the last activity - type: string - averageSentiment: - description: Average sentiment of the member. From 0 to 100. - type: number - identities: - description: List of platforms the member has identities in. - type: array - items: - type: string - activeOn: - description: List of platforms the member is active on. - type: array - items: - type: string - - MemberRelationsResponse: - description: Relations of a member. - type: object - properties: - tags: - description: Tags associated with the member. - type: array - items: - $ref: '#/components/schemas/Tag' - notes: - description: Notes associated with the member. - type: array - items: - $ref: '#/components/schemas/Note' - tasks: - description: Tasks associated with the member. - type: array - items: - $ref: '#/components/schemas/Task' - organizations: - description: Organizations associated with the member. - type: array - items: - $ref: '#/components/schemas/Organization' - - MemberResponse: - description: A member of your community. - type: object - allOf: - - $ref: '#/components/schemas/Member' - - $ref: '#/components/schemas/MemberRelationsResponse' - - # List - MemberList: - description: List and count of members. - type: object - properties: - rows: - description: List of members - type: array - items: - $ref: '#/components/schemas/MemberResponse' - count: - description: Count - type: integer - limit: - description: Limit of records returned - type: integer - offset: - description: Offset, for pagination - type: integer - xml: - name: MembersList diff --git a/backend/src/api/components/note/examples.yaml b/backend/src/api/components/note/examples.yaml deleted file mode 100644 index 743eea2785..0000000000 --- a/backend/src/api/components/note/examples.yaml +++ /dev/null @@ -1,100 +0,0 @@ -components: - examples: - Note2: - value: - id: 39c850f6-fb96-4d16-8e8c-cd7072e33925 - body: Refused to have a user feedback call - importHash: null - createdAt: '2022-10-03T16:00:57.867Z' - updatedAt: '2022-10-03T16:00:57.867Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - members: - - id: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - username: - github: gilfoyle - twitter: gilfoyle - attributes: - bio: - github: Systems engineer at Pied Piper - default: It's not magic. It's talent and sweat - twitter: It's not magic. It's talent and sweat - url: - github: https://github.com/gilfoyle - default: https://t.co/g - twitter: https://t.co/g - location: - custom: Erlich's house - github: Palo alto - default: Erlich's house - displayName: Gilfoyle - email: gilfoyle@piedpiper.io - score: -1 - joinedAt: '2022-10-03T15:17:03.540Z' - importHash: null - reach: - total: 10000 - github: 5000 - twitter: 5000 - createdAt: '2022-10-03T15:17:03.547Z' - updatedAt: '2022-10-03T15:17:27.073Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - - Note: - value: - id: 196c07da-14e0-419e-bd9a-5f15c721a694 - body: Likes frunks - importHash: null - createdAt: '2022-10-05T11:58:30.977Z' - updatedAt: '2022-10-05T11:58:30.977Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - members: - - id: 2effc566-1932-44f3-a821-2d692933a953 - username: - github: dinesh - twitter: dinesh.chugtai - attributes: - bio: - github: Lead developer at Pied Piper - default: Pakistani Denzel. Tesla and gold chain owner. - twitter: Pakistani Denzel. Tesla and gold chain owner. - url: - github: https://github.com/dinesh - default: https://t.co/d - twitter: https://t.co/d - location: - custom: Silicon Valley - github: Palo alto - default: Silicon Valley - displayName: Dinesh - email: dinesh@piedpiper.io - score: 9 - joinedAt: '2022-10-03T15:30:55.672Z' - importHash: null - reach: - total: 100 - github: 60 - twitter: 40 - createdAt: '2022-10-03T15:30:55.679Z' - updatedAt: '2022-10-05T11:39:58.095Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - - NoteList: - value: - rows: - - $ref: '#/components/examples/Note' - - $ref: '#/components/examples/Note2' - count: 2 - limit: 10 - offset: 0 diff --git a/backend/src/api/components/note/inputs.yaml b/backend/src/api/components/note/inputs.yaml deleted file mode 100644 index d3d0debf7b..0000000000 --- a/backend/src/api/components/note/inputs.yaml +++ /dev/null @@ -1,15 +0,0 @@ -components: - schemas: - NoteInputRelations: - type: object - properties: - members: - description: Members associated with the note. Each element in the array is the ID of the member. - type: array - items: - type: string - - NoteInput: - allOf: - - $ref: '#/components/schemas/NoteNoId' - - $ref: '#/components/schemas/NoteInputRelations' diff --git a/backend/src/api/components/note/models.yaml b/backend/src/api/components/note/models.yaml deleted file mode 100644 index 7ca2a67ef1..0000000000 --- a/backend/src/api/components/note/models.yaml +++ /dev/null @@ -1,22 +0,0 @@ -components: - schemas: - # defines a note, excluding the ID - NoteNoId: - description: A created note. - type: object - properties: - body: - description: The body of the note. - type: string - format: blob - createdAt: - description: Date the note was created. - type: string - format: date-time - updatedAt: - description: Date the note was last updated. - type: string - format: date-time - - xml: - name: Note diff --git a/backend/src/api/components/note/query.yaml b/backend/src/api/components/note/query.yaml deleted file mode 100644 index b28c34f3c2..0000000000 --- a/backend/src/api/components/note/query.yaml +++ /dev/null @@ -1,39 +0,0 @@ -components: - schemas: - FilterType: - type: object - additionalProperties: - oneOf: - - type: string - - $ref: '#/components/schemas/FilterType' - - NoteQuery: - description: >- - All the parameters you can use to query notes. - - properties: - filter: - description: >- - Filter. Please refer to filter docs. - type: string - format: blob - - orderBy: - type: string - enum: - - createdAt_ASC - - createdAt_DESC - - limit: - description: >- - Limit the number of records returned. Default is 10. - type: integer - minimum: 1 - maximum: 200 - default: 10 - offset: - description: >- - Offset the number of records returned. Default is 0. - type: integer - minimum: 0 - default: 0 diff --git a/backend/src/api/components/note/responses.yaml b/backend/src/api/components/note/responses.yaml deleted file mode 100644 index eb2a501768..0000000000 --- a/backend/src/api/components/note/responses.yaml +++ /dev/null @@ -1,53 +0,0 @@ -components: - schemas: - # Single note - Note: - type: object - allOf: - - $ref: '#/components/schemas/NoteNoId' - properties: - id: - description: The ID of the note. - body: - description: The body of the note. - type: string - format: blob - - NoteRelationsResponse: - description: Relations of a note. - type: object - properties: - members: - description: Members associated with the note. - type: array - items: - $ref: '#/components/schemas/Member' - - NoteResponse: - description: A note of your community. - type: object - allOf: - - $ref: '#/components/schemas/Note' - - $ref: '#/components/schemas/NoteRelationsResponse' - - # List - NoteList: - description: List and count of notes. - type: object - properties: - rows: - description: List of notes - type: array - items: - $ref: '#/components/schemas/NoteResponse' - count: - description: Count - type: integer - limit: - description: Limit of records returned - type: integer - offset: - description: Offset, for pagination - type: integer - xml: - name: NotesList diff --git a/backend/src/api/components/organization/examples.yaml b/backend/src/api/components/organization/examples.yaml deleted file mode 100644 index 9c3f1daed8..0000000000 --- a/backend/src/api/components/organization/examples.yaml +++ /dev/null @@ -1,141 +0,0 @@ -components: - examples: - OrganizationCreate: - value: - id: 31bff99a-2eac-49f5-b015-cba95aa6e530 - name: Pied Piper - url: https://piedpiper.io - description: The new internet - emails: - - richard@piedpiper.io - - hello@piedpiper.io - phoneNumbers: null - logo: null - tags: - - new-internet - - making-the-world-a-better-place - - not-like-hooli - twitter: - bio: The internet we deserve - handle: PiedPiper - location: The valley - followers: 5000 - following: 20 - linkedin: - handle: company/PiedPiper - crunchbase: - handle: company/PiedPiper - employees: 50 - revenueRange: - max: 50 - min: 10 - importHash: null - createdAt: '2022-10-03T16:15:21.812Z' - updatedAt: '2022-10-03T16:15:21.812Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - memberCount: 2 - activityCount: 4 - - Organization: - value: - id: 31bff99a-2eac-49f5-b015-cba95aa6e530 - name: Pied Piper - url: https://piedpiper.io - description: The new internet - emails: - - richard@piedpiper.io - - hello@piedpiper.io - phoneNumbers: null - logo: null - tags: - - new-internet - - making-the-world-a-better-place - - not-like-hooli - identities: - - github - - twitter - activeOn: - - github - lastActive: '2022-10-03T16:15:21.812Z' - joinedAt: '2022-05-03T11:16:32.812Z' - twitter: - bio: The internet we deserve - handle: PiedPiper - location: The valley - followers: 5000 - following: 20 - linkedin: - handle: company/PiedPiper - crunchbase: - handle: company/PiedPiper - employees: 50 - revenueRange: - max: 50 - min: 10 - importHash: null - createdAt: '2022-10-03T16:15:21.812Z' - updatedAt: '2022-10-03T16:15:21.812Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - memberCount: 2 - activityCount: 4 - - Organization2: - value: - id: 65257687-0bfa-498e-8b2f-53559f41522b - name: Hooli - url: https://hooli.xyz - description: Hooli is an international corporation founded by Gavin Belson and Peter Gregory - emails: - - gavin@hooli.xyz - phoneNumbers: null - logo: null - tags: - - hooli - - tethics - - not-google - identities: - - devto - - github - - twitter - activeOn: - - devto - lastActive: '2022-10-04' - joinedAt: '2020-01-30' - twitter: - bio: Hooli is an international corporation founded by Gavin Belson and Peter Gregory - handle: hooli - location: Menlo Park - followers: 500000 - following: 0 - linkedin: - handle: company/Hooli - crunchbase: - handle: company/Hooli - employees: 4000 - revenueRange: - max: 500 - min: 100 - importHash: null - createdAt: '2022-10-05T12:03:11.228Z' - updatedAt: '2022-10-05T12:03:11.228Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - memberCount: 0 - activityCount: 0 - - OrganizationList: - value: - rows: - - $ref: '#/components/examples/Organization' - - $ref: '#/components/examples/Organization2' - count: 2 - limit: 10 - offset: 0 diff --git a/backend/src/api/components/organization/inputs.yaml b/backend/src/api/components/organization/inputs.yaml deleted file mode 100644 index 6b8276301b..0000000000 --- a/backend/src/api/components/organization/inputs.yaml +++ /dev/null @@ -1,16 +0,0 @@ -components: - schemas: - OrganizationInputRelations: - type: object - properties: - members: - description: Members associated with the organization. Each element in the array is the ID of the member. - type: array - items: - type: string - format: uuid - - OrganizationInput: - allOf: - - $ref: '#/components/schemas/OrganizationNoId' - - $ref: '#/components/schemas/OrganizationInputRelations' diff --git a/backend/src/api/components/organization/models.yaml b/backend/src/api/components/organization/models.yaml deleted file mode 100644 index 695131f34f..0000000000 --- a/backend/src/api/components/organization/models.yaml +++ /dev/null @@ -1,116 +0,0 @@ -components: - schemas: - # defines a organization, excluding the ID - OrganizationNoId: - description: A created organization. - type: object - required: - - name - properties: - name: - description: The name of the organization. - type: string - url: - description: The URL of the organization. - type: string - description: - description: A short description of the organization. - type: string - format: blob - logo: - description: A URL for logo of the organization. - type: string - emails: - description: The emails for contacting the organization. - type: array - items: - type: string - phoneNumbers: - description: The phone numbers for contacting for the organization. - type: array - items: - type: string - parentUrl: - description: The URL of the parent organization if it has one (for example if it has been acquired). - type: string - tags: - description: Tags associated with the organization. - type: array - items: - type: string - twitter: - description: Twitter information for the organization. - type: object - properties: - handle: - description: The Twitter handle for the organization. - type: string - id: - description: The Twitter ID for the organization. - type: string - bio: - description: The Twitter bio for the organization. - type: string - followers: - description: The number of followers on Twitter. - type: integer - location: - description: The Twitter location for the organization. - type: string - site: - description: The website linked to the organization's Twitter profile. - type: string - avatar: - description: The URL for the organization's Twitter avatar. - type: string - employees: - description: The number of employees of the organization. - type: integer - revenueRange: - description: The estimated revenue range of the organization. - type: object - properties: - min: - description: The minimum estimated revenue of the organization. - type: integer - max: - description: The maximum estimated revenue of the organization. - type: integer - linkedin: - description: 'LinkedIn information for the organization.' - type: object - properties: - handle: - description: The LinkedIn handle for the organization. - type: string - crunchbase: - description: 'Crunchbase information for the organization.' - type: object - properties: - handle: - description: The Crunchbase handle for the organization. - type: string - activeOn: - description: List of platforms the organization members are active on. - type: array - items: - type: string - identities: - description: List of platforms the organization members have identities in. - type: array - items: - type: string - memberCount: - description: Number of members organization has. - type: integer - createdAt: - description: Date the organization was created. - type: string - format: date-time - updatedAt: - description: Date the organization was last updated. - type: string - format: date-time - - xml: - name: Organization diff --git a/backend/src/api/components/organization/query.yaml b/backend/src/api/components/organization/query.yaml deleted file mode 100644 index 14f00224f7..0000000000 --- a/backend/src/api/components/organization/query.yaml +++ /dev/null @@ -1,47 +0,0 @@ -components: - schemas: - FilterType: - type: object - additionalProperties: - oneOf: - - type: string - - $ref: '#/components/schemas/FilterType' - - OrganizationQuery: - description: >- - All the parameters you can use to query organizations. - - properties: - filter: - description: >- - Filter. Please refer to filter docs. - type: string - format: blob - - orderBy: - type: string - enum: - - createdAt_ASC - - createdAt_DESC - - memberCount_ASC - - memberCount_DESC - - activityCount_ASC - - activityCount_DESC - - joinedAt_ASC - - joinedAt_DESC - - lastActive_ASC - - lastActive_DESC - - limit: - description: >- - Limit the number of records returned. Default is 10. - type: integer - minimum: 1 - maximum: 200 - default: 10 - offset: - description: >- - Offset the number of records returned. Default is 0. - type: integer - minimum: 0 - default: 0 diff --git a/backend/src/api/components/organization/responses.yaml b/backend/src/api/components/organization/responses.yaml deleted file mode 100644 index d210b950a5..0000000000 --- a/backend/src/api/components/organization/responses.yaml +++ /dev/null @@ -1,71 +0,0 @@ -components: - schemas: - # Single organization - Organization: - type: object - allOf: - - $ref: '#/components/schemas/OrganizationNoId' - properties: - id: - description: The ID of the organization. - body: - description: The body of the organization. - type: string - format: blob - - OrganizationRelationsResponse: - description: Relations of a organization. - type: object - properties: - members: - description: Members associated with the organization. - type: array - items: - $ref: '#/components/schemas/Member' - activeOn: - description: The platforms where the organization is active. - type: array - items: - type: string - identities: - description: The list of identities of the members in the organization. - type: array - items: - type: string - lastActive: - description: The last time the organization was active. - type: string - format: date-time - joinedAt: - description: The date the first member from the organization joined the community. - type: string - format: date-time - - OrganizationResponse: - description: A organization of your community. - type: object - allOf: - - $ref: '#/components/schemas/Organization' - - $ref: '#/components/schemas/OrganizationRelationsResponse' - - # List - OrganizationList: - description: List and count of organizations. - type: object - properties: - rows: - description: List of organizations - type: array - items: - $ref: '#/components/schemas/OrganizationResponse' - count: - description: Count - type: integer - limit: - description: Limit of records returned - type: integer - offset: - description: Offset, for pagination - type: integer - xml: - name: OrganizationsList diff --git a/backend/src/api/components/parameters.yaml b/backend/src/api/components/parameters.yaml deleted file mode 100644 index 7e28f60f71..0000000000 --- a/backend/src/api/components/parameters.yaml +++ /dev/null @@ -1,70 +0,0 @@ -components: - schemas: - MemberType: - type: string - enum: - - member - MemberScore: - type: integer - minimum: -1 - maximum: 10 - MemberSort: - type: string - enum: - - activitiesCount_ASC - - activitiesCount_DESC - - score_ASC - - score_ASC - - joinedAt_ASC - - joinedAt_DESC - - createdAt_ASC - - createdAt_DESC - - organisation_ASC - - organisation_DESC - - location_ASC - - location_DESC - ActivitySort: - type: string - enum: - - timestamp_DESC - - timestamp_ASC - - createdAt_DESC - - createdAt_ASC - - score_DESC - - score_ASC - - type_DESC - - type_ASC - - platform_DESC - - platform_ASC - - createdBy_DESC - - createdBy_ASC - ConversationSort: - type: string - enum: - - createdAt_DESC - - createdAt_ASC - - activityCount_DESC - - activityCount_ASC - - platform_DESC - - platform_ASC - - channel_DESC - - channel_ASC - - createdBy_DESC - - createdBy_ASC - TagSort: - type: string - enum: - - name_ASC - - name_DESC - - createdAt_DESC - - createdAt_ASC - - MemberAttributeSettingsSort: - type: string - enum: - - label_ASC - - label_DESC - - type_ASC - - type_DESC - - createdAt_DESC - - createdAt_ASC diff --git a/backend/src/api/components/tag/examples.yaml b/backend/src/api/components/tag/examples.yaml deleted file mode 100644 index 621a7de40e..0000000000 --- a/backend/src/api/components/tag/examples.yaml +++ /dev/null @@ -1,130 +0,0 @@ -components: - examples: - Tag: - value: - id: dca36c33-38cd-4e68-8ba8-515167e00971 - name: attended-hooli-con - importHash: null - createdAt: '2022-10-05T11:42:17.414Z' - updatedAt: '2022-10-05T11:42:17.414Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - members: - - id: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - username: - github: gilfoyle - twitter: gilfoyle - attributes: - bio: - github: Systems engineer at Pied Piper - default: It's not magic. It's talent and sweat - twitter: It's not magic. It's talent and sweat - url: - github: https://github.com/gilfoyle - default: https://t.co/g - twitter: https://t.co/g - location: - custom: Erlich's house - github: Palo alto - default: Erlich's house - displayName: Gilfoyle - email: gilfoyle@piedpiper.io - score: 8 - joinedAt: '2022-10-03T15:17:03.540Z' - importHash: null - reach: - total: 10000 - github: 5000 - twitter: 5000 - createdAt: '2022-10-03T15:17:03.547Z' - updatedAt: '2022-10-05T11:40:32.560Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - - Tag2: - value: - id: 38807625-6302-47b5-9f35-58566ddec83b - name: developer - createdAt: '2022-10-05T11:41:20.162Z' - updatedAt: '2022-10-05T11:41:20.162Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - members: - - id: 2effc566-1932-44f3-a821-2d692933a953 - username: - github: dinesh - twitter: dinesh.chugtai - attributes: - bio: - github: Lead developer at Pied Piper - default: Pakistani Denzel. Tesla and gold chain owner. - twitter: Pakistani Denzel. Tesla and gold chain owner. - url: - github: https://github.com/dinesh - default: https://t.co/d - twitter: https://t.co/d - location: - custom: Silicon Valley - github: Palo alto - default: Silicon Valley - displayName: Dinesh - email: dinesh@piedpiper.io - score: 9 - joinedAt: '2022-10-03T15:30:55.672Z' - reach: - total: 100 - github: 60 - twitter: 40 - createdAt: '2022-10-03T15:30:55.679Z' - updatedAt: '2022-10-05T11:39:58.095Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - - id: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - username: - github: gilfoyle - twitter: gilfoyle - attributes: - bio: - github: Systems engineer at Pied Piper - default: It's not magic. It's talent and sweat - twitter: It's not magic. It's talent and sweat - url: - github: https://github.com/gilfoyle - default: https://t.co/g - twitter: https://t.co/g - location: - custom: Erlich's house - github: Palo alto - default: Erlich's house - displayName: Gilfoyle - email: gilfoyle@piedpiper.io - score: 8 - joinedAt: '2022-10-03T15:17:03.540Z' - importHash: null - reach: - total: 10000 - github: 5000 - twitter: 5000 - createdAt: '2022-10-03T15:17:03.547Z' - updatedAt: '2022-10-05T11:40:32.560Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - - TagList: - value: - rows: - - $ref: '#/components/examples/Tag' - - $ref: '#/components/examples/Tag2' - count: 2 - limit: 10 - offset: 0 diff --git a/backend/src/api/components/tag/models.yaml b/backend/src/api/components/tag/models.yaml deleted file mode 100644 index 6cbdd74c61..0000000000 --- a/backend/src/api/components/tag/models.yaml +++ /dev/null @@ -1,59 +0,0 @@ -components: - schemas: - # defines the attributes of a tag, excluding the ID. - TagNoId: - description: A tag associated with a member. - type: object - required: - - name - - tenantId - properties: - name: - description: The name of the tag - type: string - createdAt: - description: Date the tag was created - type: string - format: date-time - updatedAt: - description: Date the tag was last updated - type: string - format: date-time - tenantId: - description: Your workspace/tenant id - type: string - format: uuid - - xml: - name: Tag - - # Defines a complete tag, including the ID. - Tag: - type: object - allOf: - - $ref: '#/components/schemas/TagNoId' - properties: - id: - description: The unique identifier for a tag. - - # Responses: - TagList: - description: List and count of tags. - type: object - properties: - rows: - description: List of tags - type: array - items: - $ref: '#/components/schemas/Tag' - count: - description: Count - type: integer - limit: - description: Limit of records returned - type: integer - offset: - description: Offset, for pagination - type: integer - xml: - name: TagsList diff --git a/backend/src/api/components/task/examples.yaml b/backend/src/api/components/task/examples.yaml deleted file mode 100644 index b5f680467c..0000000000 --- a/backend/src/api/components/task/examples.yaml +++ /dev/null @@ -1,113 +0,0 @@ -components: - examples: - Task: - value: - id: 8a127785-f11d-4102-804d-5b79ccddd4cc - name: Ask for tips on building a new Anton - body: null - status: null - dueDate: '2022-05-27T15:13:30.000Z' - importHash: null - createdAt: '2022-10-03T16:00:18.701Z' - updatedAt: '2022-10-03T16:00:18.701Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - assignedToId: null - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - members: - - id: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - username: - github: gilfoyle - twitter: gilfoyle - attributes: - bio: - github: Systems engineer at Pied Piper - default: It's not magic. It's talent and sweat - twitter: It's not magic. It's talent and sweat - url: - github: https://github.com/gilfoyle - default: https://t.co/g - twitter: https://t.co/g - location: - custom: Erlich's house - github: Palo alto - default: Erlich's house - displayName: Gilfoyle - email: gilfoyle@piedpiper.io - score: -1 - joinedAt: '2022-10-03T15:17:03.540Z' - importHash: null - reach: - total: 10000 - github: 5000 - twitter: 5000 - createdAt: '2022-10-03T15:17:03.547Z' - updatedAt: '2022-10-03T15:17:27.073Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - activities: [] - - Task2: - value: - id: ef22fb05-a41b-472e-9917-a4d10d19fcc6 - name: Ask if we can use as quote - body: null - status: null - dueDate: '2022-08-27T00:00:00.000Z' - importHash: null - createdAt: '2022-10-05T11:55:55.606Z' - updatedAt: '2022-10-05T11:55:55.606Z' - deletedAt: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - assignedToId: null - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - members: [] - activities: - - id: 782b426d-adc8-4fb4-a4ee-ab0bb07ffca0 - type: message - timestamp: '2020-05-27T15:13:30.000Z' - platform: discord - isContribution: true - score: 1 - sourceId: '1234' - sourceParentId: null - attributes: - reactions: 43 - channel: dev - body: It's not magic. It's talend and sweat. - title: null - url: discord.gg/1234 - sentiment: - label: negative - mixed: 1.1410574428737164 - neutral: 11.00325882434845 - negative: 85.99738478660583 - positive: 1.8582981079816818 - sentiment: 2 - importHash: null - createdAt: '2022-10-03T15:18:11.294Z' - updatedAt: '2022-10-03T15:21:49.402Z' - deletedAt: null - memberId: ab7a9fe9-4576-46b1-a710-8b8eaeff87a5 - conversationId: null - parentId: null - tenantId: 8642a2bd-965e-4acd-be8c-dfedc83ef0af - createdById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - updatedById: debc3c7f-4c5d-4bec-9130-17bb0aea8b75 - - TaskList: - value: - rows: - - $ref: '#/components/examples/Task' - - $ref: '#/components/examples/Task2' - count: 2 - limit: 10 - offset: 0 - - TaskFindAndUpdateAll: - value: - rowsUpdated: 5 diff --git a/backend/src/api/components/task/inputs.yaml b/backend/src/api/components/task/inputs.yaml deleted file mode 100644 index 93c55eb398..0000000000 --- a/backend/src/api/components/task/inputs.yaml +++ /dev/null @@ -1,52 +0,0 @@ -components: - schemas: - TaskInputRelations: - type: object - properties: - members: - description: Members associated with the task. Each element in the array is the ID of the member. - type: array - items: - type: string - format: uuid - - activities: - description: Activities associated with the task. Each element in the array is the ID of the activity. - type: array - items: - type: string - format: uuid - - assignees: - description: Users assigned with the task. Each element in the array is the ID of the user. - type: string - format: uuid - default: null - - TaskInput: - allOf: - - $ref: '#/components/schemas/TaskNoId' - - $ref: '#/components/schemas/TaskInputRelations' - - TaskBatchInput: - type: object - properties: - operation: - description: Batch operation name. - type: string - enum: - - findAndUpdateAll - payload: - type: object - description: Payload to send to the batch operation - properties: - filter: - description: >- - Filter to select the task entities. Please refer to filter docs. - type: string - format: blob - - update: - description: >- - key value object with desired updated fields. - type: object diff --git a/backend/src/api/components/task/models.yaml b/backend/src/api/components/task/models.yaml deleted file mode 100644 index 1a527b2f6b..0000000000 --- a/backend/src/api/components/task/models.yaml +++ /dev/null @@ -1,32 +0,0 @@ -components: - schemas: - # defines a task, excluding the ID - TaskNoId: - description: A created task. - type: object - properties: - name: - description: The name of the task. - type: string - body: - description: The body of the task. - type: string - format: blob - status: - description: The status of the task. - type: string - enum: - - in-progress - - done - default: null - createdAt: - description: Date the task was created. - type: string - format: date-time - updatedAt: - description: Date the task was last updated. - type: string - format: date-time - - xml: - name: Task diff --git a/backend/src/api/components/task/query.yaml b/backend/src/api/components/task/query.yaml deleted file mode 100644 index e8ed2dbf68..0000000000 --- a/backend/src/api/components/task/query.yaml +++ /dev/null @@ -1,39 +0,0 @@ -components: - schemas: - FilterType: - type: object - additionalProperties: - oneOf: - - type: string - - $ref: '#/components/schemas/FilterType' - - TaskQuery: - description: >- - All the parameters you can use to query tasks. - - properties: - filter: - description: >- - Filter. Please refer to filter docs. - type: string - format: blob - - orderBy: - type: string - enum: - - createdAt_ASC - - createdAt_DESC - - limit: - description: >- - Limit the number of records returned. Default is 10. - type: integer - minimum: 1 - maximum: 200 - default: 10 - offset: - description: >- - Offset the number of records returned. Default is 0. - type: integer - minimum: 0 - default: 0 diff --git a/backend/src/api/components/task/responses.yaml b/backend/src/api/components/task/responses.yaml deleted file mode 100644 index 4ed2827cb1..0000000000 --- a/backend/src/api/components/task/responses.yaml +++ /dev/null @@ -1,71 +0,0 @@ -components: - schemas: - # Single task - Task: - type: object - allOf: - - $ref: '#/components/schemas/TaskNoId' - properties: - id: - description: The ID of the task. - body: - description: The body of the task. - type: string - format: blob - - TaskRelationsResponse: - description: Relations of a task. - type: object - properties: - members: - description: Members associated with the task. - type: array - items: - $ref: '#/components/schemas/Member' - - activities: - description: Activities associated with the task. - type: array - items: - $ref: '#/components/schemas/Activity' - - assignedTo: - description: The workspace member assigned to the task. - $ref: '#/components/schemas/Member' - - TaskResponse: - description: A task of your community. - type: object - allOf: - - $ref: '#/components/schemas/Task' - - $ref: '#/components/schemas/TaskRelationsResponse' - - # List - TaskList: - description: List and count of tasks. - type: object - properties: - rows: - description: List of tasks - type: array - items: - $ref: '#/components/schemas/TaskResponse' - count: - description: Count - type: integer - limit: - description: Limit of records returned - type: integer - offset: - description: Offset, for pagination - type: integer - xml: - name: TasksList - - TaskFindAndUpdateAll: - description: Returns number of tasks updated - type: object - properties: - rowsUpdated: - description: Number of tasks updated - type: integer diff --git a/backend/src/api/conversation/conversationCreate.ts b/backend/src/api/conversation/conversationCreate.ts deleted file mode 100644 index cc544a6a05..0000000000 --- a/backend/src/api/conversation/conversationCreate.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Permissions from '../../security/permissions' -import ConversationService from '../../services/conversationService' -import PermissionChecker from '../../services/user/permissionChecker' - -/** - * POST /tenant/{tenantId}/conversation - * @summary Create a conversation - * @tag Conversations - * @security Bearer - * @description Create a conversation. - * @pathParam {string} tenantId - Your workspace/tenant ID - * @bodyContent {ConversationNoId} application/json - * @response 200 - Ok - * @responseContent {Conversation} 200.application/json - * @responseExample {Conversation} 200.application/json.Conversation - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.conversationCreate) - - const payload = await new ConversationService(req).create(req.body) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/conversation/conversationDestroy.ts b/backend/src/api/conversation/conversationDestroy.ts deleted file mode 100644 index 4fecce5047..0000000000 --- a/backend/src/api/conversation/conversationDestroy.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Permissions from '../../security/permissions' -import ConversationService from '../../services/conversationService' -import PermissionChecker from '../../services/user/permissionChecker' - -/** - * DELETE /tenant/{tenantId}/conversation/{id} - * @summary Delete a conversation - * @tag Conversations - * @security Bearer - * @description Delete a conversation. - * @pathParam {string} tenantId - Your workspace/tenant ID - * @pathParam {string} id - The ID of the conversation - * @response 200 - Ok - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.conversationDestroy) - - await new ConversationService(req).destroyAll(req.query.ids) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/conversation/conversationFind.ts b/backend/src/api/conversation/conversationFind.ts deleted file mode 100644 index 833fab2e76..0000000000 --- a/backend/src/api/conversation/conversationFind.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Permissions from '../../security/permissions' -import ConversationService from '../../services/conversationService' -import PermissionChecker from '../../services/user/permissionChecker' - -/** - * GET /tenant/{tenantId}/conversation/{id} - * @summary Find a conversation - * @tag Conversations - * @security Bearer - * @description Find a conversation by ID. - * @pathParam {string} tenantId - Your workspace/tenant ID. - * @pathParam {string} id - The ID of the conversation. - * @response 200 - Ok - * @responseContent {Conversation} 200.application/json - * @responseExample {Conversation} 200.application/json.Conversation - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.conversationRead) - - const payload = await new ConversationService(req).findById(req.params.id) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/conversation/conversationList.ts b/backend/src/api/conversation/conversationList.ts deleted file mode 100644 index 10202442d4..0000000000 --- a/backend/src/api/conversation/conversationList.ts +++ /dev/null @@ -1,39 +0,0 @@ -import Permissions from '../../security/permissions' -import track from '../../segment/track' -import ConversationService from '../../services/conversationService' -import PermissionChecker from '../../services/user/permissionChecker' - -/** - * GET /tenant/{tenantId}/conversation - * @summary List conversations - * @tag Conversations - * @security Bearer - * @description Get a list of conversations with filtering, sorting and offsetting. - * @pathParam {string} tenantId - Your workspace/tenant ID - * @queryParam {string} [filter[title]] - Filter by the title of the conversation. - * @queryParam {string} [filter[slug]] - Filter by the slug of the conversation. - * @queryParam {string} [filter[published]] - Filter by whether it is published or not. - * @queryParam {string} [filter[platform]] - Filter by the platform of the conversation. - * @queryParam {string} [filter[channel]] - Filter by the channel of the conversation. - * @queryParam {string} [filter[activitiesCountRange]] - activitiesCount lower bound. If you want a range, send this parameter twice with [min] and [max]. If you send it once it will be interpreted as a lower bound. - * @queryParam {string} [filter[createdAtRange]] - Send this parameter twice with [min] and [max]. - * @queryParam {ConversationSort} [orderBy] - Sort the results. Default timestamp_DESC. - * @queryParam {number} [offset] - Skip the first n results. Default 0. - * @queryParam {number} [limit] - Limit the number of results. Default 50. - * @response 200 - Ok - * @responseContent {ConversationList} 200.application/json - * @responseExample {ConversationList} 200.application/json.Conversations - * @response 401 - Unauthorized - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.conversationRead) - - const payload = await new ConversationService(req).findAndCountAll(req.query) - - if (req.query.filter && Object.keys(req.query.filter).length > 0) { - track('Conversations Filtered', { filter: req.query.filter }, { ...req }) - } - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/conversation/conversationQuery.ts b/backend/src/api/conversation/conversationQuery.ts deleted file mode 100644 index a592a64a51..0000000000 --- a/backend/src/api/conversation/conversationQuery.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Permissions from '../../security/permissions' -import track from '../../segment/track' -import ConversationService from '../../services/conversationService' -import PermissionChecker from '../../services/user/permissionChecker' - -// /** -// * POST /tenant/{tenantId}/conversation -// * @summary Create or update an conversation -// * @tag Activities -// * @security Bearer -// * @description Create or update an conversation. Existence is checked by sourceId and tenantId. -// * @pathParam {string} tenantId - Your workspace/tenant ID -// * @bodyContent {ConversationUpsertInput} application/json -// * @response 200 - Ok -// * @responseContent {Conversation} 200.application/json -// * @responseExample {ConversationUpsert} 200.application/json.Conversation -// * @response 401 - Unauthorized -// * @response 404 - Not found -// * @response 429 - Too many requests -// */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.conversationRead) - - const payload = await new ConversationService(req).query(req.body) - - if (req.query.filter && Object.keys(req.query.filter).length > 0) { - track('Conversations Advanced Filter', { ...payload }, { ...req }) - } - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/conversation/conversationSettingsUpdate.ts b/backend/src/api/conversation/conversationSettingsUpdate.ts deleted file mode 100644 index 0c9072d246..0000000000 --- a/backend/src/api/conversation/conversationSettingsUpdate.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Error403 from '../../errors/Error403' -import Permissions from '../../security/permissions' -import ConversationService from '../../services/conversationService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.conversationEdit) - - if (req.body.customUrl) { - await req.responseHandler.error( - req, - res, - new Error403( - req.language, - 'communityHelpCenter.errors.planNotSupportingCustomUrls', - req.currentTenant.plan, - ), - ) - return - } - - const payload = await new ConversationService(req).updateSettings(req.body) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/conversation/conversationUpdate.ts b/backend/src/api/conversation/conversationUpdate.ts deleted file mode 100644 index 61ae340c28..0000000000 --- a/backend/src/api/conversation/conversationUpdate.ts +++ /dev/null @@ -1,27 +0,0 @@ -import Permissions from '../../security/permissions' -import ConversationService from '../../services/conversationService' -import PermissionChecker from '../../services/user/permissionChecker' - -/** - * PUT /tenant/{tenantId}/conversation/{id} - * @summary Update an conversation - * @tag Conversations - * @security Bearer - * @description Update a conversation given an ID. - * @pathParam {string} tenantId - Your workspace/tenant ID - * @pathParam {string} id - The ID of the conversation - * @bodyContent {ConversationNoId} application/json - * @response 200 - Ok - * @responseContent {Conversation} 200.application/json - * @responseExample {Conversation} 200.application/json.Conversation - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.conversationEdit) - - const payload = await new ConversationService(req).update(req.params.id, req.body) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/conversation/index.ts b/backend/src/api/conversation/index.ts deleted file mode 100644 index 0cd63eeaeb..0000000000 --- a/backend/src/api/conversation/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { safeWrap } from '../../middlewares/errorMiddleware' - -export default (app) => { - app.post(`/tenant/:tenantId/conversation`, safeWrap(require('./conversationCreate').default)) - app.put(`/tenant/:tenantId/conversation/:id`, safeWrap(require('./conversationUpdate').default)) - app.delete(`/tenant/:tenantId/conversation`, safeWrap(require('./conversationDestroy').default)) - app.post(`/tenant/:tenantId/conversation/query`, safeWrap(require('./conversationQuery').default)) - app.get(`/tenant/:tenantId/conversation`, safeWrap(require('./conversationList').default)) - app.get(`/tenant/:tenantId/conversation/:id`, safeWrap(require('./conversationFind').default)) - app.post( - `/tenant/:tenantId/conversation/settings`, - safeWrap(require('./conversationSettingsUpdate').default), - ) -} diff --git a/backend/src/api/cubejs/cubeJsAuth.ts b/backend/src/api/cubejs/cubeJsAuth.ts deleted file mode 100644 index ce35e89d06..0000000000 --- a/backend/src/api/cubejs/cubeJsAuth.ts +++ /dev/null @@ -1,8 +0,0 @@ -import CubeJsService from '../../services/cubejs/cubeJsService' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' - -export default async (req, res) => { - const segments = SequelizeRepository.getSegmentIds(req) - const payload = await CubeJsService.generateJwtToken(req.params.tenantId, segments) - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/cubejs/cubeJsVerifyToken.ts b/backend/src/api/cubejs/cubeJsVerifyToken.ts deleted file mode 100644 index e3ad64f5ff..0000000000 --- a/backend/src/api/cubejs/cubeJsVerifyToken.ts +++ /dev/null @@ -1,6 +0,0 @@ -import CubeJsService from '../../services/cubejs/cubeJsService' - -export default async (req, res) => { - const payload = await CubeJsService.verifyToken(req.language, req.body.token, req.params.tenantId) - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/cubejs/index.ts b/backend/src/api/cubejs/index.ts deleted file mode 100644 index 3cff27f772..0000000000 --- a/backend/src/api/cubejs/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { safeWrap } from '../../middlewares/errorMiddleware' - -export default (app) => { - app.get(`/tenant/:tenantId/cubejs/auth`, safeWrap(require('./cubeJsAuth').default)) - app.post(`/tenant/:tenantId/cubejs/verify`, safeWrap(require('./cubeJsVerifyToken').default)) -} diff --git a/backend/src/api/customViews/customViewCreate.ts b/backend/src/api/customViews/customViewCreate.ts index 9f89d3cfa7..0e6aedf682 100644 --- a/backend/src/api/customViews/customViewCreate.ts +++ b/backend/src/api/customViews/customViewCreate.ts @@ -4,12 +4,11 @@ import CustomViewService from '../../services/customViewService' import PermissionChecker from '../../services/user/permissionChecker' /** - * POST /tenant/{tenantId}/customview + * POST /customview * @summary Create a custom view * @tag CustomViews * @security Bearer * @description Create a custom view - * @pathParam {string} tenantId - Your workspace/tenant ID * @bodyContent {CustomViewInput} application/json * @response 200 - Ok * @responseContent {CustomView} 200.application/json @@ -23,7 +22,7 @@ export default async (req, res) => { const payload = await new CustomViewService(req).create(req.body) - track('Custom view Manually Created', { ...payload }, { ...req }) + track('Custom view Manually Created', { ...req.body }, { ...req }) await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/customViews/customViewDestroy.ts b/backend/src/api/customViews/customViewDestroy.ts index fb2fbfb8f1..340d33f184 100644 --- a/backend/src/api/customViews/customViewDestroy.ts +++ b/backend/src/api/customViews/customViewDestroy.ts @@ -3,12 +3,11 @@ import CustomViewService from '../../services/customViewService' import PermissionChecker from '../../services/user/permissionChecker' /** - * DELETE /tenant/{tenantId}/customview/{id} + * DELETE /customview/{id} * @summary Delete an custom view * @tag CustomViews * @security Bearer * @description Delete a custom view given an ID - * @pathParam {string} tenantId - Your workspace/tenant ID * @pathParam {string} id - The ID of the custom view * @response 200 - Ok * @response 401 - Unauthorized diff --git a/backend/src/api/customViews/customViewQuery.ts b/backend/src/api/customViews/customViewQuery.ts index 0ced458371..fc402bc957 100644 --- a/backend/src/api/customViews/customViewQuery.ts +++ b/backend/src/api/customViews/customViewQuery.ts @@ -1,15 +1,14 @@ import Permissions from '../../security/permissions' +import track from '../../segment/track' import CustomViewService from '../../services/customViewService' import PermissionChecker from '../../services/user/permissionChecker' -import track from '../../segment/track' /** - * GET /tenant/{tenantId}/customview/query + * GET /customview/query * @summary Query custom views * @tag CustomViews * @security Bearer * @description Query custom views. It accepts filters and sorting options. - * @pathParam {string} tenantId - Your workspace/tenant ID * @queryParam {string[]} placement - The placements to filter by * @queryParam {string} visibility - The visibility to filter by * @response 200 - Ok @@ -25,7 +24,7 @@ export default async (req, res) => { const payload = await new CustomViewService(req).findAll(req.query) if (req.query.filter && Object.keys(req.query.filter).length > 0) { - track('Custom views Filter', { ...payload }, { ...req }) + track('Custom views Filter', { ...req.query }, { ...req }) } await req.responseHandler.success(req, res, payload) diff --git a/backend/src/api/customViews/customViewUpdate.ts b/backend/src/api/customViews/customViewUpdate.ts index 6b8fc04cab..1210878911 100644 --- a/backend/src/api/customViews/customViewUpdate.ts +++ b/backend/src/api/customViews/customViewUpdate.ts @@ -3,12 +3,11 @@ import CustomViewService from '../../services/customViewService' import PermissionChecker from '../../services/user/permissionChecker' /** - * PUT /tenant/{tenantId}/customview/{id} + * PUT /customview/{id} * @summary Update an custom view * @tag CustomViews * @security Bearer * @description Update an custom view given an ID. - * @pathParam {string} tenantId - Your workspace/tenant ID * @pathParam {string} id - The ID of the custom view * @bodyContent {CustomViewUpsertInput} application/json * @response 200 - Ok diff --git a/backend/src/api/customViews/customViewUpdateBulk.ts b/backend/src/api/customViews/customViewUpdateBulk.ts index 9a79ddc9da..824005cc91 100644 --- a/backend/src/api/customViews/customViewUpdateBulk.ts +++ b/backend/src/api/customViews/customViewUpdateBulk.ts @@ -1,14 +1,14 @@ import CustomViewService from '@/services/customViewService' + import Permissions from '../../security/permissions' import PermissionChecker from '../../services/user/permissionChecker' /** - * PUT /tenant/{tenantId}/customview + * PUT /customview * @summary Update custom views in bulk * @tag CustomViews * @security Bearer * @description Update custom view of given an IDs. - * @pathParam {string} tenantId - Your workspace/tenant ID * @pathParam {string} id - The ID of the custom view * @bodyContent {CustomViewUpsertInput} application/json * @response 200 - Ok diff --git a/backend/src/api/customViews/index.ts b/backend/src/api/customViews/index.ts index ad01aa5bb8..1d1cf821b7 100644 --- a/backend/src/api/customViews/index.ts +++ b/backend/src/api/customViews/index.ts @@ -1,9 +1,9 @@ import { safeWrap } from '../../middlewares/errorMiddleware' export default (app) => { - app.post(`/tenant/:tenantId/customview`, safeWrap(require('./customViewCreate').default)) - app.put(`/tenant/:tenantId/customview/:id`, safeWrap(require('./customViewUpdate').default)) - app.patch(`/tenant/:tenantId/customview`, safeWrap(require('./customViewUpdateBulk').default)) - app.delete(`/tenant/:tenantId/customview`, safeWrap(require('./customViewDestroy').default)) - app.get(`/tenant/:tenantId/customview`, safeWrap(require('./customViewQuery').default)) + app.post(`/customview`, safeWrap(require('./customViewCreate').default)) + app.put(`/customview/:id`, safeWrap(require('./customViewUpdate').default)) + app.patch(`/customview`, safeWrap(require('./customViewUpdateBulk').default)) + app.delete(`/customview`, safeWrap(require('./customViewDestroy').default)) + app.get(`/customview`, safeWrap(require('./customViewQuery').default)) } diff --git a/backend/src/api/dashboard/dashboardGet.ts b/backend/src/api/dashboard/dashboardGet.ts new file mode 100644 index 0000000000..ae43c956be --- /dev/null +++ b/backend/src/api/dashboard/dashboardGet.ts @@ -0,0 +1,12 @@ +import DashboardService from '@/services/dashboardService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberRead) + + const payload = await new DashboardService(req).get(req.query) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/dashboard/dashboardMetricsGet.ts b/backend/src/api/dashboard/dashboardMetricsGet.ts new file mode 100644 index 0000000000..ddf277da0a --- /dev/null +++ b/backend/src/api/dashboard/dashboardMetricsGet.ts @@ -0,0 +1,12 @@ +import DashboardService from '@/services/dashboardService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberRead) + + const payload = await new DashboardService(req).getMetrics(req.query) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/dashboard/index.ts b/backend/src/api/dashboard/index.ts new file mode 100644 index 0000000000..7d370cfe07 --- /dev/null +++ b/backend/src/api/dashboard/index.ts @@ -0,0 +1,6 @@ +import { safeWrap } from '../../middlewares/errorMiddleware' + +export default (app) => { + app.get(`/dashboard`, safeWrap(require('./dashboardGet').default)) + app.get(`/dashboard/metrics`, safeWrap(require('./dashboardMetricsGet').default)) +} diff --git a/backend/src/api/dataQuality/dataQualityMember.ts b/backend/src/api/dataQuality/dataQualityMember.ts new file mode 100644 index 0000000000..9763cb244f --- /dev/null +++ b/backend/src/api/dataQuality/dataQualityMember.ts @@ -0,0 +1,33 @@ +import DataQualityService from '@/services/dataQualityService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * GET /data-quality/member + * @summary Find a member data issues + * @tag Data Quality + * @security Bearer + * @description Find a data quality issues for members + * @response 200 - Ok + * @responseContent {DataQualityResponse} 200.application/json + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberRead) + + const segmentId = req.query.segments?.length > 0 ? req.query.segments[0] : null + if (!segmentId) { + await req.responseHandler.error(req, res, { + code: 400, + message: 'Segment ID is required', + }) + return + } + + const payload = await new DataQualityService(req).findMemberIssues(req.query, segmentId) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/dataQuality/dataQualityOrganization.ts b/backend/src/api/dataQuality/dataQualityOrganization.ts new file mode 100644 index 0000000000..f835984087 --- /dev/null +++ b/backend/src/api/dataQuality/dataQualityOrganization.ts @@ -0,0 +1,33 @@ +import DataQualityService from '@/services/dataQualityService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * GET /data-quality/organization + * @summary Find a organization data issues + * @tag Data Quality + * @security Bearer + * @description Find a data quality issues for organizations + * @response 200 - Ok + * @responseContent {DataQualityResponse} 200.application/json + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.organizationRead) + + const segmentId = req.query.segments?.length > 0 ? req.query.segments[0] : null + if (!segmentId) { + await req.responseHandler.error(req, res, { + code: 400, + message: 'Segment ID is required', + }) + return + } + + const payload = await new DataQualityService(req).findOrganizationIssues() + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/dataQuality/index.ts b/backend/src/api/dataQuality/index.ts new file mode 100644 index 0000000000..e1ad34c80d --- /dev/null +++ b/backend/src/api/dataQuality/index.ts @@ -0,0 +1,6 @@ +import { safeWrap } from '../../middlewares/errorMiddleware' + +export default (app) => { + app.get(`/data-quality/member`, safeWrap(require('./dataQualityMember').default)) + app.get(`/data-quality/organization`, safeWrap(require('./dataQualityOrganization').default)) +} diff --git a/backend/src/api/eagleEyeContent/eagleEyeContentQuery.ts b/backend/src/api/eagleEyeContent/eagleEyeContentQuery.ts index fda773c582..d52972b180 100644 --- a/backend/src/api/eagleEyeContent/eagleEyeContentQuery.ts +++ b/backend/src/api/eagleEyeContent/eagleEyeContentQuery.ts @@ -3,28 +3,13 @@ import track from '../../segment/track' import EagleEyeContentService from '../../services/eagleEyeContentService' import PermissionChecker from '../../services/user/permissionChecker' -// /** -// * POST /tenant/{tenantId}/eagleEyeContent -// * @summary Create or update an eagleEyeContent -// * @tag Activities -// * @security Bearer -// * @description Create or update an eagleEyeContent. Existence is checked by sourceId and tenantId. -// * @pathParam {string} tenantId - Your workspace/tenant ID -// * @bodyContent {EagleEyeContentUpsertInput} application/json -// * @response 200 - Ok -// * @responseContent {EagleEyeContent} 200.application/json -// * @responseExample {EagleEyeContentUpsert} 200.application/json.EagleEyeContent -// * @response 401 - Unauthorized -// * @response 404 - Not found -// * @response 429 - Too many requests -// */ export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.eagleEyeContentRead) const payload = await new EagleEyeContentService(req).query(req.body) - if (req.query.filter && Object.keys(req.query.filter).length > 0) { - track('EagleEyeContent Advanced Filter', { ...payload }, { ...req }) + if (req.body?.filter && Object.keys(req.body.filter).length > 0) { + track('EagleEyeContent Advanced Filter', { ...req.body }, { ...req }) } await req.responseHandler.success(req, res, payload) diff --git a/backend/src/api/eagleEyeContent/eagleEyeContentSearch.ts b/backend/src/api/eagleEyeContent/eagleEyeContentSearch.ts index 88659e218a..a8491a0f37 100644 --- a/backend/src/api/eagleEyeContent/eagleEyeContentSearch.ts +++ b/backend/src/api/eagleEyeContent/eagleEyeContentSearch.ts @@ -7,6 +7,6 @@ export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.eagleEyeActionCreate) const payload = await new EagleEyeContentService(req).search() - track('EagleEye backend search', { ...req.body }, { ...req }) + track('EagleEye backend search', {}, { ...req }) await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/eagleEyeContent/eagleEyeContentTrack.ts b/backend/src/api/eagleEyeContent/eagleEyeContentTrack.ts index 0457266246..a0af99bc1d 100644 --- a/backend/src/api/eagleEyeContent/eagleEyeContentTrack.ts +++ b/backend/src/api/eagleEyeContent/eagleEyeContentTrack.ts @@ -1,8 +1,9 @@ +import { Error404 } from '@crowd/common' + import Permissions from '../../security/permissions' -import PermissionChecker from '../../services/user/permissionChecker' -import EagleEyeContentService from '../../services/eagleEyeContentService' import track from '../../segment/track' -import Error404 from '../../errors/Error404' +import EagleEyeContentService from '../../services/eagleEyeContentService' +import PermissionChecker from '../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.eagleEyeContentRead) diff --git a/backend/src/api/eagleEyeContent/index.ts b/backend/src/api/eagleEyeContent/index.ts index 99d8a1ff27..c4451e2fa7 100644 --- a/backend/src/api/eagleEyeContent/index.ts +++ b/backend/src/api/eagleEyeContent/index.ts @@ -1,59 +1,27 @@ import { safeWrap } from '../../middlewares/errorMiddleware' -import { featureFlagMiddleware } from '../../middlewares/featureFlagMiddleware' -import { FeatureFlag } from '../../types/common' export default (app) => { - app.post( - `/tenant/:tenantId/eagleEyeContent/query`, - featureFlagMiddleware(FeatureFlag.EAGLE_EYE, 'entities.eagleEye.errors.planLimitExceeded'), - safeWrap(require('./eagleEyeContentQuery').default), - ) + app.post(`/eagleEyeContent/query`, safeWrap(require('./eagleEyeContentQuery').default)) - app.post( - `/tenant/:tenantId/eagleEyeContent`, - featureFlagMiddleware(FeatureFlag.EAGLE_EYE, 'entities.eagleEye.errors.planLimitExceeded'), - safeWrap(require('./eagleEyeContentUpsert').default), - ) + app.post(`/eagleEyeContent`, safeWrap(require('./eagleEyeContentUpsert').default)) - app.post( - `/tenant/:tenantId/eagleEyeContent/track`, - featureFlagMiddleware(FeatureFlag.EAGLE_EYE, 'entities.eagleEye.errors.planLimitExceeded'), - safeWrap(require('./eagleEyeContentTrack').default), - ) + app.post(`/eagleEyeContent/track`, safeWrap(require('./eagleEyeContentTrack').default)) - app.get( - `/tenant/:tenantId/eagleEyeContent/reply`, - featureFlagMiddleware(FeatureFlag.EAGLE_EYE, 'entities.eagleEye.errors.planLimitExceeded'), - safeWrap(require('./eagleEyeContentReply').default), - ) + app.get(`/eagleEyeContent/reply`, safeWrap(require('./eagleEyeContentReply').default)) - app.get( - `/tenant/:tenantId/eagleEyeContent/search`, - featureFlagMiddleware(FeatureFlag.EAGLE_EYE, 'entities.eagleEye.errors.planLimitExceeded'), - safeWrap(require('./eagleEyeContentSearch').default), - ) + app.get(`/eagleEyeContent/search`, safeWrap(require('./eagleEyeContentSearch').default)) - app.get( - `/tenant/:tenantId/eagleEyeContent/:id`, - featureFlagMiddleware(FeatureFlag.EAGLE_EYE, 'entities.eagleEye.errors.planLimitExceeded'), - safeWrap(require('./eagleEyeContentFind').default), - ) + app.get(`/eagleEyeContent/:id`, safeWrap(require('./eagleEyeContentFind').default)) app.post( - `/tenant/:tenantId/eagleEyeContent/:contentId/action`, - featureFlagMiddleware(FeatureFlag.EAGLE_EYE, 'entities.eagleEye.errors.planLimitExceeded'), + `/eagleEyeContent/:contentId/action`, safeWrap(require('./eagleEyeActionCreate').default), ) - app.put( - `/tenant/:tenantId/eagleEyeContent/settings`, - featureFlagMiddleware(FeatureFlag.EAGLE_EYE, 'entities.eagleEye.errors.planLimitExceeded'), - safeWrap(require('./eagleEyeSettingsUpdate').default), - ) + app.put(`/eagleEyeContent/settings`, safeWrap(require('./eagleEyeSettingsUpdate').default)) app.delete( - `/tenant/:tenantId/eagleEyeContent/:contentId/action/:actionId`, - featureFlagMiddleware(FeatureFlag.EAGLE_EYE, 'entities.eagleEye.errors.planLimitExceeded'), + `/eagleEyeContent/:contentId/action/:actionId`, safeWrap(require('./eagleEyeActionDestroy').default), ) } diff --git a/backend/src/api/eventTracking/eventTrack.ts b/backend/src/api/eventTracking/eventTrack.ts deleted file mode 100644 index 735651f54a..0000000000 --- a/backend/src/api/eventTracking/eventTrack.ts +++ /dev/null @@ -1,11 +0,0 @@ -import PermissionChecker from '../../services/user/permissionChecker' -import Permissions from '../../security/permissions' -import EventTrackingService from '../../services/eventTrackingService' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.quickstartGuideRead) - - const payload = await new EventTrackingService(req).trackEvent(req.body) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/eventTracking/index.ts b/backend/src/api/eventTracking/index.ts deleted file mode 100644 index c0413ec500..0000000000 --- a/backend/src/api/eventTracking/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { safeWrap } from '../../middlewares/errorMiddleware' - -export default (app) => { - app.post(`/tenant/:tenantId/event-tracking`, safeWrap(require('./eventTrack').default)) -} diff --git a/backend/src/api/index.ts b/backend/src/api/index.ts index 3646945d8f..2182975ac7 100644 --- a/backend/src/api/index.ts +++ b/backend/src/api/index.ts @@ -1,33 +1,42 @@ -import express from 'express' import bodyParser from 'body-parser' +import bunyanMiddleware from 'bunyan-middleware' import cors from 'cors' +import express from 'express' import helmet from 'helmet' -import bunyanMiddleware from 'bunyan-middleware' import * as http from 'http' -import { Unleash } from 'unleash-client' -import { getRedisClient, getRedisPubSubPair, RedisPubSubReceiver } from '@crowd/redis' +import os from 'os' +import { QueryTypes } from 'sequelize' + +import { BadRequestError } from '@crowd/common' +import { getDbConnection } from '@crowd/data-access-layer/src/database' import { getServiceLogger } from '@crowd/logging' -import { ApiWebsocketMessage, Edition } from '@crowd/types' import { getOpensearchClient } from '@crowd/opensearch' -import { getServiceTracer } from '@crowd/tracing' -import { API_CONFIG, REDIS_CONFIG, UNLEASH_CONFIG, OPENSEARCH_CONFIG } from '../conf' +import { RedisPubSubReceiver, getRedisClient, getRedisPubSubPair } from '@crowd/redis' +import { telemetryExpressMiddleware } from '@crowd/telemetry' +import { Client as TemporalClient, getTemporalClient } from '@crowd/temporal' +import { ApiWebsocketMessage } from '@crowd/types' + +import SequelizeRepository from '@/database/repositories/sequelizeRepository' +import { productDatabaseMiddleware } from '@/middlewares/productDbMiddleware' + +import { OPENSEARCH_CONFIG, PRODUCT_DB_CONFIG, REDIS_CONFIG, TEMPORAL_CONFIG } from '../conf' import { authMiddleware } from '../middlewares/authMiddleware' -import { tenantMiddleware } from '../middlewares/tenantMiddleware' -import { segmentMiddleware } from '../middlewares/segmentMiddleware' import { databaseMiddleware } from '../middlewares/databaseMiddleware' -import { createRateLimiter } from './apiRateLimiter' -import { languageMiddleware } from '../middlewares/languageMiddleware' -import authSocial from './auth/authSocial' -import setupSwaggerUI from './apiDocumentation' -import { responseHandlerMiddleware } from '../middlewares/responseHandlerMiddleware' import { errorMiddleware } from '../middlewares/errorMiddleware' +import { languageMiddleware } from '../middlewares/languageMiddleware' +import { opensearchMiddleware } from '../middlewares/opensearchMiddleware' import { passportStrategyMiddleware } from '../middlewares/passportStrategyMiddleware' import { redisMiddleware } from '../middlewares/redisMiddleware' +import { responseHandlerMiddleware } from '../middlewares/responseHandlerMiddleware' +import { segmentMiddleware } from '../middlewares/segmentMiddleware' +import { tenantMiddleware } from '../middlewares/tenantMiddleware' + +import { createRateLimiter } from './apiRateLimiter' +import authSocial from './auth/authSocial' +import { publicRouter } from './public' import WebSockets from './websockets' -import { opensearchMiddleware } from '../middlewares/opensearchMiddleware' const serviceLogger = getServiceLogger() -getServiceTracer() const app = express() @@ -36,7 +45,7 @@ const server = http.createServer(app) setImmediate(async () => { const redis = await getRedisClient(REDIS_CONFIG, true) - const opensearch = getOpensearchClient(OPENSEARCH_CONFIG) + const opensearch = await getOpensearchClient(OPENSEARCH_CONFIG) const redisPubSubPair = await getRedisPubSubPair(REDIS_CONFIG) const userNamespace = await WebSockets.initialize(server) @@ -63,6 +72,8 @@ setImmediate(async () => { } }) + app.use(telemetryExpressMiddleware('api.request.duration')) + // Enables CORS app.use(cors({ origin: true })) @@ -77,6 +88,28 @@ setImmediate(async () => { }), ) + app.use((req, res, next) => { + // @ts-ignore + req.profileSql = req.headers['x-profile-sql'] === 'true' + next() + }) + + app.use((req, res, next) => { + res.setHeader('X-Hostname', os.hostname()) + next() + }) + + app.use((req, res, next) => { + // this middleware fixes the issue with logging and datadog + // explained in detail here: https://github.com/CrowdDotDev/crowd.dev/pull/2144 + // in short: the hostname field in logs breaks how datadog assigns k8s cluster info + if (req.log.fields.hostname) { + delete req.log.fields.hostname + } + + next() + }) + // Initializes and adds the database middleware. app.use(databaseMiddleware) @@ -86,42 +119,56 @@ setImmediate(async () => { // bind opensearch app.use(opensearchMiddleware(opensearch)) - // Bind unleash to request - if (UNLEASH_CONFIG.url && API_CONFIG.edition === Edition.CROWD_HOSTED) { - const unleash = new Unleash({ - url: `${UNLEASH_CONFIG.url}/api`, - appName: 'crowd-api', - customHeaders: { - Authorization: UNLEASH_CONFIG.backendApiKey, - }, + // temp check for production + if (TEMPORAL_CONFIG.serverUrl) { + // Bind temporal to request + const temporal = await getTemporalClient(TEMPORAL_CONFIG) + app.use((req: any, res, next) => { + req.temporal = temporal + next() }) + } - unleash.on('error', (err) => { - serviceLogger.error(err, 'Unleash client error!') - }) + // Enables Helmet, a set of tools to + // increase security. + app.use(helmet()) - let isReady = false + const defaultRateLimiter = createRateLimiter({ + max: 200, + windowMs: 60 * 1000, + }) - setInterval(async () => { - if (!isReady) { - serviceLogger.error('Unleash client is not ready yet, exiting...') - process.exit(1) - } - }, 60 * 1000) - - await new Promise((resolve) => { - unleash.on('ready', () => { - serviceLogger.info('Unleash client is ready!') - isReady = true - resolve() - }) - }) + app.use(defaultRateLimiter) - app.use((req: any, res, next) => { - req.unleash = unleash - next() - }) - } + app.use( + bodyParser.json({ + limit: '5mb', + }), + ) + + app.use(bodyParser.urlencoded({ limit: '5mb', extended: true })) + + app.use((err: any, req: any, res: any, next: any) => { + if (err.type === 'entity.parse.failed') { + next(new BadRequestError('Invalid JSON body')) + return + } + next(err) + }) + + app.use((req, res, next) => { + // @ts-ignore + req.userData = { + ip: req.ip, + userAgent: req.headers ? req.headers['user-agent'] : null, + } + + next() + }) + + // Public API uses its own OAuth2 auth and error flow + // Must be mounted before internal endpoints. + app.use('/', publicRouter()) // initialize passport strategies app.use(passportStrategyMiddleware) @@ -136,36 +183,37 @@ setImmediate(async () => { // to set the currentUser to the requests app.use(authMiddleware) - // Setup the Documentation - setupSwaggerUI(app) - - // Default rate limiter - const defaultRateLimiter = createRateLimiter({ - max: 200, - windowMs: 60 * 1000, - message: 'errors.429', + app.use('/health', async (req: any, res) => { + try { + const seq = SequelizeRepository.getSequelize(req) + + const [osPingRes, redisPingRes, dbPingRes, temporalPingRes] = await Promise.all([ + // ping opensearch + opensearch.ping().then((res) => res.body), + // ping redis, + redis.ping().then((res) => res === 'PONG'), + // ping database + seq.query('select 1', { type: QueryTypes.SELECT }).then((rows) => rows.length === 1), + // ping temporal + req.temporal + ? (req.temporal as TemporalClient).workflowService.getSystemInfo({}).then(() => true) + : Promise.resolve(true), + ]) + + if (osPingRes && redisPingRes && dbPingRes && temporalPingRes) { + res.sendStatus(200) + } else { + res.status(500).json({ + opensearch: osPingRes, + redis: redisPingRes, + database: dbPingRes, + temporal: temporalPingRes, + }) + } + } catch (err) { + res.status(500).json({ error: err.message, stack: err.stack }) + } }) - app.use(defaultRateLimiter) - - // Enables Helmet, a set of tools to - // increase security. - app.use(helmet()) - - app.use( - bodyParser.json({ - limit: '5mb', - verify(req, res, buf) { - const url = (req).originalUrl - if (url.startsWith('/webhooks/stripe') || url.startsWith('/webhooks/sendgrid')) { - // Stripe and sendgrid webhooks needs the body raw - // for verifying the webhook with signing secret - ;(req).rawBody = buf.toString() - } - }, - }), - ) - - app.use(bodyParser.urlencoded({ limit: '5mb', extended: true })) // Configure the Entity routes const routes = express.Router() @@ -173,49 +221,42 @@ setImmediate(async () => { // Enable Passport for Social Sign-in authSocial(app, routes) - require('./auditLog').default(routes) + // Enable product db only if it's configured + if (PRODUCT_DB_CONFIG) { + const productDbClient = await getDbConnection(PRODUCT_DB_CONFIG) + app.use(productDatabaseMiddleware(productDbClient)) + require('./product').default(routes) + } + require('./auth').default(routes) - require('./plan').default(routes) - require('./tenant').default(routes) + + app.use(tenantMiddleware) + app.use(segmentMiddleware) + + require('./auditLog').default(routes) + require('./merge-suggestions').default(routes) require('./user').default(routes) require('./settings').default(routes) require('./member').default(routes) - require('./widget').default(routes) require('./activity').default(routes) - require('./tag').default(routes) - require('./widget').default(routes) - require('./cubejs').default(routes) - require('./report').default(routes) require('./integration').default(routes) - require('./microservice').default(routes) - require('./conversation').default(routes) require('./eagleEyeContent').default(routes) - require('./automation').default(routes) - require('./task').default(routes) - require('./note').default(routes) require('./organization').default(routes) - require('./quickstart-guide').default(routes) require('./slack').default(routes) require('./segment').default(routes) - require('./eventTracking').default(routes) + require('./systemStatus').default(routes) require('./customViews').default(routes) - require('./premium/enrichment').default(routes) - // Loads the Tenant if the :tenantId param is passed - routes.param('tenantId', tenantMiddleware) - routes.param('tenantId', segmentMiddleware) + require('./dashboard').default(routes) + require('./mergeAction').default(routes) + require('./dataQuality').default(routes) + require('./collections').default(routes) + require('./categories').default(routes) - app.use('/', routes) - - const webhookRoutes = express.Router() - require('./webhooks').default(webhookRoutes) - - app.use('/webhooks', webhookRoutes) + await require('./nango').default(routes) - const io = require('@pm2/io') + app.use('/', routes) app.use(errorMiddleware) - - app.use(io.expressErrorHandler()) }) export default server diff --git a/backend/src/api/integration/helpers/confluenceAuthenticate.ts b/backend/src/api/integration/helpers/confluenceAuthenticate.ts new file mode 100644 index 0000000000..36df49cb21 --- /dev/null +++ b/backend/src/api/integration/helpers/confluenceAuthenticate.ts @@ -0,0 +1,12 @@ +import Permissions from '../../../security/permissions' +import IntegrationService from '../../../services/integrationService' +import PermissionChecker from '../../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.tenantEdit) + const integrationService = new IntegrationService(req) + const payload = req.body.id + ? await integrationService.updateConfluenceIntegration(req.body) + : await integrationService.connectConfluenceIntegration(req.body) + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/integration/helpers/devtoValidators.ts b/backend/src/api/integration/helpers/devtoValidators.ts index e850da57cd..258accf4d6 100644 --- a/backend/src/api/integration/helpers/devtoValidators.ts +++ b/backend/src/api/integration/helpers/devtoValidators.ts @@ -1,8 +1,9 @@ -import Error400 from '../../../errors/Error400' +import { Error400 } from '@crowd/common' + import Permissions from '../../../security/permissions' +import { checkAPIKey } from '../../../serverless/integrations/usecases/devto/checkAPIKey' import { getOrganization } from '../../../serverless/integrations/usecases/devto/getOrganization' import { getUserByUsername } from '../../../serverless/integrations/usecases/devto/getUser' -import { checkAPIKey } from '../../../serverless/integrations/usecases/devto/checkAPIKey' import PermissionChecker from '../../../services/user/permissionChecker' export default async (req, res) => { diff --git a/backend/src/api/integration/helpers/discourseTestWebhook.ts b/backend/src/api/integration/helpers/discourseTestWebhook.ts index ca52d73255..19166a97c8 100644 --- a/backend/src/api/integration/helpers/discourseTestWebhook.ts +++ b/backend/src/api/integration/helpers/discourseTestWebhook.ts @@ -1,7 +1,7 @@ -import Permissions from '../../../security/permissions' -import PermissionChecker from '../../../services/user/permissionChecker' import IncomingWebhookRepository from '../../../database/repositories/incomingWebhookRepository' import SequelizeRepository from '../../../database/repositories/sequelizeRepository' +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.tenantEdit) diff --git a/backend/src/api/integration/helpers/discourseValidator.ts b/backend/src/api/integration/helpers/discourseValidator.ts index 14e3b1878e..a88935b49f 100644 --- a/backend/src/api/integration/helpers/discourseValidator.ts +++ b/backend/src/api/integration/helpers/discourseValidator.ts @@ -1,5 +1,7 @@ import axios from 'axios' -import Error400 from '../../../errors/Error400' + +import { Error400 } from '@crowd/common' + import Permissions from '../../../security/permissions' import PermissionChecker from '../../../services/user/permissionChecker' diff --git a/backend/src/api/integration/helpers/gerritAuthenticate.ts b/backend/src/api/integration/helpers/gerritAuthenticate.ts new file mode 100644 index 0000000000..660d95d068 --- /dev/null +++ b/backend/src/api/integration/helpers/gerritAuthenticate.ts @@ -0,0 +1,10 @@ +import Permissions from '../../../security/permissions' +import IntegrationService from '../../../services/integrationService' +import PermissionChecker from '../../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.tenantEdit) + + const payload = await new IntegrationService(req).gerritConnectOrUpdate(req.body) + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/integration/helpers/getIntegrationRepositories.ts b/backend/src/api/integration/helpers/getIntegrationRepositories.ts new file mode 100644 index 0000000000..75c3c271a0 --- /dev/null +++ b/backend/src/api/integration/helpers/getIntegrationRepositories.ts @@ -0,0 +1,14 @@ +import Permissions from '../../../security/permissions' +import IntegrationService from '../../../services/integrationService' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * GET /integration/:id/repositories + * Unified endpoint to get repository mappings for any code platform integration + * (github, gitlab, git, gerrit) + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.tenantEdit) + const payload = await new IntegrationService(req).getIntegrationRepositories(req.params.id) + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/integration/helpers/gitAuthenticate.ts b/backend/src/api/integration/helpers/gitAuthenticate.ts index b53437a284..73176ccd77 100644 --- a/backend/src/api/integration/helpers/gitAuthenticate.ts +++ b/backend/src/api/integration/helpers/gitAuthenticate.ts @@ -4,6 +4,11 @@ import PermissionChecker from '../../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.tenantEdit) - const payload = await new IntegrationService(req).gitConnectOrUpdate(req.body) + const integrationData = { + ...req.body, + remotes: req.body.remotes?.map((remote) => ({ url: remote, forkedFrom: null })) || [], + } + + const payload = await new IntegrationService(req).gitConnectOrUpdate(integrationData) await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/integration/helpers/gitGetRemotes.ts b/backend/src/api/integration/helpers/gitGetRemotes.ts deleted file mode 100644 index 9318ed6399..0000000000 --- a/backend/src/api/integration/helpers/gitGetRemotes.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Permissions from '../../../security/permissions' -import IntegrationService from '../../../services/integrationService' -import PermissionChecker from '../../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.tenantEdit) - const payload = await new IntegrationService(req).gitGetRemotes() - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/integration/helpers/githubConnectInstallation.ts b/backend/src/api/integration/helpers/githubConnectInstallation.ts new file mode 100644 index 0000000000..d7da9b0f43 --- /dev/null +++ b/backend/src/api/integration/helpers/githubConnectInstallation.ts @@ -0,0 +1,9 @@ +import Permissions from '../../../security/permissions' +import IntegrationService from '../../../services/integrationService' +import PermissionChecker from '../../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.tenantEdit) + const payload = await new IntegrationService(req).connectGithubInstallation(req.body.installId) + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/integration/helpers/githubGetInstallations.ts b/backend/src/api/integration/helpers/githubGetInstallations.ts new file mode 100644 index 0000000000..08448c107d --- /dev/null +++ b/backend/src/api/integration/helpers/githubGetInstallations.ts @@ -0,0 +1,9 @@ +import Permissions from '../../../security/permissions' +import IntegrationService from '../../../services/integrationService' +import PermissionChecker from '../../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.tenantEdit) + const payload = await new IntegrationService(req).getGithubInstallations() + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/integration/helpers/githubMapRepos.ts b/backend/src/api/integration/helpers/githubMapRepos.ts index 7edce4b9d3..3fc55abf78 100644 --- a/backend/src/api/integration/helpers/githubMapRepos.ts +++ b/backend/src/api/integration/helpers/githubMapRepos.ts @@ -4,6 +4,10 @@ import PermissionChecker from '../../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.tenantEdit) - const payload = await new IntegrationService(req).mapGithubRepos(req.params.id, req.body.mapping) + const payload = await new IntegrationService(req).mapGithubRepos( + req.params.id, + req.body.mapping, + true, + ) await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/integration/helpers/githubMapReposGet.ts b/backend/src/api/integration/helpers/githubMapReposGet.ts deleted file mode 100644 index a630c64252..0000000000 --- a/backend/src/api/integration/helpers/githubMapReposGet.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Permissions from '../../../security/permissions' -import IntegrationService from '../../../services/integrationService' -import PermissionChecker from '../../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.tenantEdit) - const payload = await new IntegrationService(req).getGithubRepos(req.params.id) - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/integration/helpers/githubNangoConnect.ts b/backend/src/api/integration/helpers/githubNangoConnect.ts new file mode 100644 index 0000000000..94d3d44a3b --- /dev/null +++ b/backend/src/api/integration/helpers/githubNangoConnect.ts @@ -0,0 +1,13 @@ +import Permissions from '../../../security/permissions' +import IntegrationService from '../../../services/integrationService' +import PermissionChecker from '../../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.tenantEdit) + const payload = await new IntegrationService(req).githubNangoConnect( + req.body.settings, + req.body.mapping, + req.body.integrationId, + ) + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/integration/helpers/githubOrgRepos.ts b/backend/src/api/integration/helpers/githubOrgRepos.ts new file mode 100644 index 0000000000..2f9a1be2e0 --- /dev/null +++ b/backend/src/api/integration/helpers/githubOrgRepos.ts @@ -0,0 +1,11 @@ +import { GithubIntegrationService } from '@crowd/common_services' + +import Permissions from '@/security/permissions' +import PermissionChecker from '@/services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.integrationEdit) + + const payload = await GithubIntegrationService.getOrgRepos(req.params.org) + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/integration/helpers/githubSearchOrgs.ts b/backend/src/api/integration/helpers/githubSearchOrgs.ts new file mode 100644 index 0000000000..a6527bcd2f --- /dev/null +++ b/backend/src/api/integration/helpers/githubSearchOrgs.ts @@ -0,0 +1,15 @@ +import { GithubIntegrationService } from '@crowd/common_services' + +import Permissions from '@/security/permissions' +import PermissionChecker from '@/services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.integrationEdit) + + const payload = await GithubIntegrationService.findOrgs( + req.query.query, + req.query.limit, + req.query.offset, + ) + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/integration/helpers/githubSearchRepos.ts b/backend/src/api/integration/helpers/githubSearchRepos.ts new file mode 100644 index 0000000000..f4725b906a --- /dev/null +++ b/backend/src/api/integration/helpers/githubSearchRepos.ts @@ -0,0 +1,15 @@ +import { GithubIntegrationService } from '@crowd/common_services' + +import Permissions from '@/security/permissions' +import PermissionChecker from '@/services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.integrationEdit) + + const payload = await new GithubIntegrationService(req.log).findGithubRepos( + req.query.query, + req.query.limit, + req.query.offset, + ) + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/integration/helpers/gitlabAuthenticate.ts b/backend/src/api/integration/helpers/gitlabAuthenticate.ts new file mode 100644 index 0000000000..88c35fc1ac --- /dev/null +++ b/backend/src/api/integration/helpers/gitlabAuthenticate.ts @@ -0,0 +1,93 @@ +import crypto from 'crypto' +import { Response } from 'express' + +import { generateUUIDv4 as uuid } from '@crowd/common' + +import { GITLAB_CONFIG } from '../../../conf' +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/// credits to lucia-auth library for these functions + +const createUrl = (url: string | URL, urlSearchParams: Record): URL => { + const newUrl = new URL(url) + for (const [key, value] of Object.entries(urlSearchParams)) { + // eslint-disable-next-line no-continue + if (!value) continue + newUrl.searchParams.set(key, value) + } + return newUrl +} + +const getRandomValues = (bytes: number): Uint8Array => { + const buffer = crypto.randomBytes(bytes) + return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength) +} + +const DEFAULT_ALPHABET = 'abcdefghijklmnopqrstuvwxyz1234567890' + +export const generateRandomString = (size: number, alphabet = DEFAULT_ALPHABET): string => { + // eslint-disable-next-line no-bitwise + const mask = (2 << (Math.log(alphabet.length - 1) / Math.LN2)) - 1 + // eslint-disable-next-line no-bitwise + const step = -~((1.6 * mask * size) / alphabet.length) + + let bytes = getRandomValues(step) + let id = '' + let index = 0 + + while (id.length !== size) { + // eslint-disable-next-line no-bitwise + id += alphabet[bytes[index] & mask] ?? '' + index += 1 + if (index > bytes.length) { + bytes = getRandomValues(step) + index = 0 + } + } + return id +} + +const encodeBase64 = (data: string | ArrayLike | ArrayBufferLike) => { + if (typeof Buffer === 'function') { + // node or bun + const bufferData = typeof data === 'string' ? data : new Uint8Array(data) + return Buffer.from(bufferData).toString('base64') + } + if (typeof data === 'string') return btoa(data) + return btoa(String.fromCharCode(...new Uint8Array(data))) +} + +const encodeBase64Url = (data: string | ArrayLike | ArrayBufferLike) => + encodeBase64(data).replaceAll('=', '').replaceAll('+', '-').replaceAll('/', '_') + +/// end credits + +export default async (req, res: Response) => { + // Checking we have permision to edit the project + new PermissionChecker(req).validateHas(Permissions.values.integrationEdit) + + const handle = uuid() + + const callbackUrl = GITLAB_CONFIG.callbackUrl + + const gitlabState = { + crowdToken: req.query.crowdToken, + tenantId: req.params.tenantId, + handle, + } + + const scopes = ['api', 'read_api', 'read_user', 'read_repository', 'profile', 'email'] + + // Build the authorization URL + const authUrl = createUrl('https://gitlab.com/oauth/authorize', { + client_id: GITLAB_CONFIG.clientId, + response_type: 'code', + state: encodeBase64Url(JSON.stringify(gitlabState)), + redirect_uri: callbackUrl, + scope: scopes.join(' '), + }) + + // Redirect user to the authorization URL + res.redirect(authUrl.toString()) +} diff --git a/backend/src/api/integration/helpers/gitlabAuthenticateCallback.ts b/backend/src/api/integration/helpers/gitlabAuthenticateCallback.ts new file mode 100644 index 0000000000..3b350b8c69 --- /dev/null +++ b/backend/src/api/integration/helpers/gitlabAuthenticateCallback.ts @@ -0,0 +1,13 @@ +import Permissions from '../../../security/permissions' +import IntegrationService from '../../../services/integrationService' +import PermissionChecker from '../../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.integrationEdit) + + const code = req.query.code + + const integration = await new IntegrationService(req).gitlabConnect(code) + + await req.responseHandler.success(req, res, integration) +} diff --git a/backend/src/api/integration/helpers/gitlabMapRepos.ts b/backend/src/api/integration/helpers/gitlabMapRepos.ts new file mode 100644 index 0000000000..4394d83530 --- /dev/null +++ b/backend/src/api/integration/helpers/gitlabMapRepos.ts @@ -0,0 +1,13 @@ +import Permissions from '../../../security/permissions' +import IntegrationService from '../../../services/integrationService' +import PermissionChecker from '../../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.tenantEdit) + const payload = await new IntegrationService(req).mapGitlabRepos( + req.params.id, + req.body.mapping, + req.body.projectIds, + ) + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/integration/helpers/groupsioGetUserSubscriptions.ts b/backend/src/api/integration/helpers/groupsioGetUserSubscriptions.ts new file mode 100644 index 0000000000..b533c1228b --- /dev/null +++ b/backend/src/api/integration/helpers/groupsioGetUserSubscriptions.ts @@ -0,0 +1,9 @@ +import Permissions from '../../../security/permissions' +import IntegrationService from '../../../services/integrationService' +import PermissionChecker from '../../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.tenantEdit) + const payload = await new IntegrationService(req).groupsioGetUserSubscriptions(req.body) + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/integration/helpers/hubspotConnect.ts b/backend/src/api/integration/helpers/hubspotConnect.ts deleted file mode 100644 index 4b6332d3ae..0000000000 --- a/backend/src/api/integration/helpers/hubspotConnect.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Permissions from '../../../security/permissions' -import IntegrationService from '../../../services/integrationService' -import PermissionChecker from '../../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.integrationEdit) - const payload = await new IntegrationService(req).hubspotConnect() - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/integration/helpers/hubspotGetLists.ts b/backend/src/api/integration/helpers/hubspotGetLists.ts deleted file mode 100644 index 9723f44ef4..0000000000 --- a/backend/src/api/integration/helpers/hubspotGetLists.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Permissions from '../../../security/permissions' -import IntegrationService from '../../../services/integrationService' -import PermissionChecker from '../../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.integrationEdit) - const payload = await new IntegrationService(req).hubspotGetLists() - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/integration/helpers/hubspotGetMappableFields.ts b/backend/src/api/integration/helpers/hubspotGetMappableFields.ts deleted file mode 100644 index 524f1846b1..0000000000 --- a/backend/src/api/integration/helpers/hubspotGetMappableFields.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Permissions from '../../../security/permissions' -import IntegrationService from '../../../services/integrationService' -import PermissionChecker from '../../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.tenantEdit) - const payload = await new IntegrationService(req).hubspotGetMappableFields() - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/integration/helpers/hubspotOnboard.ts b/backend/src/api/integration/helpers/hubspotOnboard.ts deleted file mode 100644 index 68b447238a..0000000000 --- a/backend/src/api/integration/helpers/hubspotOnboard.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Permissions from '../../../security/permissions' -import IntegrationService from '../../../services/integrationService' -import PermissionChecker from '../../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.tenantEdit) - const payload = await new IntegrationService(req).hubspotOnboard(req.body) - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/integration/helpers/hubspotStopSyncMember.ts b/backend/src/api/integration/helpers/hubspotStopSyncMember.ts deleted file mode 100644 index 8843e17d55..0000000000 --- a/backend/src/api/integration/helpers/hubspotStopSyncMember.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Permissions from '../../../security/permissions' -import IntegrationService from '../../../services/integrationService' -import PermissionChecker from '../../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.integrationEdit) - const payload = await new IntegrationService(req).hubspotStopSyncMember(req.body) - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/integration/helpers/hubspotStopSyncOrganization.ts b/backend/src/api/integration/helpers/hubspotStopSyncOrganization.ts deleted file mode 100644 index ef84437137..0000000000 --- a/backend/src/api/integration/helpers/hubspotStopSyncOrganization.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Permissions from '../../../security/permissions' -import IntegrationService from '../../../services/integrationService' -import PermissionChecker from '../../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.integrationEdit) - const payload = await new IntegrationService(req).hubspotStopSyncOrganization(req.body) - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/integration/helpers/hubspotSyncMember.ts b/backend/src/api/integration/helpers/hubspotSyncMember.ts deleted file mode 100644 index ce9d23fe4b..0000000000 --- a/backend/src/api/integration/helpers/hubspotSyncMember.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Permissions from '../../../security/permissions' -import IntegrationService from '../../../services/integrationService' -import PermissionChecker from '../../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.integrationEdit) - const payload = await new IntegrationService(req).hubspotSyncMember(req.body) - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/integration/helpers/hubspotSyncOrganization.ts b/backend/src/api/integration/helpers/hubspotSyncOrganization.ts deleted file mode 100644 index f540146516..0000000000 --- a/backend/src/api/integration/helpers/hubspotSyncOrganization.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Permissions from '../../../security/permissions' -import IntegrationService from '../../../services/integrationService' -import PermissionChecker from '../../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.integrationEdit) - const payload = await new IntegrationService(req).hubspotSyncOrganization(req.body) - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/integration/helpers/hubspotUpdateProperties.ts b/backend/src/api/integration/helpers/hubspotUpdateProperties.ts deleted file mode 100644 index cbd4e92d6a..0000000000 --- a/backend/src/api/integration/helpers/hubspotUpdateProperties.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Permissions from '../../../security/permissions' -import IntegrationService from '../../../services/integrationService' -import PermissionChecker from '../../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.tenantEdit) - const payload = await new IntegrationService(req).hubspotUpdateProperties() - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/integration/helpers/jiraConnectOrUpdate.ts b/backend/src/api/integration/helpers/jiraConnectOrUpdate.ts new file mode 100644 index 0000000000..a4398bb9c2 --- /dev/null +++ b/backend/src/api/integration/helpers/jiraConnectOrUpdate.ts @@ -0,0 +1,12 @@ +import Permissions from '../../../security/permissions' +import IntegrationService from '../../../services/integrationService' +import PermissionChecker from '../../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.tenantEdit) + const integrationService = new IntegrationService(req) + const payload = req.body.id + ? await integrationService.updateJiraIntegration(req.body) + : await integrationService.connectJiraIntegration(req.body) + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/integration/helpers/linkedinConnect.ts b/backend/src/api/integration/helpers/linkedinConnect.ts index 4aeac8aa79..114ad4ea6b 100644 --- a/backend/src/api/integration/helpers/linkedinConnect.ts +++ b/backend/src/api/integration/helpers/linkedinConnect.ts @@ -4,6 +4,7 @@ import PermissionChecker from '../../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.tenantEdit) - const payload = await new IntegrationService(req).linkedinConnect() + const segmentId = req.body.segments[0] + const payload = await new IntegrationService(req).linkedinConnect(segmentId) await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/integration/helpers/redditOnboard.ts b/backend/src/api/integration/helpers/redditOnboard.ts index 5176fc8dcf..25772db5af 100644 --- a/backend/src/api/integration/helpers/redditOnboard.ts +++ b/backend/src/api/integration/helpers/redditOnboard.ts @@ -4,6 +4,7 @@ import PermissionChecker from '../../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.tenantEdit) - const payload = await new IntegrationService(req).redditOnboard(req.body.subreddits) + const segmentId = req.body.segments[0] + const payload = await new IntegrationService(req).redditOnboard(req.body.subreddits, segmentId) await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/integration/helpers/redditValidator.ts b/backend/src/api/integration/helpers/redditValidator.ts index 93632482e6..d0980e0f9f 100644 --- a/backend/src/api/integration/helpers/redditValidator.ts +++ b/backend/src/api/integration/helpers/redditValidator.ts @@ -1,8 +1,37 @@ import axios from 'axios' -import Error400 from '../../../errors/Error400' + +import { Error400 } from '@crowd/common' +import { RedisCache, RedisClient } from '@crowd/redis' + +import { REDDIT_CONFIG } from '@/conf' + import Permissions from '../../../security/permissions' -import PermissionChecker from '../../../services/user/permissionChecker' import track from '../../../segment/track' +import PermissionChecker from '../../../services/user/permissionChecker' + +const getRedditToken = async (redis: RedisClient, logger: any) => { + const cache = new RedisCache('reddit-global-access-token', redis, logger) + const token = await cache.get('reddit-token') + if (token) { + return token + } + const result = await axios.post( + 'https://www.reddit.com/api/v1/access_token', + 'grant_type=client_credentials', + { + auth: { + username: REDDIT_CONFIG.clientId, + password: REDDIT_CONFIG.clientSecret, + }, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }, + ) + // cache for 30 minutes + await cache.set('reddit-token', result.data.access_token, 30 * 60) + return result.data.access_token +} export default async (req, res) => { new PermissionChecker(req).validateHasAny([ @@ -11,14 +40,28 @@ export default async (req, res) => { ]) if (req.query.subreddit) { + let token: string try { - const result = await axios.get( - `https://www.reddit.com/r/${req.query.subreddit}/new.json?limit=1`, + token = await getRedditToken(req.redis, req.log) + } catch (e) { + req.log.error(e) + return req.responseHandler.error(req, res, new Error400(req.language)) + } + try { + const result = await axios.post( + `https://oauth.reddit.com/api/search_reddit_names`, + `query=${req.query.subreddit}&exact=true`, + { + headers: { + ContentType: 'application/x-www-form-urlencoded', + Authorization: `Bearer ${token}`, + }, + }, ) if ( result.status === 200 && - result.data.data.children && - result.data.data.children.length > 0 + result.data.names && + result.data.names.includes(req.query.subreddit) ) { track( 'Reddit: subreddit input', @@ -28,9 +71,20 @@ export default async (req, res) => { }, { ...req }, ) - return req.responseHandler.success(req, res, result.data.data.children) + return req.responseHandler.success(req, res, true) } + // for other status codes we return error + track( + 'Reddit: subreddit input', + { + subreddit: req.query.subreddit, + valid: false, + }, + { ...req }, + ) + return req.responseHandler.error(req, res, new Error400(req.language)) } catch (e) { + req.log.error('Error validating subreddit', e) track( 'Reddit: subreddit input', { @@ -39,6 +93,7 @@ export default async (req, res) => { }, { ...req }, ) + req.log.error(e) return req.responseHandler.error(req, res, new Error400(req.language)) } } @@ -50,5 +105,6 @@ export default async (req, res) => { }, { ...req }, ) + req.log.error('Reddit: subreddit input is empty') return req.responseHandler.error(req, res, new Error400(req.language)) } diff --git a/backend/src/api/integration/helpers/slackAuthenticate.ts b/backend/src/api/integration/helpers/slackAuthenticate.ts index 99982f4497..a5e16e8605 100644 --- a/backend/src/api/integration/helpers/slackAuthenticate.ts +++ b/backend/src/api/integration/helpers/slackAuthenticate.ts @@ -1,7 +1,8 @@ import passport from 'passport' + +import SequelizeRepository from '../../../database/repositories/sequelizeRepository' import Permissions from '../../../security/permissions' import PermissionChecker from '../../../services/user/permissionChecker' -import SequelizeRepository from '../../../database/repositories/sequelizeRepository' export default async (req, res, next) => { // Checking we have permision to edit the project diff --git a/backend/src/api/integration/helpers/stackOverflowValidator.ts b/backend/src/api/integration/helpers/stackOverflowValidator.ts index 78c104c6a6..f2a9810bdf 100644 --- a/backend/src/api/integration/helpers/stackOverflowValidator.ts +++ b/backend/src/api/integration/helpers/stackOverflowValidator.ts @@ -1,10 +1,12 @@ import axios from 'axios' -import Error400 from '../../../errors/Error400' + +import { Error400 } from '@crowd/common' + +import { STACKEXCHANGE_CONFIG } from '../../../conf' import Permissions from '../../../security/permissions' -import PermissionChecker from '../../../services/user/permissionChecker' import track from '../../../segment/track' import { StackOverflowTagsResponse } from '../../../serverless/integrations/types/stackOverflowTypes' -import { STACKEXCHANGE_CONFIG } from '../../../conf' +import PermissionChecker from '../../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHasAny([ diff --git a/backend/src/api/integration/helpers/stackOverflowVolume.ts b/backend/src/api/integration/helpers/stackOverflowVolume.ts index b3a276131d..fa75ad79d5 100644 --- a/backend/src/api/integration/helpers/stackOverflowVolume.ts +++ b/backend/src/api/integration/helpers/stackOverflowVolume.ts @@ -1,8 +1,10 @@ import axios from 'axios' -import Error400 from '../../../errors/Error400' + +import { Error400 } from '@crowd/common' + +import { STACKEXCHANGE_CONFIG } from '../../../conf' import Permissions from '../../../security/permissions' import PermissionChecker from '../../../services/user/permissionChecker' -import { STACKEXCHANGE_CONFIG } from '../../../conf' export default async (req, res) => { new PermissionChecker(req).validateHasAny([ diff --git a/backend/src/api/integration/helpers/twitterAuthenticate.ts b/backend/src/api/integration/helpers/twitterAuthenticate.ts index 514b318fb6..fdd7ac8706 100644 --- a/backend/src/api/integration/helpers/twitterAuthenticate.ts +++ b/backend/src/api/integration/helpers/twitterAuthenticate.ts @@ -1,12 +1,14 @@ import crypto from 'crypto' -import { PlatformType } from '@crowd/types' import { Response } from 'express' -import { RedisCache } from '@crowd/redis' + import { generateUUIDv4 as uuid } from '@crowd/common' +import { RedisCache } from '@crowd/redis' +import { PlatformType } from '@crowd/types' + import { TWITTER_CONFIG } from '../../../conf' +import SequelizeRepository from '../../../database/repositories/sequelizeRepository' import Permissions from '../../../security/permissions' import PermissionChecker from '../../../services/user/permissionChecker' -import SequelizeRepository from '../../../database/repositories/sequelizeRepository' /// credits to lucia-auth library for these functions diff --git a/backend/src/api/integration/helpers/twitterAuthenticateCallback.ts b/backend/src/api/integration/helpers/twitterAuthenticateCallback.ts index 86545f3060..60287fd0c6 100644 --- a/backend/src/api/integration/helpers/twitterAuthenticateCallback.ts +++ b/backend/src/api/integration/helpers/twitterAuthenticateCallback.ts @@ -1,10 +1,12 @@ -import { RedisCache } from '@crowd/redis' import axios from 'axios' -import PermissionChecker from '../../../services/user/permissionChecker' -import Permissions from '../../../security/permissions' -import IntegrationService from '../../../services/integrationService' + +import { RedisCache } from '@crowd/redis' + import { API_CONFIG, TWITTER_CONFIG } from '../../../conf' import SegmentRepository from '../../../database/repositories/segmentRepository' +import Permissions from '../../../security/permissions' +import IntegrationService from '../../../services/integrationService' +import PermissionChecker from '../../../services/user/permissionChecker' const errorURL = `${API_CONFIG.frontendUrl}/integrations?twitter-error=true` diff --git a/backend/src/api/integration/index.ts b/backend/src/api/integration/index.ts index 70561bf299..5f23e93d68 100644 --- a/backend/src/api/integration/index.ts +++ b/backend/src/api/integration/index.ts @@ -1,11 +1,13 @@ import passport from 'passport' + +import { DEFAULT_TENANT_ID } from '@crowd/common' +import { RedisCache } from '@crowd/redis' + import { API_CONFIG, SLACK_CONFIG, TWITTER_CONFIG } from '../../conf' import SegmentRepository from '../../database/repositories/segmentRepository' import { authMiddleware } from '../../middlewares/authMiddleware' import { safeWrap } from '../../middlewares/errorMiddleware' import TenantService from '../../services/tenantService' -import { FeatureFlag } from '@/types/common' -import { featureFlagMiddleware } from '@/middlewares/featureFlagMiddleware' const decodeBase64Url = (data) => { data = data.replaceAll('-', '+').replaceAll('_', '/') @@ -16,152 +18,103 @@ const decodeBase64Url = (data) => { } export default (app) => { - app.post(`/tenant/:tenantId/integration/query`, safeWrap(require('./integrationQuery').default)) - app.post(`/tenant/:tenantId/integration`, safeWrap(require('./integrationCreate').default)) - app.put(`/tenant/:tenantId/integration/:id`, safeWrap(require('./integrationUpdate').default)) - app.post(`/tenant/:tenantId/integration/import`, safeWrap(require('./integrationImport').default)) - app.delete(`/tenant/:tenantId/integration`, safeWrap(require('./integrationDestroy').default)) + app.post(`/integration/query`, safeWrap(require('./integrationQuery').default)) + app.post(`/integration`, safeWrap(require('./integrationCreate').default)) + app.put(`/integration/:id`, safeWrap(require('./integrationUpdate').default)) + app.delete(`/integration`, safeWrap(require('./integrationDestroy').default)) + app.get(`/integration/autocomplete`, safeWrap(require('./integrationAutocomplete').default)) + app.get(`/integration/global`, safeWrap(require('./integrationGlobal').default)) + app.get(`/integration/global/status`, safeWrap(require('./integrationGlobalStatus').default)) + app.get( - `/tenant/:tenantId/integration/autocomplete`, - safeWrap(require('./integrationAutocomplete').default), + '/integration/github-installations', + safeWrap(require('./helpers/githubGetInstallations').default), ) - app.get(`/tenant/:tenantId/integration`, safeWrap(require('./integrationList').default)) - app.get(`/tenant/:tenantId/integration/:id`, safeWrap(require('./integrationFind').default)) - app.put( - `/authenticate/:tenantId/:code`, - safeWrap(require('./helpers/githubAuthenticate').default), - ) - app.put( - `/tenant/:tenantId/integration/:id/github/repos`, - safeWrap(require('./helpers/githubMapRepos').default), + app.post( + '/integration/github-connect-installation', + safeWrap(require('./helpers/githubConnectInstallation').default), ) + + app.get(`/integration`, safeWrap(require('./integrationList').default)) + app.get(`/integration/:id`, safeWrap(require('./integrationFind').default)) + + // Unified endpoint for all code platform integrations (github, gitlab, git, gerrit) app.get( - `/tenant/:tenantId/integration/:id/github/repos`, - safeWrap(require('./helpers/githubMapReposGet').default), + `/integration/:id/repositories`, + safeWrap(require('./helpers/getIntegrationRepositories').default), ) - app.put( - `/discord-authenticate/:tenantId/:guild_id`, - safeWrap(require('./helpers/discordAuthenticate').default), - ) - app.put(`/reddit-onboard/:tenantId`, safeWrap(require('./helpers/redditOnboard').default)) - app.put('/linkedin-connect/:tenantId', safeWrap(require('./helpers/linkedinConnect').default)) - app.post('/linkedin-onboard/:tenantId', safeWrap(require('./helpers/linkedinOnboard').default)) - app.put(`/tenant/:tenantId/git-connect`, safeWrap(require('./helpers/gitAuthenticate').default)) - app.get('/tenant/:tenantId/git', safeWrap(require('./helpers/gitGetRemotes').default)) + + app.put(`/authenticate/:code`, safeWrap(require('./helpers/githubAuthenticate').default)) + app.put(`/integration/:id/github/repos`, safeWrap(require('./helpers/githubMapRepos').default)) + app.get( - '/tenant/:tenantId/devto-validate', - safeWrap(require('./helpers/devtoValidators').default), + `/integration/github/search/orgs`, + safeWrap(require('./helpers/githubSearchOrgs').default), ) app.get( - '/tenant/:tenantId/reddit-validate', - safeWrap(require('./helpers/redditValidator').default), - ) - app.post( - '/tenant/:tenantId/devto-connect', - safeWrap(require('./helpers/devtoCreateOrUpdate').default), - ) - app.post( - '/tenant/:tenantId/hackernews-connect', - safeWrap(require('./helpers/hackerNewsCreateOrUpdate').default), - ) - - app.post( - '/tenant/:tenantId/stackoverflow-connect', - safeWrap(require('./helpers/stackOverflowCreateOrUpdate').default), + `/integration/github/search/repos`, + safeWrap(require('./helpers/githubSearchRepos').default), ) app.get( - '/tenant/:tenantId/stackoverflow-validate', - safeWrap(require('./helpers/stackOverflowValidator').default), + `/integration/github/orgs/:org/repos`, + safeWrap(require('./helpers/githubOrgRepos').default), ) - app.get( - '/tenant/:tenantId/stackoverflow-volume', - safeWrap(require('./helpers/stackOverflowVolume').default), + app.post('/github-nango-connect', safeWrap(require('./helpers/githubNangoConnect').default)) + app.put( + `/discord-authenticate/:guild_id`, + safeWrap(require('./helpers/discordAuthenticate').default), ) + app.put(`/reddit-onboard`, safeWrap(require('./helpers/redditOnboard').default)) + app.put('/linkedin-connect', safeWrap(require('./helpers/linkedinConnect').default)) + app.post('/linkedin-onboard', safeWrap(require('./helpers/linkedinOnboard').default)) - app.post( - '/tenant/:tenantId/discourse-connect', - safeWrap(require('./helpers/discourseCreateOrUpdate').default), - ) + app.post(`/integration/progress/list`, safeWrap(require('./integrationProgressList').default)) - app.post( - '/tenant/:tenantId/discourse-validate', - safeWrap(require('./helpers/discourseValidator').default), - ) + app.get(`/integration/progress/:id`, safeWrap(require('./integrationProgress').default)) - app.post( - '/tenant/:tenantId/discourse-test-webhook', - safeWrap(require('./helpers/discourseTestWebhook').default), - ) + app.get(`/integration/mapped-repos/:id`, safeWrap(require('./integrationMappedRepos').default)) - app.post( - '/tenant/:tenantId/hubspot-connect', - featureFlagMiddleware(FeatureFlag.HUBSPOT, 'hubspot.errors.notInPlan'), - safeWrap(require('./helpers/hubspotConnect').default), - ) + // Git + app.put(`/git-connect`, safeWrap(require('./helpers/gitAuthenticate').default)) + app.put(`/confluence-connect`, safeWrap(require('./helpers/confluenceAuthenticate').default)) + app.put(`/gerrit-connect`, safeWrap(require('./helpers/gerritAuthenticate').default)) + app.get('/devto-validate', safeWrap(require('./helpers/devtoValidators').default)) + app.get('/reddit-validate', safeWrap(require('./helpers/redditValidator').default)) + app.post('/devto-connect', safeWrap(require('./helpers/devtoCreateOrUpdate').default)) + app.post('/hackernews-connect', safeWrap(require('./helpers/hackerNewsCreateOrUpdate').default)) app.post( - '/tenant/:tenantId/hubspot-onboard', - featureFlagMiddleware(FeatureFlag.HUBSPOT, 'hubspot.errors.notInPlan'), - safeWrap(require('./helpers/hubspotOnboard').default), + '/stackoverflow-connect', + safeWrap(require('./helpers/stackOverflowCreateOrUpdate').default), ) + app.get('/stackoverflow-validate', safeWrap(require('./helpers/stackOverflowValidator').default)) + app.get('/stackoverflow-volume', safeWrap(require('./helpers/stackOverflowVolume').default)) - app.post( - '/tenant/:tenantId/hubspot-update-properties', - featureFlagMiddleware(FeatureFlag.HUBSPOT, 'hubspot.errors.notInPlan'), - safeWrap(require('./helpers/hubspotUpdateProperties').default), - ) + app.post('/discourse-connect', safeWrap(require('./helpers/discourseCreateOrUpdate').default)) - app.get( - '/tenant/:tenantId/hubspot-mappable-fields', - featureFlagMiddleware(FeatureFlag.HUBSPOT, 'hubspot.errors.notInPlan'), - safeWrap(require('./helpers/hubspotGetMappableFields').default), - ) + app.post('/discourse-validate', safeWrap(require('./helpers/discourseValidator').default)) - app.get( - '/tenant/:tenantId/hubspot-get-lists', - featureFlagMiddleware(FeatureFlag.HUBSPOT, 'hubspot.errors.notInPlan'), - safeWrap(require('./helpers/hubspotGetLists').default), - ) + app.post('/discourse-test-webhook', safeWrap(require('./helpers/discourseTestWebhook').default)) - app.post( - '/tenant/:tenantId/hubspot-sync-member', - featureFlagMiddleware(FeatureFlag.HUBSPOT, 'hubspot.errors.notInPlan'), - safeWrap(require('./helpers/hubspotSyncMember').default), - ) + app.post('/groupsio-connect', safeWrap(require('./helpers/groupsioConnectOrUpdate').default)) - app.post( - '/tenant/:tenantId/hubspot-stop-sync-member', - featureFlagMiddleware(FeatureFlag.HUBSPOT, 'hubspot.errors.notInPlan'), - safeWrap(require('./helpers/hubspotStopSyncMember').default), - ) + app.post('/groupsio-get-token', safeWrap(require('./helpers/groupsioGetToken').default)) - app.post( - '/tenant/:tenantId/hubspot-sync-organization', - featureFlagMiddleware(FeatureFlag.HUBSPOT, 'hubspot.errors.notInPlan'), - safeWrap(require('./helpers/hubspotSyncOrganization').default), - ) + app.post('/groupsio-verify-group', safeWrap(require('./helpers/groupsioVerifyGroup').default)) app.post( - '/tenant/:tenantId/hubspot-stop-sync-organization', - featureFlagMiddleware(FeatureFlag.HUBSPOT, 'hubspot.errors.notInPlan'), - safeWrap(require('./helpers/hubspotStopSyncOrganization').default), + '/groupsio-get-user-subscriptions', + safeWrap(require('./helpers/groupsioGetUserSubscriptions').default), ) - app.post( - '/tenant/:tenantId/groupsio-connect', - safeWrap(require('./helpers/groupsioConnectOrUpdate').default), - ) + app.post('/jira-connect', safeWrap(require('./helpers/jiraConnectOrUpdate').default)) - app.post( - '/tenant/:tenantId/groupsio-get-token', - safeWrap(require('./helpers/groupsioGetToken').default), - ) + app.get('/gitlab/connect', safeWrap(require('./helpers/gitlabAuthenticate').default)) - app.post( - '/tenant/:tenantId/groupsio-verify-group', - safeWrap(require('./helpers/groupsioVerifyGroup').default), - ) + app.get('/gitlab/callback', safeWrap(require('./helpers/gitlabAuthenticateCallback').default)) + + app.put(`/integration/:id/gitlab/repos`, safeWrap(require('./helpers/gitlabMapRepos').default)) if (TWITTER_CONFIG.clientId) { /** @@ -173,14 +126,10 @@ export default (app) => { * This state is sent using the authenticator options and * manipulated through twitterStrategy.staticPKCEStore */ - app.get( - '/twitter/:tenantId/connect', - safeWrap(require('./helpers/twitterAuthenticate').default), - () => { - // The request will be redirected for authentication, so this - // function will not be called. - }, - ) + app.get('/twitter/connect', safeWrap(require('./helpers/twitterAuthenticate').default), () => { + // The request will be redirected for authentication, so this + // function will not be called. + }) /** * OAuth2 callback endpoint. After user successfully @@ -203,23 +152,31 @@ export default (app) => { // session: false, // failureRedirect: `${API_CONFIG.frontendUrl}/integrations?error=true`, // }), - (req, _res, next) => { + async (req, _res, next) => { const stateQueryParam = req.query.state const decodedState = decodeBase64Url(stateQueryParam) - const stateObject = JSON.parse(decodedState) - const { crowdToken } = stateObject + req.state = JSON.parse(decodedState) + next() + }, + (req, _res, next) => { + const { crowdToken } = req.state req.headers.authorization = `Bearer ${crowdToken}` next() }, authMiddleware, async (req, _res, next) => { - const stateQueryParam = req.query.state - const decodedState = decodeBase64Url(stateQueryParam) - const stateObject = JSON.parse(decodedState) - const { tenantId } = stateObject + const tenantId = DEFAULT_TENANT_ID req.currentTenant = await new TenantService(req).findById(tenantId) next() }, + async (req, _res, next) => { + const cache = new RedisCache('twitterPKCE', req.redis, req.log) + const state = await cache.get(req.currentUser.id) + const { segmentIds } = JSON.parse(state) + const segmentRepository = new SegmentRepository(req) + req.currentSegments = await segmentRepository.findInIds(segmentIds) + next() + }, safeWrap(require('./helpers/twitterAuthenticateCallback').default), ) } @@ -230,7 +187,7 @@ export default (app) => { */ if (SLACK_CONFIG.clientId) { // path to start the OAuth flow - app.get('/slack/:tenantId/connect', safeWrap(require('./helpers/slackAuthenticate').default)) + app.get('/slack/connect', safeWrap(require('./helpers/slackAuthenticate').default)) // OAuth callback url app.get( @@ -250,7 +207,7 @@ export default (app) => { }, authMiddleware, async (req, _res, next) => { - const { tenantId } = req.state + const tenantId = DEFAULT_TENANT_ID req.currentTenant = await new TenantService(req).findById(tenantId) next() }, diff --git a/backend/src/api/integration/integrationCreate.ts b/backend/src/api/integration/integrationCreate.ts index e8a2bfbe25..dfe0ce7b6b 100644 --- a/backend/src/api/integration/integrationCreate.ts +++ b/backend/src/api/integration/integrationCreate.ts @@ -1,12 +1,10 @@ -import PermissionChecker from '../../services/user/permissionChecker' import Permissions from '../../security/permissions' import IntegrationService from '../../services/integrationService' +import PermissionChecker from '../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.integrationCreate) - new PermissionChecker(req).validateIntegrationsProtectedFields(req.body) - const payload = await new IntegrationService(req).create(req.body) await req.responseHandler.success(req, res, payload) diff --git a/backend/src/api/integration/integrationFind.ts b/backend/src/api/integration/integrationFind.ts index d6cc02f2c2..039c82786a 100644 --- a/backend/src/api/integration/integrationFind.ts +++ b/backend/src/api/integration/integrationFind.ts @@ -1,6 +1,6 @@ -import PermissionChecker from '../../services/user/permissionChecker' import Permissions from '../../security/permissions' import IntegrationService from '../../services/integrationService' +import PermissionChecker from '../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.integrationRead) diff --git a/backend/src/api/integration/integrationGlobal.ts b/backend/src/api/integration/integrationGlobal.ts new file mode 100644 index 0000000000..4eae57cee7 --- /dev/null +++ b/backend/src/api/integration/integrationGlobal.ts @@ -0,0 +1,10 @@ +import Permissions from '../../security/permissions' +import IntegrationService from '../../services/integrationService' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.integrationRead) + const payload = await new IntegrationService(req).findGlobalIntegrations(req.query) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/integration/integrationGlobalStatus.ts b/backend/src/api/integration/integrationGlobalStatus.ts new file mode 100644 index 0000000000..a135ca34f4 --- /dev/null +++ b/backend/src/api/integration/integrationGlobalStatus.ts @@ -0,0 +1,11 @@ +import Permissions from '../../security/permissions' +import IntegrationService from '../../services/integrationService' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.integrationRead) + + const payload = await new IntegrationService(req).findGlobalIntegrationsStatusCount(req.query) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/integration/integrationImport.ts b/backend/src/api/integration/integrationImport.ts deleted file mode 100644 index 97019f5136..0000000000 --- a/backend/src/api/integration/integrationImport.ts +++ /dev/null @@ -1,13 +0,0 @@ -import PermissionChecker from '../../services/user/permissionChecker' -import Permissions from '../../security/permissions' -import IntegrationService from '../../services/integrationService' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.integrationImport) - - await new IntegrationService(req).import(req.body, req.body.importHash) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/integration/integrationList.ts b/backend/src/api/integration/integrationList.ts index 7c9f3b9157..08d0571343 100644 --- a/backend/src/api/integration/integrationList.ts +++ b/backend/src/api/integration/integrationList.ts @@ -1,6 +1,6 @@ -import PermissionChecker from '../../services/user/permissionChecker' import Permissions from '../../security/permissions' import IntegrationService from '../../services/integrationService' +import PermissionChecker from '../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.integrationRead) diff --git a/backend/src/api/integration/integrationMappedRepos.ts b/backend/src/api/integration/integrationMappedRepos.ts new file mode 100644 index 0000000000..5255c5784a --- /dev/null +++ b/backend/src/api/integration/integrationMappedRepos.ts @@ -0,0 +1,11 @@ +import Permissions from '../../security/permissions' +import IntegrationService from '../../services/integrationService' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.integrationRead) + + const payload = await new IntegrationService(req).getIntegrationMappedRepos(req.params.id) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/integration/integrationProgress.ts b/backend/src/api/integration/integrationProgress.ts new file mode 100644 index 0000000000..d88fa67ab9 --- /dev/null +++ b/backend/src/api/integration/integrationProgress.ts @@ -0,0 +1,11 @@ +import Permissions from '../../security/permissions' +import IntegrationService from '../../services/integrationService' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.integrationRead) + + const payload = await new IntegrationService(req).getIntegrationProgress(req.params.id) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/integration/integrationProgressList.ts b/backend/src/api/integration/integrationProgressList.ts new file mode 100644 index 0000000000..ae7419ffc4 --- /dev/null +++ b/backend/src/api/integration/integrationProgressList.ts @@ -0,0 +1,11 @@ +import Permissions from '../../security/permissions' +import IntegrationService from '../../services/integrationService' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.integrationRead) + + const payload = await new IntegrationService(req).getIntegrationProgressList() + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/integration/integrationQuery.ts b/backend/src/api/integration/integrationQuery.ts index ba78b4b4cf..ac8f8c9bf7 100644 --- a/backend/src/api/integration/integrationQuery.ts +++ b/backend/src/api/integration/integrationQuery.ts @@ -23,8 +23,8 @@ export default async (req, res) => { const payload = await new IntegrationService(req).query(req.body) - if (req.query.filter && Object.keys(req.query.filter).length > 0) { - track('Integrations Advanced Filter', { ...payload }, { ...req }) + if (req.body?.filter && Object.keys(req.body.filter).length > 0) { + track('Integrations Advanced Filter', { ...req.body }, { ...req }) } await req.responseHandler.success(req, res, payload) diff --git a/backend/src/api/integration/integrationUpdate.ts b/backend/src/api/integration/integrationUpdate.ts index 72670d3c20..621aec29fc 100644 --- a/backend/src/api/integration/integrationUpdate.ts +++ b/backend/src/api/integration/integrationUpdate.ts @@ -4,7 +4,6 @@ import PermissionChecker from '../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.integrationEdit) - new PermissionChecker(req).validateIntegrationsProtectedFields(req.body) const payload = await new IntegrationService(req).update(req.params.id, req.body) diff --git a/backend/src/api/member/affiliation/index.ts b/backend/src/api/member/affiliation/index.ts new file mode 100644 index 0000000000..0f93a5b477 --- /dev/null +++ b/backend/src/api/member/affiliation/index.ts @@ -0,0 +1,17 @@ +import { safeWrap } from '@/middlewares/errorMiddleware' + +export default (app) => { + // Member Affiliation List + app.get(`/member/:memberId/affiliation`, safeWrap(require('./memberAffiliationList').default)) + + // Member Affiliation Create Multiple + app.patch( + `/member/:memberId/affiliation`, + safeWrap(require('./memberAffiliationUpdateMultiple').default), + ) + + app.post( + `/member/:memberId/affiliation/override`, + safeWrap(require('./memberAffiliationChangeOverride').default), + ) +} diff --git a/backend/src/api/member/affiliation/memberAffiliationChangeOverride.ts b/backend/src/api/member/affiliation/memberAffiliationChangeOverride.ts new file mode 100644 index 0000000000..114f92e12a --- /dev/null +++ b/backend/src/api/member/affiliation/memberAffiliationChangeOverride.ts @@ -0,0 +1,17 @@ +import MemberAffiliationsService from '@/services/member/memberAffiliationsService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberEdit) + + const memberAffiliationsService = new MemberAffiliationsService(req) + + const payload = await memberAffiliationsService.changeAffiliationOverride({ + ...req.body, + memberId: req.params.memberId, + }) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/member/affiliation/memberAffiliationList.ts b/backend/src/api/member/affiliation/memberAffiliationList.ts new file mode 100644 index 0000000000..37ce9ccfa0 --- /dev/null +++ b/backend/src/api/member/affiliation/memberAffiliationList.ts @@ -0,0 +1,27 @@ +import MemberAffiliationsService from '@/services/member/memberAffiliationsService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * GET /member/:memberId/affiliation + * @summary List member affiliations + * @tag Members + * @security Bearer + * @description Query member affiliations. + * @pathParam {string} memberId - member ID + * @response 200 - Ok + * @responseContent {MemberList} 200.application/json + * @responseExample {MemberList} 200.application/json.MemberAffiliation + * @response 401 - Unauthorized + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberRead) + + const memberAffiliationsService = new MemberAffiliationsService(req) + + const payload = await memberAffiliationsService.list(req.params.memberId) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/member/affiliation/memberAffiliationUpdateMultiple.ts b/backend/src/api/member/affiliation/memberAffiliationUpdateMultiple.ts new file mode 100644 index 0000000000..4896b7e593 --- /dev/null +++ b/backend/src/api/member/affiliation/memberAffiliationUpdateMultiple.ts @@ -0,0 +1,30 @@ +import MemberAffiliationsService from '@/services/member/memberAffiliationsService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * PUT /member/:memberId/affiliation + * @summary Upsert member affiliations + * @tag Members + * @security Bearer + * @description Upsert multiple member affiliations. + * @pathParam {string} memberId - member ID + * @response 200 - Ok + * @responseContent {MemberList} 200.application/json + * @responseExample {MemberList} 200.application/json.MemberAffiliation + * @response 401 - Unauthorized + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberEdit) + + const memberAffiliationsService = new MemberAffiliationsService(req) + + const payload = await memberAffiliationsService.upsertMultiple( + req.params.memberId, + req.body.affiliations, + ) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/member/attributes/index.ts b/backend/src/api/member/attributes/index.ts new file mode 100644 index 0000000000..9c13c6d6e1 --- /dev/null +++ b/backend/src/api/member/attributes/index.ts @@ -0,0 +1,9 @@ +import { safeWrap } from '@/middlewares/errorMiddleware' + +export default (app) => { + // Member Attributes + app.get(`/member/:memberId/attributes`, safeWrap(require('./memberAttributesList').default)) + + // Member Attributes Update + app.patch(`/member/:memberId/attributes`, safeWrap(require('./memberAttributesUpdate').default)) +} diff --git a/backend/src/api/member/attributes/memberAttributesList.ts b/backend/src/api/member/attributes/memberAttributesList.ts new file mode 100644 index 0000000000..66b7ec85cc --- /dev/null +++ b/backend/src/api/member/attributes/memberAttributesList.ts @@ -0,0 +1,27 @@ +import MemberAttributesService from '@/services/member/memberAttributesService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * GET /member/:memberId/attributes + * @summary Query member attributes + * @tag Members + * @security Bearer + * @description Query member attributes. + * @pathParam {string} memberId - member ID + * @response 200 - Ok + * @responseContent {MemberList} 200.application/json + * @responseExample {MemberList} 200.application/json.Attributes + * @response 401 - Unauthorized + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberRead) + + const memberAttributesService = new MemberAttributesService(req) + + const payload = await memberAttributesService.list(req.params.memberId) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/member/attributes/memberAttributesUpdate.ts b/backend/src/api/member/attributes/memberAttributesUpdate.ts new file mode 100644 index 0000000000..38f1f5ad6f --- /dev/null +++ b/backend/src/api/member/attributes/memberAttributesUpdate.ts @@ -0,0 +1,38 @@ +import MemberAttributesService from '@/services/member/memberAttributesService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * PATCH /member/:memberId/attributes + * @summary Update member attributes + * @tag Members + * @security Bearer + * @description Update member attributes. + * @pathParam {string} memberId - member ID + * @response 200 - Ok + * @responseContent {MemberList} 200.application/json + * @responseExample {MemberList} 200.application/json.Attributes + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberEdit) + + const memberAttributesService = new MemberAttributesService(req) + + // defaults to true unless query param is 'false' + const manuallyChanged = req.query.manuallyChanged !== 'false' + + // remove segments from body + delete req.body.segments + + const payload = await memberAttributesService.update( + req.params.memberId, + req.body, + manuallyChanged, + ) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/member/identity/index.ts b/backend/src/api/member/identity/index.ts new file mode 100644 index 0000000000..c2019c562f --- /dev/null +++ b/backend/src/api/member/identity/index.ts @@ -0,0 +1,18 @@ +import { safeWrap } from '@/middlewares/errorMiddleware' + +export default (app) => { + // Member Identity List + app.get(`/member/:memberId/identity`, safeWrap(require('./memberIdentityList').default)) + + // Member Identity Create + app.post(`/member/:memberId/identity`, safeWrap(require('./memberIdentityCreate').default)) + + // Member Identity Create Multiple + app.put(`/member/:memberId/identity`, safeWrap(require('./memberIdentityCreateMultiple').default)) + + // Member Identity Update + app.patch(`/member/:memberId/identity/:id`, safeWrap(require('./memberIdentityUpdate').default)) + + // Member Identity Delete + app.delete(`/member/:memberId/identity/:id`, safeWrap(require('./memberIdentityDelete').default)) +} diff --git a/backend/src/api/member/identity/memberIdentityCreate.ts b/backend/src/api/member/identity/memberIdentityCreate.ts new file mode 100644 index 0000000000..374b768575 --- /dev/null +++ b/backend/src/api/member/identity/memberIdentityCreate.ts @@ -0,0 +1,27 @@ +import MemberIdentityService from '@/services/member/memberIdentityService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * POST /member/:memberId/identity + * @summary Create member identity + * @tag Members + * @security Bearer + * @description Create one member identity. + * @pathParam {string} memberId - member ID + * @response 200 - Ok + * @responseContent {MemberList} 200.application/json + * @responseExample {MemberList} 200.application/json.MemberIdentity + * @response 401 - Unauthorized + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberEdit) + + const memberIdentityService = new MemberIdentityService(req) + + const payload = await memberIdentityService.create(req.params.memberId, req.body) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/member/identity/memberIdentityCreateMultiple.ts b/backend/src/api/member/identity/memberIdentityCreateMultiple.ts new file mode 100644 index 0000000000..1ac2913ada --- /dev/null +++ b/backend/src/api/member/identity/memberIdentityCreateMultiple.ts @@ -0,0 +1,30 @@ +import MemberIdentityService from '@/services/member/memberIdentityService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * PUT /member/:memberId/identity + * @summary Create member identities + * @tag Members + * @security Bearer + * @description Create multiple member identity. + * @pathParam {string} memberId - member ID + * @response 200 - Ok + * @responseContent {MemberList} 200.application/json + * @responseExample {MemberList} 200.application/json.MemberIdentity + * @response 401 - Unauthorized + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberEdit) + + const memberIdentityService = new MemberIdentityService(req) + + const payload = await memberIdentityService.createMultiple( + req.params.memberId, + req.body.identities, + ) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/member/identity/memberIdentityDelete.ts b/backend/src/api/member/identity/memberIdentityDelete.ts new file mode 100644 index 0000000000..cb2c81bd1e --- /dev/null +++ b/backend/src/api/member/identity/memberIdentityDelete.ts @@ -0,0 +1,28 @@ +import MemberIdentityService from '@/services/member/memberIdentityService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * DELETE /member/:memberId/identity/:identityId + * @summary Remove member identity + * @tag Members + * @security Bearer + * @description Remove member identity. + * @pathParam {string} memberId - member ID | {string} identityId - member identity ID + * @response 200 - Ok + * @responseContent {MemberList} 200.application/json + * @responseExample {MemberList} 200.application/json.MemberIdentity + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberEdit) + + const memberIdentityService = new MemberIdentityService(req) + + const payload = await memberIdentityService.delete(req.params.id, req.params.memberId) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/member/identity/memberIdentityList.ts b/backend/src/api/member/identity/memberIdentityList.ts new file mode 100644 index 0000000000..b053b8ce77 --- /dev/null +++ b/backend/src/api/member/identity/memberIdentityList.ts @@ -0,0 +1,28 @@ +import MemberIdentityService from '@/services/member/memberIdentityService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * GET /member/:memberId/identity + * @summary Query member identities + * @tag Members + * @security Bearer + * @description Query member identities. + * @pathParam {string} memberId - member ID + * @response 200 - Ok + * @responseContent {MemberList} 200.application/json + * @responseExample {MemberList} 200.application/json.Member + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberRead) + + const memberIdentityService = new MemberIdentityService(req) + + const payload = await memberIdentityService.list(req.params.memberId) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/member/identity/memberIdentityUpdate.ts b/backend/src/api/member/identity/memberIdentityUpdate.ts new file mode 100644 index 0000000000..d39168c1f4 --- /dev/null +++ b/backend/src/api/member/identity/memberIdentityUpdate.ts @@ -0,0 +1,28 @@ +import MemberIdentityService from '@/services/member/memberIdentityService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * PATCH /member/:memberId/identity/:identityId + * @summary Update member identity + * @tag Members + * @security Bearer + * @description Update member identity. + * @pathParam {string} memberId - member ID | {string} identityId - member identity ID + * @response 200 - Ok + * @responseContent {MemberList} 200.application/json + * @responseExample {MemberList} 200.application/json.MemberIdentity + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberEdit) + + const memberIdentityService = new MemberIdentityService(req) + + const payload = await memberIdentityService.update(req.params.id, req.params.memberId, req.body) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/member/index.ts b/backend/src/api/member/index.ts index 6b173ffe3b..16d9606b44 100644 --- a/backend/src/api/member/index.ts +++ b/backend/src/api/member/index.ts @@ -1,35 +1,32 @@ import { safeWrap } from '../../middlewares/errorMiddleware' -import { featureFlagMiddleware } from '../../middlewares/featureFlagMiddleware' -import { FeatureFlag } from '../../types/common' export default (app) => { - app.post(`/tenant/:tenantId/member/query`, safeWrap(require('./memberQuery').default)) - - app.post( - `/tenant/:tenantId/member/export`, - featureFlagMiddleware(FeatureFlag.CSV_EXPORT, 'errors.csvExport.planLimitExceeded'), - safeWrap(require('./memberExport').default), - ) - - app.post(`/tenant/:tenantId/member`, safeWrap(require('./memberCreate').default)) - app.put(`/tenant/:tenantId/member/:id`, safeWrap(require('./memberUpdate').default)) - app.post(`/tenant/:tenantId/member/import`, safeWrap(require('./memberImport').default)) - app.delete(`/tenant/:tenantId/member`, safeWrap(require('./memberDestroy').default)) - app.get( - `/tenant/:tenantId/member/autocomplete`, - safeWrap(require('./memberAutocomplete').default), - ) - app.get( - `/tenant/:tenantId/member/orautocomplete`, - safeWrap(require('./memberAutocomplete').default), - ) - app.get(`/tenant/:tenantId/member`, safeWrap(require('./memberList').default)) - app.get(`/tenant/:tenantId/member/active`, safeWrap(require('./memberActiveList').default)) - app.get(`/tenant/:tenantId/member/:id`, safeWrap(require('./memberFind').default)) - app.put(`/tenant/:tenantId/member/:memberId/merge`, safeWrap(require('./memberMerge').default)) - app.put( - `/tenant/:tenantId/member/:memberId/no-merge`, - safeWrap(require('./memberNotMerge').default), - ) - app.patch(`/tenant/:tenantId/member`, safeWrap(require('./memberUpdateBulk').default)) + app.post(`/member/query`, safeWrap(require('./memberQuery').default)) + + app.post(`/member/export`, safeWrap(require('./memberExport').default)) + + app.post(`/member`, safeWrap(require('./memberCreate').default)) + app.put(`/member/:id`, safeWrap(require('./memberUpdate').default)) + app.delete(`/member`, safeWrap(require('./memberDestroy').default)) + app.post(`/member/autocomplete`, safeWrap(require('./memberAutocomplete').default)) + app.get(`/member/bot-suggestions`, safeWrap(require('./memberBotSuggestionsList').default)) + + app.get(`/member/:id`, safeWrap(require('./memberFind').default)) + app.get(`/member/github/:id`, safeWrap(require('./memberFindGithub').default)) + app.put(`/member/:memberId/merge`, safeWrap(require('./memberMerge').default)) + + app.get(`/member/:memberId/can-revert-merge`, safeWrap(require('./memberCanRevertMerge').default)) + + app.post(`/member/:memberId/unmerge/preview`, safeWrap(require('./memberUnmergePreview').default)) + app.post(`/member/:memberId/unmerge`, safeWrap(require('./memberUnmerge').default)) + + app.put(`/member/:memberId/no-merge`, safeWrap(require('./memberNotMerge').default)) + app.patch(`/member`, safeWrap(require('./memberUpdateBulk').default)) + + require('./identity').default(app) + require('./organization').default(app) + require('./attributes').default(app) + require('./affiliation').default(app) + + app.post(`/member/:id/data-issue`, safeWrap(require('./memberDataIssueCreate').default)) } diff --git a/backend/src/api/member/memberActiveList.ts b/backend/src/api/member/memberActiveList.ts deleted file mode 100644 index 931968f66b..0000000000 --- a/backend/src/api/member/memberActiveList.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { IActiveMemberFilter } from '../../database/repositories/types/memberTypes' -import Error400 from '../../errors/Error400' -import Permissions from '../../security/permissions' -import MemberService from '../../services/memberService' -import PermissionChecker from '../../services/user/permissionChecker' - -/** - * GET /tenant/{tenantId}/member/active - * @summary List active members - * @tag Members - * @security Bearer - * @description List active members. It accepts filters, sorting options and pagination. - * @pathParam {string} tenantId - Your workspace/tenant ID - * @queryParam {string} [filter[platforms]] - Filter by activity platforms (comma separated list without spaces) - * @queryParam {string} [filter[isTeamMember]] - If true we will return just team members, if false we will return just non-team members, if undefined we will return both. - * @queryParam {string} [filter[isBot]] - If true we will return just members who are bots, if false we will return just non-bot members, if undefined we will return both. - * @queryParam {string} [filter[isOrganization]] - If true we will return just members who are organizations (such as linkedin organizations that post), if false we will return just non-organization members, if undefined we will return both. - * @queryParam {string} [filter[activityTimestampFrom]] - Filter by activity timestamp from (required) - * @queryParam {string} [filter[activityTimestampTo]] - Filter by activity timestamp to (required) - * @queryParam {string} [filter[activityIsContribution]] - Filter by activities that are contributions - * @queryParam {string} [orderBy] - How to sort results. Available values: activityCount_DESC, activityCount_ASC, activeDaysCount_DESC, activeDaysCount_ASC (default activityCount_DESC) - * @queryParam {number} [offset] - Skip the first n results. Default 0. - * @queryParam {number} [limit] - Limit the number of results. Default 20. - * @response 200 - Ok - * @response 401 - Unauthorized - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.memberRead) - - let offset = 0 - if (req.query.offset) { - offset = parseInt(req.query.offset, 10) - } - let limit = 20 - if (req.query.limit) { - limit = parseInt(req.query.limit, 10) - } - - if (req.query.filter?.activityTimestampFrom === undefined) { - throw new Error400(req.language, 'errors.members.activeList.activityTimestampFrom') - } - - if (req.query.filter?.activityTimestampTo === undefined) { - throw new Error400(req.language, 'errors.members.activeList.activityTimestampTo') - } - - const filters: IActiveMemberFilter = { - platforms: - req.query.filter?.platforms !== undefined - ? req.query.filter?.platforms.split(',') - : undefined, - isTeamMember: - req.query.filter?.isTeamMember === undefined - ? undefined - : req.query.filter?.isTeamMember === 'true', - isBot: req.query.filter?.isBot === undefined ? undefined : req.query.filter?.isBot === 'true', - isOrganization: - req.query.filter?.isOrganization === undefined - ? undefined - : req.query.filter?.isOrganization === 'true', - activityTimestampFrom: req.query.filter?.activityTimestampFrom, - activityTimestampTo: req.query.filter?.activityTimestampTo, - activityIsContribution: req.query.filter?.activityIsContribution === 'true', - } - - const orderBy = req.query.orderBy || 'activityCount_DESC' - - const payload = await new MemberService(req).findAndCountActive( - filters, - offset, - limit, - orderBy, - req.query.segments, - ) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/member/memberAutocomplete.ts b/backend/src/api/member/memberAutocomplete.ts index 859ae2eafa..b334514007 100644 --- a/backend/src/api/member/memberAutocomplete.ts +++ b/backend/src/api/member/memberAutocomplete.ts @@ -5,7 +5,7 @@ import PermissionChecker from '../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.memberAutocomplete) - const payload = await new MemberService(req).findAllAutocomplete(req.query.query, req.query.limit) + const payload = await new MemberService(req).findAllAutocomplete(req.body) await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/member/memberBotSuggestionsList.ts b/backend/src/api/member/memberBotSuggestionsList.ts new file mode 100644 index 0000000000..50a582c9d2 --- /dev/null +++ b/backend/src/api/member/memberBotSuggestionsList.ts @@ -0,0 +1,25 @@ +import MemberService from '@/services/memberService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * GET /member/bot-suggestions + * @summary List member bot suggestions + * @tag Members + * @security Bearer + * @description List member bot suggestions with pagination + * @queryParam {number} [offset] - Skip the first n results. Default 0. + * @queryParam {number} [limit] - Limit the number of results. Default 20. + * @response 200 - Ok + * @responseContent {MemberList} 200.application/json + * @response 401 - Unauthorized + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberRead) + + const payload = await new MemberService(req).findMembersWithBotSuggestions(req.query) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/member/memberCanRevertMerge.ts b/backend/src/api/member/memberCanRevertMerge.ts new file mode 100644 index 0000000000..9b985c4290 --- /dev/null +++ b/backend/src/api/member/memberCanRevertMerge.ts @@ -0,0 +1,14 @@ +import Permissions from '../../security/permissions' +import MemberService from '../../services/memberService' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberEdit) + + const payload = await new MemberService(req).canRevertMerge( + req.params.memberId, + req.query.identityId as string, + ) + + await req.responseHandler.success(req, res, payload, 200) +} diff --git a/backend/src/api/member/memberCreate.ts b/backend/src/api/member/memberCreate.ts index 76d667c634..183b2969a1 100644 --- a/backend/src/api/member/memberCreate.ts +++ b/backend/src/api/member/memberCreate.ts @@ -4,12 +4,11 @@ import MemberService from '../../services/memberService' import PermissionChecker from '../../services/user/permissionChecker' /** - * POST /tenant/{tenantId}/member + * POST /member * @summary Create or update a member * @tag Members * @security Bearer * @description Create or update a member. Existence is checked by platform and username. - * @pathParam {string} tenantId - Your workspace/tenant ID * @bodyContent {MemberUpsertInput} application/json * @response 200 - Ok * @responseContent {Member} 200.application/json @@ -23,7 +22,7 @@ export default async (req, res) => { const payload = await new MemberService(req).upsert(req.body) - track('Member Manually Created', { ...payload }, { ...req }) + track('Member Manually Created', { ...req.body }, { ...req }) await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/member/memberDataIssueCreate.ts b/backend/src/api/member/memberDataIssueCreate.ts new file mode 100644 index 0000000000..426315dbd9 --- /dev/null +++ b/backend/src/api/member/memberDataIssueCreate.ts @@ -0,0 +1,17 @@ +import { DataIssueEntity } from '@crowd/types' + +import DataIssueService from '@/services/dataIssueService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.dataIssueCreate) + + const payload = await new DataIssueService(req).createDataIssue( + { ...req.body, entity: DataIssueEntity.PERSON }, + req.params.id, + ) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/member/memberDestroy.ts b/backend/src/api/member/memberDestroy.ts index 4f5477a41e..7158b265aa 100644 --- a/backend/src/api/member/memberDestroy.ts +++ b/backend/src/api/member/memberDestroy.ts @@ -3,12 +3,11 @@ import MemberService from '../../services/memberService' import PermissionChecker from '../../services/user/permissionChecker' /** - * DELETE /tenant/{tenantId}/member/{id} + * DELETE /member/{id} * @summary Delete a member * @tag Members * @security Bearer * @description Delete a member given an ID - * @pathParam {string} tenantId - Your workspace/tenant ID * @pathParam {string} id - The ID of the member * @response 200 - Ok * @response 401 - Unauthorized diff --git a/backend/src/api/member/memberExport.ts b/backend/src/api/member/memberExport.ts index 49b04518de..07f1feae9a 100644 --- a/backend/src/api/member/memberExport.ts +++ b/backend/src/api/member/memberExport.ts @@ -1,19 +1,17 @@ -import { RedisCache } from '@crowd/redis' -import { getSecondsTillEndOfMonth } from '../../utils/timing' +import { generateUUIDv4 } from '@crowd/common' +import { ITriggerCSVExport, TemporalWorkflowId } from '@crowd/types' + import Permissions from '../../security/permissions' import identifyTenant from '../../segment/identifyTenant' import track from '../../segment/track' -import MemberService from '../../services/memberService' import PermissionChecker from '../../services/user/permissionChecker' -import { FeatureFlagRedisKey } from '../../types/common' /** - * POST /tenant/{tenantId}/member/export + * POST /member/export * @summary Export members as CSV * @tag Members * @security Bearer * @description Export members. It accepts filters, sorting options and pagination. - * @pathParam {string} tenantId - Your workspace/tenant ID * @bodyContent {MemberQuery} application/json * @response 200 - Ok * @response 401 - Unauthorized @@ -23,27 +21,30 @@ import { FeatureFlagRedisKey } from '../../types/common' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.memberRead) - const payload = await new MemberService(req).export(req.body) - - const csvCountCache = new RedisCache(FeatureFlagRedisKey.CSV_EXPORT_COUNT, req.redis, req.log) - - const csvCount = await csvCountCache.get(req.currentTenant.id) - - const secondsRemainingUntilEndOfMonth = getSecondsTillEndOfMonth() - - if (!csvCount) { - await csvCountCache.set(req.currentTenant.id, '0', secondsRemainingUntilEndOfMonth) - } else { - await csvCountCache.set( - req.currentTenant.id, - (parseInt(csvCount, 10) + 1).toString(), - secondsRemainingUntilEndOfMonth, - ) - } + await req.temporal.workflow.start('exportMembersToCSV', { + taskQueue: 'exports', + workflowId: `${TemporalWorkflowId.MEMBERS_CSV_EXPORTS}/${ + req.currentTenant.id + }/${generateUUIDv4()}`, + retry: { + maximumAttempts: 1, + }, + args: [ + { + tenantId: req.currentTenant.id, + segmentIds: req.body.segments, + criteria: req.body, + sendTo: [req.currentUser.email], + } as ITriggerCSVExport, + ], + searchAttributes: { + TenantId: [req.currentTenant.id], + }, + }) identifyTenant(req) - track('Member CSV Export', {}, { ...req }, req.currentUser.id) + track('Member CSV Export', {}, { ...req.body }, req.currentUser.id) - await req.responseHandler.success(req, res, payload) + await req.responseHandler.success(req, res, {}) } diff --git a/backend/src/api/member/memberFind.ts b/backend/src/api/member/memberFind.ts index f480ca72d9..d4e38ef6d3 100644 --- a/backend/src/api/member/memberFind.ts +++ b/backend/src/api/member/memberFind.ts @@ -3,12 +3,11 @@ import MemberService from '../../services/memberService' import PermissionChecker from '../../services/user/permissionChecker' /** - * GET /tenant/{tenantId}/member/{id} + * GET /member/{id} * @summary Find a member * @tag Members * @security Bearer * @description Find a single member by ID. - * @pathParam {string} tenantId - Your workspace/tenant ID * @pathParam {string} id - The ID of the member * @response 200 - Ok * @responseContent {MemberResponse} 200.application/json @@ -20,7 +19,24 @@ import PermissionChecker from '../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.memberRead) - const payload = await new MemberService(req).findById(req.params.id) + const segmentId = req.query.segments?.length > 0 ? req.query.segments[0] : null + const includeAllAttributes = + req.query.includeAllAttributes === 'true' || req.query.includeAllAttributes === true + + if (!segmentId) { + await req.responseHandler.error(req, res, { + code: 400, + message: 'Segment ID is required', + }) + return + } + + const payload = await new MemberService(req).findById( + req.params.id, + segmentId, + req.query.include, + includeAllAttributes, + ) await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/member/memberFindGithub.ts b/backend/src/api/member/memberFindGithub.ts new file mode 100644 index 0000000000..a355d899df --- /dev/null +++ b/backend/src/api/member/memberFindGithub.ts @@ -0,0 +1,25 @@ +import Permissions from '../../security/permissions' +import MemberService from '../../services/memberService' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * GET /member/{id} + * @summary Find a member + * @tag Members + * @security Bearer + * @description Find a single member by ID. + * @pathParam {string} id - The ID of the member + * @response 200 - Ok + * @responseContent {MemberResponse} 200.application/json + * @responseExample {MemberFind} 200.application/json.Member + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberRead) + + const payload = await new MemberService(req).findGithub(req.params.id) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/member/memberImport.ts b/backend/src/api/member/memberImport.ts deleted file mode 100644 index 67ebfab0b2..0000000000 --- a/backend/src/api/member/memberImport.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Permissions from '../../security/permissions' -import MemberService from '../../services/memberService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.memberImport) - - await new MemberService(req).import(req.body, req.body.importHash) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/member/memberList.ts b/backend/src/api/member/memberList.ts index 793dcf37e6..153dc9a5b0 100644 --- a/backend/src/api/member/memberList.ts +++ b/backend/src/api/member/memberList.ts @@ -4,7 +4,7 @@ import PermissionChecker from '../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.memberRead) - const payload = await new MemberService(req).findAndCountAll(req.query) + const payload = await new MemberService(req).query(req.query) await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/member/memberMerge.ts b/backend/src/api/member/memberMerge.ts index fcc2dc4971..38b7b43562 100644 --- a/backend/src/api/member/memberMerge.ts +++ b/backend/src/api/member/memberMerge.ts @@ -1,16 +1,27 @@ +import { CommonMemberService, invalidateMemberQueryCache } from '@crowd/common_services' +import { optionsQx } from '@crowd/data-access-layer' + import Permissions from '../../security/permissions' import track from '../../segment/track' -import MemberService from '../../services/memberService' import PermissionChecker from '../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.memberEdit) - const payload = await new MemberService(req).merge(req.params.memberId, req.body.memberToMerge) + const { memberId } = req.params + const { memberToMerge } = req.body + + const service = new CommonMemberService(optionsQx(req), req.temporal, req.log) + + const payload = await service.merge(memberId, memberToMerge, req) - track('Merge members', { ...payload }, { ...req }) + try { + await invalidateMemberQueryCache(req.redis, [memberId, memberToMerge]) + } catch (error) { + req.log.warn({ error }, 'Cache invalidation failed after member merge') + } - const status = payload.status || 200 + track('Merge members', { memberId, memberToMergeId: memberToMerge }, req) - await req.responseHandler.success(req, res, payload, status) + return req.responseHandler.success(req, res, payload, payload.status ?? 200) } diff --git a/backend/src/api/member/memberNotMerge.ts b/backend/src/api/member/memberNotMerge.ts index 46f995dffb..9af450539a 100644 --- a/backend/src/api/member/memberNotMerge.ts +++ b/backend/src/api/member/memberNotMerge.ts @@ -10,7 +10,11 @@ export default async (req, res) => { req.body.memberToNotMerge, ) - track('Ignore merge members', { ...payload }, { ...req }) + track( + 'Ignore merge members', + { memberId: req.params.memberId, memberToNotMergeId: req.body.memberToNotMerge }, + { ...req }, + ) await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/member/memberOrganizationAutocomplete.ts b/backend/src/api/member/memberOrganizationAutocomplete.ts deleted file mode 100644 index 859ae2eafa..0000000000 --- a/backend/src/api/member/memberOrganizationAutocomplete.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Permissions from '../../security/permissions' -import MemberService from '../../services/memberService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.memberAutocomplete) - - const payload = await new MemberService(req).findAllAutocomplete(req.query.query, req.query.limit) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/member/memberQuery.ts b/backend/src/api/member/memberQuery.ts index 5c7f690cbf..f7bc8bf51b 100644 --- a/backend/src/api/member/memberQuery.ts +++ b/backend/src/api/member/memberQuery.ts @@ -4,12 +4,11 @@ import MemberService from '../../services/memberService' import PermissionChecker from '../../services/user/permissionChecker' /** - * POST /tenant/{tenantId}/member/query + * POST /member/query * @summary Query members * @tag Members * @security Bearer * @description Query members. It accepts filters, sorting options and pagination. - * @pathParam {string} tenantId - Your workspace/tenant ID * @bodyContent {MemberQuery} application/json * @response 200 - Ok * @responseContent {MemberList} 200.application/json @@ -21,16 +20,8 @@ import PermissionChecker from '../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.memberRead) - let payload - const newVersion = req.headers['x-crowd-api-version'] === '1' - const memberService = new MemberService(req) - - if (newVersion) { - payload = await memberService.queryV2(req.body) - } else { - payload = await memberService.query(req.body) - } + const payload = await memberService.query(req.body) if (req.body.filter && Object.keys(req.body.filter).length > 0) { track('Member Advanced Filter', { ...req.body }, { ...req }) diff --git a/backend/src/api/member/memberUnmerge.ts b/backend/src/api/member/memberUnmerge.ts new file mode 100644 index 0000000000..55106d7867 --- /dev/null +++ b/backend/src/api/member/memberUnmerge.ts @@ -0,0 +1,11 @@ +import Permissions from '../../security/permissions' +import MemberService from '../../services/memberService' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberEdit) + + const payload = await new MemberService(req).unmerge(req.params.memberId, req.body) + + return req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/member/memberUnmergePreview.ts b/backend/src/api/member/memberUnmergePreview.ts new file mode 100644 index 0000000000..e91d4d11b8 --- /dev/null +++ b/backend/src/api/member/memberUnmergePreview.ts @@ -0,0 +1,15 @@ +import Permissions from '../../security/permissions' +import MemberService from '../../services/memberService' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberEdit) + + const payload = await new MemberService(req).unmergePreview( + req.params.memberId, + req.body.identityId, + req.body.revertPreviousMerge, + ) + + await req.responseHandler.success(req, res, payload, 200) +} diff --git a/backend/src/api/member/memberUpdate.ts b/backend/src/api/member/memberUpdate.ts index 174985a92b..5e49c90d5a 100644 --- a/backend/src/api/member/memberUpdate.ts +++ b/backend/src/api/member/memberUpdate.ts @@ -1,16 +1,13 @@ -import lodash from 'lodash' import Permissions from '../../security/permissions' -import track from '../../segment/track' import MemberService from '../../services/memberService' import PermissionChecker from '../../services/user/permissionChecker' /** - * PUT /tenant/{tenantId}/member/{id} + * PUT /member/{id} * @summary Update a member * @tag Members * @security Bearer * @description Update a member - * @pathParam {string} tenantId - Your workspace/tenant ID * @pathParam {string} id - The ID of the member * @bodyContent {MemberUpdateInput} application/json * @response 200 - Ok @@ -23,16 +20,13 @@ import PermissionChecker from '../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.memberEdit) - const member = await new MemberService(req).findById(req.params.id) - const payload = await new MemberService(req).update(req.params.id, req.body) + const { invalidateCache, ...data } = req.body - const differentTagIds = lodash.difference( - payload.tags.map((t) => t.id), - member.tags.map((t) => t.id), - ) - if (differentTagIds.length > 0) { - track('Member Tagged', { id: payload.id, tagIds: [...differentTagIds] }, { ...req }) - } + const payload = await new MemberService(req).update(req.params.id, data, { + syncToOpensearch: true, + manualChange: true, + invalidateCache: invalidateCache ?? false, + }) await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/member/organization/index.ts b/backend/src/api/member/organization/index.ts new file mode 100644 index 0000000000..1a8723e0c5 --- /dev/null +++ b/backend/src/api/member/organization/index.ts @@ -0,0 +1,24 @@ +import { safeWrap } from '@/middlewares/errorMiddleware' + +export default (app) => { + // Member Organiaztion List + app.get(`/member/:memberId/organization`, safeWrap(require('./memberOrganizationList').default)) + + // Member Organiaztion Create + app.post( + `/member/:memberId/organization`, + safeWrap(require('./memberOrganizationCreate').default), + ) + + // Member Organiaztion Update + app.patch( + `/member/:memberId/organization/:id`, + safeWrap(require('./memberOrganizationUpdate').default), + ) + + // Member Organiaztion Delete + app.delete( + `/member/:memberId/organization/:id`, + safeWrap(require('./memberOrganizationDelete').default), + ) +} diff --git a/backend/src/api/member/organization/memberOrganizationCreate.ts b/backend/src/api/member/organization/memberOrganizationCreate.ts new file mode 100644 index 0000000000..462e222733 --- /dev/null +++ b/backend/src/api/member/organization/memberOrganizationCreate.ts @@ -0,0 +1,26 @@ +import MemberOrganizationsService from '@/services/member/memberOrganizationsService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * POST /member/:memberId/organization + * @summary Create member organization + * @tag Members + * @security Bearer + * @description Create one member organization. + * @response 200 - Ok + * @responseContent {MemberList} 200.application/json + * @responseExample {MemberList} 200.application/json.Organization + * @response 401 - Unauthorized + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberEdit) + + const memberOrganizationsService = new MemberOrganizationsService(req) + + const payload = await memberOrganizationsService.create(req.params.memberId, req.body) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/member/organization/memberOrganizationDelete.ts b/backend/src/api/member/organization/memberOrganizationDelete.ts new file mode 100644 index 0000000000..84a7f0e338 --- /dev/null +++ b/backend/src/api/member/organization/memberOrganizationDelete.ts @@ -0,0 +1,28 @@ +import MemberOrganizationsService from '@/services/member/memberOrganizationsService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * DELETE /member/:memberId/organization/:memberOrganizationId + * @summary Remove member organization + * @tag Members + * @security Bearer + * @description Remove member organization. + * @pathParam {string} memberId - member ID | {string} memberOrganizationId - member organization ID + * @response 200 - Ok + * @responseContent {MemberList} 200.application/json + * @responseExample {MemberList} 200.application/json.Organization + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberEdit) + + const memberOrganizationsService = new MemberOrganizationsService(req) + + const payload = await memberOrganizationsService.delete(req.params.id, req.params.memberId) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/member/organization/memberOrganizationList.ts b/backend/src/api/member/organization/memberOrganizationList.ts new file mode 100644 index 0000000000..451a29ed1c --- /dev/null +++ b/backend/src/api/member/organization/memberOrganizationList.ts @@ -0,0 +1,26 @@ +import MemberOrganizationsService from '@/services/member/memberOrganizationsService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * GET /member/:memberId/organization + * @summary Query member organizations + * @tag Members + * @security Bearer + * @description Query member organization. + * @response 200 - Ok + * @responseContent {MemberList} 200.application/json + * @responseExample {MemberList} 200.application/json.Organization + * @response 401 - Unauthorized + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberRead) + + const memberOrganizationsService = new MemberOrganizationsService(req) + + const payload = await memberOrganizationsService.list(req.params.memberId) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/member/organization/memberOrganizationUpdate.ts b/backend/src/api/member/organization/memberOrganizationUpdate.ts new file mode 100644 index 0000000000..117a5a3f10 --- /dev/null +++ b/backend/src/api/member/organization/memberOrganizationUpdate.ts @@ -0,0 +1,32 @@ +import MemberOrganizationsService from '@/services/member/memberOrganizationsService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * PATCH /member/:memberId/organization/:memberOrganizationId + * @summary Update member organization + * @tag Members + * @security Bearer + * @description Update member organization. + * @pathParam {string} memberId - member ID | {string} memberOrganizationId - member organization ID + * @response 200 - Ok + * @responseContent {MemberList} 200.application/json + * @responseExample {MemberList} 200.application/json.Organization + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberEdit) + + const memberOrganizationsService = new MemberOrganizationsService(req) + + const payload = await memberOrganizationsService.update( + req.params.id, + req.params.memberId, + req.body, + ) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/merge-suggestions/index.ts b/backend/src/api/merge-suggestions/index.ts new file mode 100644 index 0000000000..d2abce4d40 --- /dev/null +++ b/backend/src/api/merge-suggestions/index.ts @@ -0,0 +1,6 @@ +import { safeWrap } from '../../middlewares/errorMiddleware' + +export default (app) => { + app.post(`/membersToMerge`, safeWrap(require('./membersToMergeList').default)) + app.post(`/organizationsToMerge`, safeWrap(require('./organizationsToMergeList').default)) +} diff --git a/backend/src/api/merge-suggestions/membersToMergeList.ts b/backend/src/api/merge-suggestions/membersToMergeList.ts new file mode 100644 index 0000000000..37bff94915 --- /dev/null +++ b/backend/src/api/merge-suggestions/membersToMergeList.ts @@ -0,0 +1,11 @@ +import Permissions from '../../security/permissions' +import MemberService from '../../services/memberService' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberRead) + + const payload = await new MemberService(req).findMembersWithMergeSuggestions(req.body) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/merge-suggestions/organizationsToMergeList.ts b/backend/src/api/merge-suggestions/organizationsToMergeList.ts new file mode 100644 index 0000000000..6d79943185 --- /dev/null +++ b/backend/src/api/merge-suggestions/organizationsToMergeList.ts @@ -0,0 +1,11 @@ +import Permissions from '../../security/permissions' +import OrganizationService from '../../services/organizationService' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.organizationRead) + + const payload = await new OrganizationService(req).findOrganizationsWithMergeSuggestions(req.body) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/mergeAction/index.ts b/backend/src/api/mergeAction/index.ts new file mode 100644 index 0000000000..f322a36151 --- /dev/null +++ b/backend/src/api/mergeAction/index.ts @@ -0,0 +1,5 @@ +import { safeWrap } from '../../middlewares/errorMiddleware' + +export default (app) => { + app.get(`/mergeActions`, safeWrap(require('./mergeActionQuery').default)) +} diff --git a/backend/src/api/mergeAction/mergeActionQuery.ts b/backend/src/api/mergeAction/mergeActionQuery.ts new file mode 100644 index 0000000000..6a839f865d --- /dev/null +++ b/backend/src/api/mergeAction/mergeActionQuery.ts @@ -0,0 +1,29 @@ +import MergeActionsService from '@/services/MergeActionsService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * GET /mergeAction + * @summary Query mergeActions + * @tag MergeActions + * @security Bearer + * @description Query mergeActions. It accepts filters and pagination. + * @queryParam {string} entityId - ID of the entity + * @queryParam {string} type - type of the entity (e.g., org or member) + * @queryParam {number} [limit] - number of records to return (optional, default to 20) + * @queryParam {number} [offset] - number of records to skip (optional, default to 0) + * @response 200 - Ok + * @responseContent {MergeActionList} 200.application/json + * @responseExample {MergeActionList} 200.application/json.MergeAction + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.mergeActionRead) + + const payload = await new MergeActionsService(req).query(req.query) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/microservice/index.ts b/backend/src/api/microservice/index.ts deleted file mode 100644 index 8adc67ec6b..0000000000 --- a/backend/src/api/microservice/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { safeWrap } from '../../middlewares/errorMiddleware' - -export default (app) => { - app.post(`/tenant/:tenantId/microservice`, safeWrap(require('./microserviceCreate').default)) - app.post(`/tenant/:tenantId/microservice/query`, safeWrap(require('./microserviceQuery').default)) - app.put(`/tenant/:tenantId/microservice/:id`, safeWrap(require('./microserviceUpdate').default)) - app.post( - `/tenant/:tenantId/microservice/import`, - safeWrap(require('./microserviceImport').default), - ) - app.delete(`/tenant/:tenantId/microservice`, safeWrap(require('./microserviceDestroy').default)) - app.get( - `/tenant/:tenantId/microservice/autocomplete`, - safeWrap(require('./microserviceAutocomplete').default), - ) - app.get(`/tenant/:tenantId/microservice`, safeWrap(require('./microserviceList').default)) - app.get(`/tenant/:tenantId/microservice/:id`, safeWrap(require('./microserviceFind').default)) -} diff --git a/backend/src/api/microservice/microserviceAutocomplete.ts b/backend/src/api/microservice/microserviceAutocomplete.ts deleted file mode 100644 index 34ad935db5..0000000000 --- a/backend/src/api/microservice/microserviceAutocomplete.ts +++ /dev/null @@ -1,14 +0,0 @@ -import Permissions from '../../security/permissions' -import MicroserviceService from '../../services/microserviceService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.microserviceAutocomplete) - - const payload = await new MicroserviceService(req).findAllAutocomplete( - req.query.query, - req.query.limit, - ) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/microservice/microserviceCreate.ts b/backend/src/api/microservice/microserviceCreate.ts deleted file mode 100644 index 35b28c69d4..0000000000 --- a/backend/src/api/microservice/microserviceCreate.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Permissions from '../../security/permissions' -import MicroserviceService from '../../services/microserviceService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - const permissionsChecker = new PermissionChecker(req) - permissionsChecker.validateHas(Permissions.values.microserviceCreate) - permissionsChecker.validateMicroservicesProtectedFields(req.body) - - const payload = await new MicroserviceService(req).create(req.body) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/microservice/microserviceDestroy.ts b/backend/src/api/microservice/microserviceDestroy.ts deleted file mode 100644 index aff8ab3f74..0000000000 --- a/backend/src/api/microservice/microserviceDestroy.ts +++ /dev/null @@ -1,13 +0,0 @@ -import PermissionChecker from '../../services/user/permissionChecker' -import Permissions from '../../security/permissions' -import MicroserviceService from '../../services/microserviceService' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.microserviceDestroy) - - await new MicroserviceService(req).destroyAll(req.query.ids) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/microservice/microserviceFind.ts b/backend/src/api/microservice/microserviceFind.ts deleted file mode 100644 index c5b3b98ccd..0000000000 --- a/backend/src/api/microservice/microserviceFind.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Permissions from '../../security/permissions' -import MicroserviceService from '../../services/microserviceService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.microserviceRead) - - const payload = await new MicroserviceService(req).findById(req.params.id) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/microservice/microserviceImport.ts b/backend/src/api/microservice/microserviceImport.ts deleted file mode 100644 index 1b7276d731..0000000000 --- a/backend/src/api/microservice/microserviceImport.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Permissions from '../../security/permissions' -import MicroserviceService from '../../services/microserviceService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.microserviceImport) - - await new MicroserviceService(req).import(req.body, req.body.importHash) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/microservice/microserviceList.ts b/backend/src/api/microservice/microserviceList.ts deleted file mode 100644 index 9bcf0600bb..0000000000 --- a/backend/src/api/microservice/microserviceList.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Permissions from '../../security/permissions' -import MicroserviceService from '../../services/microserviceService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.microserviceRead) - - const payload = await new MicroserviceService(req).findAndCountAll(req.query) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/microservice/microserviceQuery.ts b/backend/src/api/microservice/microserviceQuery.ts deleted file mode 100644 index 84f41cd3d6..0000000000 --- a/backend/src/api/microservice/microserviceQuery.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Permissions from '../../security/permissions' -import track from '../../segment/track' -import MicroserviceService from '../../services/microserviceService' -import PermissionChecker from '../../services/user/permissionChecker' - -// /** -// * POST /tenant/{tenantId}/microservice -// * @summary Create or update an microservice -// * @tag Activities -// * @security Bearer -// * @description Create or update an microservice. Existence is checked by sourceId and tenantId. -// * @pathParam {string} tenantId - Your workspace/tenant ID -// * @bodyContent {MicroserviceUpsertInput} application/json -// * @response 200 - Ok -// * @responseContent {Microservice} 200.application/json -// * @responseExample {MicroserviceUpsert} 200.application/json.Microservice -// * @response 401 - Unauthorized -// * @response 404 - Not found -// * @response 429 - Too many requests -// */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.microserviceRead) - - const payload = await new MicroserviceService(req).query(req.body) - - if (req.query.filter && Object.keys(req.query.filter).length > 0) { - track('Microservices Advanced Filter', { ...payload }, { ...req }) - } - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/microservice/microserviceUpdate.ts b/backend/src/api/microservice/microserviceUpdate.ts deleted file mode 100644 index 9b9aa030d3..0000000000 --- a/backend/src/api/microservice/microserviceUpdate.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Permissions from '../../security/permissions' -import MicroserviceService from '../../services/microserviceService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - const permissionsChecker = new PermissionChecker(req) - permissionsChecker.validateHas(Permissions.values.microserviceEdit) - permissionsChecker.validateMicroservicesProtectedFields(req.body) - - const payload = await new MicroserviceService(req).update(req.params.id, req.body) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/nango/index.ts b/backend/src/api/nango/index.ts new file mode 100644 index 0000000000..f5e136fd11 --- /dev/null +++ b/backend/src/api/nango/index.ts @@ -0,0 +1,16 @@ +import { NANGO_CLOUD_CONFIG, getNangoCloudSessionToken, initNangoCloudClient } from '@crowd/nango' + +import { safeWrap } from '@/middlewares/errorMiddleware' + +export default async (app) => { + if (NANGO_CLOUD_CONFIG()) { + await initNangoCloudClient() + app.get( + '/nango/session', + safeWrap(async (req, res) => { + const data = await getNangoCloudSessionToken() + await req.responseHandler.success(req, res, data) + }), + ) + } +} diff --git a/backend/src/api/note/index.ts b/backend/src/api/note/index.ts deleted file mode 100644 index f744207e34..0000000000 --- a/backend/src/api/note/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { safeWrap } from '../../middlewares/errorMiddleware' - -export default (app) => { - app.post(`/tenant/:tenantId/note/query`, safeWrap(require('./noteQuery').default)) - app.post(`/tenant/:tenantId/note`, safeWrap(require('./noteCreate').default)) - app.put(`/tenant/:tenantId/note/:id`, safeWrap(require('./noteUpdate').default)) - app.post(`/tenant/:tenantId/note/import`, safeWrap(require('./noteImport').default)) - app.delete(`/tenant/:tenantId/note`, safeWrap(require('./noteDestroy').default)) - app.get(`/tenant/:tenantId/note/autocomplete`, safeWrap(require('./noteAutocomplete').default)) - app.get(`/tenant/:tenantId/note`, safeWrap(require('./noteList').default)) - app.get(`/tenant/:tenantId/note/:id`, safeWrap(require('./noteFind').default)) -} diff --git a/backend/src/api/note/noteAutocomplete.ts b/backend/src/api/note/noteAutocomplete.ts deleted file mode 100644 index 17ff10de69..0000000000 --- a/backend/src/api/note/noteAutocomplete.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Permissions from '../../security/permissions' -import TaskService from '../../services/taskService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.taskAutocomplete) - - const payload = await new TaskService(req).findAllAutocomplete(req.query.query, req.query.limit) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/note/noteCreate.ts b/backend/src/api/note/noteCreate.ts deleted file mode 100644 index e8b6f214f6..0000000000 --- a/backend/src/api/note/noteCreate.ts +++ /dev/null @@ -1,29 +0,0 @@ -import Permissions from '../../security/permissions' -import NoteService from '../../services/noteService' -import PermissionChecker from '../../services/user/permissionChecker' -import track from '../../segment/track' - -/** - * POST /tenant/{tenantId}/note - * @summary Create a note - * @tag Notes - * @security Bearer - * @description Create a note - * @pathParam {string} tenantId - Your workspace/tenant ID - * @bodyContent {NoteNoId} application/json - * @response 200 - Ok - * @responseContent {Note} 200.application/json - * @responseExample {Note} 200.application/json.Note - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.noteCreate) - - const payload = await new NoteService(req).create(req.body) - - track('Note Created', { id: payload.id }, { ...req }) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/note/noteDestroy.ts b/backend/src/api/note/noteDestroy.ts deleted file mode 100644 index 1dbc30aee3..0000000000 --- a/backend/src/api/note/noteDestroy.ts +++ /dev/null @@ -1,29 +0,0 @@ -import Permissions from '../../security/permissions' -import NoteService from '../../services/noteService' -import PermissionChecker from '../../services/user/permissionChecker' -import track from '../../segment/track' - -/** - * DELETE /tenant/{tenantId}/note/{id} - * @summary Delete a note - * @tag Notes - * @security Bearer - * @description Delete a note. - * @pathParam {string} tenantId - Your workspace/tenant ID - * @pathParam {string} id - The ID of the note - * @response 200 - Ok - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.noteDestroy) - - await new NoteService(req).destroyAll(req.query.ids) - - const payload = true - - track('Note Destroyed', {}, { ...req }) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/note/noteFind.ts b/backend/src/api/note/noteFind.ts deleted file mode 100644 index f264a53cc7..0000000000 --- a/backend/src/api/note/noteFind.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Permissions from '../../security/permissions' -import NoteService from '../../services/noteService' -import PermissionChecker from '../../services/user/permissionChecker' - -/** - * GET /tenant/{tenantId}/note/{id} - * @summary Find a note - * @tag Notes - * @security Bearer - * @description Find a note by ID. - * @pathParam {string} tenantId - Your workspace/tenant ID. - * @pathParam {string} id - The ID of the note. - * @response 200 - Ok - * @responseContent {NoteResponse} 200.application/json - * @responseExample {Note} 200.application/json.Note - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.noteRead) - - const payload = await new NoteService(req).findById(req.params.id) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/note/noteImport.ts b/backend/src/api/note/noteImport.ts deleted file mode 100644 index 53ce5d5ef6..0000000000 --- a/backend/src/api/note/noteImport.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Permissions from '../../security/permissions' -import NoteService from '../../services/noteService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.noteImport) - - await new NoteService(req).import(req.body, req.body.importHash) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/note/noteList.ts b/backend/src/api/note/noteList.ts deleted file mode 100644 index 28e6ab40db..0000000000 --- a/backend/src/api/note/noteList.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Permissions from '../../security/permissions' -import NoteService from '../../services/noteService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.noteRead) - - const payload = await new NoteService(req).findAndCountAll(req.query) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/note/noteQuery.ts b/backend/src/api/note/noteQuery.ts deleted file mode 100644 index c55a50ca66..0000000000 --- a/backend/src/api/note/noteQuery.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Permissions from '../../security/permissions' -import track from '../../segment/track' -import NoteService from '../../services/noteService' -import PermissionChecker from '../../services/user/permissionChecker' - -/** - * POST /tenant/{tenantId}/note/query - * @summary Query notes - * @tag Notes - * @security Bearer - * @description Query notes. It accepts filters, sorting options and pagination. - * @pathParam {string} tenantId - Your workspace/tenant ID - * @bodyContent {NoteQuery} application/json - * @response 200 - Ok - * @responseContent {NoteList} 200.application/json - * @responseExample {NoteList} 200.application/json.Note - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.noteRead) - - const payload = await new NoteService(req).query(req.body) - - if (req.query.filter && Object.keys(req.query.filter).length > 0) { - track('Notes Advanced Filter', { ...payload }, { ...req }) - } - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/note/noteUpdate.ts b/backend/src/api/note/noteUpdate.ts deleted file mode 100644 index 1ffe33a9e9..0000000000 --- a/backend/src/api/note/noteUpdate.ts +++ /dev/null @@ -1,30 +0,0 @@ -import Permissions from '../../security/permissions' -import NoteService from '../../services/noteService' -import PermissionChecker from '../../services/user/permissionChecker' -import track from '../../segment/track' - -/** - * PUT /tenant/{tenantId}/note/{id} - * @summary Update a note - * @tag Notes - * @security Bearer - * @description Update a note - * @pathParam {string} tenantId - Your workspace/tenant ID - * @pathParam {string} id - The ID of the note - * @bodyContent {NoteInput} application/json - * @response 200 - Ok - * @responseContent {Note} 200.application/json - * @responseExample {Note} 200.application/json.Note - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.noteEdit) - - const payload = await new NoteService(req).update(req.params.id, req.body) - - track('Note Updated', { id: payload.id }, { ...req }) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/organization/index.ts b/backend/src/api/organization/index.ts index 9d69c713a9..d923e4b749 100644 --- a/backend/src/api/organization/index.ts +++ b/backend/src/api/organization/index.ts @@ -1,28 +1,44 @@ import { safeWrap } from '../../middlewares/errorMiddleware' export default (app) => { - app.post(`/tenant/:tenantId/organization`, safeWrap(require('./organizationCreate').default)) - app.post(`/tenant/:tenantId/organization/query`, safeWrap(require('./organizationQuery').default)) - app.put(`/tenant/:tenantId/organization/:id`, safeWrap(require('./organizationUpdate').default)) - app.post( - `/tenant/:tenantId/organization/import`, - safeWrap(require('./organizationImport').default), + app.post(`/organization`, safeWrap(require('./organizationCreate').default)) + app.post(`/organization/query`, safeWrap(require('./organizationQuery').default)) + app.put(`/organization/:id`, safeWrap(require('./organizationUpdate').default)) + app.delete(`/organization`, safeWrap(require('./organizationDestroy').default)) + app.post(`/organization/autocomplete`, safeWrap(require('./organizationAutocomplete').default)) + app.get(`/organization/:id`, safeWrap(require('./organizationFind').default)) + + app.put(`/organization/:organizationId/merge`, safeWrap(require('./organizationMerge').default)) + + app.put( + `/organization/:organizationId/no-merge`, + safeWrap(require('./organizationNotMerge').default), ) - app.delete(`/tenant/:tenantId/organization`, safeWrap(require('./organizationDestroy').default)) + app.get( - `/tenant/:tenantId/organization/autocomplete`, - safeWrap(require('./organizationAutocomplete').default), + `/organization/:organizationId/can-revert-merge`, + safeWrap(require('./organizationCanRevertMerge').default), ) - app.get(`/tenant/:tenantId/organization`, safeWrap(require('./organizationList').default)) - app.get(`/tenant/:tenantId/organization/:id`, safeWrap(require('./organizationFind').default)) - app.put( - `/tenant/:tenantId/organization/:organizationId/merge`, - safeWrap(require('./organizationMerge').default), + app.post( + `/organization/:organizationId/unmerge/preview`, + safeWrap(require('./organizationUnmergePreview').default), ) - app.put( - `/tenant/:tenantId/organization/:organizationId/no-merge`, - safeWrap(require('./organizationNotMerge').default), + app.post( + `/organization/:organizationId/unmerge`, + safeWrap(require('./organizationUnmerge').default), + ) + + app.post(`/organization/export`, safeWrap(require('./organizationExport').default)) + + app.post(`/organization/id`, safeWrap(require('./organizationByIds').default)) + + // list organizations across all segments + app.post(`/organization/list`, safeWrap(require('./organizationList').default)) + + app.post( + `/organization/:id/data-issue`, + safeWrap(require('./organizationDataIssueCreate').default), ) } diff --git a/backend/src/api/organization/organizationAutocomplete.ts b/backend/src/api/organization/organizationAutocomplete.ts index c21cdb612b..5eaa3bc0c1 100644 --- a/backend/src/api/organization/organizationAutocomplete.ts +++ b/backend/src/api/organization/organizationAutocomplete.ts @@ -5,10 +5,7 @@ import PermissionChecker from '../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.organizationAutocomplete) - const payload = await new OrganizationService(req).findAllAutocomplete( - req.query.query, - req.query.limit, - ) + const payload = await new OrganizationService(req).findAllAutocomplete(req.body) await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/organization/organizationByIds.ts b/backend/src/api/organization/organizationByIds.ts new file mode 100644 index 0000000000..6d53e0eb6c --- /dev/null +++ b/backend/src/api/organization/organizationByIds.ts @@ -0,0 +1,11 @@ +import Permissions from '../../security/permissions' +import OrganizationService from '../../services/organizationService' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.organizationRead) + + const payload = await new OrganizationService(req).findByIds(req.body.ids) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/organization/organizationCanRevertMerge.ts b/backend/src/api/organization/organizationCanRevertMerge.ts new file mode 100644 index 0000000000..420ecfea5b --- /dev/null +++ b/backend/src/api/organization/organizationCanRevertMerge.ts @@ -0,0 +1,19 @@ +import Permissions from '../../security/permissions' +import OrganizationService from '../../services/organizationService' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.organizationEdit) + + const identity = { + ...req.query.identity, + verified: req.query?.identity?.verified === 'true', + } + + const payload = await new OrganizationService(req).canRevertMerge( + req.params.organizationId, + identity, + ) + + await req.responseHandler.success(req, res, payload, 200) +} diff --git a/backend/src/api/organization/organizationCreate.ts b/backend/src/api/organization/organizationCreate.ts index 197ecf42a8..bd20632631 100644 --- a/backend/src/api/organization/organizationCreate.ts +++ b/backend/src/api/organization/organizationCreate.ts @@ -4,12 +4,11 @@ import OrganizationService from '../../services/organizationService' import PermissionChecker from '../../services/user/permissionChecker' /** - * POST /tenant/{tenantId}/organization + * POST /organization * @summary Create a organization * @tag Organizations * @security Bearer * @description Create a organization - * @pathParam {string} tenantId - Your workspace/tenant ID * @bodyContent {OrganizationInput} application/json * @response 200 - Ok * @responseContent {Organization} 200.application/json @@ -21,10 +20,9 @@ import PermissionChecker from '../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.organizationCreate) - const enrichP = req.body?.shouldEnrich || false - const payload = await new OrganizationService(req).createOrUpdate(req.body, enrichP) + const payload = await new OrganizationService(req).createOrUpdate(req.body) - track('Organization Manually Created', { ...payload }, { ...req }) + track('Organization Manually Created', { ...req.body }, { ...req }) await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/organization/organizationDataIssueCreate.ts b/backend/src/api/organization/organizationDataIssueCreate.ts new file mode 100644 index 0000000000..19d09c1a13 --- /dev/null +++ b/backend/src/api/organization/organizationDataIssueCreate.ts @@ -0,0 +1,17 @@ +import { DataIssueEntity } from '@crowd/types' + +import DataIssueService from '@/services/dataIssueService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.dataIssueCreate) + + const payload = await new DataIssueService(req).createDataIssue( + { ...req.body, entity: DataIssueEntity.ORGANIZATION }, + req.params.id, + ) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/organization/organizationDestroy.ts b/backend/src/api/organization/organizationDestroy.ts index d9eaad1665..4227a06f25 100644 --- a/backend/src/api/organization/organizationDestroy.ts +++ b/backend/src/api/organization/organizationDestroy.ts @@ -3,12 +3,11 @@ import OrganizationService from '../../services/organizationService' import PermissionChecker from '../../services/user/permissionChecker' /** - * DELETE /tenant/{tenantId}/organization/{id} + * DELETE /organization/{id} * @summary Delete a organization * @tag Organizations * @security Bearer * @description Delete a organization. - * @pathParam {string} tenantId - Your workspace/tenant ID * @pathParam {string} id - The ID of the organization * @response 200 - Ok * @response 401 - Unauthorized diff --git a/backend/src/api/organization/organizationExport.ts b/backend/src/api/organization/organizationExport.ts new file mode 100644 index 0000000000..978299f933 --- /dev/null +++ b/backend/src/api/organization/organizationExport.ts @@ -0,0 +1,50 @@ +import { generateUUIDv4 } from '@crowd/common' +import { ITriggerCSVExport, TemporalWorkflowId } from '@crowd/types' + +import Permissions from '../../security/permissions' +import identifyTenant from '../../segment/identifyTenant' +import track from '../../segment/track' +import PermissionChecker from '../../services/user/permissionChecker' + +/** + * POST /organization/export + * @summary Export organizations as CSV + * @tag Organizations + * @security Bearer + * @description Export organizations. It accepts filters, sorting options and pagination. + * @bodyContent {OrganizationQuery} application/json + * @response 200 - Ok + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.organizationRead) + + await req.temporal.workflow.start('exportOrganizationsToCSV', { + taskQueue: 'exports', + workflowId: `${TemporalWorkflowId.ORGANIZATIONS_CSV_EXPORTS}/${ + req.currentTenant.id + }/${generateUUIDv4()}`, + retry: { + maximumAttempts: 1, + }, + args: [ + { + tenantId: req.currentTenant.id, + segmentIds: req.body.segments, + criteria: req.body, + sendTo: [req.currentUser.email], + } as ITriggerCSVExport, + ], + searchAttributes: { + TenantId: [req.currentTenant.id], + }, + }) + + identifyTenant(req) + + track('Organization CSV Export', {}, { ...req.body }, req.currentUser.id) + + await req.responseHandler.success(req, res, {}) +} diff --git a/backend/src/api/organization/organizationFind.ts b/backend/src/api/organization/organizationFind.ts index dfc109b4b4..d4a8dc0bd0 100644 --- a/backend/src/api/organization/organizationFind.ts +++ b/backend/src/api/organization/organizationFind.ts @@ -1,16 +1,13 @@ -import isFeatureEnabled from '@/feature-flags/isFeatureEnabled' import Permissions from '../../security/permissions' import OrganizationService from '../../services/organizationService' import PermissionChecker from '../../services/user/permissionChecker' -import { FeatureFlag } from '@/types/common' /** - * GET /tenant/{tenantId}/organization/{id} + * GET /organization/{id} * @summary Find an organization * @tag Organizations * @security Bearer * @description Find an organization by ID. - * @pathParam {string} tenantId - Your workspace/tenant ID * @pathParam {string} id - The ID of the organization * @response 200 - Ok * @responseContent {OrganizationResponse} 200.application/json @@ -23,17 +20,6 @@ export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.organizationRead) const segmentId = req.query.segmentId - if (!segmentId) { - const segmentsEnabled = await isFeatureEnabled(FeatureFlag.SEGMENTS, req) - if (segmentsEnabled) { - await req.responseHandler.error(req, res, { - code: 400, - message: 'Segment ID is required', - }) - return - } - } - const payload = await new OrganizationService(req).findById(req.params.id, segmentId) await req.responseHandler.success(req, res, payload) diff --git a/backend/src/api/organization/organizationImport.ts b/backend/src/api/organization/organizationImport.ts deleted file mode 100644 index 766aa89581..0000000000 --- a/backend/src/api/organization/organizationImport.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Permissions from '../../security/permissions' -import OrganizationService from '../../services/organizationService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.organizationImport) - - await new OrganizationService(req).import(req.body.data, req.body.importHash) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/organization/organizationList.ts b/backend/src/api/organization/organizationList.ts index 163784bb66..839083c29d 100644 --- a/backend/src/api/organization/organizationList.ts +++ b/backend/src/api/organization/organizationList.ts @@ -1,11 +1,27 @@ +import OrganizationService from '@/services/organizationService' + import Permissions from '../../security/permissions' -import OrganizationService from '../../services/organizationService' import PermissionChecker from '../../services/user/permissionChecker' +/** + * POST /organization/list + * @summary List organizations across all segments + * @tag Organizations + * @security Bearer + * @description List organizations across all segments. It accepts filters, sorting options and pagination. + * @bodyContent {OrganizationQuery} application/json + * @response 200 - Ok + * @responseContent {OrganizationList} 200.application/json + * @responseExample {OrganizationList} 200.application/json.Organization + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.organizationRead) - const payload = await new OrganizationService(req).findAndCountAll(req.query) + const orgService = new OrganizationService(req) + const payload = await orgService.listOrganizationsAcrossAllSegments(req.body) await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/organization/organizationMerge.ts b/backend/src/api/organization/organizationMerge.ts index 040c9deac7..620a4af874 100644 --- a/backend/src/api/organization/organizationMerge.ts +++ b/backend/src/api/organization/organizationMerge.ts @@ -1,4 +1,5 @@ import OrganizationService from '@/services/organizationService' + import Permissions from '../../security/permissions' import track from '../../segment/track' import PermissionChecker from '../../services/user/permissionChecker' @@ -6,14 +7,19 @@ import PermissionChecker from '../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.organizationEdit) - const payload = await new OrganizationService(req).merge( - req.params.organizationId, - req.body.organizationToMerge, - ) + const primaryOrgId = req.params.organizationId + const secondaryOrgId = req.body.organizationToMerge + const segmentId = req.body.segments[0] + + const requestPayload = { + primary: primaryOrgId, + secondary: secondaryOrgId, + segmentId, + } - track('Merge organizations', { ...payload }, { ...req }) + await new OrganizationService(req).mergeSync(primaryOrgId, secondaryOrgId, segmentId) - const status = payload.status || 200 + track('Merge organizations', requestPayload, { ...req }) - await req.responseHandler.success(req, res, payload, status) + await req.responseHandler.success(req, res, requestPayload) } diff --git a/backend/src/api/organization/organizationNotMerge.ts b/backend/src/api/organization/organizationNotMerge.ts index facad8025e..fdba37295e 100644 --- a/backend/src/api/organization/organizationNotMerge.ts +++ b/backend/src/api/organization/organizationNotMerge.ts @@ -1,4 +1,5 @@ import OrganizationService from '@/services/organizationService' + import Permissions from '../../security/permissions' import track from '../../segment/track' import PermissionChecker from '../../services/user/permissionChecker' @@ -10,7 +11,14 @@ export default async (req, res) => { req.body.organizationToNotMerge, ) - track('Ignore merge organizations', {}, { ...req }) + track( + 'Ignore merge organizations', + { + organizationId: req.params.organizationId, + organizationToNotMergeId: req.body.organizationToNotMerge, + }, + { ...req }, + ) await req.responseHandler.success(req, res, { status: 200 }) } diff --git a/backend/src/api/organization/organizationQuery.ts b/backend/src/api/organization/organizationQuery.ts index 62914272fc..d1bd52a0f9 100644 --- a/backend/src/api/organization/organizationQuery.ts +++ b/backend/src/api/organization/organizationQuery.ts @@ -4,12 +4,11 @@ import OrganizationService from '../../services/organizationService' import PermissionChecker from '../../services/user/permissionChecker' /** - * POST /tenant/{tenantId}/organization/query + * POST /organization/query * @summary Query organizations * @tag Organizations * @security Bearer * @description Query organizations. It accepts filters, sorting options and pagination. - * @pathParam {string} tenantId - Your workspace/tenant ID * @bodyContent {OrganizationQuery} application/json * @response 200 - Ok * @responseContent {OrganizationList} 200.application/json @@ -23,8 +22,8 @@ export default async (req, res) => { const payload = await new OrganizationService(req).query(req.body) - if (req.query.filter && Object.keys(req.query.filter).length > 0) { - track('Organizations Advanced Filter', { ...payload }, { ...req }) + if (req.body?.filter && Object.keys(req.body.filter).length > 0) { + track('Organizations Advanced Filter', { ...req.body }, { ...req }) } await req.responseHandler.success(req, res, payload) diff --git a/backend/src/api/organization/organizationUnmerge.ts b/backend/src/api/organization/organizationUnmerge.ts new file mode 100644 index 0000000000..b4ca0c6c67 --- /dev/null +++ b/backend/src/api/organization/organizationUnmerge.ts @@ -0,0 +1,12 @@ +import OrganizationService from '@/services/organizationService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.organizationEdit) + + const payload = await new OrganizationService(req).unmerge(req.params.organizationId, req.body) + + await req.responseHandler.success(req, res, payload, 200) +} diff --git a/backend/src/api/organization/organizationUnmergePreview.ts b/backend/src/api/organization/organizationUnmergePreview.ts new file mode 100644 index 0000000000..bdfe6fc0bb --- /dev/null +++ b/backend/src/api/organization/organizationUnmergePreview.ts @@ -0,0 +1,15 @@ +import Permissions from '../../security/permissions' +import OrganizationService from '../../services/organizationService' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.organizationEdit) + + const payload = await new OrganizationService(req).unmergePreview( + req.params.organizationId, + req.body.identity, + req.body.revertPreviousMerge, + ) + + await req.responseHandler.success(req, res, payload, 200) +} diff --git a/backend/src/api/organization/organizationUpdate.ts b/backend/src/api/organization/organizationUpdate.ts index 2bfed8211e..290a55cf5d 100644 --- a/backend/src/api/organization/organizationUpdate.ts +++ b/backend/src/api/organization/organizationUpdate.ts @@ -3,12 +3,11 @@ import OrganizationService from '../../services/organizationService' import PermissionChecker from '../../services/user/permissionChecker' /** - * PUT /tenant/{tenantId}/organization/{id} + * PUT /organization/{id} * @summary Update an organization * @tag Organizations * @security Bearer * @description Update a organization - * @pathParam {string} tenantId - Your workspace/tenant ID * @pathParam {string} id - The ID of the organization * @bodyContent {OrganizationInput} application/json * @response 200 - Ok @@ -21,7 +20,13 @@ import PermissionChecker from '../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.organizationEdit) - const payload = await new OrganizationService(req).update(req.params.id, req.body, true) + const payload = await new OrganizationService(req).update( + req.params.id, + req.body, + true, + true, + true, + ) await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/plan/index.ts b/backend/src/api/plan/index.ts deleted file mode 100644 index 0a7dd01433..0000000000 --- a/backend/src/api/plan/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { safeWrap } from '../../middlewares/errorMiddleware' - -export default (app) => { - app.post(`/plan/stripe/webhook`, safeWrap(require('./stripe/webhook').default)) - app.post(`/tenant/:tenantId/plan/stripe/portal`, safeWrap(require('./stripe/portal').default)) - app.post(`/tenant/:tenantId/plan/stripe/checkout`, safeWrap(require('./stripe/checkout').default)) -} diff --git a/backend/src/api/plan/stripe/checkout.ts b/backend/src/api/plan/stripe/checkout.ts deleted file mode 100644 index b4556f375b..0000000000 --- a/backend/src/api/plan/stripe/checkout.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { PLANS_CONFIG } from '../../../conf' -import Error400 from '../../../errors/Error400' -import Error403 from '../../../errors/Error403' -import Plans from '../../../security/plans' -import TenantService from '../../../services/tenantService' -import { tenantSubdomain } from '../../../services/tenantSubdomain' - -export default async (req, res) => { - if (!PLANS_CONFIG.stripeSecretKey) { - throw new Error400(req.language, 'tenant.stripeNotConfigured') - } - - const stripe = require('stripe')(PLANS_CONFIG.stripeSecretKey) - - const { currentTenant } = req - const { currentUser } = req - - if (!currentTenant || !currentUser) { - throw new Error403(req.language) - } - - if ( - currentTenant.plan !== Plans.values.essential && - currentTenant.planStatus !== 'cancel_at_period_end' && - currentTenant.planUserId !== currentUser.id - ) { - throw new Error403(req.language) - } - - let { planStripeCustomerId } = currentTenant - - if (!planStripeCustomerId || currentTenant.planUserId !== currentUser.id) { - const stripeCustomer = await stripe.customers.create({ - email: currentUser.email, - metadata: { - tenantId: currentTenant.id, - }, - }) - - planStripeCustomerId = stripeCustomer.id - } - - await new TenantService(req).updatePlanUser( - currentTenant.id, - planStripeCustomerId, - currentUser.id, - ) - - const session = await stripe.checkout.sessions.create({ - payment_method_types: ['card'], - line_items: [ - { - price: Plans.selectStripePriceIdByPlan(req.body.plan), - quantity: 1, - }, - ], - mode: 'subscription', - success_url: `${tenantSubdomain.frontendUrl(currentTenant)}/plan`, - cancel_url: `${tenantSubdomain.frontendUrl(currentTenant)}/plan`, - customer: planStripeCustomerId, - }) - - await req.responseHandler.success(req, res, session) -} diff --git a/backend/src/api/plan/stripe/portal.ts b/backend/src/api/plan/stripe/portal.ts deleted file mode 100644 index bc7ae1a9a9..0000000000 --- a/backend/src/api/plan/stripe/portal.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { PLANS_CONFIG } from '../../../conf' -import Error400 from '../../../errors/Error400' -import Error403 from '../../../errors/Error403' -import Plans from '../../../security/plans' -import TenantService from '../../../services/tenantService' -import { tenantSubdomain } from '../../../services/tenantSubdomain' - -export default async (req, res) => { - if (!PLANS_CONFIG.stripeSecretKey) { - throw new Error400(req.language, 'tenant.stripeNotConfigured') - } - - const stripe = require('stripe')(PLANS_CONFIG.stripeSecretKey) - - const { currentTenant } = req - const { currentUser } = req - - if (!currentTenant || !currentUser) { - throw new Error403(req.language) - } - - if ( - currentTenant.plan !== Plans.values.essential && - currentTenant.planStatus !== 'cancel_at_period_end' && - currentTenant.planUserId !== currentUser.id - ) { - throw new Error403(req.language) - } - - let { planStripeCustomerId } = currentTenant - - if (!planStripeCustomerId || currentTenant.planUserId !== currentUser.id) { - const stripeCustomer = await stripe.customers.create({ - email: currentUser.email, - metadata: { - tenantId: currentTenant.id, - }, - }) - - planStripeCustomerId = stripeCustomer.id - } - - await new TenantService(req).updatePlanUser( - currentTenant.id, - planStripeCustomerId, - currentUser.id, - ) - - const session = await stripe.billingPortal.sessions.create({ - customer: planStripeCustomerId, - return_url: `${tenantSubdomain.frontendUrl(currentTenant)}/plan`, - }) - - await req.responseHandler.success(req, res, session) -} diff --git a/backend/src/api/plan/stripe/webhook.ts b/backend/src/api/plan/stripe/webhook.ts deleted file mode 100644 index 269fa7e2bb..0000000000 --- a/backend/src/api/plan/stripe/webhook.ts +++ /dev/null @@ -1,60 +0,0 @@ -import lodash from 'lodash' -import { PLANS_CONFIG } from '../../../conf' -import Plans from '../../../security/plans' -import TenantService from '../../../services/tenantService' - -export default async (req, res) => { - const stripe = require('stripe')(PLANS_CONFIG.stripeSecretKey) - - const event = stripe.webhooks.constructEvent( - req.rawBody, - req.headers['stripe-signature'], - PLANS_CONFIG.stripWebhookSigningSecret, - ) - - if (event.type === 'checkout.session.completed') { - let data = event.data.object - data = await stripe.checkout.sessions.retrieve(data.id, { expand: ['line_items'] }) - - const stripePriceId = lodash.get(data, 'line_items.data[0].price.id') - - if (!stripePriceId) { - throw new Error('line_items.data[0].price.id NULL!') - } - - const plan = Plans.selectPlanByStripePriceId(stripePriceId) - const planStripeCustomerId = data.customer - - await new TenantService(req).updatePlanStatus(planStripeCustomerId, plan, 'active') - } - - if (event.type === 'customer.subscription.updated') { - const data = event.data.object - - const stripePriceId = lodash.get(data, 'items.data[0].price.id') - const plan = Plans.selectPlanByStripePriceId(stripePriceId) - const planStripeCustomerId = data.customer - - if (Plans.selectPlanStatus(data) === 'canceled') { - await new TenantService(req).updatePlanToFree(planStripeCustomerId) - } else { - await new TenantService(req).updatePlanStatus( - planStripeCustomerId, - plan, - Plans.selectPlanStatus(data), - ) - } - } - - if (event.type === 'customer.subscription.deleted') { - const data = event.data.object - - const planStripeCustomerId = data.customer - - await new TenantService(req).updatePlanToFree(planStripeCustomerId) - } - - await req.responseHandler.success(req, res, { - received: true, - }) -} diff --git a/backend/src/api/premium/enrichment/index.ts b/backend/src/api/premium/enrichment/index.ts deleted file mode 100644 index 5e3aff7413..0000000000 --- a/backend/src/api/premium/enrichment/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { safeWrap } from '../../../middlewares/errorMiddleware' -import { featureFlagMiddleware } from '../../../middlewares/featureFlagMiddleware' -import { FeatureFlag } from '../../../types/common' - -export default (app) => { - app.put( - `/tenant/:tenantId/enrichment/member/bulk`, - featureFlagMiddleware(FeatureFlag.MEMBER_ENRICHMENT, 'enrichment.errors.planLimitExceeded'), - safeWrap(require('./memberEnrichBulk').default), - ) - app.put( - `/tenant/:tenantId/enrichment/member/:id/`, - featureFlagMiddleware(FeatureFlag.MEMBER_ENRICHMENT, 'enrichment.errors.planLimitExceeded'), - safeWrap(require('./memberEnrich').default), - ) -} diff --git a/backend/src/api/premium/enrichment/memberEnrich.ts b/backend/src/api/premium/enrichment/memberEnrich.ts deleted file mode 100644 index afd356bf3f..0000000000 --- a/backend/src/api/premium/enrichment/memberEnrich.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { RedisCache } from '@crowd/redis' -import { getServiceLogger } from '@crowd/logging' -import { getSecondsTillEndOfMonth } from '../../../utils/timing' -import Permissions from '../../../security/permissions' -import identifyTenant from '../../../segment/identifyTenant' -import MemberEnrichmentService from '../../../services/premium/enrichment/memberEnrichmentService' -import PermissionChecker from '../../../services/user/permissionChecker' -import { FeatureFlagRedisKey } from '../../../types/common' -import track from '../../../segment/track' - -const log = getServiceLogger() - -/** - * PUT /tenant/{tenantId}/enrichment/member/{id} - * @summary Enrich a member - * @tag Members - * @security Bearer - * @description Enrich a member. - * @pathParam {string} tenantId - Your workspace/tenant ID - * @pathParam {string} id - The ID of the member - * @response 200 - Ok - * @responseContent {MemberResponse} 200.application/json - * @responseExample {MemberFind} 200.application/json.Member - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.memberEdit) - - const payload = await new MemberEnrichmentService(req).enrichOne(req.params.id) - - track('Single member enrichment', { memberId: req.params.id }, { ...req }) - - const memberEnrichmentCountCache = new RedisCache( - FeatureFlagRedisKey.MEMBER_ENRICHMENT_COUNT, - req.redis, - req.log, - ) - - const memberEnrichmentCount = await memberEnrichmentCountCache.get(req.currentTenant.id) - - const secondsRemainingUntilEndOfMonth = getSecondsTillEndOfMonth() - - log.info(secondsRemainingUntilEndOfMonth, 'Seconds remaining') - - if (!memberEnrichmentCount) { - await memberEnrichmentCountCache.set(req.currentTenant.id, '0', secondsRemainingUntilEndOfMonth) - } else { - await memberEnrichmentCountCache.set( - req.currentTenant.id, - (parseInt(memberEnrichmentCount, 10) + 1).toString(), - secondsRemainingUntilEndOfMonth, - ) - } - - identifyTenant(req) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/premium/enrichment/memberEnrichBulk.ts b/backend/src/api/premium/enrichment/memberEnrichBulk.ts deleted file mode 100644 index 5afa9c7389..0000000000 --- a/backend/src/api/premium/enrichment/memberEnrichBulk.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { RedisCache } from '@crowd/redis' -import { getServiceLogger } from '@crowd/logging' -import { getSecondsTillEndOfMonth } from '../../../utils/timing' -import Error403 from '../../../errors/Error403' -import Permissions from '../../../security/permissions' -import identifyTenant from '../../../segment/identifyTenant' -import { sendBulkEnrichMessage } from '../../../serverless/utils/nodeWorkerSQS' -import PermissionChecker from '../../../services/user/permissionChecker' -import { FeatureFlag, FeatureFlagRedisKey } from '../../../types/common' -import track from '../../../segment/track' -import { PLAN_LIMITS } from '../../../feature-flags/isFeatureEnabled' -import SequelizeRepository from '../../../database/repositories/sequelizeRepository' - -const log = getServiceLogger() - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.memberEdit) - const membersToEnrich = req.body.members - const tenant = req.currentTenant.id - const segmentIds = SequelizeRepository.getSegmentIds(req) - - const memberEnrichmentCountCache = new RedisCache( - FeatureFlagRedisKey.MEMBER_ENRICHMENT_COUNT, - req.redis, - req.log, - ) - const memberEnrichmentCount = await memberEnrichmentCountCache.get(req.currentTenant.id) - - log.info(parseInt(memberEnrichmentCount, 10) + membersToEnrich.length, 'Total: ') - - // Check if requested enrich count is over limit - if ( - parseInt(memberEnrichmentCount, 10) + membersToEnrich.length > - PLAN_LIMITS[req.currentTenant.plan][FeatureFlag.MEMBER_ENRICHMENT] - ) { - track( - `Bulk member enrichment`, - { count: membersToEnrich.length, memberIds: membersToEnrich, overTheLimit: true }, - { ...req }, - ) - - await req.responseHandler.error( - req, - res, - new Error403(req.language, 'enrichment.errors.requestedEnrichmentMoreThanLimit'), - ) - return - } - - track( - 'Bulk member enrichment', - { count: membersToEnrich.length, memberIds: membersToEnrich, overTheLimit: false }, - { ...req }, - ) - - // send the message - await sendBulkEnrichMessage(tenant, membersToEnrich, segmentIds) - - // update enrichment count, we'll also check failed enrichments and deduct these from grand total in bulkEnrichmentWorker - const secondsRemainingUntilEndOfMonth = getSecondsTillEndOfMonth() - - if (!memberEnrichmentCount) { - await memberEnrichmentCountCache.set(req.currentTenant.id, '0', secondsRemainingUntilEndOfMonth) - } else { - await memberEnrichmentCountCache.set( - req.currentTenant.id, - (parseInt(memberEnrichmentCount, 10) + membersToEnrich.length).toString(), - secondsRemainingUntilEndOfMonth, - ) - } - - identifyTenant(req) - - await req.responseHandler.success(req, res, membersToEnrich) -} diff --git a/backend/src/api/product/index.ts b/backend/src/api/product/index.ts new file mode 100644 index 0000000000..e9ec8b99de --- /dev/null +++ b/backend/src/api/product/index.ts @@ -0,0 +1,7 @@ +import { safeWrap } from '../../middlewares/errorMiddleware' + +export default (app) => { + app.post(`/product/event`, safeWrap(require('./productEventCreate').default)) + app.post(`/product/session`, safeWrap(require('./productSessionCreate').default)) + app.put(`/product/session/:id`, safeWrap(require('./productSessionUpdate').default)) +} diff --git a/backend/src/api/product/productEventCreate.ts b/backend/src/api/product/productEventCreate.ts new file mode 100644 index 0000000000..7ce3189a2c --- /dev/null +++ b/backend/src/api/product/productEventCreate.ts @@ -0,0 +1,15 @@ +import { Error403 } from '@crowd/common' + +import ProductAnalyticsService from '@/services/productAnalyticsService' + +export default async (req, res) => { + if (!req.currentUser || !req.currentUser.id) { + throw new Error403(req.language) + } + + await new ProductAnalyticsService(req).createEvent(req.body) + + const payload = true + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/product/productSessionCreate.ts b/backend/src/api/product/productSessionCreate.ts new file mode 100644 index 0000000000..5846fc8973 --- /dev/null +++ b/backend/src/api/product/productSessionCreate.ts @@ -0,0 +1,23 @@ +import { Error403 } from '@crowd/common' + +import ProductAnalyticsService from '@/services/productAnalyticsService' + +export default async (req, res) => { + if (!req.currentUser || !req.currentUser.id) { + throw new Error403(req.language) + } + + // cloudflare headers to get the real ip & country + const ipAddress = req.headers['cf-connecting-ip'] + const country = req.headers['cf-ipcountry'] + + req.body = { + ...req.body, + ipAddress, + country, + } + + const payload = await new ProductAnalyticsService(req).createSession(req.body) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/product/productSessionUpdate.ts b/backend/src/api/product/productSessionUpdate.ts new file mode 100644 index 0000000000..4edf15bb45 --- /dev/null +++ b/backend/src/api/product/productSessionUpdate.ts @@ -0,0 +1,15 @@ +import { Error403 } from '@crowd/common' + +import ProductAnalyticsService from '@/services/productAnalyticsService' + +export default async (req, res) => { + if (!req.currentUser || !req.currentUser.id) { + throw new Error403(req.language) + } + + await new ProductAnalyticsService(req).updateSession(req.params.id, req.body) + + const payload = true + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/public/index.ts b/backend/src/api/public/index.ts new file mode 100644 index 0000000000..dfbcd078cb --- /dev/null +++ b/backend/src/api/public/index.ts @@ -0,0 +1,13 @@ +import { Router } from 'express' + +import { errorHandler } from './middlewares/errorHandler' +import { v1Router } from './v1' + +export function publicRouter(): Router { + const router = Router() + + router.use('/v1', v1Router()) + router.use(errorHandler) + + return router +} diff --git a/backend/src/api/public/middlewares/errorHandler.ts b/backend/src/api/public/middlewares/errorHandler.ts new file mode 100644 index 0000000000..56a9882351 --- /dev/null +++ b/backend/src/api/public/middlewares/errorHandler.ts @@ -0,0 +1,104 @@ +import type { ErrorRequestHandler, NextFunction, Request, Response } from 'express' +import { + InsufficientScopeError as Auth0InsufficientScopeError, + UnauthorizedError as Auth0UnauthorizedError, +} from 'express-oauth2-jwt-bearer' + +import { + ConflictError, + HttpError, + InsufficientScopeError, + InternalError, + UnauthorizedError, +} from '@crowd/common' +import { SlackChannel, SlackPersona, sendSlackNotification } from '@crowd/slack' + +/** + * Converts errors to structured JSON: `{ error: { code, message } }`. + * Defaults to 500 Internal Error for unhandled errors. + */ +export const errorHandler: ErrorRequestHandler = ( + error: any, + req: Request, + res: Response, + _next: NextFunction, +) => { + if (error instanceof ConflictError) { + req.log.warn({ context: error.context }, 'Public API conflict') + sendSlackNotification( + SlackChannel.CDP_LFX_SELF_SERVE_ALERTS, + SlackPersona.WARNING_PROPAGATOR, + `Public API Conflict 409: ${req.method} ${req.url}`, + [ + { + title: 'Request', + text: `*Method:* \`${req.method}\`\n*URL:* \`${req.url}\``, + }, + { + title: 'Conflict', + text: `*Message:* ${error.message}`, + }, + ...(error.context + ? [{ title: 'Context', text: `\`\`\`${JSON.stringify(error.context, null, 2)}\`\`\`` }] + : []), + ], + ) + res.status(error.status).json(error.toJSON()) + return + } + + if (error instanceof HttpError) { + res.status(error.status).json(error.toJSON()) + return + } + + if (error instanceof Auth0InsufficientScopeError) { + const httpErr = new InsufficientScopeError(error.message || undefined) + res.status(httpErr.status).json(httpErr.toJSON()) + return + } + + if (error instanceof Auth0UnauthorizedError) { + const httpErr = new UnauthorizedError(error.message || undefined) + res.status(httpErr.status).json(httpErr.toJSON()) + return + } + + req.log.error( + { + error: { name: error?.name, message: error?.message, stack: error?.stack }, + url: req.url, + method: req.method, + query: req.query, + body: req.body, + }, + 'Unhandled error in public API', + ) + + sendSlackNotification( + SlackChannel.CDP_ALERTS, + SlackPersona.ERROR_REPORTER, + `Public API Error 500: ${req.method} ${req.url}`, + [ + { + title: 'Request', + text: `*Method:* \`${req.method}\`\n*URL:* \`${req.url}\``, + }, + { + title: 'Error', + text: `*Name:* \`${error?.name || 'Unknown'}\`\n*Message:* ${error?.message || 'No message'}`, + }, + ...(error?.stack + ? [ + { + title: 'Stack Trace', + text: `\`\`\`${error.stack.substring(0, 2700)}\`\`\``, + }, + ] + : []), + ], + ) + + const unknownError = new InternalError() + res.status(unknownError.status).json(unknownError.toJSON()) +} diff --git a/backend/src/api/public/middlewares/oauth2Middleware.ts b/backend/src/api/public/middlewares/oauth2Middleware.ts new file mode 100644 index 0000000000..77ee9d3b6b --- /dev/null +++ b/backend/src/api/public/middlewares/oauth2Middleware.ts @@ -0,0 +1,74 @@ +import type { NextFunction, Request, RequestHandler, Response } from 'express' +import { auth } from 'express-oauth2-jwt-bearer' + +import { UnauthorizedError } from '@crowd/common' + +import type { Auth0Configuration } from '@/conf/configTypes' +import type { Auth0TokenPayload } from '@/types/api' + +function resolveIssuer(req: Request): string | undefined { + const token = req.headers.authorization?.split(' ')[1] + if (!token) return undefined + try { + const { iss } = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString()) + return typeof iss === 'string' ? iss : undefined + } catch { + return undefined + } +} + +function resolveActor(req: Request, _res: Response, next: NextFunction): void { + const payload = (req.auth?.payload ?? {}) as Auth0TokenPayload + + const rawId = payload.sub ?? payload.azp + + if (!rawId) { + next(new UnauthorizedError('Token missing caller identity')) + return + } + + const id = rawId.replace(/@clients$/, '') + + const scopes = typeof payload.scope === 'string' ? payload.scope.split(' ').filter(Boolean) : [] + + req.actor = { id, type: 'service', scopes } + + next() +} + +export function oauth2Middleware(config: Auth0Configuration): RequestHandler[] { + const issuers = config.issuerBaseURLs + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + + if (issuers.length === 0) { + throw new Error('No auth0 issuers configured') + } + + const handlersByIssuer = new Map( + issuers.map((issuerBaseURL) => [ + issuerBaseURL.replace(/\/$/, ''), + auth({ issuerBaseURL, audience: config.audience }), + ]), + ) + + const verifyJwt: RequestHandler = (req, res, next) => { + const iss = resolveIssuer(req) + if (!iss) { + next(new UnauthorizedError('Missing or malformed bearer token')) + return + } + + const handler = handlersByIssuer.get(iss.replace(/\/$/, '')) + + if (!handler) { + next(new UnauthorizedError('Unknown token issuer')) + return + } + + handler(req, res, next) + } + + return [verifyJwt, resolveActor] +} diff --git a/backend/src/api/public/middlewares/requireScopes.ts b/backend/src/api/public/middlewares/requireScopes.ts new file mode 100644 index 0000000000..31bad381c2 --- /dev/null +++ b/backend/src/api/public/middlewares/requireScopes.ts @@ -0,0 +1,25 @@ +import type { NextFunction, Request, Response } from 'express' + +import { InsufficientScopeError, UnauthorizedError } from '@crowd/common' + +import { Scope } from '@/security/scopes' + +export const requireScopes = + (required: Scope[], mode: 'all' | 'any' = 'all') => + (req: Request, _res: Response, next: NextFunction) => { + if (!req.actor) { + next(new UnauthorizedError()) + return + } + + const granted = new Set(req.actor.scopes) + const hasAccess = + mode === 'all' ? required.every((s) => granted.has(s)) : required.some((s) => granted.has(s)) + + if (!hasAccess) { + next(new InsufficientScopeError()) + return + } + + next() + } diff --git a/backend/src/api/public/middlewares/staticApiKeyMiddleware.ts b/backend/src/api/public/middlewares/staticApiKeyMiddleware.ts new file mode 100644 index 0000000000..76d928f8a8 --- /dev/null +++ b/backend/src/api/public/middlewares/staticApiKeyMiddleware.ts @@ -0,0 +1,48 @@ +import crypto from 'crypto' +import type { NextFunction, Request, RequestHandler, Response } from 'express' + +import { UnauthorizedError } from '@crowd/common' +import { findApiKeyByHash, optionsQx, touchApiKeyLastUsed } from '@crowd/data-access-layer' + +export function staticApiKeyMiddleware(): RequestHandler { + return async (req: Request, _res: Response, next: NextFunction): Promise => { + try { + const authHeader = req.headers.authorization + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + next(new UnauthorizedError('Missing or invalid Authorization header')) + return + } + + const providedKey = authHeader.slice('Bearer '.length) + const keyHash = crypto.createHash('sha256').update(providedKey).digest('hex') + + const qx = optionsQx(req) + const apiKey = await findApiKeyByHash(qx, keyHash) + + if (!apiKey) { + next(new UnauthorizedError('Invalid API key')) + return + } + + if (apiKey.revokedAt) { + next(new UnauthorizedError('API key has been revoked')) + return + } + + if (apiKey.expiresAt && apiKey.expiresAt < new Date()) { + next(new UnauthorizedError('API key has expired')) + return + } + + // fire and forget — don't block the request + touchApiKeyLastUsed(qx, apiKey.id).catch(() => {}) + + req.actor = { id: apiKey.name, type: 'service', scopes: apiKey.scopes } + + next() + } catch (err) { + next(err) + } + } +} diff --git a/backend/src/api/public/openapi.yaml b/backend/src/api/public/openapi.yaml new file mode 100644 index 0000000000..0a3089ca9a --- /dev/null +++ b/backend/src/api/public/openapi.yaml @@ -0,0 +1,1617 @@ +openapi: 3.1.0 +info: + title: CDP Public API + version: 1.0.0 + license: + name: Apache-2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 + description: > + Public REST API for the Community Data Platform (CDP). Provides transactional + endpoints for identity verification, work experience management, project + affiliations, and contributor lookup. + + + Two authentication methods are supported depending on the endpoint: + + - **OAuth 2.0 Bearer (Auth0)** — used by LFX One for member, organization, + and affiliation management endpoints. + + - **Static API Key** — bearer token with scopes managed in the CDP database. + +servers: + - url: https://cm.lfx.dev/api/v1 + description: Production + - url: https://lf-staging.crowd.dev/api/v1 + description: Staging + +security: [] + +tags: + - name: Members + description: Resolve member profiles by identity. + - name: Member Identities + description: Manage and verify member identities across platforms. + - name: Maintainer Roles + description: Retrieve maintainer roles for a member. + - name: Work Experiences + description: Manage and verify member work experiences (organization affiliations). + - name: Project Affiliations + description: View and override per-project affiliation data for a member. + - name: Organizations + description: Look up and create organizations. + - name: Affiliations + description: Bulk contributor affiliation lookups by GitHub handle. + +paths: + # ────────────────────────────────────────────── + # Members + # ────────────────────────────────────────────── + /members: + post: + operationId: createMember + summary: Create a member profile + description: > + Create a new member profile in CDP with one or more identities. + tags: + - Members + security: + - OAuth2Bearer: + - write:members + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - displayName + - identities + properties: + displayName: + type: string + minLength: 1 + description: Display name for the member profile. + identities: + type: array + minItems: 1 + description: Initial identities for the member. + items: + $ref: '#/components/schemas/MemberIdentityInput' + example: + displayName: Jane Doe + identities: + - value: abc123 + platform: lfid + type: username + source: lfxOne + verified: true + verifiedBy: jane@lfx.dev + responses: + '201': + description: Member created successfully. + content: + application/json: + schema: + type: object + required: + - memberId + properties: + memberId: + type: string + format: uuid + example: + memberId: 550e8400-e29b-41d4-a716-446655440000 + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '409': + description: Identity already exists on another member. + content: + application/json: + schema: + $ref: '#/components/schemas/HttpError' + example: + error: + code: CONFLICT + message: Identity already exists on another member + + /members/resolve: + post: + operationId: resolveMember + summary: Resolve a CDP member profile + description: > + Resolve memberId from identities. LFX One should always make a first + request to this API to retrieve the corresponding memberId from CDP. + If a member is found, use it. If not, create a member. + tags: + - Members + security: + - OAuth2Bearer: + - read:members + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - lfids + properties: + lfids: + type: array + description: LFX IDs to search for. + minItems: 1 + items: + type: string + minLength: 1 + emails: + type: array + description: Optional email addresses to include in the lookup. + items: + type: string + format: email + example: + lfids: + - abc123 + emails: + - user@example.com + responses: + '200': + description: Member resolved successfully. + content: + application/json: + schema: + type: object + required: + - memberId + properties: + memberId: + type: string + format: uuid + example: + memberId: 550e8400-e29b-41d4-a716-446655440000 + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: Profile not found. + content: + application/json: + schema: + $ref: '#/components/schemas/HttpError' + example: + error: + code: NOT_FOUND + message: Member not found + '409': + description: Multiple member profiles matched. + content: + application/json: + schema: + $ref: '#/components/schemas/HttpError' + example: + error: + code: CONFLICT + message: Multiple member profiles matched + + # ────────────────────────────────────────────── + # Member Identities + # ────────────────────────────────────────────── + /members/{memberId}/identities: + get: + operationId: getMemberIdentities + summary: List member identities + description: Retrieve all identities for a member profile. + tags: + - Member Identities + security: + - OAuth2Bearer: + - read:member-identities + parameters: + - $ref: '#/components/parameters/MemberId' + responses: + '200': + description: Identities retrieved successfully. + content: + application/json: + schema: + type: object + required: + - identities + properties: + identities: + type: array + items: + $ref: '#/components/schemas/MemberIdentity' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/MemberNotFound' + + post: + operationId: createMemberIdentity + summary: Add a new identity + description: > + Add a new identity to a member profile. Returns 409 if the identity + already exists on this member or is verified on another member. + tags: + - Member Identities + security: + - OAuth2Bearer: + - write:member-identities + parameters: + - $ref: '#/components/parameters/MemberId' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - value + - platform + - type + - source + - verified + properties: + value: + type: string + minLength: 1 + description: Identity value (e.g. username, email address). + platform: + type: string + minLength: 1 + description: Platform name (e.g. github, linkedin). + type: + type: string + enum: + - username + - email + description: Identity type. + source: + type: string + minLength: 1 + description: Source system that created this identity. + verified: + type: boolean + description: Whether the identity is verified. + verifiedBy: + type: string + description: Required when `verified` is true. Identifier of who verified. + example: + value: johndoe + platform: github + type: username + source: lfxOne + verified: true + verifiedBy: admin@lfx.dev + responses: + '201': + description: Identity created. + content: + application/json: + schema: + $ref: '#/components/schemas/MemberIdentity' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/MemberNotFound' + '409': + description: Identity already exists on another member. + content: + application/json: + schema: + $ref: '#/components/schemas/HttpError' + example: + error: + code: CONFLICT + message: Identity already exists on another member + + /members/{memberId}/identities/{identityId}: + patch: + operationId: verifyMemberIdentity + summary: Verify or reject an identity + description: > + Set an identity as verified or rejected. When rejected (`verified: false`), + the identity is either soft-deleted (no linked activities) or unmerged to + a new profile (has linked activities). + tags: + - Member Identities + security: + - OAuth2Bearer: + - write:member-identities + parameters: + - $ref: '#/components/parameters/MemberId' + - name: identityId + in: path + required: true + description: UUID of the identity to verify or reject. + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - verified + - verifiedBy + properties: + verified: + type: boolean + description: > + `true` to verify the identity, `false` to reject it. + verifiedBy: + type: string + description: Identifier of who performed the verification. + example: + verified: true + verifiedBy: admin@lfx.dev + responses: + '200': + description: > + Identity updated. Returned when verifying, or when rejecting an + identity that has linked activities (triggers unmerge). + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/MemberIdentity' + - type: object + properties: + unmergedToMemberId: + type: string + format: uuid + description: > + Present only when rejection triggered an unmerge. The + new member profile ID that the identity was moved to. + '204': + description: > + Identity rejected and deleted (no linked activities). No response body. + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: Member or identity not found. + content: + application/json: + schema: + $ref: '#/components/schemas/HttpError' + '409': + description: Identity already exists on another member. + content: + application/json: + schema: + $ref: '#/components/schemas/HttpError' + example: + error: + code: CONFLICT + message: Identity already exists on another member + + # ────────────────────────────────────────────── + # Maintainer Roles + # ────────────────────────────────────────────── + /members/{memberId}/maintainer-roles: + get: + operationId: getMemberMaintainerRoles + summary: List maintainer roles + description: Retrieve all maintainer roles for a member across projects. + tags: + - Maintainer Roles + security: + - OAuth2Bearer: + - read:maintainer-roles + parameters: + - $ref: '#/components/parameters/MemberId' + responses: + '200': + description: Maintainer roles retrieved successfully. + content: + application/json: + schema: + type: object + required: + - maintainerRoles + properties: + maintainerRoles: + type: array + items: + $ref: '#/components/schemas/MaintainerRole' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/MemberNotFound' + + # ────────────────────────────────────────────── + # Work Experiences + # ────────────────────────────────────────────── + /members/{memberId}/work-experiences: + get: + operationId: getMemberWorkExperiences + summary: List work experiences + description: Retrieve all work experiences for a member. + tags: + - Work Experiences + security: + - OAuth2Bearer: + - read:work-experiences + parameters: + - $ref: '#/components/parameters/MemberId' + responses: + '200': + description: Work experiences retrieved successfully. + content: + application/json: + schema: + type: object + required: + - memberId + - workExperiences + properties: + memberId: + type: string + format: uuid + workExperiences: + type: array + items: + $ref: '#/components/schemas/WorkExperience' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/MemberNotFound' + + post: + operationId: createMemberWorkExperience + summary: Add a work experience + description: > + Add a new work experience to a member profile. Returns 409 if a work + experience with the same dates already exists. + tags: + - Work Experiences + security: + - OAuth2Bearer: + - write:work-experiences + parameters: + - $ref: '#/components/parameters/MemberId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WorkExperienceInput' + example: + organizationId: 550e8400-e29b-41d4-a716-446655440000 + jobTitle: Senior Engineer + verified: true + verifiedBy: admin@lfx.dev + source: lfxOne + startDate: '2020-01-01T00:00:00.000Z' + endDate: null + responses: + '201': + description: Work experience created. + content: + application/json: + schema: + $ref: '#/components/schemas/WorkExperience' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/MemberNotFound' + '409': + description: A work experience with the same dates already exists. + content: + application/json: + schema: + $ref: '#/components/schemas/HttpError' + example: + error: + code: CONFLICT + message: A work experience with the same dates already exists + + /members/{memberId}/work-experiences/{workExperienceId}: + put: + operationId: updateMemberWorkExperience + summary: Update a work experience + description: Replace all fields of an existing work experience. + tags: + - Work Experiences + security: + - OAuth2Bearer: + - write:work-experiences + parameters: + - $ref: '#/components/parameters/MemberId' + - $ref: '#/components/parameters/WorkExperienceId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WorkExperienceInput' + responses: + '200': + description: Work experience updated. + content: + application/json: + schema: + $ref: '#/components/schemas/WorkExperience' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: Member or work experience not found. + content: + application/json: + schema: + $ref: '#/components/schemas/HttpError' + + patch: + operationId: verifyMemberWorkExperience + summary: Verify or reject a work experience + description: > + Set a work experience as verified or rejected. When rejected + (`verified: false`), the work experience is soft-deleted and + affiliations are recalculated. + tags: + - Work Experiences + security: + - OAuth2Bearer: + - write:work-experiences + parameters: + - $ref: '#/components/parameters/MemberId' + - $ref: '#/components/parameters/WorkExperienceId' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - verified + - verifiedBy + properties: + verified: + type: boolean + description: > + `true` to verify, `false` to reject (soft-delete). + verifiedBy: + type: string + description: Identifier of who performed the verification. + example: + verified: true + verifiedBy: admin@lfx.dev + responses: + '200': + description: Work experience updated. + content: + application/json: + schema: + $ref: '#/components/schemas/WorkExperience' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: Member or work experience not found. + content: + application/json: + schema: + $ref: '#/components/schemas/HttpError' + + delete: + operationId: deleteMemberWorkExperience + summary: Delete a work experience + description: > + Soft-delete a work experience from a member profile. Affiliations are + automatically recalculated. + tags: + - Work Experiences + security: + - OAuth2Bearer: + - write:work-experiences + parameters: + - $ref: '#/components/parameters/MemberId' + - $ref: '#/components/parameters/WorkExperienceId' + responses: + '204': + description: Work experience deleted. No response body. + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: Member or work experience not found. + content: + application/json: + schema: + $ref: '#/components/schemas/HttpError' + + # ────────────────────────────────────────────── + # Project Affiliations + # ────────────────────────────────────────────── + /members/{memberId}/project-affiliations: + get: + operationId: getMemberProjectAffiliations + summary: List project affiliations + description: > + Retrieve per-project affiliation data for a member, including maintainer + roles and resolved affiliations. Affiliations come from project-level + overrides when available, otherwise from work experiences. + tags: + - Project Affiliations + security: + - OAuth2Bearer: + - read:project-affiliations + parameters: + - $ref: '#/components/parameters/MemberId' + responses: + '200': + description: Project affiliations retrieved successfully. + content: + application/json: + schema: + type: object + required: + - projectAffiliations + properties: + projectAffiliations: + type: array + items: + $ref: '#/components/schemas/ProjectAffiliation' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/MemberNotFound' + + /members/{memberId}/project-affiliations/{projectId}: + patch: + operationId: patchMemberProjectAffiliation + summary: Override project affiliations + description: > + Replace all project-level affiliation overrides for a member on a + specific project. Pass an empty `affiliations` array to clear overrides + (falling back to work experience-based affiliations). + tags: + - Project Affiliations + security: + - OAuth2Bearer: + - write:project-affiliations + parameters: + - $ref: '#/components/parameters/MemberId' + - name: projectId + in: path + required: true + description: UUID of the project (segment ID). + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - affiliations + properties: + affiliations: + type: array + description: > + Affiliation overrides. Pass an empty array to clear. + items: + type: object + required: + - organizationId + - dateStart + properties: + organizationId: + type: string + format: uuid + dateStart: + type: string + format: date-time + description: Start date of the affiliation period. + dateEnd: + type: + - string + - 'null' + format: date-time + description: End date, or null if currently active. + verifiedBy: + type: string + maxLength: 255 + description: Required when `affiliations` is non-empty. + example: + affiliations: + - organizationId: 550e8400-e29b-41d4-a716-446655440000 + dateStart: '2020-01-01T00:00:00.000Z' + dateEnd: null + verifiedBy: admin@lfx.dev + responses: + '200': + description: Project affiliations updated. + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectAffiliation' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: Member or project not found. + content: + application/json: + schema: + $ref: '#/components/schemas/HttpError' + + # ────────────────────────────────────────────── + # Organizations + # ────────────────────────────────────────────── + /organizations: + get: + operationId: getOrganization + summary: Look up an organization by domain + description: Find a verified organization by its primary domain. + tags: + - Organizations + security: + - OAuth2Bearer: + - read:organizations + parameters: + - name: domain + in: query + required: true + description: Primary domain of the organization. + schema: + type: string + minLength: 1 + example: linuxfoundation.org + responses: + '200': + description: Organization found. + content: + application/json: + schema: + $ref: '#/components/schemas/Organization' + example: + id: 550e8400-e29b-41d4-a716-446655440000 + name: Linux Foundation + logo: https://example.com/logo.png + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: No verified organization found for the given domain. + content: + application/json: + schema: + $ref: '#/components/schemas/HttpError' + example: + error: + code: NOT_FOUND + message: Organization not found + + post: + operationId: createOrganization + summary: Create an organization + description: > + Create a new organization with a verified primary domain. If an + organization with the same domain already exists, it returns the + existing one. + tags: + - Organizations + security: + - OAuth2Bearer: + - write:organizations + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - name + - domain + - source + - logo + properties: + name: + type: string + minLength: 1 + description: Display name of the organization. + domain: + type: string + minLength: 1 + description: Primary domain of the organization. + source: + type: string + minLength: 1 + description: Source system creating the organization. + logo: + type: string + format: uri + description: URL of the organization's logo. + example: + name: Acme Corp + domain: acme.com + source: lfxOne + logo: https://example.com/logo.png + responses: + '201': + description: Organization created (or existing one returned). + content: + application/json: + schema: + type: object + required: + - id + - name + properties: + id: + type: string + format: uuid + name: + type: string + example: + id: 550e8400-e29b-41d4-a716-446655440000 + name: Acme Corp + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + + # ────────────────────────────────────────────── + # Affiliations (Static API Key) + # ────────────────────────────────────────────── + /affiliations: + post: + operationId: getBulkAffiliations + summary: Bulk contributor lookup + description: > + Look up affiliation data for up to 100 GitHub handles in a single + request. Handles that have no matching LFX profile are returned in the + `notFound` array. + tags: + - Affiliations + security: + - StaticApiKey: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - githubHandles + properties: + githubHandles: + type: array + description: > + List of GitHub login handles to look up (case-insensitive). + minItems: 1 + maxItems: 100 + items: + type: string + minLength: 1 + example: + githubHandles: + - torvalds + - gvanrossum + parameters: + - name: page + in: query + description: Page number (1-based). + schema: + type: integer + minimum: 1 + default: 1 + - name: pageSize + in: query + description: Number of contributors to return per page. + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + responses: + '200': + description: Affiliations resolved successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/BulkAffiliationsResponse' + example: + total: 2 + totalFound: 2 + page: 1 + pageSize: 20 + contributorsInPage: 2 + contributors: + - githubHandle: torvalds + name: Linus Torvalds + emails: + - torvalds@linux-foundation.org + affiliations: + - organization: Linux Foundation + startDate: '2007-01-01T00:00:00.000Z' + endDate: null + notFound: [] + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '429': + $ref: '#/components/responses/TooManyRequests' + + /affiliations/{githubHandle}: + get: + operationId: getAffiliationByHandle + summary: Single contributor lookup + description: > + Look up affiliation data for one developer by GitHub handle. Useful for + debugging and ad-hoc queries. + tags: + - Affiliations + security: + - StaticApiKey: [] + parameters: + - name: githubHandle + in: path + required: true + description: GitHub login handle (case-insensitive). + schema: + type: string + minLength: 1 + example: torvalds + responses: + '200': + description: Developer found. + content: + application/json: + schema: + $ref: '#/components/schemas/Contributor' + example: + githubHandle: torvalds + name: Linus Torvalds + emails: + - torvalds@linux-foundation.org + affiliations: + - organization: Linux Foundation + startDate: '2007-01-01T00:00:00.000Z' + endDate: null + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: No LFX profile found for the given GitHub handle. + content: + application/json: + schema: + $ref: '#/components/schemas/HttpError' + example: + error: + code: NOT_FOUND + message: "No LFX profile found for GitHub login 'nonexistent-user'." + '429': + $ref: '#/components/responses/TooManyRequests' + +components: + securitySchemes: + OAuth2Bearer: + type: oauth2 + description: > + OAuth 2.0 client credentials flow via Auth0. The consuming service + obtains a JWT using its client ID and secret, then passes it as + `Authorization: Bearer ` + flows: + clientCredentials: + tokenUrl: https://linuxfoundation.auth0.com/oauth/token + scopes: + read:members: Read member profiles + write:members: Create member profiles + read:member-identities: Read member identities + write:member-identities: Create and verify member identities + read:maintainer-roles: Read maintainer roles + read:work-experiences: Read work experiences + write:work-experiences: Create, update, verify, and delete work experiences + read:project-affiliations: Read project affiliations + write:project-affiliations: Override project affiliations + read:organizations: Look up organizations + write:organizations: Create organizations + StaticApiKey: + type: http + scheme: bearer + description: > + Static API key — pass as `Authorization: Bearer `. Keys are + managed in the CDP database with SHA-256 hashing, expiration, and + revocation support. + + parameters: + MemberId: + name: memberId + in: path + required: true + description: UUID of the member. + schema: + type: string + format: uuid + WorkExperienceId: + name: workExperienceId + in: path + required: true + description: UUID of the work experience. + schema: + type: string + format: uuid + + schemas: + MemberIdentityInput: + type: object + required: + - value + - platform + - type + - source + - verified + properties: + value: + type: string + minLength: 1 + description: Identity value (e.g. username, email address). + platform: + type: string + minLength: 1 + description: Platform name (e.g. github, lfid). + type: + type: string + enum: + - username + - email + description: Identity type. + source: + type: string + minLength: 1 + description: Source system that created this identity. + verified: + type: boolean + description: Whether the identity is verified. + verifiedBy: + type: string + description: Required when `verified` is true. Identifier of who verified. + + MemberIdentity: + type: object + required: + - id + - value + - platform + - type + - verified + - verifiedBy + - source + - createdAt + - updatedAt + properties: + id: + type: string + format: uuid + value: + type: string + description: Identity value (username or email). + platform: + type: string + description: Platform name (e.g. github, linkedin, lfid). + type: + type: string + enum: + - username + - email + description: Identity type. + verified: + type: boolean + verifiedBy: + type: + - string + - 'null' + description: Identifier of who verified this identity, or null. + source: + type: + - string + - 'null' + description: Source system that created this identity. + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + + MaintainerRole: + type: object + required: + - id + - memberId + - segmentId + - role + - url + - repoType + properties: + id: + type: string + format: uuid + memberId: + type: string + format: uuid + segmentId: + type: string + format: uuid + dateStart: + type: + - string + - 'null' + format: date + description: Start date of the maintainer role. + dateEnd: + type: + - string + - 'null' + format: date + description: End date of the maintainer role, or null if current. + url: + type: string + description: Repository URL. + repoType: + type: string + enum: + - github + - gitlab + - git + - gerrit + description: Repository platform type. + role: + type: string + description: Role name (e.g. maintainer, committer). + maintainerFile: + type: + - string + - 'null' + description: Path to the maintainer file in the repository, if applicable. + + WorkExperience: + type: object + required: + - id + - organizationId + - organizationName + - jobTitle + - verified + - verifiedBy + - source + - startDate + - endDate + - createdAt + - updatedAt + properties: + id: + type: string + format: uuid + organizationId: + type: string + format: uuid + organizationName: + type: + - string + - 'null' + description: Display name of the organization. + organizationLogo: + type: + - string + - 'null' + description: URL of the organization logo. + jobTitle: + type: + - string + - 'null' + verified: + type: boolean + verifiedBy: + type: + - string + - 'null' + source: + type: + - string + - 'null' + description: Source system that created this work experience. + startDate: + type: + - string + - 'null' + format: date-time + endDate: + type: + - string + - 'null' + format: date-time + description: End date, or null if currently active. + createdAt: + type: + - string + - 'null' + format: date-time + updatedAt: + type: + - string + - 'null' + format: date-time + + WorkExperienceInput: + type: object + required: + - organizationId + - jobTitle + - verified + - verifiedBy + - source + - startDate + properties: + organizationId: + type: string + format: uuid + jobTitle: + type: string + description: Job title at the organization. + verified: + type: boolean + verifiedBy: + type: string + description: Identifier of who verified this work experience. + source: + type: string + description: Source system (e.g. lfxOne). + startDate: + type: string + format: date-time + description: Start date of the work experience. + endDate: + type: + - string + - 'null' + format: date-time + description: End date, or null if currently active. + + ProjectAffiliation: + type: object + required: + - id + - projectSlug + - projectName + - projectLogo + - contributionCount + - roles + - affiliations + properties: + id: + type: string + format: uuid + description: Segment (project) ID. + projectSlug: + type: string + projectName: + type: string + projectLogo: + type: + - string + - 'null' + contributionCount: + type: integer + description: Total number of contributions in this project. + roles: + type: array + items: + $ref: '#/components/schemas/ProjectRole' + affiliations: + type: array + items: + $ref: '#/components/schemas/ProjectAffiliationEntry' + + ProjectRole: + type: object + required: + - id + - role + - startDate + - endDate + - repoUrl + - repoFileUrl + properties: + id: + type: string + format: uuid + role: + type: string + startDate: + type: + - string + - 'null' + format: date-time + endDate: + type: + - string + - 'null' + format: date-time + repoUrl: + type: + - string + - 'null' + repoFileUrl: + type: + - string + - 'null' + + ProjectAffiliationEntry: + type: object + required: + - id + - organizationId + - organizationName + - organizationLogo + - verified + - verifiedBy + - startDate + - endDate + - type + properties: + id: + type: string + format: uuid + organizationId: + type: string + format: uuid + organizationName: + type: string + organizationLogo: + type: + - string + - 'null' + verified: + type: boolean + verifiedBy: + type: + - string + - 'null' + startDate: + type: + - string + - 'null' + format: date-time + endDate: + type: + - string + - 'null' + format: date-time + type: + type: string + enum: + - project + - work-history + description: > + `project` — manually overridden at the project level. + `work-history` — derived from work experiences. + source: + type: + - string + - 'null' + description: Present only for `work-history` type affiliations. + + AffiliationPeriod: + type: object + required: + - organization + - startDate + - endDate + properties: + organization: + type: string + description: Name of the organization. + startDate: + type: + - string + - 'null' + format: date-time + description: Start date of the affiliation period. + endDate: + type: + - string + - 'null' + format: date-time + description: End date, or null if currently active. + + Contributor: + type: object + required: + - githubHandle + - name + - emails + - affiliations + properties: + githubHandle: + type: string + description: Verified GitHub login handle. + name: + type: + - string + - 'null' + description: Display name from the LFX profile. + emails: + type: array + description: Verified email addresses linked to the profile. + items: + type: string + format: email + affiliations: + type: array + description: Resolved affiliation periods, most recent first. + items: + $ref: '#/components/schemas/AffiliationPeriod' + + BulkAffiliationsResponse: + type: object + required: + - total + - totalFound + - page + - pageSize + - contributorsInPage + - contributors + - notFound + properties: + total: + type: integer + description: Total number of handles submitted in the request. + totalFound: + type: integer + description: Number of handles that matched an LFX profile. + page: + type: integer + description: Current page number. + pageSize: + type: integer + description: Maximum contributors per page. + contributorsInPage: + type: integer + description: Number of contributors returned in this page. + contributors: + type: array + items: + $ref: '#/components/schemas/Contributor' + notFound: + type: array + description: Handles from the request with no matching LFX profile. + items: + type: string + + Organization: + type: object + required: + - id + - name + properties: + id: + type: string + format: uuid + name: + type: string + description: Display name of the organization. + logo: + type: string + description: URL of the organization logo. Only present if available. + + HttpError: + type: object + required: + - error + properties: + error: + type: object + required: + - code + - message + properties: + code: + type: string + description: Machine-readable error code. + message: + type: string + description: Human-readable error description. + + responses: + BadRequest: + description: Invalid request body or query parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/HttpError' + example: + error: + code: BAD_REQUEST + message: Validation failed + + Unauthorized: + description: Missing or invalid authentication credentials. + content: + application/json: + schema: + $ref: '#/components/schemas/HttpError' + example: + error: + code: UNAUTHORIZED + message: Invalid or missing authentication + + Forbidden: + description: Authentication valid but insufficient scopes. + content: + application/json: + schema: + $ref: '#/components/schemas/HttpError' + example: + error: + code: INSUFFICIENT_SCOPE + message: Insufficient scope for this operation + + MemberNotFound: + description: No member found with the given ID. + content: + application/json: + schema: + $ref: '#/components/schemas/HttpError' + example: + error: + code: NOT_FOUND + message: Member not found + + TooManyRequests: + description: Rate limit exceeded (60 requests per 60 seconds). + content: + application/json: + schema: + $ref: '#/components/schemas/HttpError' + example: + error: + code: RATE_LIMITED + message: Too many requests, please try again later diff --git a/backend/src/api/public/v1/affiliations/getAffiliationByHandle.ts b/backend/src/api/public/v1/affiliations/getAffiliationByHandle.ts new file mode 100644 index 0000000000..3273149efb --- /dev/null +++ b/backend/src/api/public/v1/affiliations/getAffiliationByHandle.ts @@ -0,0 +1,36 @@ +import type { Request, Response } from 'express' + +import { NotFoundError } from '@crowd/common' +import { + findMembersByGithubHandles, + findVerifiedEmailsByMemberIds, + optionsQx, + resolveAffiliationsByMemberIds, +} from '@crowd/data-access-layer' + +import { ok } from '@/utils/api' + +export async function getAffiliationByHandle(req: Request, res: Response): Promise { + const handle = req.params.githubHandle.toLowerCase() + const qx = optionsQx(req) + + const members = await findMembersByGithubHandles(qx, [handle]) + if (members.length === 0) { + throw new NotFoundError(`No LFX profile found for GitHub login '${req.params.githubHandle}'.`) + } + + const member = members[0] + const memberIds = [member.memberId] + + const [emailRows, affiliationsByMember] = await Promise.all([ + findVerifiedEmailsByMemberIds(qx, memberIds), + resolveAffiliationsByMemberIds(qx, memberIds), + ]) + + ok(res, { + githubHandle: member.githubHandle, + name: member.displayName, + emails: emailRows.map((r) => r.email), + affiliations: affiliationsByMember.get(member.memberId) ?? [], + }) +} diff --git a/backend/src/api/public/v1/affiliations/getAffiliations.ts b/backend/src/api/public/v1/affiliations/getAffiliations.ts new file mode 100644 index 0000000000..9179081a13 --- /dev/null +++ b/backend/src/api/public/v1/affiliations/getAffiliations.ts @@ -0,0 +1,89 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { + findMembersByGithubHandles, + findVerifiedEmailsByMemberIds, + optionsQx, + resolveAffiliationsByMemberIds, +} from '@crowd/data-access-layer' + +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +const MAX_HANDLES = 100 +const DEFAULT_PAGE_SIZE = 20 + +const bodySchema = z.object({ + githubHandles: z + .array(z.string().trim().min(1).toLowerCase()) + .min(1) + .max(MAX_HANDLES, `Maximum ${MAX_HANDLES} handles per request`), +}) + +const querySchema = z.object({ + page: z.coerce.number().int().min(1).default(1), + pageSize: z.coerce.number().int().min(1).max(MAX_HANDLES).default(DEFAULT_PAGE_SIZE), +}) + +export async function getAffiliations(req: Request, res: Response): Promise { + const { githubHandles } = validateOrThrow(bodySchema, req.body) + const { page, pageSize } = validateOrThrow(querySchema, req.query) + const qx = optionsQx(req) + + const offset = (page - 1) * pageSize + + // Step 1: find all verified members across all handles + const allMemberRows = await findMembersByGithubHandles(qx, githubHandles) + + const foundHandles = new Set(allMemberRows.map((r) => r.githubHandle.toLowerCase())) + const notFound = githubHandles.filter((h) => !foundHandles.has(h)) + + const pageMemberRows = allMemberRows.slice(offset, offset + pageSize) + + if (pageMemberRows.length === 0) { + ok(res, { + total: githubHandles.length, + totalFound: allMemberRows.length, + page, + pageSize, + contributorsInPage: 0, + contributors: [], + notFound, + }) + return + } + + const memberIds = pageMemberRows.map((r) => r.memberId) + + // Step 2: fetch verified emails for current page + const emailRows = await findVerifiedEmailsByMemberIds(qx, memberIds) + + const emailsByMember = new Map() + for (const row of emailRows) { + const list = emailsByMember.get(row.memberId) ?? [] + list.push(row.email) + emailsByMember.set(row.memberId, list) + } + + // Step 3: resolve affiliations for current page only + const affiliationsByMember = await resolveAffiliationsByMemberIds(qx, memberIds) + + // Step 4: build response + const contributors = pageMemberRows.map((member) => ({ + githubHandle: member.githubHandle, + name: member.displayName, + emails: emailsByMember.get(member.memberId) ?? [], + affiliations: affiliationsByMember.get(member.memberId) ?? [], + })) + + ok(res, { + total: githubHandles.length, + totalFound: allMemberRows.length, + page, + pageSize, + contributorsInPage: contributors.length, + contributors, + notFound, + }) +} diff --git a/backend/src/api/public/v1/affiliations/index.ts b/backend/src/api/public/v1/affiliations/index.ts new file mode 100644 index 0000000000..5f2fc9355e --- /dev/null +++ b/backend/src/api/public/v1/affiliations/index.ts @@ -0,0 +1,26 @@ +import { Router } from 'express' + +import { createRateLimiter } from '@/api/apiRateLimiter' +import { requireScopes } from '@/api/public/middlewares/requireScopes' +import { safeWrap } from '@/middlewares/errorMiddleware' +import { SCOPES } from '@/security/scopes' + +import { getAffiliationByHandle } from './getAffiliationByHandle' +import { getAffiliations } from './getAffiliations' + +const rateLimiter = createRateLimiter({ max: 60, windowMs: 60 * 1000 }) + +export function memberOrganizationAffiliationsRouter(): Router { + const router = Router() + + router.use(rateLimiter) + + router.post('/', requireScopes([SCOPES.READ_AFFILIATIONS]), safeWrap(getAffiliations)) + router.get( + '/:githubHandle', + requireScopes([SCOPES.READ_AFFILIATIONS]), + safeWrap(getAffiliationByHandle), + ) + + return router +} diff --git a/backend/src/api/public/v1/akrites/index.ts b/backend/src/api/public/v1/akrites/index.ts new file mode 100644 index 0000000000..00eee5aba0 --- /dev/null +++ b/backend/src/api/public/v1/akrites/index.ts @@ -0,0 +1,122 @@ +import { Router } from 'express' + +import { createRateLimiter } from '@/api/apiRateLimiter' +import { requireScopes } from '@/api/public/middlewares/requireScopes' +import { safeWrap } from '@/middlewares/errorMiddleware' +import { SCOPES } from '@/security/scopes' + +import { activityFeedHandler } from '../ossprey/activityFeed' +import { metricsHandler } from '../ossprey/metrics' +import { packageListHandler } from '../ossprey/packageList' +import { packageScatterHandler } from '../ossprey/packageScatter' +import { batchGetStewardship } from '../packages/batchGetStewardship' +import { getPackage } from '../packages/getPackage' +import { getPackageAdvisories } from '../packages/getPackageAdvisories' +import { getPackageHistory } from '../packages/getPackageHistory' +import { getPackagesMetrics } from '../packages/getPackagesMetrics' +import { assignStewardHandler } from '../stewardships/assignSteward' +import { escalateHandler } from '../stewardships/escalate' +import { getMyActivityHandler } from '../stewardships/getMyActivity' +import { getMyPackagesHandler } from '../stewardships/getMyPackages' +import { openStewardship } from '../stewardships/openStewardship' +import { updateStatusHandler } from '../stewardships/updateStatus' + +const rateLimiter = createRateLimiter({ max: 60, windowMs: 60 * 1000 }) + +export function akritesRouter(): Router { + const router = Router() + + router.get( + '/metrics', + requireScopes([SCOPES.READ_PACKAGES, SCOPES.READ_STEWARDSHIPS], 'all'), + safeWrap(metricsHandler), + ) + // /packages/scatter registered before router.use('/packages', ...) so Express evaluates this + // explicit route first; without this ordering the sub-router would receive the request first + // and call next() on no match, adding unnecessary overhead. + router.get( + '/packages/scatter', + rateLimiter, + requireScopes([SCOPES.READ_PACKAGES, SCOPES.READ_STEWARDSHIPS], 'all'), + safeWrap(packageScatterHandler), + ) + router.get( + '/packages', + rateLimiter, + requireScopes([SCOPES.READ_PACKAGES, SCOPES.READ_STEWARDSHIPS], 'all'), + safeWrap(packageListHandler), + ) + router.get( + '/activity', + requireScopes([SCOPES.READ_PACKAGES, SCOPES.READ_STEWARDSHIPS], 'all'), + safeWrap(activityFeedHandler), + ) + + // --- packages --- + router.post( + /^\/packages:batch-stewardship\/?$/, + rateLimiter, + requireScopes([SCOPES.READ_PACKAGES, SCOPES.READ_STEWARDSHIPS], 'all'), + safeWrap(batchGetStewardship), + ) + const packagesSubRouter = Router() + packagesSubRouter.use(rateLimiter) + packagesSubRouter.get( + '/metrics', + requireScopes([SCOPES.READ_PACKAGES, SCOPES.READ_STEWARDSHIPS], 'all'), + safeWrap(getPackagesMetrics), + ) + packagesSubRouter.get( + '/detail', + requireScopes([SCOPES.READ_PACKAGES, SCOPES.READ_STEWARDSHIPS], 'all'), + safeWrap(getPackage), + ) + packagesSubRouter.get( + '/advisories', + requireScopes([SCOPES.READ_PACKAGES, SCOPES.READ_STEWARDSHIPS], 'all'), + safeWrap(getPackageAdvisories), + ) + packagesSubRouter.get( + '/history', + requireScopes([SCOPES.READ_PACKAGES, SCOPES.READ_STEWARDSHIPS], 'all'), + safeWrap(getPackageHistory), + ) + router.use('/packages', packagesSubRouter) + + // --- stewardships --- + const stewardshipsSubRouter = Router() + stewardshipsSubRouter.use(rateLimiter) + stewardshipsSubRouter.get( + '/me/packages', + requireScopes([SCOPES.READ_STEWARDSHIPS]), + safeWrap(getMyPackagesHandler), + ) + stewardshipsSubRouter.get( + '/me/activity', + requireScopes([SCOPES.READ_STEWARDSHIPS]), + safeWrap(getMyActivityHandler), + ) + stewardshipsSubRouter.post( + '/open', + requireScopes([SCOPES.WRITE_STEWARDSHIPS]), + safeWrap(openStewardship), + ) + stewardshipsSubRouter.post( + '/:id/assign', + requireScopes([SCOPES.WRITE_STEWARDSHIPS]), + safeWrap(assignStewardHandler), + ) + stewardshipsSubRouter.post( + '/:id/escalate', + requireScopes([SCOPES.WRITE_STEWARDSHIPS]), + safeWrap(escalateHandler), + ) + stewardshipsSubRouter.patch( + '/:id/status', + requireScopes([SCOPES.WRITE_STEWARDSHIPS]), + safeWrap(updateStatusHandler), + ) + router.use('/stewardships', stewardshipsSubRouter) + + return router +} diff --git a/backend/src/api/public/v1/akrites/openapi.yaml b/backend/src/api/public/v1/akrites/openapi.yaml new file mode 100644 index 0000000000..e202292b7e --- /dev/null +++ b/backend/src/api/public/v1/akrites/openapi.yaml @@ -0,0 +1,1671 @@ +openapi: 3.1.0 +info: + title: CDP Public API — Akrites + version: 1.0.0 + description: > + Unified namespace for OSSPREY dashboard read endpoints and stewardship write + actions. All routes require an OAuth 2.0 bearer token (Auth0 M2M or user session). + + + **Rate limits:** packages and stewardships sub-groups each have an independent + 60 requests/min per-IP bucket. + +servers: + - url: https://cm.lfx.dev/api/v1 + description: Production + - url: https://lf-staging.crowd.dev/api/v1 + description: Staging + +security: + - BearerAuth: [] + +tags: + - name: Dashboard + description: KPI bar metrics and activity feed. + - name: Packages + description: Package list, scatter plot, detail, and batch stewardship lookup. + - name: Stewardships + description: Open, assign, escalate, and update stewardship status. + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + + schemas: + # ── Shared primitives ────────────────────────────────────────────────────── + + Error: + type: object + required: [error] + properties: + error: + type: object + required: [code, message] + properties: + code: + type: string + example: VALIDATION_ERROR + message: + type: string + example: Invalid query parameter. + + HealthBand: + type: string + enum: [healthy, fair, concerning, critical] + description: > + Derived from `scorecardScore`: + `null or < 3.0` → critical · `< 5.0` → concerning · `< 7.0` → fair · `≥ 7.0` → healthy + + StewardshipStatus: + type: string + enum: + - unassigned + - open + - assessing + - active + - needs_attention + - escalated + - blocked + - inactive + + EscalationResolutionPath: + type: string + enum: + - right_of_first_refusal + - replace_the_dependency + - find_vendor_for_lts + - consortium_adopts_maintainership + - compensating_controls_monitor + - namespace_takeover + + InactiveReason: + type: string + enum: + - quarterly_cadence_missed + - stepped_down + - no_longer_critical + + Steward: + type: object + required: [userId, role, assignedAt] + properties: + userId: + type: string + description: Auth0 sub of the assigned steward. + example: auth0|abc123 + role: + type: string + enum: [lead, co_steward] + assignedAt: + type: string + format: date-time + + StewardshipRecord: + type: object + required: [id, packageId, status, origin, version, createdAt, updatedAt] + properties: + id: + type: string + packageId: + type: string + status: + $ref: '#/components/schemas/StewardshipStatus' + origin: + type: string + version: + type: integer + openedAt: + type: string + format: date-time + nullable: true + lastStatusAt: + type: string + format: date-time + nullable: true + inactiveReason: + $ref: '#/components/schemas/InactiveReason' + nullable: true + resolutionPath: + $ref: '#/components/schemas/EscalationResolutionPath' + nullable: true + statusNote: + type: string + nullable: true + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + + StewardEntry: + type: object + required: [id, stewardshipId, userId, role, assignedAt] + properties: + id: + type: string + stewardshipId: + type: string + userId: + type: string + name: + type: string + nullable: true + role: + type: string + enum: [lead, co_steward] + assignedAt: + type: string + format: date-time + assignedBy: + type: string + nullable: true + + # ── Dashboard schemas ────────────────────────────────────────────────────── + + OsspreyMetrics: + type: object + required: + - criticalPackages + - coveragePercent + - coverageTrend + - activeStewards + - unassignedCritical + - needsAttention + - escalated + properties: + criticalPackages: + type: integer + description: Total number of packages marked as critical (is_critical = true). + coveragePercent: + type: number + format: float + description: Percentage of critical packages with an active stewardship (assessing, active, or needs_attention). + coverageTrend: + type: number + format: float + nullable: true + description: Coverage delta vs. previous period. Currently always null (requires snapshot mechanism). + activeStewards: + type: integer + description: Distinct stewards assigned to a non-inactive stewardship. + unassignedCritical: + type: integer + description: Critical packages with no stewardship or status = unassigned. + needsAttention: + type: integer + description: Critical packages whose stewardship status is needs_attention. + escalated: + type: integer + description: Critical packages whose stewardship status is escalated. + + ActivityEntry: + type: object + required: + - id + - stewardshipId + - packagePurl + - packageName + - packageEcosystem + - actorUserId + - actorName + - actorType + - activityType + - stewardshipStatus + - createdAt + properties: + id: + type: string + stewardshipId: + type: string + packagePurl: + type: string + packageName: + type: string + packageEcosystem: + type: string + actorUserId: + type: string + nullable: true + actorName: + type: string + nullable: true + description: Display name (currently same as actorUserId; resolution from members table is pending). + actorType: + type: string + example: user + activityType: + type: string + example: status_change + content: + type: string + nullable: true + metadata: + type: object + nullable: true + additionalProperties: true + stewardshipStatus: + $ref: '#/components/schemas/StewardshipStatus' + createdAt: + type: string + format: date-time + + # ── Package schemas ──────────────────────────────────────────────────────── + + PackageListRow: + type: object + required: + - purl + - name + - ecosystem + - openVulns + - maintainerCount + - healthBand + - stewards + properties: + purl: + type: string + example: pkg:npm/%40angular/core@17.0.0 + name: + type: string + ecosystem: + type: string + criticalityScore: + type: number + format: float + nullable: true + stewardshipId: + type: string + nullable: true + stewardshipStatus: + $ref: '#/components/schemas/StewardshipStatus' + nullable: true + openVulns: + type: integer + maxVulnSeverity: + type: string + enum: [critical, high, medium, low] + nullable: true + maintainerCount: + type: integer + scorecardScore: + type: number + format: float + nullable: true + healthBand: + $ref: '#/components/schemas/HealthBand' + latestReleaseAt: + type: string + format: date-time + nullable: true + lastActivity: + type: object + nullable: true + required: [type, content, at] + properties: + type: + type: string + content: + type: string + nullable: true + at: + type: string + format: date-time + stewards: + type: array + items: + $ref: '#/components/schemas/StewardEntry' + + StatusCounts: + type: object + description: Count of packages per stewardship status (used to drive filter pill badges). + required: + [all, unassigned, open, assessing, active, needs_attention, escalated, blocked, inactive] + properties: + all: + type: integer + description: Total count across all statuses (matches total from the package list without status filter). + unassigned: + type: integer + open: + type: integer + assessing: + type: integer + active: + type: integer + needs_attention: + type: integer + escalated: + type: integer + blocked: + type: integer + inactive: + type: integer + example: + all: 95 + unassigned: 42 + open: 5 + assessing: 12 + active: 30 + needs_attention: 3 + escalated: 1 + blocked: 0 + inactive: 2 + + ScatterPoint: + type: object + required: + - purl + - name + - criticalityScore + - healthScore + - healthBand + - openVulns + - advisoryCount + properties: + purl: + type: string + name: + type: string + criticalityScore: + type: integer + description: Impact score scaled to 0–100. + healthScore: + type: integer + description: OpenSSF Scorecard score scaled to 0–100. + healthBand: + $ref: '#/components/schemas/HealthBand' + stewardshipStatus: + $ref: '#/components/schemas/StewardshipStatus' + nullable: true + stewardshipId: + type: string + nullable: true + openVulns: + type: integer + advisoryCount: + type: integer + + PackageMetrics: + type: object + required: [criticalPackages] + properties: + criticalPackages: + type: integer + description: Total packages marked as critical (is_critical = true). + + Advisory: + type: object + required: [osvId, severity, resolution] + properties: + osvId: + type: string + example: GHSA-xxxx-xxxx-xxxx + severity: + type: string + enum: [critical, high, medium, low] + nullable: true + resolution: + type: string + nullable: true + + PackageHistoryEvent: + type: object + required: [id, actorType, activityType, createdAt] + properties: + id: + type: string + actorUserId: + type: string + nullable: true + actorType: + type: string + example: user + activityType: + type: string + example: state_changed + content: + type: string + nullable: true + metadata: + type: object + nullable: true + additionalProperties: true + createdAt: + type: string + format: date-time + + PackageDetail: + type: object + required: + [purl, name, ecosystem, general, assessment, security, provenance, stewardship, history] + properties: + purl: + type: string + name: + type: string + ecosystem: + type: string + latestVersion: + type: string + nullable: true + general: + type: object + properties: + healthScore: + type: integer + nullable: true + description: OpenSSF Scorecard score scaled to 0–100 (scorecardScore × 10, rounded). + healthBand: + $ref: '#/components/schemas/HealthBand' + impact: + type: object + properties: + impactScore: + type: integer + nullable: true + description: Criticality score scaled to 0–100. + downloadsLastMonth: + type: string + nullable: true + description: Raw download count string from the registry. + dependentPackages: + type: integer + nullable: true + dependentRepos: + type: integer + nullable: true + transitiveReach: + type: integer + nullable: true + riskSignals: + type: object + properties: + lifecycle: + type: string + nullable: true + maintainerBusFactor: + type: integer + nullable: true + lastRelease: + type: string + format: date-time + nullable: true + hasSecurityFile: + type: boolean + nullable: true + hasSecurityPolicy: + type: boolean + nullable: true + description: repos.security_policy_enabled + branchProtectionEnabled: + type: boolean + nullable: true + description: repos.branch_protection_enabled + openSSFScorecard: + type: number + format: float + nullable: true + assessment: + nullable: true + description: Reserved for future stewardship assessment fields (G1). + security: + type: object + properties: + securityContacts: + type: array + nullable: true + items: + type: string + advisories: + type: array + items: + $ref: '#/components/schemas/Advisory' + cvd: + type: object + properties: + isPvrEnabled: + type: boolean + nullable: true + tier0Steward: + type: string + nullable: true + criticalVulnerabilityFlag: + type: boolean + nullable: true + provenance: + type: object + properties: + repositoryMapping: + type: object + properties: + declaredRepo: + type: string + nullable: true + mappingConfidence: + type: number + format: float + nullable: true + mappingLabel: + type: string + enum: [High, Medium, Low] + nullable: true + lastCommitAt: + type: string + format: date-time + nullable: true + supplyChainIntegrity: + type: object + properties: + buildProvenance: + type: string + nullable: true + description: Not yet ingested. + signedReleases: + type: string + nullable: true + description: Not yet ingested. + stewardship: + type: object + properties: + id: + type: string + nullable: true + status: + $ref: '#/components/schemas/StewardshipStatus' + origin: + type: string + nullable: true + version: + type: integer + nullable: true + openedAt: + type: string + format: date-time + nullable: true + lastStatusAt: + type: string + format: date-time + nullable: true + resolutionPath: + $ref: '#/components/schemas/EscalationResolutionPath' + nullable: true + statusNote: + type: string + nullable: true + stewards: + type: array + nullable: true + items: + $ref: '#/components/schemas/StewardEntry' + lastActivityAt: + type: string + format: date-time + nullable: true + history: + nullable: true + description: Always null in /detail — full history available at GET /packages/history. + + PackageStewardshipSummary: + type: object + description: Slim stewardship summary returned per-purl by the batch endpoint. + nullable: true + required: [name, ecosystem] + properties: + name: + type: string + ecosystem: + type: string + lifecycle: + type: string + enum: [active, stable, declining, abandoned] + nullable: true + health: + type: number + format: float + nullable: true + impact: + type: integer + nullable: true + description: Criticality score scaled to 0–100. + openVulns: + type: object + nullable: true + properties: + low: + type: integer + medium: + type: integer + high: + type: integer + critical: + type: integer + stewardship: + $ref: '#/components/schemas/StewardshipStatus' + nullable: true + stewards: + type: array + nullable: true + items: + $ref: '#/components/schemas/Steward' + lastActivityAt: + type: string + format: date-time + nullable: true + lastActivityDescription: + type: string + nullable: true + + # ── Pagination wrapper ───────────────────────────────────────────────────── + + PaginationMeta: + type: object + required: [total, page, pageSize] + properties: + total: + type: integer + page: + type: integer + pageSize: + type: integer + +paths: + # ── Dashboard ────────────────────────────────────────────────────────────── + + /akrites/metrics: + get: + operationId: getAkritesMetrics + summary: Get KPI bar metrics + description: > + Returns aggregate counts that power the global KPI bar on the OSSPREY + dashboard (total packages, coverage %, active stewards, unassigned + critical, needs-attention, escalated). + tags: + - Dashboard + responses: + '200': + description: Metrics snapshot. + content: + application/json: + schema: + $ref: '#/components/schemas/OsspreyMetrics' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /akrites/activity: + get: + operationId: getAkritesActivity + summary: Get stewardship activity feed + description: > + Returns a paginated, reverse-chronological list of stewardship activity + events across all packages (status changes, assignments, escalations, etc.). + tags: + - Dashboard + parameters: + - name: page + in: query + schema: + type: integer + minimum: 1 + default: 1 + - name: pageSize + in: query + schema: + type: integer + minimum: 1 + maximum: 100 + default: 25 + responses: + '200': + description: Paginated activity feed. + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginationMeta' + - type: object + required: [rows] + properties: + rows: + type: array + items: + $ref: '#/components/schemas/ActivityEntry' + '400': + description: Validation error. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + # ── Packages (ossprey-derived read endpoints) ────────────────────────────── + + /akrites/packages: + get: + operationId: listAkritesPackages + summary: List packages with stewardship data + description: > + Paginated list of critical packages enriched with stewardship status, + vulnerability counts, health band, and the latest stewardship activity. + Supports rich filtering and sorting. + tags: + - Packages + parameters: + - name: page + in: query + schema: + type: integer + minimum: 1 + default: 1 + - name: pageSize + in: query + schema: + type: integer + minimum: 1 + maximum: 250 + default: 25 + - name: ecosystem + in: query + schema: + type: string + - name: lifecycle + in: query + schema: + type: string + enum: [active, stable, declining, abandoned] + - name: name + in: query + description: Substring match on package name. + schema: + type: string + - name: status + in: query + schema: + $ref: '#/components/schemas/StewardshipStatus' + - name: healthBand + in: query + schema: + $ref: '#/components/schemas/HealthBand' + - name: vulnSeverity + in: query + description: > + Filter by highest open vulnerability severity present on the package. + `none` returns packages with zero vulnerabilities; `any` disables the filter. + schema: + type: string + enum: [any, high, critical, none] + - name: staleOnly + in: query + schema: + type: boolean + default: false + - name: unstewardedOnly + in: query + schema: + type: boolean + default: false + - name: busFactor1Only + in: query + description: Return only packages whose maintainer count is 1. + schema: + type: boolean + default: false + - name: sortBy + in: query + schema: + type: string + enum: [name, risk, impact, openVulns, health] + default: risk + - name: sortDir + in: query + schema: + type: string + enum: [asc, desc] + default: desc + responses: + '200': + description: Paginated package list. + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginationMeta' + - type: object + required: [rows, statusCounts] + properties: + rows: + type: array + items: + $ref: '#/components/schemas/PackageListRow' + statusCounts: + $ref: '#/components/schemas/StatusCounts' + '400': + description: Validation error. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /akrites/packages/scatter: + get: + operationId: getAkritesPackagesScatter + summary: Get risk matrix scatter data + description: > + Returns all critical packages as scatter-plot points with impact score + (x-axis) and health score (y-axis). No pagination — the full dataset is + returned. + tags: + - Packages + responses: + '200': + description: Scatter plot dataset. + content: + application/json: + schema: + type: object + required: [points, total] + properties: + points: + type: array + items: + $ref: '#/components/schemas/ScatterPoint' + total: + type: integer + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /akrites/packages/metrics: + get: + operationId: getAkritesPackagesMetrics + summary: Get package count metrics + description: > + Returns total and critical package counts. Lighter alternative to + `/akrites/metrics` when only package-level counts are needed. + tags: + - Packages + responses: + '200': + description: Package count metrics. + content: + application/json: + schema: + $ref: '#/components/schemas/PackageMetrics' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /akrites/packages/detail: + get: + operationId: getAkritesPackageDetail + summary: Get package detail + description: > + Returns the full detail view for a single package identified by its + PURL, including risk signals, security advisories, repository provenance, + and current stewardship state. + tags: + - Packages + parameters: + - name: purl + in: query + required: true + description: > + Package URL (PURL) — must start with `pkg:`. + Version qualifiers are normalised server-side. + schema: + type: string + example: pkg:npm/%40angular/core@17.0.0 + responses: + '200': + description: Package detail. + content: + application/json: + schema: + $ref: '#/components/schemas/PackageDetail' + '400': + description: Validation error (malformed purl). + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Package not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /akrites/packages/advisories: + get: + operationId: getAkritesPackageAdvisories + summary: Get advisories for a package + description: > + Returns all open security advisories for a single package identified by + PURL. Intended for lazy-loading the Security tab in the package detail drawer. + tags: + - Packages + parameters: + - name: purl + in: query + required: true + description: Package URL (PURL) — must start with `pkg:`. + schema: + type: string + example: pkg:npm/%40angular/core@17.0.0 + responses: + '200': + description: Advisory list. + content: + application/json: + schema: + type: object + required: [advisories, total] + properties: + advisories: + type: array + items: + $ref: '#/components/schemas/Advisory' + total: + type: integer + '400': + description: Validation error (malformed purl). + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Package not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /akrites/packages/history: + get: + operationId: getAkritesPackageHistory + summary: Get stewardship history for a package + description: > + Returns the full activity log for the stewardship associated with the + given PURL, ordered newest-first. Returns an empty list if no stewardship + exists. Intended for lazy-loading the History tab in the package detail drawer. + tags: + - Packages + parameters: + - name: purl + in: query + required: true + description: Package URL (PURL) — must start with `pkg:`. + schema: + type: string + example: pkg:npm/%40angular/core@17.0.0 + responses: + '200': + description: Stewardship activity history. + content: + application/json: + schema: + type: object + required: [events, total] + properties: + events: + type: array + items: + $ref: '#/components/schemas/PackageHistoryEvent' + total: + type: integer + '400': + description: Validation error (malformed purl). + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Package not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /akrites/packages:batch-stewardship: + post: + operationId: batchGetStewardship + summary: Batch stewardship lookup by PURL + description: > + Given up to 100 PURLs, returns a map of `purl → stewardship summary` + for each. Missing packages are returned as `null`. Useful for enriching + external package listings with CDP stewardship state. + tags: + - Packages + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [purls] + properties: + purls: + type: array + minItems: 1 + maxItems: 100 + items: + type: string + description: Must start with `pkg:`. + example: pkg:npm/%40angular/core@17.0.0 + responses: + '200': + description: Per-purl stewardship map. + content: + application/json: + schema: + type: object + required: [packages] + properties: + packages: + type: object + description: > + Keys are the original PURLs from the request. + Values are null when the package is not found in CDP. + additionalProperties: + oneOf: + - $ref: '#/components/schemas/PackageStewardshipSummary' + - type: 'null' + '400': + description: Validation error. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + # ── Stewardships ─────────────────────────────────────────────────────────── + + /akrites/stewardships/me/packages: + get: + operationId: getMyPackages + summary: List packages stewarded by the authenticated user + description: > + Returns a paginated list of packages where the authenticated user + (`req.actor.id`) is an active steward (`lead` or `co_steward`). + Includes package metadata, health, open vulnerabilities, last activity, + and the user's role and stewardship status. + The `meta.statusCounts` object always reflects counts across **all** + of the user's stewardships, regardless of the active `status` filter, + so the tab bar can render all buckets at once. + tags: + - Stewardships + parameters: + - name: page + in: query + schema: + type: integer + minimum: 1 + default: 1 + - name: pageSize + in: query + schema: + type: integer + minimum: 1 + maximum: 100 + default: 25 + - name: status + in: query + description: Filter by stewardship status. + schema: + type: string + enum: [assessing, active, needs_attention, escalated, blocked] + - name: search + in: query + description: Case-insensitive substring match on package name or PURL. + schema: + type: string + - name: ecosystem + in: query + schema: + type: string + enum: [npm, maven, pypi, go, cargo] + - name: healthBand + in: query + schema: + type: string + enum: [healthy, fair, concerning, critical] + - name: vulnSeverity + in: query + description: Filter to packages with at least one vulnerability of this severity or worse. + schema: + type: string + enum: [high, critical] + - name: sortBy + in: query + schema: + type: string + enum: [risk, health, vulns, name, last_activity] + default: risk + - name: sortDir + in: query + schema: + type: string + enum: [asc, desc] + default: desc + responses: + '200': + description: Paginated list of the user's stewarded packages. + content: + application/json: + schema: + type: object + required: [data, meta] + properties: + data: + type: array + items: + type: object + required: + - purl + - name + - ecosystem + - openVulns + - stewardshipId + - stewardshipStatus + - myRole + properties: + purl: + type: string + example: pkg:npm/minimist + name: + type: string + example: minimist + ecosystem: + type: string + example: npm + lifecycle: + type: string + nullable: true + example: abandoned + healthScore: + type: integer + minimum: 0 + maximum: 100 + nullable: true + description: Scorecard score scaled to 0–100. + healthBand: + type: string + enum: [healthy, fair, concerning, critical] + openVulns: + type: integer + example: 2 + vulnSeverity: + type: string + enum: [critical, high, medium, low] + nullable: true + description: Worst open vulnerability severity. + lastActivityDescription: + type: string + nullable: true + example: Escalated for intervention + lastActivityAt: + type: string + format: date-time + nullable: true + stewardshipId: + type: string + example: '42' + stewardshipStatus: + type: string + enum: [assessing, active, needs_attention, escalated, blocked] + myRole: + type: string + enum: [lead, co_steward] + meta: + type: object + required: [total, page, pageSize, statusCounts] + properties: + total: + type: integer + page: + type: integer + pageSize: + type: integer + statusCounts: + type: object + required: [assessing, active, needs_attention, escalated, blocked] + properties: + assessing: + type: integer + active: + type: integer + needs_attention: + type: integer + escalated: + type: integer + blocked: + type: integer + '400': + description: Validation error. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /akrites/stewardships/me/activity: + get: + operationId: getMyActivity + summary: Latest activity feed for the authenticated user's stewardships + description: > + Returns the most recent stewardship activity events scoped to packages + where the authenticated user is an active steward. Results are + **deduplicated by stewardship** — only the single most recent event per + package is returned — and sorted newest-first. + Designed to power the "Latest activity" strip on the My Stewardships + page. Default `pageSize` is 3 (one card per attention-needed status); + increase for a "load more" experience. + tags: + - Stewardships + parameters: + - name: page + in: query + schema: + type: integer + minimum: 1 + default: 1 + - name: pageSize + in: query + schema: + type: integer + minimum: 1 + maximum: 100 + default: 3 + - name: status + in: query + description: > + Comma-separated list of stewardship statuses to filter. + Example: `needs_attention,blocked,escalated,assessing` + schema: + type: string + responses: + '200': + description: Deduplicated activity feed for the user's stewardships. + content: + application/json: + schema: + type: object + required: [data, meta] + properties: + data: + type: array + items: + type: object + required: + - stewardshipId + - packageName + - purl + - packageEcosystem + - stewardshipStatus + - activityType + - createdAt + properties: + stewardshipId: + type: string + example: '42' + packageName: + type: string + example: jackson-databind + purl: + type: string + example: pkg:maven/com.fasterxml.jackson.core/jackson-databind + packageEcosystem: + type: string + example: maven + stewardshipStatus: + type: string + enum: + [ + assessing, + active, + needs_attention, + escalated, + blocked, + unassigned, + open, + inactive, + ] + activityType: + type: string + example: advisory_detected + description: + type: string + nullable: true + example: New security advisory detected + createdAt: + type: string + format: date-time + suggestedAction: + type: string + nullable: true + description: Label for the primary CTA button on the activity card. + example: Review & respond + meta: + type: object + required: [total, page, pageSize] + properties: + total: + type: integer + page: + type: integer + pageSize: + type: integer + '400': + description: Validation error. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /akrites/stewardships/open: + post: + operationId: openStewardship + summary: Open a stewardship for a package + description: > + Creates a new stewardship record for the package identified by the given + PURL, setting its status to `open`. The authenticated user is recorded + as the actor who opened it. + tags: + - Stewardships + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [purl] + properties: + purl: + type: string + description: Must start with `pkg:`. + example: pkg:npm/%40angular/core@17.0.0 + responses: + '200': + description: Stewardship opened. + content: + application/json: + schema: + type: object + required: [stewardship] + properties: + stewardship: + $ref: '#/components/schemas/StewardshipRecord' + '400': + description: Validation error. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Package not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /akrites/stewardships/{id}/assign: + post: + operationId: assignSteward + summary: Assign a steward to a stewardship + description: > + Assigns a user as a steward (lead or co-steward) for the given + stewardship. Optionally transitions the stewardship status to + `assessing` in the same operation. + tags: + - Stewardships + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [userId, role] + properties: + userId: + type: string + description: Auth0 sub of the user to assign. + example: auth0|abc123 + role: + type: string + enum: [lead, co_steward] + note: + type: string + minLength: 1 + description: Optional note stored in the steward_added activity metadata. + moveToAssessing: + type: boolean + default: false + description: > + When true, automatically transitions stewardship status to + `assessing` after assignment. + responses: + '200': + description: Steward assigned. + content: + application/json: + schema: + type: object + required: [stewardship, stewards] + properties: + stewardship: + $ref: '#/components/schemas/StewardshipRecord' + stewards: + type: array + items: + $ref: '#/components/schemas/StewardEntry' + '400': + description: Validation error. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Stewardship not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /akrites/stewardships/{id}/escalate: + post: + operationId: escalateStewardship + summary: Escalate a stewardship + description: > + Transitions the stewardship to `escalated` status and records the chosen + resolution path. Optionally attaches a free-text note. + tags: + - Stewardships + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [resolutionPath] + properties: + resolutionPath: + $ref: '#/components/schemas/EscalationResolutionPath' + notes: + type: string + minLength: 1 + responses: + '200': + description: Stewardship escalated. + content: + application/json: + schema: + type: object + required: [stewardship] + properties: + stewardship: + $ref: '#/components/schemas/StewardshipRecord' + '400': + description: Validation error. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Stewardship not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /akrites/stewardships/{id}/status: + patch: + operationId: updateStewardshipStatus + summary: Update stewardship status + description: > + Updates the stewardship status. Valid target statuses are: + `assessing`, `active`, `needs_attention`, `blocked`, `inactive`. + When setting `inactive`, `inactiveReason` is required. + tags: + - Stewardships + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [status] + properties: + status: + type: string + enum: [assessing, active, needs_attention, blocked, inactive] + inactiveReason: + $ref: '#/components/schemas/InactiveReason' + description: Required when status is `inactive`. + notes: + type: string + minLength: 1 + responses: + '200': + description: Stewardship status updated. + content: + application/json: + schema: + type: object + required: [stewardship] + properties: + stewardship: + $ref: '#/components/schemas/StewardshipRecord' + '400': + description: > + Validation error. Also returned when `status` is `inactive` but + `inactiveReason` is missing. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Stewardship not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' diff --git a/backend/src/api/public/v1/index.ts b/backend/src/api/public/v1/index.ts new file mode 100644 index 0000000000..1dd4501886 --- /dev/null +++ b/backend/src/api/public/v1/index.ts @@ -0,0 +1,51 @@ +import { Router } from 'express' + +import { NotFoundError } from '@crowd/common' + +import { createRateLimiter } from '@/api/apiRateLimiter' +import { safeWrap } from '@/middlewares/errorMiddleware' +import { SCOPES } from '@/security/scopes' + +import { AUTH0_CONFIG } from '../../../conf' +import { oauth2Middleware } from '../middlewares/oauth2Middleware' +import { requireScopes } from '../middlewares/requireScopes' +import { staticApiKeyMiddleware } from '../middlewares/staticApiKeyMiddleware' + +import { memberOrganizationAffiliationsRouter } from './affiliations' +import { akritesRouter } from './akrites' +import { membersRouter } from './members' +import { organizationsRouter } from './organizations' +import { osspreyRouter } from './ossprey' +import { packagesRouter } from './packages' +import { batchGetStewardship } from './packages/batchGetStewardship' +import { stewardshipsRouter } from './stewardships' + +const packagesRateLimiter = createRateLimiter({ max: 60, windowMs: 60 * 1000 }) + +export function v1Router(): Router { + const router = Router() + + router.use('/members', oauth2Middleware(AUTH0_CONFIG), membersRouter()) + router.use('/organizations', oauth2Middleware(AUTH0_CONFIG), organizationsRouter()) + router.use('/affiliations', staticApiKeyMiddleware(), memberOrganizationAffiliationsRouter()) + + // TODO[deprecate]: /packages, /stewardships, /ossprey are superseded by /akrites — remove once consumers have migrated + router.post( + /^\/packages:batch-stewardship\/?$/, + oauth2Middleware(AUTH0_CONFIG), + packagesRateLimiter, + requireScopes([SCOPES.READ_PACKAGES, SCOPES.READ_STEWARDSHIPS], 'all'), + safeWrap(batchGetStewardship), + ) + router.use('/packages', oauth2Middleware(AUTH0_CONFIG), packagesRouter()) + router.use('/stewardships', oauth2Middleware(AUTH0_CONFIG), stewardshipsRouter()) + router.use('/ossprey', oauth2Middleware(AUTH0_CONFIG), osspreyRouter()) + + router.use('/akrites', oauth2Middleware(AUTH0_CONFIG), akritesRouter()) + + router.use(() => { + throw new NotFoundError() + }) + + return router +} diff --git a/backend/src/api/public/v1/members/createMember.ts b/backend/src/api/public/v1/members/createMember.ts new file mode 100644 index 0000000000..075be732ee --- /dev/null +++ b/backend/src/api/public/v1/members/createMember.ts @@ -0,0 +1,92 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { captureApiChange, memberCreateAction, memberEditIdentitiesAction } from '@crowd/audit-logs' +import { getProperDisplayName } from '@crowd/common' +import { + insertManyMemberIdentities, + createMember as insertMember, + optionsQx, +} from '@crowd/data-access-layer' +import { MemberIdentityType } from '@crowd/types' + +import { created } from '@/utils/api' +import { rethrowDbConflict } from '@/utils/err' +import { validateOrThrow } from '@/utils/validation' + +const bodySchema = z.object({ + displayName: z.string().trim().min(1), + identities: z + .array( + z + .object({ + value: z.string().min(1), + platform: z.string().min(1), + type: z.enum(MemberIdentityType), + source: z.string().min(1), + verified: z.boolean(), + verifiedBy: z.string().optional(), + }) + .refine((data) => !data.verified || data.verifiedBy, { + message: 'verifiedBy is required when verified is true', + path: ['verifiedBy'], + }), + ) + .min(1), +}) + +export async function createMember(req: Request, res: Response): Promise { + const { displayName, identities } = validateOrThrow(bodySchema, req.body) + const qx = optionsQx(req) + + const normalizedDisplayName = getProperDisplayName(displayName) + + const { dbMember, dbIdentities } = await qx.tx(async (tx) => { + try { + const dbMember = await insertMember(tx, { + displayName: normalizedDisplayName, + joinedAt: new Date().toISOString(), + attributes: {}, + reach: {}, + // OpenSearch sync only keeps members that either have activities or have manuallyCreated set. + manuallyCreated: true, + }) + + const dbIdentities = await insertManyMemberIdentities( + tx, + identities.map((identity) => ({ + ...identity, + memberId: dbMember.id, + value: identity.value.trim().toLowerCase(), + })), + true, + true, + ) + + return { dbMember, dbIdentities } + } catch (error) { + return rethrowDbConflict(error) + } + }) + + await captureApiChange( + req, + memberCreateAction(dbMember.id, async (captureNewState) => { + captureNewState({ + memberId: dbMember.id, + displayName: dbMember.displayName, + manuallyCreated: true, + }) + }), + ) + + await captureApiChange( + req, + memberEditIdentitiesAction(dbMember.id, async (captureOldState, captureNewState) => { + captureOldState({}) + captureNewState(dbIdentities) + }), + ) + + created(res, { memberId: dbMember.id }) +} diff --git a/backend/src/api/public/v1/members/identities/createMemberIdentity.ts b/backend/src/api/public/v1/members/identities/createMemberIdentity.ts new file mode 100644 index 0000000000..5d20ee7398 --- /dev/null +++ b/backend/src/api/public/v1/members/identities/createMemberIdentity.ts @@ -0,0 +1,133 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { captureApiChange, memberEditIdentitiesAction } from '@crowd/audit-logs' +import { NotFoundError } from '@crowd/common' +import { + MemberField, + findMemberById, + findMemberIdentitiesByValue, + createMemberIdentity as insertMemberIdentity, + optionsQx, + touchMemberUpdatedAt, + updateMemberIdentity, +} from '@crowd/data-access-layer' +import { IMemberIdentity, MemberIdentityType } from '@crowd/types' + +import { created, ok } from '@/utils/api' +import { rethrowDbConflict } from '@/utils/err' +import { validateOrThrow } from '@/utils/validation' + +const paramsSchema = z.object({ + memberId: z.uuid(), +}) + +const bodySchema = z + .object({ + value: z.string().min(1), + platform: z.string().min(1), + type: z.enum(MemberIdentityType), + source: z.string().min(1), + verified: z.boolean(), + verifiedBy: z.string().optional(), + }) + .refine((data) => !data.verified || data.verifiedBy, { + message: 'verifiedBy is required when verified is true', + path: ['verifiedBy'], + }) + +export async function createMemberIdentity(req: Request, res: Response): Promise { + const { memberId } = validateOrThrow(paramsSchema, req.params) + const data = validateOrThrow(bodySchema, req.body) + + const qx = optionsQx(req) + const member = await findMemberById(qx, memberId, [MemberField.ID]) + if (!member) { + throw new NotFoundError('Member not found') + } + + // The data-sink writes identity values as trimmed lowercase, so normalize here + // to keep idempotency checks reliable against existing rows. + const normalizedValue = data.value.trim().toLowerCase() + + let result!: IMemberIdentity + let alreadyExisted = false + + await captureApiChange( + req, + memberEditIdentitiesAction(memberId, async (captureOldState, captureNewState) => { + captureOldState({}) + + await qx.tx(async (tx) => { + const existing = await findMemberIdentitiesByValue(tx, memberId, normalizedValue, { + type: data.type, + }) + const exactMatch = existing.find((i) => i.platform === data.platform) + + try { + if (exactMatch) { + alreadyExisted = true + result = exactMatch + } else { + result = await insertMemberIdentity( + tx, + { + memberId, + platform: data.platform, + value: normalizedValue, + type: data.type, + source: data.source, + verified: data.verified, + verifiedBy: data.verifiedBy, + }, + true, + true, + ) + } + + // A verified identity confirms the same value for this member, so keep same-value + // identities in sync instead of leaving stale unverified duplicates behind. + if (data.verified && existing.length > 0) { + const updatedResults: IMemberIdentity[] = [] + for (const identity of existing) { + const updated = await updateMemberIdentity(tx, memberId, identity.id, { + verified: true, + verifiedBy: data.verifiedBy, + }) + if (updated) updatedResults.push(updated) + } + + if (alreadyExisted) { + result = updatedResults.find((r) => r.id === exactMatch.id) ?? result + } + } + } catch (error) { + const ctx = { platform: data.platform, value: normalizedValue, type: data.type } + rethrowDbConflict(error, ctx) + } + + await touchMemberUpdatedAt(tx, memberId) + }) + + captureNewState(result) + }), + ) + + const response = { + id: result.id, + value: result.value, + platform: result.platform, + type: result.type, + verified: result.verified, + verifiedBy: result.verifiedBy ?? null, + source: result.source ?? null, + createdAt: result.createdAt, + updatedAt: result.updatedAt, + } + + if (alreadyExisted) { + ok(res, response) + } else { + created(res, response) + } +} diff --git a/backend/src/api/public/v1/members/identities/getMemberIdentities.ts b/backend/src/api/public/v1/members/identities/getMemberIdentities.ts new file mode 100644 index 0000000000..aa94fb7204 --- /dev/null +++ b/backend/src/api/public/v1/members/identities/getMemberIdentities.ts @@ -0,0 +1,44 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { NotFoundError } from '@crowd/common' +import { + MemberField, + fetchMemberIdentities, + findMemberById, + optionsQx, +} from '@crowd/data-access-layer' + +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +const paramsSchema = z.object({ + memberId: z.uuid(), +}) + +export async function getMemberIdentities(req: Request, res: Response): Promise { + const { memberId } = validateOrThrow(paramsSchema, req.params) + const qx = optionsQx(req) + + const member = await findMemberById(qx, memberId, [MemberField.ID]) + + if (!member) throw new NotFoundError('Member not found') + + const rawIdentities = await fetchMemberIdentities(qx, memberId) + + const identities = rawIdentities.map( + ({ id, value, platform, type, verified, verifiedBy, source, createdAt, updatedAt }) => ({ + id, + value, + platform, + type, + verified, + verifiedBy: verifiedBy ?? null, + source, + createdAt, + updatedAt, + }), + ) + + ok(res, { identities }) +} diff --git a/backend/src/api/public/v1/members/identities/verifyMemberIdentity.ts b/backend/src/api/public/v1/members/identities/verifyMemberIdentity.ts new file mode 100644 index 0000000000..a7f940b7c6 --- /dev/null +++ b/backend/src/api/public/v1/members/identities/verifyMemberIdentity.ts @@ -0,0 +1,204 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { + captureApiChange, + memberUnmergeAction, + memberVerifyIdentityAction, +} from '@crowd/audit-logs' +import { InternalError, NotFoundError } from '@crowd/common' +import { + invalidateMemberQueryCache, + prepareMemberUnmerge, + startMemberUnmergeWorkflow, + unmergeMember, +} from '@crowd/common_services' +import { + MemberField, + deleteMemberIdentity, + findMemberById, + findMemberIdentityById, + optionsQx, + queryActivityRelations, + updateMemberIdentity, +} from '@crowd/data-access-layer' +import { SlackChannel, SlackPersona, sendSlackNotification } from '@crowd/slack' +import { + IMemberIdentity, + IMemberUnmergePreviewResult, + IUnmergePreviewResult, + MemberUnmergeResult, +} from '@crowd/types' + +import { noContent, ok } from '@/utils/api' +import { rethrowDbConflict } from '@/utils/err' +import { validateOrThrow } from '@/utils/validation' + +const paramsSchema = z.object({ + memberId: z.uuid(), + identityId: z.uuid(), +}) + +const bodySchema = z.object({ + verified: z.boolean(), + verifiedBy: z.string(), +}) + +type MemberUnmergeContext = { + preview: IUnmergePreviewResult + result: MemberUnmergeResult +} + +function toReturn(identity: IMemberIdentity) { + return { + id: identity.id, + value: identity.value, + platform: identity.platform, + type: identity.type, + verified: identity.verified, + verifiedBy: identity.verifiedBy ?? null, + source: identity.source, + createdAt: identity.createdAt, + updatedAt: identity.updatedAt, + } +} + +export async function verifyMemberIdentity(req: Request, res: Response): Promise { + const { memberId, identityId } = validateOrThrow(paramsSchema, req.params) + const { verified, verifiedBy } = validateOrThrow(bodySchema, req.body) + const qx = optionsQx(req) + + const member = await findMemberById(qx, memberId, [MemberField.ID]) + + if (!member) { + throw new NotFoundError('Member not found') + } + + const identity = await findMemberIdentityById(qx, memberId, identityId) + + if (!identity) throw new NotFoundError('Member identity not found') + + let unmerge: MemberUnmergeContext | undefined + let updatedIdentity: IMemberIdentity | undefined + + await captureApiChange( + req, + memberVerifyIdentityAction(memberId, async (captureOldState, captureNewState) => { + captureOldState(identity) + + await qx.tx(async (tx) => { + try { + updatedIdentity = await updateMemberIdentity(tx, memberId, identityId, { + verified, + verifiedBy, + }) + } catch (error) { + if (verified) { + const ctx = { platform: identity.platform, value: identity.value, type: identity.type } + rethrowDbConflict(error, ctx) + } + + throw error + } + + if (!updatedIdentity) { + throw new InternalError('Failed to update member identity') + } + + if (!verified) { + const { count } = await queryActivityRelations(tx, { + filter: { + and: [ + { + memberId: { eq: memberId }, + username: { eq: identity.value }, + platform: { eq: identity.platform }, + }, + ], + }, + limit: 1, + countOnly: true, + }) + + if (count === 0) { + await deleteMemberIdentity(tx, memberId, identityId) + } else { + const preview = await prepareMemberUnmerge(tx, memberId, identityId, false) + const result = await unmergeMember(tx, memberId, preview, req.actor.id) + unmerge = { preview, result } + } + } + }) + + captureNewState(updatedIdentity) + }), + ) + + if (unmerge) { + const { preview, result } = unmerge + + try { + await captureApiChange( + req, + memberUnmergeAction(memberId, async (captureOldState, captureNewState) => { + captureOldState({ primary: preview.primary }) + captureNewState({ + primary: result.primary, + secondary: result.secondary, + }) + }), + ) + } catch (error) { + req.log.warn({ error }, 'Audit log capture failed after identity unmerge') + sendSlackNotification( + SlackChannel.CDP_ALERTS, + SlackPersona.ERROR_REPORTER, + `Audit log capture failed after identity unmerge: member ${memberId}`, + [{ title: 'Error', text: `\`${error?.message || error}\`` }], + ) + } + + try { + await invalidateMemberQueryCache(req.redis, [result.primary.id, result.secondary.id], true) + } catch (error) { + req.log.warn({ error }, 'Cache invalidation failed after identity unmerge') + } + + try { + await startMemberUnmergeWorkflow(req.temporal, { + primaryId: result.primary.id, + secondaryId: result.secondary.id, + movedIdentities: result.movedIdentities, + primaryDisplayName: result.primary.displayName, + secondaryDisplayName: result.secondary.displayName, + actorId: req.actor.id, + }) + } catch (error) { + req.log.warn({ error }, 'Failed to start unmerge workflow after identity unmerge') + sendSlackNotification( + SlackChannel.CDP_ALERTS, + SlackPersona.ERROR_REPORTER, + `Failed to start unmerge workflow after identity unmerge: member ${memberId}`, + [ + { + title: 'Context', + text: `*Primary:* \`${result.primary.id}\`\n*Secondary:* \`${result.secondary.id}\``, + }, + { title: 'Error', text: `\`${error?.message || error}\`` }, + ], + ) + } + } + + // If verified = false and no activities (deleted): 204 No Content + if (!verified && !unmerge) { + noContent(res) + return + } + + // If verified = false and has activities (unmerge): 200 OK + unmergedToMemberId + ok(res, { + ...toReturn(updatedIdentity), + ...(unmerge && { unmergedToMemberId: unmerge.result.secondary.id }), + }) +} diff --git a/backend/src/api/public/v1/members/index.ts b/backend/src/api/public/v1/members/index.ts new file mode 100644 index 0000000000..12eab8e837 --- /dev/null +++ b/backend/src/api/public/v1/members/index.ts @@ -0,0 +1,95 @@ +import { Router } from 'express' + +import { requireScopes } from '@/api/public/middlewares/requireScopes' +import { safeWrap } from '@/middlewares/errorMiddleware' +import { SCOPES } from '@/security/scopes' + +import { createMember } from './createMember' +import { createMemberIdentity } from './identities/createMemberIdentity' +import { getMemberIdentities } from './identities/getMemberIdentities' +import { verifyMemberIdentity } from './identities/verifyMemberIdentity' +import { getMemberMaintainerRoles } from './maintainer-roles/getMemberMaintainerRoles' +import { getProjectAffiliations } from './project-affiliations/getProjectAffiliations' +import { patchProjectAffiliation } from './project-affiliations/patchProjectAffiliation' +import { resolveMemberByIdentities } from './resolveMember' +import { createMemberWorkExperience } from './work-experiences/createMemberWorkExperience' +import { deleteMemberWorkExperience } from './work-experiences/deleteMemberWorkExperience' +import { getMemberWorkExperiences } from './work-experiences/getMemberWorkExperiences' +import { updateMemberWorkExperience } from './work-experiences/updateMemberWorkExperience' +import { verifyMemberWorkExperience } from './work-experiences/verifyMemberWorkExperience' + +export function membersRouter(): Router { + const router = Router() + + router.post('/', requireScopes([SCOPES.WRITE_MEMBERS]), safeWrap(createMember)) + + router.post('/resolve', requireScopes([SCOPES.READ_MEMBERS]), safeWrap(resolveMemberByIdentities)) + + router.get( + '/:memberId/identities', + requireScopes([SCOPES.READ_MEMBER_IDENTITIES]), + safeWrap(getMemberIdentities), + ) + + router.post( + '/:memberId/identities', + requireScopes([SCOPES.WRITE_MEMBER_IDENTITIES]), + safeWrap(createMemberIdentity), + ) + + router.patch( + '/:memberId/identities/:identityId', + requireScopes([SCOPES.WRITE_MEMBER_IDENTITIES]), + safeWrap(verifyMemberIdentity), + ) + + router.get( + '/:memberId/maintainer-roles', + requireScopes([SCOPES.READ_MAINTAINER_ROLES]), + safeWrap(getMemberMaintainerRoles), + ) + + router.get( + '/:memberId/project-affiliations', + requireScopes([SCOPES.READ_PROJECT_AFFILIATIONS]), + safeWrap(getProjectAffiliations), + ) + + router.patch( + '/:memberId/project-affiliations/:projectId', + requireScopes([SCOPES.WRITE_PROJECT_AFFILIATIONS]), + safeWrap(patchProjectAffiliation), + ) + + router.post( + '/:memberId/work-experiences', + requireScopes([SCOPES.WRITE_WORK_EXPERIENCES]), + safeWrap(createMemberWorkExperience), + ) + + router.get( + '/:memberId/work-experiences', + requireScopes([SCOPES.READ_WORK_EXPERIENCES]), + safeWrap(getMemberWorkExperiences), + ) + + router.put( + '/:memberId/work-experiences/:workExperienceId', + requireScopes([SCOPES.WRITE_WORK_EXPERIENCES]), + safeWrap(updateMemberWorkExperience), + ) + + router.patch( + '/:memberId/work-experiences/:workExperienceId', + requireScopes([SCOPES.WRITE_WORK_EXPERIENCES]), + safeWrap(verifyMemberWorkExperience), + ) + + router.delete( + '/:memberId/work-experiences/:workExperienceId', + requireScopes([SCOPES.WRITE_WORK_EXPERIENCES]), + safeWrap(deleteMemberWorkExperience), + ) + + return router +} diff --git a/backend/src/api/public/v1/members/maintainer-roles/getMemberMaintainerRoles.ts b/backend/src/api/public/v1/members/maintainer-roles/getMemberMaintainerRoles.ts new file mode 100644 index 0000000000..b24dfec61c --- /dev/null +++ b/backend/src/api/public/v1/members/maintainer-roles/getMemberMaintainerRoles.ts @@ -0,0 +1,32 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { NotFoundError } from '@crowd/common' +import { + MemberField, + findMaintainerRoles, + findMemberById, + optionsQx, +} from '@crowd/data-access-layer' + +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +const paramsSchema = z.object({ + memberId: z.uuid(), +}) + +export async function getMemberMaintainerRoles(req: Request, res: Response): Promise { + const { memberId } = validateOrThrow(paramsSchema, req.params) + const qx = optionsQx(req) + + const member = await findMemberById(qx, memberId, [MemberField.ID]) + + if (!member) { + throw new NotFoundError('Member not found') + } + + const maintainerRoles = await findMaintainerRoles(qx, [memberId]) + + ok(res, { maintainerRoles }) +} diff --git a/backend/src/api/public/v1/members/project-affiliations/getProjectAffiliations.ts b/backend/src/api/public/v1/members/project-affiliations/getProjectAffiliations.ts new file mode 100644 index 0000000000..db3e18de43 --- /dev/null +++ b/backend/src/api/public/v1/members/project-affiliations/getProjectAffiliations.ts @@ -0,0 +1,86 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { NotFoundError } from '@crowd/common' +import { + MemberField, + fetchMemberProjectSegments, + fetchMemberSegmentAffiliationsWithOrg, + fetchMemberWorkExperienceAffiliations, + findMaintainerRoles, + findMemberById, + optionsQx, +} from '@crowd/data-access-layer' + +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +import { mapSegmentAffiliation, mapWorkExperienceAffiliation } from './mappers' + +const paramsSchema = z.object({ + memberId: z.uuid(), +}) + +export async function getProjectAffiliations(req: Request, res: Response): Promise { + const { memberId } = validateOrThrow(paramsSchema, req.params) + const qx = optionsQx(req) + + const member = await findMemberById(qx, memberId, [MemberField.ID]) + + if (!member) { + throw new NotFoundError('Member not found') + } + + const [projectSegments, maintainerRoles, segmentAffiliations, workExperiences] = + await Promise.all([ + fetchMemberProjectSegments(qx, memberId), + findMaintainerRoles(qx, [memberId]), + fetchMemberSegmentAffiliationsWithOrg(qx, memberId), + fetchMemberWorkExperienceAffiliations(qx, memberId), + ]) + + // Group maintainer roles by segmentId + const rolesBySegment = new Map() + for (const role of maintainerRoles) { + const existing = rolesBySegment.get(role.segmentId) ?? [] + existing.push(role) + rolesBySegment.set(role.segmentId, existing) + } + + // Group segment affiliations by segmentId + const affiliationsBySegment = new Map() + for (const aff of segmentAffiliations) { + const existing = affiliationsBySegment.get(aff.segmentId) ?? [] + existing.push(aff) + affiliationsBySegment.set(aff.segmentId, existing) + } + + const projectAffiliations = projectSegments.map((segment) => { + const roles = (rolesBySegment.get(segment.id) ?? []).map((r) => ({ + id: r.id, + role: r.role, + startDate: r.dateStart ?? null, + endDate: r.dateEnd ?? null, + repoUrl: r.url ?? null, + repoFileUrl: r.maintainerFile ?? null, + })) + + // Use segment affiliations if they exist for this project, otherwise fall back to work experiences + const segmentAffs = affiliationsBySegment.get(segment.id) + const affiliations = segmentAffs + ? segmentAffs.map(mapSegmentAffiliation) + : workExperiences.map(mapWorkExperienceAffiliation) + + return { + id: segment.id, + projectSlug: segment.slug, + projectName: segment.name, + projectLogo: segment.projectLogo ?? null, + contributionCount: Number(segment.activityCount), + roles, + affiliations, + } + }) + + ok(res, { projectAffiliations }) +} diff --git a/backend/src/api/public/v1/members/project-affiliations/mappers.ts b/backend/src/api/public/v1/members/project-affiliations/mappers.ts new file mode 100644 index 0000000000..2c5da22964 --- /dev/null +++ b/backend/src/api/public/v1/members/project-affiliations/mappers.ts @@ -0,0 +1,40 @@ +import type { + ISegmentAffiliationWithOrg, + IWorkExperienceAffiliation, +} from '@crowd/data-access-layer' + +export const AFFILIATION_TYPE = { + PROJECT: 'project', + WORK_HISTORY: 'work-history', +} as const + +export type AffiliationType = (typeof AFFILIATION_TYPE)[keyof typeof AFFILIATION_TYPE] + +export function mapSegmentAffiliation(a: ISegmentAffiliationWithOrg) { + return { + id: a.id, + organizationId: a.organizationId, + organizationName: a.organizationName, + organizationLogo: a.organizationLogo ?? null, + verified: a.verified, + verifiedBy: a.verifiedBy ?? null, + startDate: a.dateStart ?? null, + endDate: a.dateEnd ?? null, + type: AFFILIATION_TYPE.PROJECT, + } +} + +export function mapWorkExperienceAffiliation(a: IWorkExperienceAffiliation) { + return { + id: a.id, + organizationId: a.organizationId, + organizationName: a.organizationName, + organizationLogo: a.organizationLogo ?? null, + verified: a.verified ?? false, + verifiedBy: a.verifiedBy ?? null, + source: a.source ?? null, + startDate: a.dateStart ?? null, + endDate: a.dateEnd ?? null, + type: AFFILIATION_TYPE.WORK_HISTORY, + } +} diff --git a/backend/src/api/public/v1/members/project-affiliations/patchProjectAffiliation.ts b/backend/src/api/public/v1/members/project-affiliations/patchProjectAffiliation.ts new file mode 100644 index 0000000000..fe516a38e9 --- /dev/null +++ b/backend/src/api/public/v1/members/project-affiliations/patchProjectAffiliation.ts @@ -0,0 +1,132 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { captureApiChange, memberEditAffiliationsAction } from '@crowd/audit-logs' +import { NotFoundError } from '@crowd/common' +import { signalMemberUpdate } from '@crowd/common_services' +import { + MemberField, + fetchMemberProjectSegments, + fetchMemberSegmentAffiliationsForProject, + findMaintainerRoles, + findMemberById, + insertMemberSegmentAffiliations, + optionsQx, +} from '@crowd/data-access-layer' +import type { ISegmentAffiliationWithOrg } from '@crowd/data-access-layer' +import { deleteMemberSegmentAffiliations } from '@crowd/data-access-layer/src/member_segment_affiliations' + +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +import { mapSegmentAffiliation } from './mappers' + +const paramsSchema = z.object({ + memberId: z.uuid(), + projectId: z.uuid(), +}) + +const bodySchema = z + .object({ + affiliations: z.array( + z + .object({ + organizationId: z.uuid(), + dateStart: z.coerce.date(), + dateEnd: z.coerce.date().nullable().optional(), + }) + .refine((a) => a.dateEnd == null || a.dateEnd >= a.dateStart, { + message: 'dateEnd must be greater than or equal to dateStart', + }), + ), + verifiedBy: z.string().max(255).optional(), + }) + .refine((b) => b.affiliations.length === 0 || b.verifiedBy != null, { + message: 'verifiedBy is required when affiliations is non-empty', + path: ['verifiedBy'], + }) + +export async function patchProjectAffiliation(req: Request, res: Response): Promise { + const { memberId, projectId } = validateOrThrow(paramsSchema, req.params) + const { affiliations, verifiedBy } = validateOrThrow(bodySchema, req.body) + + const qx = optionsQx(req) + + const member = await findMemberById(qx, memberId, [MemberField.ID]) + if (!member) { + throw new NotFoundError('Member not found') + } + + const [segment] = await fetchMemberProjectSegments(qx, memberId, projectId) + if (!segment) { + throw new NotFoundError('Project not found') + } + + const existingAffiliations = await fetchMemberSegmentAffiliationsForProject( + qx, + memberId, + projectId, + ) + + let updatedAffiliations: ISegmentAffiliationWithOrg[] = [] + + await captureApiChange( + req, + memberEditAffiliationsAction(memberId, async (captureOldState, captureNewState) => { + captureOldState(existingAffiliations) + + const oldOrgIds = existingAffiliations.map((a) => a.organizationId) + const newOrgIds = affiliations.map((a) => a.organizationId) + const orgIdsToRecalculate = [...new Set([...oldOrgIds, ...newOrgIds])] + + await qx.tx(async (tx) => { + await deleteMemberSegmentAffiliations(tx, { memberId, segmentId: projectId }) + + if (affiliations.length > 0) { + await insertMemberSegmentAffiliations( + tx, + memberId, + projectId, + affiliations.map((a) => ({ + organizationId: a.organizationId, + dateStart: a.dateStart.toISOString(), + dateEnd: a.dateEnd?.toISOString() ?? null, + verifiedBy: verifiedBy!, + })), + ) + } + }) + + // Signal after commit so the workflow sees persisted changes + await signalMemberUpdate(req.temporal, memberId, { + memberOrganizationIds: orgIdsToRecalculate, + }) + + updatedAffiliations = await fetchMemberSegmentAffiliationsForProject(qx, memberId, projectId) + captureNewState(updatedAffiliations) + }), + ) + + const maintainerRoles = await findMaintainerRoles(qx, [memberId]) + + const roles = maintainerRoles + .filter((r) => r.segmentId === projectId) + .map((r) => ({ + id: r.id, + role: r.role, + startDate: r.dateStart ?? null, + endDate: r.dateEnd ?? null, + repoUrl: r.url ?? null, + repoFileUrl: r.maintainerFile ?? null, + })) + + ok(res, { + id: segment.id, + projectSlug: segment.slug, + projectName: segment.name, + projectLogo: segment.projectLogo ?? null, + contributionCount: Number(segment.activityCount), + roles, + affiliations: updatedAffiliations.map(mapSegmentAffiliation), + }) +} diff --git a/backend/src/api/public/v1/members/resolveMember.ts b/backend/src/api/public/v1/members/resolveMember.ts new file mode 100644 index 0000000000..0d9f654f4e --- /dev/null +++ b/backend/src/api/public/v1/members/resolveMember.ts @@ -0,0 +1,46 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { ConflictError, NotFoundError } from '@crowd/common' +import { findMemberIdsByIdentities, optionsQx } from '@crowd/data-access-layer' +import { IMemberIdentity, MemberIdentityType, PlatformType } from '@crowd/types' + +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +const bodySchema = z.object({ + lfids: z.array(z.string().trim()).min(1, 'At least one lfid is required'), + emails: z.array(z.email()).optional(), +}) + +export async function resolveMemberByIdentities(req: Request, res: Response): Promise { + const { lfids, emails } = validateOrThrow(bodySchema, req.body) + + const qx = optionsQx(req) + + const identities: Partial[] = [ + ...lfids.map((lfid) => ({ + platform: PlatformType.LFID, + type: MemberIdentityType.USERNAME, + value: lfid, + verified: true, + })), + ...(emails?.map((email) => ({ + type: MemberIdentityType.EMAIL, + value: email, + verified: true, + })) ?? []), + ] + + const memberIds = await findMemberIdsByIdentities(qx, identities) + + if (memberIds.length === 0) { + throw new NotFoundError('Member not found') + } else if (memberIds.length > 1) { + throw new ConflictError('Multiple member profiles matched', { memberIds }) + } + + const memberId = memberIds[0] + + ok(res, { memberId }) +} diff --git a/backend/src/api/public/v1/members/work-experiences/createMemberWorkExperience.ts b/backend/src/api/public/v1/members/work-experiences/createMemberWorkExperience.ts new file mode 100644 index 0000000000..86c8d7e8c7 --- /dev/null +++ b/backend/src/api/public/v1/members/work-experiences/createMemberWorkExperience.ts @@ -0,0 +1,132 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { captureApiChange, memberEditOrganizationsAction } from '@crowd/audit-logs' +import { + BadRequestError, + ConflictError, + NotFoundError, + sanitizeMemberOrganizationDateRange, +} from '@crowd/common' +import { signalMemberUpdate } from '@crowd/common_services' +import { + MemberField, + changeMemberOrganizationAffiliationOverrides, + cleanSoftDeletedMemberOrganization, + createMemberOrganization, + fetchManyMemberOrgsWithOrgData, + fetchManyOrganizationAffiliationPolicies, + findMemberById, + optionsQx, +} from '@crowd/data-access-layer' +import { deleteMemberSegmentAffiliations } from '@crowd/data-access-layer/src/member_segment_affiliations' +import type { + IMemberOrganization, + IMemberRoleWithOrganization, + MemberOrganizationDateRange, +} from '@crowd/types' + +import { created } from '@/utils/api' +import { toMemberWorkExperience } from '@/utils/mapper' +import { validateOrThrow } from '@/utils/validation' + +const paramsSchema = z.object({ + memberId: z.uuid(), +}) + +const bodySchema = z.object({ + organizationId: z.uuid(), + jobTitle: z.string(), + verified: z.boolean(), + verifiedBy: z.string(), + source: z.string(), + startDate: z.coerce.date(), + endDate: z.coerce.date().nullable().optional(), +}) + +export async function createMemberWorkExperience(req: Request, res: Response): Promise { + const { memberId } = validateOrThrow(paramsSchema, req.params) + const data = validateOrThrow(bodySchema, req.body) + + const qx = optionsQx(req) + + const member = await findMemberById(qx, memberId, [MemberField.ID]) + + if (!member) { + throw new NotFoundError('Member not found') + } + + let createdMo: IMemberRoleWithOrganization | undefined + + await captureApiChange( + req, + memberEditOrganizationsAction(memberId, async (captureOldState, captureNewState) => { + captureOldState({}) + + let dates: MemberOrganizationDateRange + + try { + dates = sanitizeMemberOrganizationDateRange(data.startDate, data.endDate, true) + } catch (error) { + throw new BadRequestError('Invalid work experience date range') + } + + const memberOrgData: IMemberOrganization = { + memberId, + organizationId: data.organizationId, + title: data.jobTitle, + dateStart: dates.dateStart, + dateEnd: dates.dateEnd, + source: data.source, + verified: data.verified, + verifiedBy: data.verifiedBy, + } + + let newMemberOrgId: string | undefined + + await qx.tx(async (tx) => { + await cleanSoftDeletedMemberOrganization(tx, memberId, data.organizationId, memberOrgData) + + newMemberOrgId = await createMemberOrganization(tx, memberId, memberOrgData) + + if (!newMemberOrgId) { + throw new ConflictError('A work experience with the same dates already exists') + } + + const orgAffiliationPolicyById = await fetchManyOrganizationAffiliationPolicies(tx, [ + data.organizationId, + ]) + + if (newMemberOrgId && orgAffiliationPolicyById.get(data.organizationId)) { + await changeMemberOrganizationAffiliationOverrides(tx, [ + { + memberId, + memberOrganizationId: newMemberOrgId, + allowAffiliation: false, + }, + ]) + await deleteMemberSegmentAffiliations(tx, { + memberId, + organizationId: data.organizationId, + }) + } + }) + + // Signal after commit so the workflow sees persisted changes + await signalMemberUpdate(req.temporal, memberId, { + memberOrganizationIds: [data.organizationId], + }) + + const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId]) + createdMo = (orgsMap.get(memberId) ?? []).find((mo) => mo.id === newMemberOrgId) + + captureNewState(createdMo ?? null) + }), + ) + + if (!createdMo) { + throw new NotFoundError('Work experience not found after creation') + } + + created(res, toMemberWorkExperience(createdMo)) +} diff --git a/backend/src/api/public/v1/members/work-experiences/deleteMemberWorkExperience.ts b/backend/src/api/public/v1/members/work-experiences/deleteMemberWorkExperience.ts new file mode 100644 index 0000000000..80784bff0b --- /dev/null +++ b/backend/src/api/public/v1/members/work-experiences/deleteMemberWorkExperience.ts @@ -0,0 +1,71 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { captureApiChange, memberEditOrganizationsAction } from '@crowd/audit-logs' +import { NotFoundError } from '@crowd/common' +import { signalMemberUpdate } from '@crowd/common_services' +import { + MemberField, + deleteMemberOrganizations, + fetchMemberOrganizations, + findMemberById, + optionsQx, +} from '@crowd/data-access-layer' + +import { noContent } from '@/utils/api' +import { getOverlappingEmailDomainMemberOrganizations } from '@/utils/mapper' +import { validateOrThrow } from '@/utils/validation' + +const paramsSchema = z.object({ + memberId: z.uuid(), + workExperienceId: z.uuid(), +}) + +export async function deleteMemberWorkExperience(req: Request, res: Response): Promise { + const { memberId, workExperienceId } = validateOrThrow(paramsSchema, req.params) + + const qx = optionsQx(req) + + const member = await findMemberById(qx, memberId, [MemberField.ID]) + + if (!member) { + throw new NotFoundError('Member not found') + } + + const memberOrgs = await fetchMemberOrganizations(qx, memberId) + const memberOrg = memberOrgs.find((mo) => mo.id === workExperienceId) + + if (!memberOrg) { + throw new NotFoundError('Work experience not found') + } + + const overlappingEmailDomainRows = getOverlappingEmailDomainMemberOrganizations( + memberOrgs, + memberOrg, + ) + + const memberOrgIdsToDelete = [ + workExperienceId, + ...overlappingEmailDomainRows.flatMap((row) => (row.id ? [row.id] : [])), + ] + + // Delete hidden grouped rows with the visible row so read responses stay consistent + await captureApiChange( + req, + memberEditOrganizationsAction(memberId, async (captureOldState, captureNewState) => { + captureOldState(memberOrg) + + await qx.tx(async (tx) => { + await deleteMemberOrganizations(tx, memberId, memberOrgIdsToDelete) + }) + + // Signal after commit so the workflow sees persisted changes + await signalMemberUpdate(req.temporal, memberId, { + memberOrganizationIds: [memberOrg.organizationId], + }) + + captureNewState(null) + }), + ) + noContent(res) +} diff --git a/backend/src/api/public/v1/members/work-experiences/getMemberWorkExperiences.ts b/backend/src/api/public/v1/members/work-experiences/getMemberWorkExperiences.ts new file mode 100644 index 0000000000..c253f49f3b --- /dev/null +++ b/backend/src/api/public/v1/members/work-experiences/getMemberWorkExperiences.ts @@ -0,0 +1,36 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { NotFoundError } from '@crowd/common' +import { + MemberField, + fetchManyMemberOrgsWithOrgData, + findMemberById, + optionsQx, +} from '@crowd/data-access-layer' + +import { ok } from '@/utils/api' +import { groupMemberOrganizations, toMemberWorkExperience } from '@/utils/mapper' +import { validateOrThrow } from '@/utils/validation' + +const paramsSchema = z.object({ + memberId: z.uuid(), +}) + +export async function getMemberWorkExperiences(req: Request, res: Response): Promise { + const { memberId } = validateOrThrow(paramsSchema, req.params) + const qx = optionsQx(req) + + const member = await findMemberById(qx, memberId, [MemberField.ID]) + + if (!member) { + throw new NotFoundError('Member not found') + } + + const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId]) + const workExperiences = groupMemberOrganizations(orgsMap.get(memberId) ?? []).map( + toMemberWorkExperience, + ) + + ok(res, { memberId, workExperiences }) +} diff --git a/backend/src/api/public/v1/members/work-experiences/updateMemberWorkExperience.ts b/backend/src/api/public/v1/members/work-experiences/updateMemberWorkExperience.ts new file mode 100644 index 0000000000..b0e04ebb05 --- /dev/null +++ b/backend/src/api/public/v1/members/work-experiences/updateMemberWorkExperience.ts @@ -0,0 +1,139 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { captureApiChange, memberEditOrganizationsAction } from '@crowd/audit-logs' +import { BadRequestError, NotFoundError, sanitizeMemberOrganizationDateRange } from '@crowd/common' +import { signalMemberUpdate } from '@crowd/common_services' +import { + MemberField, + cleanSoftDeletedMemberOrganization, + fetchManyMemberOrgsWithOrgData, + fetchMemberOrganizations, + findMemberById, + optionsQx, + updateMemberOrganization, +} from '@crowd/data-access-layer' +import type { MemberOrganizationDateRange, MemberOrganizationUpdate } from '@crowd/types' + +import { ok } from '@/utils/api' +import { + getOverlappingEmailDomainMemberOrganizations, + groupMemberOrganizations, + toMemberWorkExperience, +} from '@/utils/mapper' +import { validateOrThrow } from '@/utils/validation' + +const paramsSchema = z.object({ + memberId: z.uuid(), + workExperienceId: z.uuid(), +}) + +const bodySchema = z.object({ + organizationId: z.uuid(), + jobTitle: z.string(), + verified: z.boolean(), + verifiedBy: z.string(), + source: z.string(), + startDate: z.coerce.date(), + endDate: z.coerce.date().nullable().optional(), +}) + +export async function updateMemberWorkExperience(req: Request, res: Response): Promise { + const { memberId, workExperienceId } = validateOrThrow(paramsSchema, req.params) + const data = validateOrThrow(bodySchema, req.body) + + const qx = optionsQx(req) + + const member = await findMemberById(qx, memberId, [MemberField.ID]) + + if (!member) { + throw new NotFoundError('Member not found') + } + + const memberOrgs = await fetchMemberOrganizations(qx, memberId) + const existing = memberOrgs.find((mo) => mo.id === workExperienceId) + + if (!existing) { + throw new NotFoundError('Work experience not found') + } + + let dates: MemberOrganizationDateRange + + try { + dates = sanitizeMemberOrganizationDateRange(data.startDate, data.endDate, true) + } catch (error) { + throw new BadRequestError('Invalid work experience date range') + } + + const update: MemberOrganizationUpdate = { + organizationId: data.organizationId, + title: data.jobTitle, + verified: data.verified, + verifiedBy: data.verifiedBy, + source: data.source, + dateStart: dates.dateStart, + dateEnd: dates.dateEnd, + } + + let updated: ReturnType | undefined + + await captureApiChange( + req, + memberEditOrganizationsAction(memberId, async (captureOldState, captureNewState) => { + captureOldState(existing) + + await qx.tx(async (tx) => { + await cleanSoftDeletedMemberOrganization(tx, memberId, data.organizationId, update) + await updateMemberOrganization(tx, memberId, workExperienceId, update) + + const overlapBasis = { ...existing, ...update } + + const overlappingEmailDomainRows = getOverlappingEmailDomainMemberOrganizations( + memberOrgs, + overlapBasis, + ) + + const groupedUpdate: MemberOrganizationUpdate = {} + + // Keep grouped rows aligned for shared display fields; dates stay on the edited row + if (data.jobTitle !== undefined) { + groupedUpdate.title = data.jobTitle + } + if (data.verified !== undefined) { + groupedUpdate.verified = data.verified + } + if (data.verifiedBy !== undefined) { + groupedUpdate.verifiedBy = data.verifiedBy + } + + if (overlappingEmailDomainRows.length > 0 && Object.keys(groupedUpdate).length > 0) { + for (const overlappingRow of overlappingEmailDomainRows.filter( + (row): row is typeof row & { id: string } => !!row.id, + )) { + await updateMemberOrganization(tx, memberId, overlappingRow.id, groupedUpdate) + } + } + }) + + // Signal after commit so the workflow sees persisted changes + await signalMemberUpdate(req.temporal, memberId, { + memberOrganizationIds: [data.organizationId], + }) + + const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId]) + + const updatedMo = groupMemberOrganizations(orgsMap.get(memberId) ?? []).find( + (mo) => mo.id === workExperienceId, + ) + + if (!updatedMo) { + throw new NotFoundError('Work experience not found') + } + + captureNewState(updatedMo) + updated = toMemberWorkExperience(updatedMo) + }), + ) + + ok(res, updated) +} diff --git a/backend/src/api/public/v1/members/work-experiences/verifyMemberWorkExperience.ts b/backend/src/api/public/v1/members/work-experiences/verifyMemberWorkExperience.ts new file mode 100644 index 0000000000..a4fb2b3a95 --- /dev/null +++ b/backend/src/api/public/v1/members/work-experiences/verifyMemberWorkExperience.ts @@ -0,0 +1,115 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { captureApiChange, memberVerifyWorkExperienceAction } from '@crowd/audit-logs' +import { NotFoundError } from '@crowd/common' +import { signalMemberUpdate } from '@crowd/common_services' +import { + MemberField, + deleteMemberOrganizations, + fetchManyMemberOrgsWithOrgData, + fetchMemberOrganizations, + findMemberById, + optionsQx, + updateMemberOrganization, +} from '@crowd/data-access-layer' +import { IMemberOrganization, IMemberRoleWithOrganization } from '@crowd/types' + +import { ok } from '@/utils/api' +import { + getOverlappingEmailDomainMemberOrganizations, + groupMemberOrganizations, + toMemberWorkExperience, +} from '@/utils/mapper' +import { validateOrThrow } from '@/utils/validation' + +const paramsSchema = z.object({ + memberId: z.uuid(), + workExperienceId: z.uuid(), +}) + +const bodySchema = z.object({ + verified: z.boolean(), + verifiedBy: z.string(), +}) + +export async function verifyMemberWorkExperience(req: Request, res: Response): Promise { + const { memberId, workExperienceId } = validateOrThrow(paramsSchema, req.params) + const { verified, verifiedBy } = validateOrThrow(bodySchema, req.body) + + const qx = optionsQx(req) + + const member = await findMemberById(qx, memberId, [MemberField.ID]) + + if (!member) { + throw new NotFoundError('Member not found') + } + + const memberOrgs = await fetchMemberOrganizations(qx, memberId) + const memberOrg = memberOrgs.find((mo) => mo.id === workExperienceId) + + if (!memberOrg) { + throw new NotFoundError('Work experience not found') + } + + const overlappingEmailDomainRows = getOverlappingEmailDomainMemberOrganizations( + memberOrgs, + memberOrg, + ) + + const memberOrgIdsToDelete = [ + workExperienceId, + ...overlappingEmailDomainRows.flatMap((row) => (row.id ? [row.id] : [])), + ] + + const verifiedUpdate = { verified, verifiedBy } + + let updatedMemberOrg: IMemberOrganization | undefined + + await captureApiChange( + req, + memberVerifyWorkExperienceAction(memberId, async (captureOldState, captureNewState) => { + captureOldState(memberOrg) + + await qx.tx(async (tx) => { + if (verified) { + // Verification status belongs to the grouped work experience, not just the visible row + updatedMemberOrg = await updateMemberOrganization( + tx, + memberId, + workExperienceId, + verifiedUpdate, + ) + + for (const overlappingRow of overlappingEmailDomainRows.filter( + (row): row is typeof row & { id: string } => !!row.id, + )) { + await updateMemberOrganization(tx, memberId, overlappingRow.id, verifiedUpdate) + } + } else { + // Unverifying removes the grouped work experience from both visible and hidden rows + await deleteMemberOrganizations(tx, memberId, memberOrgIdsToDelete, true) + } + }) + + // Signal after commit so the workflow sees persisted changes + if (!verified) { + await signalMemberUpdate(req.temporal, memberId, { + memberOrganizationIds: [memberOrg.organizationId], + }) + } + + captureNewState(updatedMemberOrg ?? { ...memberOrg, verified, verifiedBy }) + }), + ) + + const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId]) + + const responseMo: IMemberRoleWithOrganization = + groupMemberOrganizations(orgsMap.get(memberId) ?? []).find( + (mo) => mo.id === workExperienceId, + ) ?? + ({ ...memberOrg, ...updatedMemberOrg, verified, verifiedBy } as IMemberRoleWithOrganization) + + ok(res, toMemberWorkExperience(responseMo)) +} diff --git a/backend/src/api/public/v1/organizations/createOrganization.ts b/backend/src/api/public/v1/organizations/createOrganization.ts new file mode 100644 index 0000000000..d8f47714a1 --- /dev/null +++ b/backend/src/api/public/v1/organizations/createOrganization.ts @@ -0,0 +1,65 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { captureApiChange, organizationCreateAction } from '@crowd/audit-logs' +import { InternalError } from '@crowd/common' +import { findOrCreateOrganization, optionsQx } from '@crowd/data-access-layer' +import { OrganizationAttributeSource, OrganizationIdentityType } from '@crowd/types' + +import { created } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +const bodySchema = z.object({ + name: z.string().trim().min(1), + domain: z.string().trim().min(1), + source: z.string().trim().min(1), + logo: z.string().trim().min(1).optional(), +}) + +export async function createOrganization(req: Request, res: Response): Promise { + const { name, domain, source, logo } = validateOrThrow(bodySchema, req.body) + + const qx = optionsQx(req) + + let organizationId: string | undefined + + await qx.tx(async (tx) => { + const orgSource = OrganizationAttributeSource.LFX_SERVE + + organizationId = await findOrCreateOrganization(tx, orgSource, { + displayName: name, + logo, + identities: [ + { + value: domain, + type: OrganizationIdentityType.PRIMARY_DOMAIN, + verified: true, + platform: orgSource, + source, + }, + ], + }) + + if (!organizationId) { + throw new InternalError('Failed to create organization') + } + + await captureApiChange( + req, + organizationCreateAction(organizationId, async (captureState) => { + captureState({ + id: organizationId, + displayName: name, + identities: [ + { + value: domain, + type: OrganizationIdentityType.PRIMARY_DOMAIN, + }, + ], + }) + }), + ) + }) + + created(res, { id: organizationId, name, logo }) +} diff --git a/backend/src/api/public/v1/organizations/getOrganization.ts b/backend/src/api/public/v1/organizations/getOrganization.ts new file mode 100644 index 0000000000..5f4afccb45 --- /dev/null +++ b/backend/src/api/public/v1/organizations/getOrganization.ts @@ -0,0 +1,57 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { NotFoundError, normalizeHostname } from '@crowd/common' +import { + OrgIdentityField, + OrganizationField, + findOrgAttributes, + findOrgById, + optionsQx, + queryOrgIdentities, +} from '@crowd/data-access-layer' +import { OrganizationIdentityType } from '@crowd/types' + +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +const querySchema = z.object({ + domain: z.string().trim().min(1), +}) + +export async function getOrganization(req: Request, res: Response): Promise { + const { domain } = validateOrThrow(querySchema, req.query) + + const qx = optionsQx(req) + + const results = await queryOrgIdentities(qx, { + fields: [OrgIdentityField.ORGANIZATION_ID], + filter: { + and: [ + { value: { eq: normalizeHostname(domain, false) } }, + { type: { eq: OrganizationIdentityType.PRIMARY_DOMAIN } }, + { verified: { eq: true } }, + ], + }, + }) + + const organizationId = results[0]?.organizationId + + if (!organizationId) { + throw new NotFoundError('Organization not found') + } + + const org = await findOrgById(qx, organizationId, [ + OrganizationField.ID, + OrganizationField.DISPLAY_NAME, + ]) + + const attributes = await findOrgAttributes(qx, organizationId) + const logo = attributes.find((a) => a.name === 'logo')?.value + + ok(res, { + id: org.id, + name: org.displayName, + ...(logo ? { logo } : {}), + }) +} diff --git a/backend/src/api/public/v1/organizations/index.ts b/backend/src/api/public/v1/organizations/index.ts new file mode 100644 index 0000000000..e15ee83e30 --- /dev/null +++ b/backend/src/api/public/v1/organizations/index.ts @@ -0,0 +1,18 @@ +import { Router } from 'express' + +import { safeWrap } from '@/middlewares/errorMiddleware' +import { SCOPES } from '@/security/scopes' + +import { requireScopes } from '../../middlewares/requireScopes' + +import { createOrganization } from './createOrganization' +import { getOrganization } from './getOrganization' + +export function organizationsRouter(): Router { + const router = Router() + + router.get('/', requireScopes([SCOPES.READ_ORGANIZATIONS]), safeWrap(getOrganization)) + router.post('/', requireScopes([SCOPES.WRITE_ORGANIZATIONS]), safeWrap(createOrganization)) + + return router +} diff --git a/backend/src/api/public/v1/ossprey/activityFeed.ts b/backend/src/api/public/v1/ossprey/activityFeed.ts new file mode 100644 index 0000000000..ea00e46a58 --- /dev/null +++ b/backend/src/api/public/v1/ossprey/activityFeed.ts @@ -0,0 +1,40 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { listStewardshipActivity } from '@crowd/data-access-layer' + +import { getPackagesQx } from '@/db/packagesDb' +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +const querySchema = z.object({ + page: z.coerce.number().int().min(1).default(1), + pageSize: z.coerce.number().int().min(1).max(100).default(25), +}) + +export async function activityFeedHandler(req: Request, res: Response): Promise { + const { page, pageSize } = validateOrThrow(querySchema, req.query) + + const qx = await getPackagesQx() + const { rows, total } = await listStewardshipActivity(qx, { page, pageSize }) + + ok(res, { + rows: rows.map((r) => ({ + id: r.id, + stewardshipId: r.stewardshipId, + packagePurl: r.packagePurl, + packageName: r.packageName, + packageEcosystem: r.packageEcosystem, + actor: r.actor, + actorType: r.actorType, + activityType: r.activityType, + content: r.content, + metadata: r.metadata, + stewardshipStatus: r.stewardshipStatus, + createdAt: r.createdAt, + })), + total, + page, + pageSize, + }) +} diff --git a/backend/src/api/public/v1/ossprey/index.ts b/backend/src/api/public/v1/ossprey/index.ts new file mode 100644 index 0000000000..3e718ab520 --- /dev/null +++ b/backend/src/api/public/v1/ossprey/index.ts @@ -0,0 +1,40 @@ +import { Router } from 'express' + +import { requireScopes } from '@/api/public/middlewares/requireScopes' +import { safeWrap } from '@/middlewares/errorMiddleware' +import { SCOPES } from '@/security/scopes' + +import { activityFeedHandler } from './activityFeed' +import { metricsHandler } from './metrics' +import { packageListHandler } from './packageList' +import { packageScatterHandler } from './packageScatter' + +// TODO[deprecate]: superseded by /v1/akrites — ossprey endpoints are now at /v1/akrites/metrics, +// /v1/akrites/packages, /v1/akrites/packages/scatter, /v1/akrites/activity — remove once consumers have migrated +export function osspreyRouter(): Router { + const router = Router() + + router.get( + '/metrics', + requireScopes([SCOPES.READ_PACKAGES, SCOPES.READ_STEWARDSHIPS], 'all'), + safeWrap(metricsHandler), + ) + // /packages/scatter must be registered before /packages to avoid Express treating 'scatter' as a path param + router.get( + '/packages/scatter', + requireScopes([SCOPES.READ_PACKAGES, SCOPES.READ_STEWARDSHIPS], 'all'), + safeWrap(packageScatterHandler), + ) + router.get( + '/packages', + requireScopes([SCOPES.READ_PACKAGES, SCOPES.READ_STEWARDSHIPS], 'all'), + safeWrap(packageListHandler), + ) + router.get( + '/activity', + requireScopes([SCOPES.READ_PACKAGES, SCOPES.READ_STEWARDSHIPS], 'all'), + safeWrap(activityFeedHandler), + ) + + return router +} diff --git a/backend/src/api/public/v1/ossprey/metrics.ts b/backend/src/api/public/v1/ossprey/metrics.ts new file mode 100644 index 0000000000..6b0aa4a429 --- /dev/null +++ b/backend/src/api/public/v1/ossprey/metrics.ts @@ -0,0 +1,12 @@ +import type { Request, Response } from 'express' + +import { getOsspreyMetrics } from '@crowd/data-access-layer' + +import { getPackagesQx } from '@/db/packagesDb' +import { ok } from '@/utils/api' + +export async function metricsHandler(req: Request, res: Response): Promise { + const qx = await getPackagesQx() + const metrics = await getOsspreyMetrics(qx) + ok(res, metrics) +} diff --git a/backend/src/api/public/v1/ossprey/openapi.yaml b/backend/src/api/public/v1/ossprey/openapi.yaml new file mode 100644 index 0000000000..808532bfb2 --- /dev/null +++ b/backend/src/api/public/v1/ossprey/openapi.yaml @@ -0,0 +1,733 @@ +openapi: 3.1.0 +info: + title: CDP Public API — OSSPREY Admin Dashboard V2 + version: 1.0.0 + description: > + Read endpoints for the OSSPREY Admin Dashboard V2. + + + **Authentication:** OAuth 2.0 bearer token (Auth0 M2M or user session). + + + **V2 scope:** These endpoints cover the four dashboard tabs (Overview, Queue, + Triage Board, Risk Matrix) and the global KPI bar. + Package Detail Drawer and write actions are deferred to the next milestone. + +servers: + - url: https://cm.lfx.dev/api/v1 + description: Production + - url: https://lf-staging.crowd.dev/api/v1 + description: Staging + +tags: + - name: Dashboard + description: KPI metrics and activity feed (Overview tab). + - name: Packages + description: Package list and scatter plot (Queue, Triage Board, Risk Matrix tabs). + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + + schemas: + Error: + type: object + required: [error] + properties: + error: + type: object + required: [code, message] + properties: + code: + type: string + example: VALIDATION_ERROR + message: + type: string + example: Invalid query parameter. + + HealthBand: + type: string + enum: [healthy, fair, concerning, critical] + description: > + Server-derived from `scorecardScore` thresholds: + `null or < 3.0` → critical · `< 5.0` → concerning · `< 7.0` → fair · `≥ 7.0` → healthy + + StewardshipStatus: + type: string + enum: + - unassigned + - open + - assessing + - active + - needs_attention + - escalated + - blocked + - inactive + + Steward: + type: object + required: [userId, role, assignedAt] + properties: + userId: + type: string + description: Auth0 sub of the assigned steward. + example: auth0|abc123 + role: + type: string + enum: [lead, co_steward] + assignedAt: + type: string + format: date-time + + PackageRow: + type: object + required: + - purl + - name + - ecosystem + - openVulns + - maintainerCount + - healthBand + - stewards + properties: + purl: + type: string + example: pkg:npm/lodash@4.17.21 + name: + type: string + example: lodash + ecosystem: + type: string + example: npm + criticalityScore: + type: + - number + - 'null' + description: packages.impact (0–1 float). Multiply × 100 for display score. + example: 0.94 + stewardshipId: + type: + - string + - 'null' + example: '4501' + stewardshipStatus: + oneOf: + - $ref: '#/components/schemas/StewardshipStatus' + - type: 'null' + openVulns: + type: integer + description: Count of advisory_packages rows for this package. + example: 3 + maxVulnSeverity: + type: + - string + - 'null' + enum: [critical, high, medium, low] + description: Worst advisory severity. Null if no advisories. + maintainerCount: + type: integer + description: Count of package_maintainers rows. Bus factor proxy. + example: 2 + scorecardScore: + type: + - number + - 'null' + description: OpenSSF Scorecard score (0–10). Null if no repo mapped. + example: 5.2 + healthBand: + $ref: '#/components/schemas/HealthBand' + latestReleaseAt: + type: + - string + - 'null' + format: date-time + description: Used by the frontend to derive the stale flag (≥ 18 months). + lastActivity: + description: Most recent stewardship activity for this package. Null if none. + oneOf: + - type: object + required: [type, at] + properties: + type: + type: + - string + - 'null' + example: state_changed + content: + type: + - string + - 'null' + example: Moved to active stewardship + at: + type: string + format: date-time + - type: 'null' + stewards: + type: array + description: Active stewards (deleted_at IS NULL). Empty array if none. + items: + $ref: '#/components/schemas/Steward' + + StatusCounts: + type: object + description: Per-status package counts for the tab bar. Computed without the active status filter. + required: + - all + - unassigned + - open + - assessing + - active + - needs_attention + - escalated + - blocked + - inactive + properties: + all: + type: integer + unassigned: + type: integer + open: + type: integer + assessing: + type: integer + active: + type: integer + needs_attention: + type: integer + escalated: + type: integer + blocked: + type: integer + inactive: + type: integer + + ScatterPoint: + type: object + required: + - purl + - name + - criticalityScore + - healthScore + - healthBand + - openVulns + - advisoryCount + properties: + purl: + type: string + example: pkg:npm/lodash@4.17.21 + name: + type: string + example: lodash + criticalityScore: + type: integer + description: ROUND(p.impact × 100). Y-axis position (0–100). + example: 94 + healthScore: + type: integer + description: ROUND(scorecard_score × 10). X-axis position (0–100). 0 if no repo. + example: 52 + healthBand: + $ref: '#/components/schemas/HealthBand' + stewardshipStatus: + oneOf: + - $ref: '#/components/schemas/StewardshipStatus' + - type: 'null' + stewardshipId: + type: + - string + - 'null' + example: '4501' + openVulns: + type: integer + example: 3 + advisoryCount: + type: integer + description: Alias of openVulns — explicit field for tooltip display. + example: 3 + + ActivityFeedItem: + type: object + required: + - id + - stewardshipId + - packagePurl + - packageName + - packageEcosystem + - actorType + - activityType + - stewardshipStatus + - createdAt + properties: + id: + type: string + example: '9182736' + stewardshipId: + type: string + example: '4501' + packagePurl: + type: string + example: pkg:npm/minimist@1.2.6 + packageName: + type: string + example: minimist + packageEcosystem: + type: string + example: npm + actorUserId: + type: + - string + - 'null' + description: Auth0 sub. Null for system events. + example: auth0|abc123 + actorName: + type: + - string + - 'null' + description: > + Display name of the actor. Currently returns actorUserId as a + placeholder — will be resolved to a display name once the + cross-DB users join is implemented. + example: auth0|abc123 + actorType: + type: string + enum: [user, system] + activityType: + type: string + enum: + - state_changed + - steward_added + - steward_removed + - escalation + - escalation_resolved + - assessment_started + - assessment_completed + - assessment_flagged + - spot_check + - note_added + example: escalation + content: + type: + - string + - 'null' + example: 'Escalated with resolution path: right_of_first_refusal' + metadata: + type: + - object + - 'null' + additionalProperties: true + stewardshipStatus: + $ref: '#/components/schemas/StewardshipStatus' + createdAt: + type: string + format: date-time + +# ────────────────────────────────────────────────────────────────────────────── +# Paths +# ────────────────────────────────────────────────────────────────────────────── +paths: + /ossprey/metrics: + get: + operationId: getOsspreyMetrics + summary: Global KPI bar metrics + description: > + Returns the seven aggregate metrics shown in the sticky KPI bar across all + four dashboard tabs. Fetch once on page load; refresh after any write action. + tags: + - Dashboard + security: + - BearerAuth: [] + responses: + '200': + description: KPI metrics. + content: + application/json: + schema: + type: object + required: + - totalPackages + - criticalPackages + - coveragePercent + - activeStewards + - unassignedCritical + - needsAttention + - escalated + properties: + totalPackages: + type: integer + description: Total critical packages (is_critical = true). + example: 1842 + criticalPackages: + type: integer + description: Critical packages with at least one critical-severity advisory. + example: 312 + coveragePercent: + type: number + format: float + description: > + Percentage of critical packages with status in + (assessing, active, needs_attention). One decimal place. + example: 33.2 + coverageTrend: + type: + - number + - 'null' + description: > + Percentage-point change vs. previous month. Null until + snapshot mechanism is implemented. + example: null + activeStewards: + type: integer + description: > + COUNT(DISTINCT user_id) from stewardship_stewards where + deleted_at IS NULL and stewardship status != inactive. + example: 42 + unassignedCritical: + type: integer + description: Critical packages with no stewardship row or status = unassigned. + example: 1230 + needsAttention: + type: integer + description: Critical packages with status = needs_attention. + example: 47 + escalated: + type: integer + description: Critical packages with status = escalated. + example: 8 + example: + totalPackages: 1842 + criticalPackages: 312 + coveragePercent: 33.2 + coverageTrend: null + activeStewards: 42 + unassignedCritical: 1230 + needsAttention: 47 + escalated: 8 + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /ossprey/activity: + get: + operationId: listStewardshipActivity + summary: Paginated stewardship activity feed + description: > + Returns recent stewardship events across all packages, ordered by most + recent first. Used to populate the activity feed on the Overview tab. + The frontend groups items by day using `createdAt`. + tags: + - Dashboard + security: + - BearerAuth: [] + parameters: + - name: page + in: query + schema: + type: integer + minimum: 1 + default: 1 + - name: pageSize + in: query + schema: + type: integer + minimum: 1 + maximum: 100 + default: 25 + responses: + '200': + description: Paginated activity feed. + content: + application/json: + schema: + type: object + required: [rows, total, page, pageSize] + properties: + rows: + type: array + items: + $ref: '#/components/schemas/ActivityFeedItem' + total: + type: integer + example: 142 + page: + type: integer + example: 1 + pageSize: + type: integer + example: 25 + example: + rows: + - id: '9182736' + stewardshipId: '4501' + packagePurl: pkg:maven/org.slf4j/slf4j-api + packageName: slf4j-api + packageEcosystem: maven + actorUserId: auth0|mock-user-alice + actorType: user + activityType: state_changed + content: Assessment complete, moving to active + metadata: + from: assessing + to: active + stewardshipStatus: active + createdAt: '2026-06-14T10:23:00Z' + total: 11 + page: 1 + pageSize: 25 + '400': + description: Validation error. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /ossprey/packages: + get: + operationId: listOsspreyPackages + summary: Filtered paginated package list + description: > + Returns a paginated, filtered, sorted list of critical packages with their + stewardship state and risk signals. + + + Used by three tabs: + + - **Queue tab** — full table with all filters + + - **Triage Board** — one request per status column, fired in parallel + (`?status=X&pageSize=50`) + + - **Summary panel** — click-through navigates to Queue with pre-filled filter + + + The response always includes `statusCounts` — per-status counts computed + without the active `status` filter, used to drive the tab bar badge numbers. + tags: + - Packages + security: + - BearerAuth: [] + parameters: + - name: page + in: query + schema: + type: integer + minimum: 1 + default: 1 + - name: pageSize + in: query + schema: + type: integer + minimum: 1 + maximum: 250 + default: 25 + - name: name + in: query + description: Case-insensitive substring search on package name. + schema: + type: string + - name: ecosystem + in: query + description: Filter by package ecosystem. Not validated server-side — any ecosystem stored in the DB is accepted. + schema: + type: string + - name: lifecycle + in: query + schema: + type: string + enum: [active, stable, declining, abandoned] + - name: status + in: query + description: > + Filter by stewardship status. `unassigned` includes packages with + no stewardship row (s.id IS NULL). + schema: + $ref: '#/components/schemas/StewardshipStatus' + - name: healthBand + in: query + schema: + $ref: '#/components/schemas/HealthBand' + - name: vulnSeverity + in: query + description: > + `any` = at least one open advisory · `high` = worst rank ≥ HIGH · + `critical` = worst rank = CRITICAL · `none` = zero advisories. + schema: + type: string + enum: [any, high, critical, none] + - name: staleOnly + in: query + description: Return only packages with no release in ≥ 18 months. + schema: + type: boolean + default: false + - name: unstewardedOnly + in: query + description: Return only packages with status = unassigned or no stewardship row. + schema: + type: boolean + default: false + - name: busFactor1Only + in: query + description: Return only packages with exactly one maintainer. + schema: + type: boolean + default: false + - name: sortBy + in: query + schema: + type: string + enum: [risk, name, impact, openVulns, health] + default: risk + - name: sortDir + in: query + schema: + type: string + enum: [asc, desc] + default: desc + responses: + '200': + description: Paginated package list. + content: + application/json: + schema: + type: object + required: [rows, total, page, pageSize, statusCounts] + properties: + rows: + type: array + items: + $ref: '#/components/schemas/PackageRow' + total: + type: integer + example: 1842 + page: + type: integer + example: 1 + pageSize: + type: integer + example: 25 + statusCounts: + $ref: '#/components/schemas/StatusCounts' + example: + rows: + - purl: pkg:maven/org.slf4j/slf4j-api + name: slf4j-api + ecosystem: maven + criticalityScore: 0.998 + stewardshipId: '101' + stewardshipStatus: active + openVulns: 0 + maxVulnSeverity: null + maintainerCount: 2 + scorecardScore: 7.5 + healthBand: healthy + latestReleaseAt: '2026-04-10T00:00:00Z' + lastActivity: + type: state_changed + content: Assessment complete, moving to active + at: '2026-06-01T10:00:00Z' + stewards: + - userId: auth0|mock-user-alice + role: lead + assignedAt: '2026-01-15T09:00:00Z' + total: 9 + page: 1 + pageSize: 25 + statusCounts: + all: 9 + unassigned: 1 + open: 1 + assessing: 1 + active: 2 + needs_attention: 1 + escalated: 1 + blocked: 1 + inactive: 1 + '400': + description: Validation error. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /ossprey/packages/scatter: + get: + operationId: getOsspreyPackagesScatter + summary: Scatter plot data for the Risk Matrix tab + description: > + Returns all packages where `is_critical = true AND has_critical_vulnerability = true` + as lightweight data points for the health-vs-impact scatter plot. + No pagination — this filter set is expected to stay around 2 000 packages. + Ordered by `impact DESC`. + + + Dot color is determined by `stewardshipStatus`. Legend checkboxes toggle + visibility client-side — no additional API calls needed. + tags: + - Packages + security: + - BearerAuth: [] + responses: + '200': + description: Scatter plot data points. + content: + application/json: + schema: + type: object + required: [points, total] + properties: + points: + type: array + items: + $ref: '#/components/schemas/ScatterPoint' + total: + type: integer + description: > + Count of packages matching the filter + (is_critical = true AND has_critical_vulnerability = true). + Equals points.length — no separate count query. + example: 2000 + example: + points: + - purl: pkg:maven/org.slf4j/slf4j-api + name: slf4j-api + criticalityScore: 100 + healthScore: 75 + healthBand: healthy + stewardshipStatus: active + stewardshipId: '101' + openVulns: 0 + advisoryCount: 0 + - purl: pkg:maven/com.fasterxml.jackson.core/jackson-databind + name: jackson-databind + criticalityScore: 99 + healthScore: 0 + healthBand: critical + stewardshipStatus: needs_attention + stewardshipId: '102' + openVulns: 2 + advisoryCount: 2 + total: 2000 + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' diff --git a/backend/src/api/public/v1/ossprey/packageList.ts b/backend/src/api/public/v1/ossprey/packageList.ts new file mode 100644 index 0000000000..fdfa82c94a --- /dev/null +++ b/backend/src/api/public/v1/ossprey/packageList.ts @@ -0,0 +1,102 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { + computeHealthBand, + getPackageStatusCounts, + listPackagesForApi, + translateActivityContent, +} from '@crowd/data-access-layer' + +import { getPackagesQx } from '@/db/packagesDb' +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +import { purlFilterSchema } from '../packages/purl' + +const MAX_PAGE_SIZE = 250 + +const boolParam = z.preprocess((v) => v === 'true', z.boolean()).default(false) + +const querySchema = z.object({ + page: z.coerce.number().int().min(1).default(1), + pageSize: z.coerce.number().int().min(1).max(MAX_PAGE_SIZE).default(25), + ecosystem: z.string().trim().optional(), + lifecycle: z.enum(['active', 'stable', 'declining', 'abandoned']).optional(), + name: z.string().trim().optional(), + purl: purlFilterSchema, + status: z + .enum([ + 'unassigned', + 'open', + 'assessing', + 'active', + 'needs_attention', + 'escalated', + 'blocked', + 'inactive', + ]) + .optional(), + healthBand: z.enum(['healthy', 'fair', 'concerning', 'critical']).optional(), + vulnSeverity: z.enum(['any', 'high', 'critical', 'none']).optional(), + staleOnly: boolParam, + unstewardedOnly: boolParam, + busFactor1Only: boolParam, + sortBy: z.enum(['name', 'risk', 'impact', 'openVulns', 'health']).default('risk'), + sortDir: z.enum(['asc', 'desc']).default('desc'), +}) + +export async function packageListHandler(req: Request, res: Response): Promise { + const params = validateOrThrow(querySchema, req.query) + + const filterOpts = { + ecosystem: params.ecosystem, + lifecycle: params.lifecycle, + name: params.name, + purl: params.purl, + healthBand: params.healthBand, + vulnSeverity: params.vulnSeverity, + staleOnly: params.staleOnly, + unstewardedOnly: params.unstewardedOnly, + busFactor1Only: params.busFactor1Only, + } + + const qx = await getPackagesQx() + const [{ rows, total }, statusCounts] = await Promise.all([ + listPackagesForApi(qx, { ...params, includeStewards: true, includeLastActivity: true }), + getPackageStatusCounts(qx, filterOpts), + ]) + + ok(res, { + rows: rows.map((r) => ({ + purl: r.purl, + name: r.name, + ecosystem: r.ecosystem, + criticalityScore: r.criticalityScore, + stewardshipId: r.stewardshipId ?? null, + stewardshipStatus: r.stewardshipStatus ?? null, + openVulns: r.openVulns, + maxVulnSeverity: r.maxVulnSeverity ?? null, + maintainerCount: r.maintainerCount, + scorecardScore: r.scorecardScore, + healthBand: computeHealthBand(r.scorecardScore != null ? Number(r.scorecardScore) : null), + latestReleaseAt: r.latestReleaseAt ? r.latestReleaseAt.toISOString() : null, + lastActivity: r.lastActivityAt + ? { + type: r.lastActivityType, + content: translateActivityContent( + r.lastActivityContent ?? null, + r.lastActivityType, + r.lastActivityMetadata, + ), + at: r.lastActivityAt.toISOString(), + } + : null, + stewards: r.stewards ?? [], + })), + total, + page: params.page, + pageSize: params.pageSize, + statusCounts, + }) +} diff --git a/backend/src/api/public/v1/ossprey/packageScatter.ts b/backend/src/api/public/v1/ossprey/packageScatter.ts new file mode 100644 index 0000000000..62e4f7ee83 --- /dev/null +++ b/backend/src/api/public/v1/ossprey/packageScatter.ts @@ -0,0 +1,35 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { listPackagesForScatter } from '@crowd/data-access-layer' + +import { getPackagesQx } from '@/db/packagesDb' +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +import { STEWARDSHIP_STATUS_VALUES } from '../packages/types' + +const statusEnum = z.enum(STEWARDSHIP_STATUS_VALUES) + +function normalizeToArray(v: unknown): unknown[] | undefined { + if (v === undefined) return undefined + if (Array.isArray(v)) return v + if (typeof v === 'string' && v.includes(',')) + return v + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + return [v] +} + +const scatterQuerySchema = z.object({ + status: z.preprocess(normalizeToArray, z.array(statusEnum).min(1)).optional(), + ecosystem: z.string().min(1).optional(), +}) + +export async function packageScatterHandler(req: Request, res: Response): Promise { + const { status, ecosystem } = validateOrThrow(scatterQuerySchema, req.query) + const qx = await getPackagesQx() + const points = await listPackagesForScatter(qx, { status, ecosystem }) + ok(res, { points, total: points.length }) +} diff --git a/backend/src/api/public/v1/packages/batchGetStewardship.ts b/backend/src/api/public/v1/packages/batchGetStewardship.ts new file mode 100644 index 0000000000..42fecbb258 --- /dev/null +++ b/backend/src/api/public/v1/packages/batchGetStewardship.ts @@ -0,0 +1,62 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { getPackagesByStewardshipPurls } from '@crowd/data-access-layer' + +import { getPackagesQx } from '@/db/packagesDb' +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +import { normalizePurl } from './purl' +import type { StewardshipSummary } from './types' + +const MAX_PURLS = 100 + +const bodySchema = z.object({ + purls: z + .array( + z + .string() + .trim() + .min(1) + .refine((v) => v.startsWith('pkg:'), { message: 'each purl must start with pkg:' }), + ) + .min(1) + .max(MAX_PURLS, `Maximum ${MAX_PURLS} purls per request`), +}) + +export async function batchGetStewardship(req: Request, res: Response): Promise { + const { purls: rawPurls } = validateOrThrow(bodySchema, req.body) + // Normalize after parsing (not in the schema) so rawPurls keeps the client's + // original form — used as the response key so clients can look up their input. + const normalizedPurls = rawPurls.map(normalizePurl) + + const qx = await getPackagesQx() + const rows = await getPackagesByStewardshipPurls(qx, normalizedPurls) + + const byPurl = new Map(rows.map((r) => [r.purl, r])) + + const packages: Record = {} + for (let i = 0; i < rawPurls.length; i++) { + const row = byPurl.get(normalizedPurls[i]) + if (!row) { + packages[rawPurls[i]] = null + } else { + packages[rawPurls[i]] = { + name: row.name, + ecosystem: row.ecosystem, + lifecycle: null, + health: null, + impact: + row.criticalityScore != null ? Math.round(Number(row.criticalityScore) * 100) : null, + openVulns: null, + stewardship: (row.stewardshipStatus ?? 'unassigned') as StewardshipSummary['stewardship'], + stewards: null, + lastActivityAt: null, + lastActivityDescription: null, + } + } + } + + ok(res, { packages }) +} diff --git a/backend/src/api/public/v1/packages/getPackage.ts b/backend/src/api/public/v1/packages/getPackage.ts new file mode 100644 index 0000000000..75f6f50fab --- /dev/null +++ b/backend/src/api/public/v1/packages/getPackage.ts @@ -0,0 +1,110 @@ +import type { Request, Response } from 'express' + +import { NotFoundError } from '@crowd/common' +import { + computeHealthBand, + getAdvisoriesByPackageId, + getPackageDetailByPurl, + getStewardshipSummary, +} from '@crowd/data-access-layer' + +import { getPackagesQx } from '@/db/packagesDb' +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +import { purlQuerySchema } from './purl' +import type { StewardshipStatus } from './types' + +function repoMappingLabel(confidence: number | null): 'High' | 'Medium' | 'Low' | null { + if (confidence === null) return null + if (confidence >= 0.8) return 'High' + if (confidence >= 0.5) return 'Medium' + return 'Low' +} + +export async function getPackage(req: Request, res: Response): Promise { + const { purl } = validateOrThrow(purlQuerySchema, req.query) + + const qx = await getPackagesQx() + const pkg = await getPackageDetailByPurl(qx, purl) + + if (!pkg) { + throw new NotFoundError() + } + + const [advisories, stewardshipSummary] = await Promise.all([ + getAdvisoriesByPackageId(qx, pkg.id), + pkg.stewardshipId ? getStewardshipSummary(qx, Number(pkg.stewardshipId)) : null, + ]) + + const scorecardScore = pkg.scorecardScore != null ? Number(pkg.scorecardScore) : null + const mappingConfidence = + pkg.repoMappingConfidence != null ? Number(pkg.repoMappingConfidence) : null + + ok(res, { + purl: pkg.purl, + name: pkg.name, + ecosystem: pkg.ecosystem, + latestVersion: pkg.latestVersion ?? null, + general: { + healthScore: scorecardScore !== null ? Math.round(scorecardScore * 10) : null, + healthBand: computeHealthBand(scorecardScore), + impact: { + impactScore: + pkg.criticalityScore != null ? Math.round(Number(pkg.criticalityScore) * 100) : null, + downloadsLastMonth: pkg.downloadsLast30d ?? null, + dependentPackages: pkg.dependentPackagesCount ?? null, + dependentRepos: pkg.dependentReposCount ?? null, + transitiveReach: pkg.transitiveReach, + }, + riskSignals: { + lifecycle: null, + maintainerBusFactor: pkg.maintainerCount, + lastRelease: pkg.latestReleaseAt ? pkg.latestReleaseAt.toISOString() : null, + hasSecurityFile: pkg.hasSecurityFile, + hasSecurityPolicy: pkg.hasSecurityPolicy, + branchProtectionEnabled: pkg.branchProtectionEnabled, + openSSFScorecard: scorecardScore, + }, + }, + assessment: null, + security: { + securityContacts: null, + advisories: advisories.map((a) => ({ + osvId: a.osvId, + severity: a.severity, + resolution: a.resolution, + })), + cvd: { + isPvrEnabled: null, + tier0Steward: null, + criticalVulnerabilityFlag: pkg.hasCriticalVulnerability, + }, + }, + provenance: { + repositoryMapping: { + declaredRepo: pkg.repoUrl ?? pkg.repositoryUrl ?? pkg.declaredRepositoryUrl ?? null, + mappingConfidence, + mappingLabel: repoMappingLabel(mappingConfidence), + lastCommitAt: pkg.repoLastCommitAt ? pkg.repoLastCommitAt.toISOString() : null, + }, + supplyChainIntegrity: { + buildProvenance: null, + signedReleases: null, + }, + }, + stewardship: { + id: pkg.stewardshipId ?? null, + status: (pkg.stewardshipStatus ?? 'unassigned') as StewardshipStatus, + origin: pkg.stewardshipOrigin ?? null, + version: pkg.stewardshipVersion ?? null, + openedAt: pkg.stewardshipOpenedAt ? pkg.stewardshipOpenedAt.toISOString() : null, + lastStatusAt: pkg.stewardshipLastStatusAt ? pkg.stewardshipLastStatusAt.toISOString() : null, + resolutionPath: pkg.stewardshipResolutionPath ?? null, + statusNote: pkg.stewardshipStatusNote ?? null, + stewards: stewardshipSummary?.stewards ?? null, + lastActivityAt: stewardshipSummary?.lastActivityAt ?? null, + }, + history: null, + }) +} diff --git a/backend/src/api/public/v1/packages/getPackageAdvisories.ts b/backend/src/api/public/v1/packages/getPackageAdvisories.ts new file mode 100644 index 0000000000..f5a8a55965 --- /dev/null +++ b/backend/src/api/public/v1/packages/getPackageAdvisories.ts @@ -0,0 +1,32 @@ +import type { Request, Response } from 'express' + +import { NotFoundError } from '@crowd/common' +import { getAdvisoriesByPackageId, getPackageDetailByPurl } from '@crowd/data-access-layer' + +import { getPackagesQx } from '@/db/packagesDb' +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +import { purlQuerySchema } from './purl' + +export async function getPackageAdvisories(req: Request, res: Response): Promise { + const { purl } = validateOrThrow(purlQuerySchema, req.query) + + const qx = await getPackagesQx() + const pkg = await getPackageDetailByPurl(qx, purl) + + if (!pkg) { + throw new NotFoundError() + } + + const advisories = await getAdvisoriesByPackageId(qx, pkg.id) + + ok(res, { + advisories: advisories.map((a) => ({ + osvId: a.osvId, + severity: a.severity, + resolution: a.resolution, + })), + total: advisories.length, + }) +} diff --git a/backend/src/api/public/v1/packages/getPackageHistory.ts b/backend/src/api/public/v1/packages/getPackageHistory.ts new file mode 100644 index 0000000000..cb3322d921 --- /dev/null +++ b/backend/src/api/public/v1/packages/getPackageHistory.ts @@ -0,0 +1,30 @@ +import type { Request, Response } from 'express' + +import { NotFoundError } from '@crowd/common' +import { getPackageDetailByPurl, listPackageHistory } from '@crowd/data-access-layer' + +import { getPackagesQx } from '@/db/packagesDb' +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +import { purlQuerySchema } from './purl' + +export async function getPackageHistory(req: Request, res: Response): Promise { + const { purl } = validateOrThrow(purlQuerySchema, req.query) + + const qx = await getPackagesQx() + const pkg = await getPackageDetailByPurl(qx, purl) + + if (!pkg) { + throw new NotFoundError() + } + + if (!pkg.stewardshipId) { + ok(res, { events: [], total: 0 }) + return + } + + const events = await listPackageHistory(qx, pkg.stewardshipId) + + ok(res, { events, total: events.length }) +} diff --git a/backend/src/api/public/v1/packages/getPackagesMetrics.ts b/backend/src/api/public/v1/packages/getPackagesMetrics.ts new file mode 100644 index 0000000000..df1b3e5595 --- /dev/null +++ b/backend/src/api/public/v1/packages/getPackagesMetrics.ts @@ -0,0 +1,12 @@ +import type { Request, Response } from 'express' + +import { getPackageMetrics } from '@crowd/data-access-layer' + +import { getPackagesQx } from '@/db/packagesDb' +import { ok } from '@/utils/api' + +export async function getPackagesMetrics(req: Request, res: Response): Promise { + const qx = await getPackagesQx() + const metrics = await getPackageMetrics(qx) + ok(res, metrics) +} diff --git a/backend/src/api/public/v1/packages/index.ts b/backend/src/api/public/v1/packages/index.ts new file mode 100644 index 0000000000..14f55f424e --- /dev/null +++ b/backend/src/api/public/v1/packages/index.ts @@ -0,0 +1,30 @@ +import { Router } from 'express' + +import { createRateLimiter } from '@/api/apiRateLimiter' +import { requireScopes } from '@/api/public/middlewares/requireScopes' +import { safeWrap } from '@/middlewares/errorMiddleware' +import { SCOPES } from '@/security/scopes' + +import { getPackage } from './getPackage' +import { getPackagesMetrics } from './getPackagesMetrics' +import { listPackages } from './listPackages' + +const rateLimiter = createRateLimiter({ max: 60, windowMs: 60 * 1000 }) + +// TODO[deprecate]: /packages/metrics and /packages/detail are superseded by /v1/akrites/packages/metrics +// and /v1/akrites/packages/detail — remove once consumers have migrated. +// NOTE: GET /packages (listPackages) is intentionally NOT replicated in /v1/akrites because it has a +// different response shape from GET /v1/akrites/packages (ossprey packageListHandler). Before removing, +// verify no consumer calls GET /v1/packages — if unused, delete listPackages and this route entirely. +export function packagesRouter(): Router { + const router = Router() + + router.use(rateLimiter) + router.use(requireScopes([SCOPES.READ_PACKAGES, SCOPES.READ_STEWARDSHIPS], 'all')) + + router.get('/', safeWrap(listPackages)) + router.get('/metrics', safeWrap(getPackagesMetrics)) + router.get('/detail', safeWrap(getPackage)) + + return router +} diff --git a/backend/src/api/public/v1/packages/listPackages.ts b/backend/src/api/public/v1/packages/listPackages.ts new file mode 100644 index 0000000000..56342085cd --- /dev/null +++ b/backend/src/api/public/v1/packages/listPackages.ts @@ -0,0 +1,117 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { getPackageStatusCounts, listPackagesForApi } from '@crowd/data-access-layer' + +import { getPackagesQx } from '@/db/packagesDb' +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +import { purlFilterSchema } from './purl' +import { STEWARDSHIP_STATUS_VALUES, type StewardshipStatus } from './types' + +const DEFAULT_PAGE_SIZE = 20 +const MAX_PAGE_SIZE = 100 + +const booleanQueryParam = z.preprocess((v) => v === 'true', z.boolean()).default(false) + +const lifecycleValues = ['active', 'stable', 'declining', 'abandoned'] as const +const healthBandValues = ['healthy', 'fair', 'concerning', 'critical'] as const +const vulnSeverityValues = ['any', 'high', 'critical', 'none'] as const + +const querySchema = z.object({ + page: z.coerce.number().int().min(1).default(1), + pageSize: z.coerce.number().int().min(1).max(MAX_PAGE_SIZE).default(DEFAULT_PAGE_SIZE), + ecosystem: z.string().trim().optional(), + lifecycle: z.enum(lifecycleValues).optional(), + name: z.string().trim().optional(), + purl: purlFilterSchema, + status: z.enum(STEWARDSHIP_STATUS_VALUES).optional(), + healthBand: z.enum(healthBandValues).optional(), + vulnSeverity: z.enum(vulnSeverityValues).optional(), + busFactor1Only: booleanQueryParam, + staleOnly: booleanQueryParam, + unstewardedOnly: booleanQueryParam, + sortBy: z.enum(['name', 'health', 'impact', 'openVulns', 'risk']).default('name'), + sortDir: z.enum(['asc', 'desc']).default('asc'), +}) + +export async function listPackages(req: Request, res: Response): Promise { + const { + page, + pageSize, + ecosystem, + lifecycle, + name, + purl, + status, + healthBand, + vulnSeverity, + busFactor1Only, + staleOnly, + unstewardedOnly, + sortBy, + sortDir, + } = validateOrThrow(querySchema, req.query) + + const filterOpts = { + ecosystem, + lifecycle, + name, + purl, + healthBand, + vulnSeverity, + staleOnly, + unstewardedOnly, + busFactor1Only, + } + + const qx = await getPackagesQx() + const [{ rows, total }, statusCounts] = await Promise.all([ + listPackagesForApi(qx, { + page, + pageSize, + status, + sortBy, + sortDir, + ...filterOpts, + includeStewards: true, + }), + getPackageStatusCounts(qx, filterOpts), + ]) + + const packages = rows.map((r) => ({ + purl: r.purl, + name: r.name, + ecosystem: r.ecosystem, + health: r.scorecardScore != null ? Math.round(Number(r.scorecardScore) * 10) : null, + impact: r.criticalityScore != null ? Math.round(Number(r.criticalityScore) * 100) : null, + lifecycle: null, + maintainerBusFactor: r.maintainerCount, + openVulns: r.openVulns, + stewardshipId: r.stewardshipId ?? null, + stewardship: (r.stewardshipStatus ?? 'unassigned') as StewardshipStatus, + stewards: r.stewards ?? [], + })) + + ok(res, { + page, + pageSize, + total, + statusCounts, + filters: { + ecosystem: ecosystem ?? null, + lifecycle: lifecycle ?? null, + name: name ?? null, + purl: purl ?? null, + status: status ?? null, + healthBand: healthBand ?? null, + vulnSeverity: vulnSeverity ?? null, + busFactor1Only, + staleOnly, + unstewardedOnly, + }, + sort: { by: sortBy, dir: sortDir }, + packages, + }) +} diff --git a/backend/src/api/public/v1/packages/mockData.ts b/backend/src/api/public/v1/packages/mockData.ts new file mode 100644 index 0000000000..3b78a2b5ae --- /dev/null +++ b/backend/src/api/public/v1/packages/mockData.ts @@ -0,0 +1,261 @@ +import type { Lifecycle, OpenVulns, SeverityLevel, Steward, StewardshipStatus } from './types' + +export interface MockPackageListItem { + purl: string + name: string + ecosystem: string + health: number + impact: number + lifecycle: Lifecycle + maintainerBusFactor: number + openVulns: OpenVulns + stewardship: StewardshipStatus | null + stewards: Steward[] | null +} + +export interface MockPackageDetail { + purl: string + name: string + ecosystem: string + general: { + healthScore: { + maintainerHealth: number + securitySupplyChain: number + developmentActivity: number + total: number + } + impact: { + impactScore: number + downloadsLastMonth: number | null + dependentPackages: number + dependentRepos: number + transitiveReach: string + } + riskSignals: { + lifecycle: Lifecycle + maintainerBusFactor: number + lastRelease: string + hasSecurityFile: null + openSSFScorecard: number + } + } + assessment: Record + security: { + securityContacts: null + advisories: Array<{ + osvId: string + severity: SeverityLevel + resolution: null + }> + cvd: { + isPvrEnabled: null + hasSecurityPolicyEnabled: null + tier0Steward: null + criticalVulnerabilityFlag: boolean + } + } + provenance: { + repositoryMapping: { declaredRepo: string; mappingConfidence: number; lastCommitAt: string } + supplyChainIntegrity: { buildProvenance: null; signedReleases: null } + } + stewardship: { + status: StewardshipStatus + stewards: Steward[] | null + lastActivityAt: string | null + } + history: Record +} + +export const MOCK_PACKAGES: MockPackageListItem[] = [ + { + purl: 'pkg:npm/lodash@4.17.21', + name: 'lodash', + ecosystem: 'npm', + health: 18, + impact: 71, + lifecycle: 'declining', + maintainerBusFactor: 1, + openVulns: { low: 0, medium: 0, high: 1, critical: 0 }, + stewardship: 'unassigned', + stewards: null, + }, + { + purl: 'pkg:maven/org.apache.commons/commons-lang3@3.12.0', + name: 'commons-lang3', + ecosystem: 'maven', + health: 62, + impact: 88, + lifecycle: 'stable', + maintainerBusFactor: 3, + openVulns: { low: 0, medium: 0, high: 0, critical: 0 }, + stewardship: 'unassigned', + stewards: null, + }, + { + purl: 'pkg:npm/minimist@1.2.6', + name: 'minimist', + ecosystem: 'npm', + health: 12, + impact: 95, + lifecycle: 'abandoned', + maintainerBusFactor: 1, + openVulns: { low: 0, medium: 1, high: 0, critical: 1 }, + stewardship: 'unassigned', + stewards: null, + }, +] + +export const MOCK_DETAILS: Record = { + 'pkg:npm/lodash@4.17.21': { + purl: 'pkg:npm/lodash@4.17.21', + name: 'lodash', + ecosystem: 'npm', + general: { + healthScore: { + maintainerHealth: 4, + securitySupplyChain: 8, + developmentActivity: 6, + total: 18, + }, + impact: { + impactScore: 71, + downloadsLastMonth: 52142891, + dependentPackages: 142312, + dependentRepos: 39104, + transitiveReach: 'Top 0.4%', + }, + riskSignals: { + lifecycle: 'declining', + maintainerBusFactor: 1, + lastRelease: '2021-02-20T00:00:00Z', + hasSecurityFile: null, + openSSFScorecard: 5.2, + }, + }, + assessment: {}, + security: { + securityContacts: null, + advisories: [{ osvId: 'CVE-2021-44906', severity: 'high', resolution: null }], + cvd: { + isPvrEnabled: null, + hasSecurityPolicyEnabled: null, + tier0Steward: null, + criticalVulnerabilityFlag: false, + }, + }, + provenance: { + repositoryMapping: { + declaredRepo: 'https://github.com/lodash/lodash', + mappingConfidence: 0.98, + lastCommitAt: '2024-09-14T00:00:00Z', + }, + supplyChainIntegrity: { buildProvenance: null, signedReleases: null }, + }, + stewardship: { status: 'unassigned', stewards: null, lastActivityAt: null }, + history: {}, + }, + 'pkg:maven/org.apache.commons/commons-lang3@3.12.0': { + purl: 'pkg:maven/org.apache.commons/commons-lang3@3.12.0', + name: 'commons-lang3', + ecosystem: 'maven', + general: { + healthScore: { + maintainerHealth: 18, + securitySupplyChain: 22, + developmentActivity: 22, + total: 62, + }, + impact: { + impactScore: 88, + downloadsLastMonth: null, + dependentPackages: 89421, + dependentRepos: 21033, + transitiveReach: 'Top 1.2%', + }, + riskSignals: { + lifecycle: 'stable', + maintainerBusFactor: 3, + lastRelease: '2022-11-05T00:00:00Z', + hasSecurityFile: null, + openSSFScorecard: 7.1, + }, + }, + assessment: {}, + security: { + securityContacts: null, + advisories: [], + cvd: { + isPvrEnabled: null, + hasSecurityPolicyEnabled: null, + tier0Steward: null, + criticalVulnerabilityFlag: false, + }, + }, + provenance: { + repositoryMapping: { + declaredRepo: 'https://github.com/apache/commons-lang', + mappingConfidence: 0.99, + lastCommitAt: '2024-10-01T00:00:00Z', + }, + supplyChainIntegrity: { buildProvenance: null, signedReleases: null }, + }, + stewardship: { status: 'unassigned', stewards: null, lastActivityAt: null }, + history: {}, + }, + 'pkg:npm/minimist@1.2.6': { + purl: 'pkg:npm/minimist@1.2.6', + name: 'minimist', + ecosystem: 'npm', + general: { + healthScore: { + maintainerHealth: 2, + securitySupplyChain: 4, + developmentActivity: 6, + total: 12, + }, + impact: { + impactScore: 95, + downloadsLastMonth: 102381944, + dependentPackages: 321042, + dependentRepos: 87231, + transitiveReach: 'Top 0.1%', + }, + riskSignals: { + lifecycle: 'abandoned', + maintainerBusFactor: 1, + lastRelease: '2022-03-17T00:00:00Z', + hasSecurityFile: null, + openSSFScorecard: 2.1, + }, + }, + assessment: {}, + security: { + securityContacts: null, + advisories: [ + { osvId: 'CVE-2021-44906', severity: 'critical', resolution: null }, + { osvId: 'CVE-2020-7598', severity: 'medium', resolution: null }, + ], + cvd: { + isPvrEnabled: null, + hasSecurityPolicyEnabled: null, + tier0Steward: null, + criticalVulnerabilityFlag: true, + }, + }, + provenance: { + repositoryMapping: { + declaredRepo: 'https://github.com/minimistjs/minimist', + mappingConfidence: 0.97, + lastCommitAt: '2022-03-17T00:00:00Z', + }, + supplyChainIntegrity: { buildProvenance: null, signedReleases: null }, + }, + stewardship: { status: 'unassigned', stewards: null, lastActivityAt: null }, + history: {}, + }, +} + +export const MOCK_METRICS = { + totalPackages: MOCK_PACKAGES.length, + criticalPackages: MOCK_PACKAGES.filter((p) => p.openVulns.critical > 0).length, +} diff --git a/backend/src/api/public/v1/packages/openapi.yaml b/backend/src/api/public/v1/packages/openapi.yaml new file mode 100644 index 0000000000..b06a0cd291 --- /dev/null +++ b/backend/src/api/public/v1/packages/openapi.yaml @@ -0,0 +1,818 @@ +openapi: 3.1.0 +info: + title: CDP Public API — Packages & Stewardship + version: 1.0.0 + description: > + Read-only endpoints for the OSSPREY Self Serve program. + + + **Authentication:** OAuth 2.0 M2M bearer token. + Package endpoints require `read:packages` or `read:stewardships`. + The batch stewardship endpoint requires `read:stewardships` only. + + + **V1 constraints:** All stewardship rows are `unassigned`. + Write endpoints and state transitions are deferred to v2. + +servers: + - url: https://cm.lfx.dev/api/v1 + description: Production + - url: https://lf-staging.crowd.dev/api/v1 + description: Staging + +tags: + - name: Packages + description: Package list and detail. + - name: Stewardship + description: Stewardship state — individual and batch. + +components: + securitySchemes: + M2MBearer: + type: http + scheme: bearer + bearerFormat: JWT + + schemas: + # ── Shared ─────────────────────────────────────────────────────────────────── + + Error: + type: object + required: [error] + properties: + error: + type: object + required: [code, message] + properties: + code: + type: string + example: NOT_FOUND + message: + type: string + example: Package not found. + + OpenVulns: + type: object + description: Open vulnerability counts by severity from advisory_packages + advisories. + required: [low, medium, high, critical] + properties: + low: + type: integer + example: 0 + medium: + type: integer + example: 0 + high: + type: integer + example: 1 + critical: + type: integer + example: 0 + + # ── Stewardship ───────────────────────────────────────────────────────────── + + StewardshipStatus: + type: string + enum: + - unassigned + - open + - assessing + - active + - needs_attention + - escalated + - blocked + - inactive + + Steward: + type: object + required: [userId, role, assignedAt] + properties: + userId: + type: string + description: Auth0 sub of the assigned steward. + example: abc123 + username: + type: + - string + - 'null' + description: Username of the steward. Null if not available. + example: jrodriguez + displayName: + type: + - string + - 'null' + description: Display name of the steward. Null if not available. + example: Jonathan R. + role: + type: string + enum: [lead, co_steward] + assignedAt: + type: string + format: date-time + + # Lean shape used in batch response and list items. + StewardshipSummary: + type: object + description: Null if the purl is not found in CDP. + required: + [name, ecosystem, stewardship, stewards, openVulns, lastActivityAt, lastActivityDescription] + properties: + name: + type: string + example: lodash + ecosystem: + type: string + example: npm + lifecycle: + type: + - string + - 'null' + enum: [active, stable, declining, abandoned, null] + health: + type: + - integer + - 'null' + example: 52 + impact: + type: + - integer + - 'null' + example: 94 + openVulns: + oneOf: + - $ref: '#/components/schemas/OpenVulns' + - type: 'null' + stewardship: + $ref: '#/components/schemas/StewardshipStatus' + stewards: + description: Assigned stewards or null. Empty in v1. + oneOf: + - type: array + items: + $ref: '#/components/schemas/Steward' + - type: 'null' + lastActivityAt: + type: + - string + - 'null' + format: date-time + description: Null in v1. + lastActivityDescription: + type: + - string + - 'null' + description: Null in v1. + + # ── Package list item ──────────────────────────────────────────────────────── + + PackageListItem: + type: object + required: [purl, name, ecosystem] + properties: + purl: + type: string + description: Used to call GET /packages/detail?purl= when a row is clicked. + example: pkg:npm/lodash + name: + type: string + example: lodash + ecosystem: + type: string + example: npm + health: + type: + - integer + - 'null' + example: 18 + impact: + type: + - integer + - 'null' + example: 71 + lifecycle: + type: + - string + - 'null' + enum: [active, stable, declining, abandoned, null] + maintainerBusFactor: + type: + - integer + - 'null' + example: 1 + openVulns: + oneOf: + - $ref: '#/components/schemas/OpenVulns' + - type: 'null' + stewardshipId: + type: + - string + - 'null' + description: Stewardship ID. Required to call mutation endpoints (assign/escalate/status). Null if no stewardship row exists. + example: '42' + stewardship: + $ref: '#/components/schemas/StewardshipStatus' + stewards: + description: Assigned stewards. Empty array if none. + type: array + items: + $ref: '#/components/schemas/Steward' + + # ── Package detail ─────────────────────────────────────────────────────────── + + Advisory: + type: object + required: [osvId, severity] + properties: + osvId: + type: string + description: GHSA or CVE identifier. + example: CVE-2021-44906 + severity: + type: string + enum: [critical, high, medium, low] + resolution: + type: + - string + - 'null' + description: Resolution status. TBD. + + SecurityContact: + type: object + required: [name] + properties: + name: + type: + - string + - 'null' + email: + type: + - string + - 'null' + format: email + + PackageDetail: + type: object + required: [purl, name, ecosystem, general, assessment, security, provenance, history] + properties: + purl: + type: string + example: pkg:npm/lodash + name: + type: string + example: lodash + ecosystem: + type: string + example: npm + general: + type: object + properties: + healthScore: + type: + - object + - 'null' + properties: + maintainerHealth: + type: + - integer + - 'null' + example: 4 + securitySupplyChain: + type: + - integer + - 'null' + example: 8 + developmentActivity: + type: + - integer + - 'null' + example: 6 + total: + type: + - integer + - 'null' + example: 18 + impact: + type: + - object + - 'null' + properties: + impactScore: + type: + - integer + - 'null' + example: 71 + downloadsLastMonth: + type: + - integer + - 'null' + description: Null for Maven (Sonatype data not yet ingested). + example: 52142891 + dependentPackages: + type: + - integer + - 'null' + example: 142312 + dependentRepos: + type: + - integer + - 'null' + example: 39104 + transitiveReach: + type: + - string + - 'null' + example: 'Top 0.4%' + riskSignals: + type: + - object + - 'null' + properties: + lifecycle: + type: + - string + - 'null' + enum: [active, stable, declining, abandoned, null] + maintainerBusFactor: + type: + - integer + - 'null' + example: 1 + lastRelease: + type: + - string + - 'null' + format: date-time + hasSecurityFile: + type: + - boolean + - 'null' + openSSFScorecard: + type: + - number + - 'null' + format: float + example: 5.2 + assessment: + type: object + description: Stewardship assessment data. Empty in v1 — assessment flow deferred to v2. + security: + type: object + properties: + securityContacts: + type: + - array + - 'null' + items: + $ref: '#/components/schemas/SecurityContact' + advisories: + type: array + items: + $ref: '#/components/schemas/Advisory' + cvd: + type: object + description: Coordinated Vulnerability Disclosure readiness signals. + properties: + isPvrEnabled: + type: + - boolean + - 'null' + description: Private Vulnerability Reporting enabled. Null in v1. + hasSecurityPolicyEnabled: + type: + - boolean + - 'null' + description: SECURITY.md present in repo. Null until enricher captures this. + tier0Steward: + type: + - string + - 'null' + description: Name of the Tier 0 steward if assigned. Null in v1. + criticalVulnerabilityFlag: + type: + - boolean + - 'null' + description: True if any open advisory has cvss >= 7.0. + provenance: + type: object + properties: + repositoryMapping: + type: + - object + - 'null' + properties: + declaredRepo: + type: + - string + - 'null' + format: uri + example: https://github.com/lodash/lodash + mappingConfidence: + type: + - number + - 'null' + format: float + example: 0.98 + lastCommitAt: + type: + - string + - 'null' + format: date-time + supplyChainIntegrity: + type: object + description: All fields null in v1 — separate ingestion workstream required. + properties: + buildProvenance: + type: + - string + - 'null' + signedReleases: + type: + - string + - 'null' + stewardship: + type: object + description: Stewardship state. + properties: + id: + type: + - string + - 'null' + description: Stewardship ID. Required to call mutation endpoints (assign/escalate/status). + example: '42' + status: + $ref: '#/components/schemas/StewardshipStatus' + stewards: + description: Assigned stewards or null. + oneOf: + - type: array + items: + $ref: '#/components/schemas/Steward' + - type: 'null' + lastActivityAt: + type: + - string + - 'null' + format: date-time + resolutionPath: + type: + - string + - 'null' + description: Set on `escalated` status. Null for all other statuses. + statusNote: + type: + - string + - 'null' + description: Free-text note for the current status. + history: + type: object + description: Package history data. Empty in v1. + + # ── Metrics ────────────────────────────────────────────────────────────────── + + PackagesMetrics: + type: object + required: [totalPackages, criticalPackages] + properties: + totalPackages: + type: integer + example: 0 + criticalPackages: + type: integer + example: 0 + +# ────────────────────────────────────────────────────────────────────────────── +# Paths +# ────────────────────────────────────────────────────────────────────────────── +paths: + /packages: + get: + operationId: listPackages + summary: List packages + tags: + - Packages + security: + - M2MBearer: + - read:packages + - read:stewardships + parameters: + - name: page + in: query + schema: + type: integer + minimum: 1 + default: 1 + - name: pageSize + in: query + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: ecosystem + in: query + schema: + type: string + - name: lifecycle + in: query + schema: + type: string + enum: [active, stable, declining, abandoned] + - name: busFactor1Only + in: query + schema: + type: boolean + default: false + description: Return only packages with bus factor = 1. + - name: staleOnly + in: query + schema: + type: boolean + default: false + description: Return only packages with no release in >= 18 months. + - name: unstewardedOnly + in: query + schema: + type: boolean + default: false + description: Return only packages with stewardship = unassigned. + - name: sortBy + in: query + schema: + type: string + enum: [name, health, impact, openVulns] + default: name + - name: sortDir + in: query + schema: + type: string + enum: [asc, desc] + default: asc + responses: + '200': + description: Paginated list of packages. + content: + application/json: + schema: + type: object + required: [page, pageSize, total, filters, sort, packages] + properties: + page: + type: integer + pageSize: + type: integer + total: + type: integer + filters: + type: object + properties: + ecosystem: + type: + - string + - 'null' + lifecycle: + type: + - string + - 'null' + busFactor1Only: + type: boolean + staleOnly: + type: boolean + unstewardedOnly: + type: boolean + sort: + type: object + properties: + by: + type: string + dir: + type: string + packages: + type: array + items: + $ref: '#/components/schemas/PackageListItem' + '400': + description: Validation error (e.g. invalid query parameters). + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Insufficient scopes. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /packages/metrics: + get: + operationId: getPackagesMetrics + summary: Overview metrics for the list page header + tags: + - Packages + security: + - M2MBearer: + - read:packages + - read:stewardships + responses: + '200': + description: Aggregate package metrics. + content: + application/json: + schema: + $ref: '#/components/schemas/PackagesMetrics' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Insufficient scopes. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /packages/detail: + get: + operationId: getPackage + summary: Get full package detail + description: > + Returns data for the drawer's Overview, Security, and Provenance tabs. + Pass the canonical purl as a query parameter — no URL-encoding needed. + Example: `?purl=pkg:npm/lodash@4.17.21`. + tags: + - Packages + security: + - M2MBearer: + - read:packages + - read:stewardships + parameters: + - name: purl + in: query + required: true + schema: + type: string + example: 'pkg:npm/lodash@4.17.21' + responses: + '200': + description: Package found. + content: + application/json: + schema: + $ref: '#/components/schemas/PackageDetail' + example: + purl: pkg:npm/lodash + name: lodash + ecosystem: npm + general: + healthScore: + maintainerHealth: 4 + securitySupplyChain: 8 + developmentActivity: 6 + total: 18 + impact: + impactScore: 71 + downloadsLastMonth: 52142891 + dependentPackages: 142312 + dependentRepos: 39104 + transitiveReach: 'Top 0.4%' + riskSignals: + lifecycle: declining + maintainerBusFactor: 1 + lastRelease: '2021-02-20T00:00:00Z' + hasSecurityFile: null + openSSFScorecard: 5.2 + assessment: {} + security: + securityContacts: null + advisories: + - osvId: CVE-2021-44906 + severity: high + resolution: null + cvd: + isPvrEnabled: null + hasSecurityPolicyEnabled: null + tier0Steward: null + criticalVulnerabilityFlag: true + provenance: + repositoryMapping: + declaredRepo: https://github.com/lodash/lodash + mappingConfidence: 0.98 + lastCommitAt: '2024-09-14T00:00:00Z' + supplyChainIntegrity: + buildProvenance: null + signedReleases: null + stewardship: + status: unassigned + stewards: null + lastActivityAt: null + history: {} + '404': + description: Package not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Insufficient scopes. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /packages:batch-stewardship: + post: + operationId: batchGetStewardship + summary: Batch stewardship state for a list of purls + description: > + Returns lean stewardship state for up to 100 purls in one request. + Purls not found in CDP return `null`. + tags: + - Stewardship + security: + - M2MBearer: + - read:stewardships + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [purls] + properties: + purls: + type: array + items: + type: string + minItems: 1 + maxItems: 100 + example: + - pkg:npm/lodash + - pkg:npm/express + - pkg:pypi/requests + responses: + '200': + description: > + Stewardship state keyed by purl. + Unknown or invalid purls return `null`. + content: + application/json: + schema: + type: object + required: [packages] + properties: + packages: + type: object + additionalProperties: + oneOf: + - $ref: '#/components/schemas/StewardshipSummary' + - type: 'null' + example: + packages: + pkg:npm/lodash@4.17.21: + name: lodash + ecosystem: npm + lifecycle: declining + health: 18 + impact: 71 + openVulns: + low: 0 + medium: 0 + high: 1 + critical: 0 + stewardship: unassigned + stewards: null + lastActivityAt: null + lastActivityDescription: null + pkg:pypi/requests: null + '400': + description: Validation error (e.g. more than 100 purls). + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Insufficient scopes. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' diff --git a/backend/src/api/public/v1/packages/purl.ts b/backend/src/api/public/v1/packages/purl.ts new file mode 100644 index 0000000000..7a043a9ed1 --- /dev/null +++ b/backend/src/api/public/v1/packages/purl.ts @@ -0,0 +1,35 @@ +/** + * Normalize a PURL for lookup against the packages table. + * + * The DB stores versionless PURLs with npm scope @ encoded as %40. + * Clients may send purls with versions (pkg:npm/lodash@4.17.21) and qualifiers. + * + * Transform order: + * 1. strip ?qualifiers and #subpath — not stored in DB + * 2. strip @version suffix — DB stores versionless PURLs + * 3. encode @ in namespace/scope (e.g. npm @babel → %40babel) + * + * The version regex matches @ followed by non-/ non-@ chars at end of string. + * This is always the version separator, not an npm scope (pkg:npm/@babel/core + * has @babel followed by /core, so it never matches the end-of-string pattern). + */ +import { z } from 'zod' + +export function normalizePurl(purl: string): string { + const withoutQualifiers = purl.replace(/[?#].*$/, '') + const withoutVersion = withoutQualifiers.replace(/@[^/@]+$/, '') + return withoutVersion.replace(/@/g, '%40') +} + +export const purlFieldSchema = z + .string() + .trim() + .min(1) + .refine((v) => v.startsWith('pkg:'), { message: 'purl must start with pkg:' }) + .transform(normalizePurl) + +export const purlQuerySchema = z.object({ purl: purlFieldSchema }) + +// Loose schema for search filters: normalizes without requiring the pkg: prefix, +// so partial inputs (e.g. "@babel/core" or "lodash") are accepted. +export const purlFilterSchema = z.string().trim().transform(normalizePurl).optional() diff --git a/backend/src/api/public/v1/packages/types.ts b/backend/src/api/public/v1/packages/types.ts new file mode 100644 index 0000000000..6b4f542af3 --- /dev/null +++ b/backend/src/api/public/v1/packages/types.ts @@ -0,0 +1,44 @@ +export const STEWARDSHIP_STATUS_VALUES = [ + 'unassigned', + 'open', + 'assessing', + 'active', + 'needs_attention', + 'escalated', + 'blocked', + 'inactive', +] as const + +export type StewardshipStatus = (typeof STEWARDSHIP_STATUS_VALUES)[number] + +export type Lifecycle = 'active' | 'stable' | 'declining' | 'abandoned' + +export type SeverityLevel = 'critical' | 'high' | 'medium' | 'low' + +export interface OpenVulns { + low: number + medium: number + high: number + critical: number +} + +export interface Steward { + userId: string + username: string | null + displayName: string | null + role: 'lead' | 'co_steward' + assignedAt: string +} + +export interface StewardshipSummary { + name: string + ecosystem: string + lifecycle: Lifecycle | null + health: number | null + impact: number | null + openVulns: OpenVulns | null + stewardship: StewardshipStatus | null + stewards: Steward[] | null + lastActivityAt: string | null + lastActivityDescription: string | null +} diff --git a/backend/src/api/public/v1/stewardships/actorSchema.ts b/backend/src/api/public/v1/stewardships/actorSchema.ts new file mode 100644 index 0000000000..fe616686fe --- /dev/null +++ b/backend/src/api/public/v1/stewardships/actorSchema.ts @@ -0,0 +1,7 @@ +import { z } from 'zod' + +export const actorInputSchema = z.object({ + username: z.string().trim().min(1).optional().nullable(), + displayName: z.string().trim().min(1).optional().nullable(), + avatarUrl: z.string().url().optional().nullable(), +}) diff --git a/backend/src/api/public/v1/stewardships/assignSteward.ts b/backend/src/api/public/v1/stewardships/assignSteward.ts new file mode 100644 index 0000000000..4428302226 --- /dev/null +++ b/backend/src/api/public/v1/stewardships/assignSteward.ts @@ -0,0 +1,57 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { NotFoundError } from '@crowd/common' +import { assignSteward } from '@crowd/data-access-layer' + +import { getPackagesQx } from '@/db/packagesDb' +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +import { actorInputSchema } from './actorSchema' + +const paramsSchema = z.object({ + id: z.coerce.number().int().positive(), +}) + +const bodySchema = z.object({ + steward: z + .object({ + userId: z.string().trim().min(1), + username: z.string().trim().min(1).optional().nullable(), + displayName: z.string().trim().min(1).optional().nullable(), + role: z.enum(['lead', 'co_steward']), + }) + .refine((d) => (d.username == null) === (d.displayName == null), { + message: 'username and displayName must both be provided or both be absent', + path: ['displayName'], + }), + note: z.string().trim().min(1).optional(), + moveToAssessing: z.boolean().optional().default(false), + actor: actorInputSchema, +}) + +export async function assignStewardHandler(req: Request, res: Response): Promise { + const { id } = validateOrThrow(paramsSchema, req.params) + const { steward, note, moveToAssessing, actor } = validateOrThrow(bodySchema, req.body) + + const qx = await getPackagesQx() + const result = await assignSteward(qx, id, { + userId: steward.userId, + username: steward.username, + displayName: steward.displayName, + role: steward.role, + note, + assignedBy: req.actor.id, + actorUsername: actor.username ?? null, + actorDisplayName: actor.displayName ?? null, + actorAvatarUrl: actor.avatarUrl ?? null, + moveToAssessing, + }) + + if (!result) { + throw new NotFoundError(`Stewardship not found: ${id}`) + } + + ok(res, { stewardship: result.stewardship, stewards: result.stewards }) +} diff --git a/backend/src/api/public/v1/stewardships/escalate.ts b/backend/src/api/public/v1/stewardships/escalate.ts new file mode 100644 index 0000000000..2bf475a854 --- /dev/null +++ b/backend/src/api/public/v1/stewardships/escalate.ts @@ -0,0 +1,42 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { NotFoundError } from '@crowd/common' +import { ESCALATION_RESOLUTION_PATHS, escalateStewardship } from '@crowd/data-access-layer' + +import { getPackagesQx } from '@/db/packagesDb' +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +import { actorInputSchema } from './actorSchema' + +const paramsSchema = z.object({ + id: z.coerce.number().int().positive(), +}) + +const bodySchema = z.object({ + resolutionPath: z.enum(ESCALATION_RESOLUTION_PATHS), + notes: z.string().trim().min(1).optional(), + actor: actorInputSchema, +}) + +export async function escalateHandler(req: Request, res: Response): Promise { + const { id } = validateOrThrow(paramsSchema, req.params) + const { resolutionPath, notes, actor } = validateOrThrow(bodySchema, req.body) + + const qx = await getPackagesQx() + const stewardship = await escalateStewardship(qx, id, { + resolutionPath, + notes, + actorUserId: req.actor.id, + actorUsername: actor.username ?? null, + actorDisplayName: actor.displayName ?? null, + actorAvatarUrl: actor.avatarUrl ?? null, + }) + + if (!stewardship) { + throw new NotFoundError(`Stewardship not found: ${id}`) + } + + ok(res, { stewardship }) +} diff --git a/backend/src/api/public/v1/stewardships/getMyActivity.ts b/backend/src/api/public/v1/stewardships/getMyActivity.ts new file mode 100644 index 0000000000..c6f2430d30 --- /dev/null +++ b/backend/src/api/public/v1/stewardships/getMyActivity.ts @@ -0,0 +1,76 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { listMyActivity } from '@crowd/data-access-layer' + +import { getPackagesQx } from '@/db/packagesDb' +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +const VALID_STATUSES = [ + 'assessing', + 'active', + 'needs_attention', + 'escalated', + 'blocked', + 'unassigned', + 'open', + 'inactive', +] as const + +const querySchema = z.object({ + page: z.coerce.number().int().min(1).default(1), + pageSize: z.coerce.number().int().min(1).max(100).default(3), + status: z + .string() + .optional() + .transform((v) => { + if (!v) return undefined + const parts = v + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + return parts.length > 0 ? parts : undefined + }) + .pipe(z.array(z.enum(VALID_STATUSES)).optional()), +}) + +const SUGGESTED_ACTIONS: Record = { + needs_attention: 'Review & respond', + blocked: 'Resolve blocker', + escalated: 'Add escalation context', + assessing: 'Continue assessment', + active: 'View stewardship', +} + +export async function getMyActivityHandler(req: Request, res: Response): Promise { + const params = validateOrThrow(querySchema, req.query) + + const qx = await getPackagesQx() + const { rows, total } = await listMyActivity(qx, { + userId: req.actor.id, + page: params.page, + pageSize: params.pageSize, + status: params.status, + }) + + ok(res, { + data: rows.map((r) => ({ + stewardshipId: r.stewardshipId, + packageName: r.packageName, + purl: r.packagePurl, + packageEcosystem: r.packageEcosystem, + stewardshipStatus: r.stewardshipStatus, + activityType: r.activityType, + description: r.content, + actor: r.actor, + createdAt: r.createdAt, + suggestedAction: SUGGESTED_ACTIONS[r.stewardshipStatus] ?? null, + })), + meta: { + total, + page: params.page, + pageSize: params.pageSize, + }, + }) +} diff --git a/backend/src/api/public/v1/stewardships/getMyPackages.ts b/backend/src/api/public/v1/stewardships/getMyPackages.ts new file mode 100644 index 0000000000..69ade91770 --- /dev/null +++ b/backend/src/api/public/v1/stewardships/getMyPackages.ts @@ -0,0 +1,54 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { computeHealthBand, listMyPackages } from '@crowd/data-access-layer' + +import { getPackagesQx } from '@/db/packagesDb' +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +const querySchema = z.object({ + page: z.coerce.number().int().min(1).default(1), + pageSize: z.coerce.number().int().min(1).max(100).default(25), + status: z.enum(['assessing', 'active', 'needs_attention', 'escalated', 'blocked']).optional(), + search: z.string().trim().optional(), + ecosystem: z.string().trim().optional(), + healthBand: z.enum(['healthy', 'fair', 'concerning', 'critical']).optional(), + vulnSeverity: z.enum(['high', 'critical']).optional(), + sortBy: z.enum(['risk', 'health', 'vulns', 'name', 'last_activity']).default('risk'), + sortDir: z.enum(['asc', 'desc']).default('desc'), +}) + +export async function getMyPackagesHandler(req: Request, res: Response): Promise { + const params = validateOrThrow(querySchema, req.query) + + const qx = await getPackagesQx() + const { rows, total, statusCounts } = await listMyPackages(qx, { + userId: req.actor.id, + ...params, + }) + + ok(res, { + data: rows.map((r) => ({ + purl: r.purl, + name: r.name, + ecosystem: r.ecosystem, + lifecycle: r.lifecycle, + healthScore: r.scorecardScore != null ? Math.round(r.scorecardScore * 10) : null, + healthBand: computeHealthBand(r.scorecardScore), + openVulns: r.openVulns, + vulnSeverity: r.maxVulnSeverity, + lastActivityDescription: r.lastActivityDescription, + lastActivityAt: r.lastActivityAt ? r.lastActivityAt.toISOString() : null, + stewardshipId: r.stewardshipId, + stewardshipStatus: r.stewardshipStatus, + myRole: r.myRole, + })), + meta: { + total, + page: params.page, + pageSize: params.pageSize, + statusCounts, + }, + }) +} diff --git a/backend/src/api/public/v1/stewardships/index.ts b/backend/src/api/public/v1/stewardships/index.ts new file mode 100644 index 0000000000..11fe360f71 --- /dev/null +++ b/backend/src/api/public/v1/stewardships/index.ts @@ -0,0 +1,38 @@ +import { Router } from 'express' + +import { createRateLimiter } from '@/api/apiRateLimiter' +import { requireScopes } from '@/api/public/middlewares/requireScopes' +import { safeWrap } from '@/middlewares/errorMiddleware' +import { SCOPES } from '@/security/scopes' + +import { assignStewardHandler } from './assignSteward' +import { escalateHandler } from './escalate' +import { openStewardship } from './openStewardship' +import { updateStatusHandler } from './updateStatus' + +const rateLimiter = createRateLimiter({ max: 60, windowMs: 60 * 1000 }) + +// TODO[deprecate]: superseded by /v1/akrites/stewardships — remove once consumers have migrated +export function stewardshipsRouter(): Router { + const router = Router() + + router.use(rateLimiter) + + router.post('/', requireScopes([SCOPES.WRITE_STEWARDSHIPS]), safeWrap(openStewardship)) + + router.put( + '/:id/steward', + requireScopes([SCOPES.WRITE_STEWARDSHIPS]), + safeWrap(assignStewardHandler), + ) + + router.put('/:id/escalate', requireScopes([SCOPES.WRITE_STEWARDSHIPS]), safeWrap(escalateHandler)) + + router.put( + '/:id/status', + requireScopes([SCOPES.WRITE_STEWARDSHIPS]), + safeWrap(updateStatusHandler), + ) + + return router +} diff --git a/backend/src/api/public/v1/stewardships/openStewardship.ts b/backend/src/api/public/v1/stewardships/openStewardship.ts new file mode 100644 index 0000000000..8b4d59a8bf --- /dev/null +++ b/backend/src/api/public/v1/stewardships/openStewardship.ts @@ -0,0 +1,38 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { NotFoundError } from '@crowd/common' +import { openStewardshipByPurl } from '@crowd/data-access-layer' + +import { getPackagesQx } from '@/db/packagesDb' +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +import { purlFieldSchema } from '../packages/purl' + +import { actorInputSchema } from './actorSchema' + +const bodySchema = z.object({ + purl: purlFieldSchema, + actor: actorInputSchema, +}) + +export async function openStewardship(req: Request, res: Response): Promise { + const { purl, actor } = validateOrThrow(bodySchema, req.body) + + const qx = await getPackagesQx() + const stewardship = await openStewardshipByPurl( + qx, + purl, + req.actor.id, + actor.username ?? null, + actor.displayName ?? null, + actor.avatarUrl ?? null, + ) + + if (!stewardship) { + throw new NotFoundError(`Package not found: ${purl}`) + } + + ok(res, { stewardship }) +} diff --git a/backend/src/api/public/v1/stewardships/openapi.yaml b/backend/src/api/public/v1/stewardships/openapi.yaml new file mode 100644 index 0000000000..6028ae4855 --- /dev/null +++ b/backend/src/api/public/v1/stewardships/openapi.yaml @@ -0,0 +1,670 @@ +openapi: 3.1.0 +info: + title: CDP Public API — Stewardship Admin Actions + version: 1.0.0 + description: > + Write endpoints for OSSPREY Program admin stewardship actions. + + + **Authentication:** OAuth 2.0 M2M bearer token. All endpoints require `write:stewardships`. + + + **V1 note:** Scope `write:stewardships` is not yet added to the Auth0 staging tenant — + scope enforcement is temporarily disabled. See CM-1235. + +servers: + - url: https://cm.lfx.dev/api/v1/akrites + description: Production + - url: https://lf-staging.crowd.dev/api/v1/akrites + description: Staging + +tags: + - name: Stewardship Actions + description: Admin-initiated stewardship mutations. + +components: + securitySchemes: + M2MBearer: + type: http + scheme: bearer + bearerFormat: JWT + + schemas: + Error: + type: object + required: [error] + properties: + error: + type: object + required: [code, message] + properties: + code: + type: string + example: NOT_FOUND + message: + type: string + example: Stewardship not found. + + ActivityActor: + type: object + required: [userId] + description: Profile of the actor who performed an activity. Stored as a snapshot on the activity log. + properties: + userId: + type: string + description: Auth0 sub of the actor. + example: auth0|abc123 + username: + type: + - string + - 'null' + description: LFX username of the actor. + example: gaspergrom + displayName: + type: + - string + - 'null' + description: Full display name of the actor. + example: Gašper Grom + avatarUrl: + type: + - string + - 'null' + format: uri + description: Avatar URL of the actor. + example: 'https://avatars.githubusercontent.com/u/12345' + + ActorInput: + type: object + required: [userId] + description: > + Profile of the actor performing this action. Stored as a snapshot on the activity log. + `userId` is required. All other fields are optional and can be null. + properties: + userId: + type: string + minLength: 1 + description: Auth0 sub of the actor. Must match the authenticated user's token sub. + example: auth0|abc123 + username: + type: + - string + - 'null' + minLength: 1 + description: LFX username of the actor. + example: gaspergrom + displayName: + type: + - string + - 'null' + minLength: 1 + description: Full display name of the actor. + example: Gašper Grom + avatarUrl: + type: + - string + - 'null' + format: uri + description: Avatar URL of the actor. + example: 'https://avatars.githubusercontent.com/u/12345' + + StewardshipStatus: + type: string + enum: + - unassigned + - open + - assessing + - active + - needs_attention + - escalated + - blocked + - inactive + + StewardshipRecord: + type: object + required: [id, packageId, status, origin, version, createdAt, updatedAt] + properties: + id: + type: string + example: '42' + packageId: + type: string + example: '1234' + status: + $ref: '#/components/schemas/StewardshipStatus' + origin: + type: string + enum: [auto_imported, self_claimed, assigned, opened_for_claim] + version: + type: integer + example: 1 + openedAt: + type: + - string + - 'null' + format: date-time + lastStatusAt: + type: + - string + - 'null' + format: date-time + inactiveReason: + type: + - string + - 'null' + enum: + - quarterly_cadence_missed + - stepped_down + - no_longer_critical + - 'null' + resolutionPath: + description: Set on `escalated` status. Null for all other statuses. + oneOf: + - $ref: '#/components/schemas/EscalationResolutionPath' + - type: 'null' + statusNote: + type: + - string + - 'null' + description: Free-text note for the current status. Set by escalate or updateStatus. Null on open. + example: Contacted maintainer, no response after 30 days. + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + + StewardRecord: + type: object + required: [id, stewardshipId, userId, role, assignedAt] + properties: + id: + type: string + example: '7' + stewardshipId: + type: string + example: '42' + userId: + type: string + description: Auth0 sub of the assigned steward. + example: abc123 + username: + type: + - string + - 'null' + description: LFX username of the steward. Null if not yet stored. + example: joanagmaia + displayName: + type: + - string + - 'null' + description: Full display name of the steward. Null if not yet stored. + example: Joana Maia + role: + type: string + enum: [lead, co_steward] + assignedAt: + type: string + format: date-time + assignedBy: + type: + - string + - 'null' + description: Auth0 sub of the admin who assigned this steward. + example: xyz789 + + EscalationResolutionPath: + type: string + enum: + - right_of_first_refusal + - replace_the_dependency + - find_vendor_for_lts + - consortium_adopts_maintainership + - compensating_controls_monitor + - namespace_takeover + +paths: + /stewardships/open: + post: + operationId: openStewardship + summary: Open a package for stewardship + description: > + Transitions the stewardship status to `open`, marking the package as available for claiming. + If a stewardship row does not exist yet, one is created. + If the stewardship is already `open`, this is a no-op (idempotent). + tags: + - Stewardship Actions + security: + - M2MBearer: + - write:stewardships + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [purl, actor] + properties: + purl: + type: string + description: Package URL (must start with `pkg:`). + example: pkg:npm/lodash + actor: + $ref: '#/components/schemas/ActorInput' + example: + purl: pkg:npm/lodash + actor: + userId: auth0|abc123 + username: gaspergrom + displayName: Gašper Grom + avatarUrl: 'https://avatars.githubusercontent.com/u/12345' + responses: + '200': + description: Stewardship opened (or already open). + content: + application/json: + schema: + type: object + required: [stewardship] + properties: + stewardship: + $ref: '#/components/schemas/StewardshipRecord' + example: + stewardship: + id: '42' + packageId: '1234' + status: open + origin: opened_for_claim + version: 1 + openedAt: '2026-06-15T10:00:00Z' + lastStatusAt: '2026-06-15T10:00:00Z' + inactiveReason: null + createdAt: '2026-06-15T10:00:00Z' + updatedAt: '2026-06-15T10:00:00Z' + '400': + description: Validation error (e.g. missing or invalid purl). + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Insufficient scopes. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Package not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /stewardships/{id}/assign: + post: + operationId: assignSteward + summary: Assign a steward to a stewardship + description: > + Assigns a user as a steward with the given role. + If the user is already an active steward, the role is updated (soft-delete + re-insert). + Returns the unchanged stewardship record and the full active stewards list after the operation. + tags: + - Stewardship Actions + security: + - M2MBearer: + - write:stewardships + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Stewardship ID. + example: 42 + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [steward, actor] + properties: + steward: + type: object + required: [userId, role] + description: > + The user to assign as steward. + `username` and `displayName` must be provided together or both omitted — sending one without the other returns 400. + properties: + userId: + type: string + minLength: 1 + description: Auth0 sub of the user to assign as steward. + example: abc123 + username: + type: + - string + - 'null' + description: LFX username of the steward. Must be provided together with `displayName`. + example: joanagmaia + displayName: + type: + - string + - 'null' + description: Full display name of the steward. Must be provided together with `username`. + example: Joana Maia + role: + type: string + enum: [lead, co_steward] + note: + type: string + minLength: 1 + description: Optional free-text note for the activity log. + moveToAssessing: + type: boolean + default: false + description: > + If true, atomically transitions the stewardship status to `assessing` + in the same transaction as the assignment. Use for the "Assign & move to + Assessing" action to avoid a second round-trip. + actor: + $ref: '#/components/schemas/ActorInput' + example: + steward: + userId: abc123 + role: lead + moveToAssessing: true + actor: + userId: auth0|xyz + username: gaspergrom + displayName: Gašper Grom + avatarUrl: 'https://avatars.githubusercontent.com/u/12345' + responses: + '200': + description: Steward assigned. + content: + application/json: + schema: + type: object + required: [stewardship, stewards] + properties: + stewardship: + $ref: '#/components/schemas/StewardshipRecord' + stewards: + type: array + items: + $ref: '#/components/schemas/StewardRecord' + example: + stewardship: + id: '42' + packageId: '1234' + status: open + origin: opened_for_claim + version: 1 + openedAt: '2026-06-15T10:00:00Z' + lastStatusAt: '2026-06-15T10:00:00Z' + inactiveReason: null + createdAt: '2026-06-15T10:00:00Z' + updatedAt: '2026-06-15T10:00:00Z' + stewards: + - id: '7' + stewardshipId: '42' + userId: abc123 + role: lead + assignedAt: '2026-06-15T10:05:00Z' + assignedBy: xyz789 + '400': + description: Validation error (e.g. invalid role). + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Insufficient scopes. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Stewardship not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /stewardships/{id}/escalate: + post: + operationId: escalateStewardship + summary: Escalate a stewardship + description: > + Transitions the stewardship to `escalated` status and logs the chosen resolution path + in the activity log. Optional free-text notes can be included. + tags: + - Stewardship Actions + security: + - M2MBearer: + - write:stewardships + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Stewardship ID. + example: 42 + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [resolutionPath, actor] + properties: + resolutionPath: + $ref: '#/components/schemas/EscalationResolutionPath' + notes: + type: string + minLength: 1 + description: Optional free-text notes for the activity log. + example: Contacted maintainer, no response after 30 days. + actor: + $ref: '#/components/schemas/ActorInput' + example: + resolutionPath: right_of_first_refusal + notes: Contacted maintainer, no response after 30 days. + actor: + userId: auth0|abc123 + username: gaspergrom + displayName: Gašper Grom + avatarUrl: 'https://avatars.githubusercontent.com/u/12345' + responses: + '200': + description: Stewardship escalated. + content: + application/json: + schema: + type: object + required: [stewardship] + properties: + stewardship: + $ref: '#/components/schemas/StewardshipRecord' + example: + stewardship: + id: '42' + packageId: '1234' + status: escalated + origin: opened_for_claim + version: 1 + openedAt: '2026-06-15T10:00:00Z' + lastStatusAt: '2026-06-15T11:00:00Z' + inactiveReason: null + createdAt: '2026-06-15T10:00:00Z' + updatedAt: '2026-06-15T11:00:00Z' + '400': + description: Validation error (e.g. invalid resolutionPath). + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Insufficient scopes. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Stewardship not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /stewardships/{id}/status: + patch: + operationId: updateStewardshipStatus + summary: Update stewardship status + description: > + Updates the stewardship status and logs a `state_changed` activity entry. + Valid target statuses: `assessing`, `active`, `needs_attention`, `blocked`, `inactive`. + When transitioning to `inactive`, `inactiveReason` is required. + tags: + - Stewardship Actions + security: + - M2MBearer: + - write:stewardships + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Stewardship ID. + example: 42 + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [status, actor] + properties: + status: + type: string + enum: + - assessing + - active + - needs_attention + - blocked + - inactive + inactiveReason: + type: string + enum: + - quarterly_cadence_missed + - stepped_down + - no_longer_critical + description: Required when `status` is `inactive`. + notes: + type: string + minLength: 1 + description: Optional free-text notes for the activity log. + actor: + $ref: '#/components/schemas/ActorInput' + examples: + set_active: + summary: Transition to active + value: + status: active + actor: + userId: auth0|abc123 + username: gaspergrom + displayName: Gašper Grom + avatarUrl: 'https://avatars.githubusercontent.com/u/12345' + set_inactive: + summary: Transition to inactive (inactiveReason required) + value: + status: inactive + inactiveReason: stepped_down + notes: Steward stepped down voluntarily. + actor: + userId: auth0|abc123 + username: gaspergrom + displayName: Gašper Grom + avatarUrl: 'https://avatars.githubusercontent.com/u/12345' + set_blocked: + summary: Transition to blocked + value: + status: blocked + notes: Waiting on upstream maintainer response. + actor: + userId: auth0|abc123 + username: gaspergrom + displayName: Gašper Grom + avatarUrl: 'https://avatars.githubusercontent.com/u/12345' + responses: + '200': + description: Status updated. + content: + application/json: + schema: + type: object + required: [stewardship] + properties: + stewardship: + $ref: '#/components/schemas/StewardshipRecord' + example: + stewardship: + id: '42' + packageId: '1234' + status: active + origin: opened_for_claim + version: 1 + openedAt: '2026-06-15T10:00:00Z' + lastStatusAt: '2026-06-15T12:00:00Z' + inactiveReason: null + createdAt: '2026-06-15T10:00:00Z' + updatedAt: '2026-06-15T12:00:00Z' + '400': + description: > + Validation error. + Includes the case where `status` is `inactive` but `inactiveReason` is missing. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Insufficient scopes. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Stewardship not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' diff --git a/backend/src/api/public/v1/stewardships/updateStatus.ts b/backend/src/api/public/v1/stewardships/updateStatus.ts new file mode 100644 index 0000000000..2bb3d058ff --- /dev/null +++ b/backend/src/api/public/v1/stewardships/updateStatus.ts @@ -0,0 +1,53 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { NotFoundError } from '@crowd/common' +import { + INACTIVE_REASONS, + STEWARDSHIP_UPDATABLE_STATUSES, + updateStewardshipStatus, +} from '@crowd/data-access-layer' + +import { getPackagesQx } from '@/db/packagesDb' +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +import { actorInputSchema } from './actorSchema' + +const paramsSchema = z.object({ + id: z.coerce.number().int().positive(), +}) + +const bodySchema = z + .object({ + status: z.enum(STEWARDSHIP_UPDATABLE_STATUSES), + inactiveReason: z.enum(INACTIVE_REASONS).optional(), + notes: z.string().trim().min(1).optional(), + actor: actorInputSchema, + }) + .refine((d) => d.status !== 'inactive' || !!d.inactiveReason, { + message: 'inactiveReason is required when status is inactive', + path: ['inactiveReason'], + }) + +export async function updateStatusHandler(req: Request, res: Response): Promise { + const { id } = validateOrThrow(paramsSchema, req.params) + const { status, inactiveReason, notes, actor } = validateOrThrow(bodySchema, req.body) + + const qx = await getPackagesQx() + const stewardship = await updateStewardshipStatus(qx, id, { + status, + inactiveReason, + notes, + actorUserId: req.actor.id, + actorUsername: actor.username ?? null, + actorDisplayName: actor.displayName ?? null, + actorAvatarUrl: actor.avatarUrl ?? null, + }) + + if (!stewardship) { + throw new NotFoundError(`Stewardship not found: ${id}`) + } + + ok(res, { stewardship }) +} diff --git a/backend/src/api/quickstart-guide/index.ts b/backend/src/api/quickstart-guide/index.ts deleted file mode 100644 index 0dee4144ac..0000000000 --- a/backend/src/api/quickstart-guide/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { safeWrap } from '../../middlewares/errorMiddleware' - -export default (app) => { - app.get(`/tenant/:tenantId/quickstart-guide`, safeWrap(require('./quickstartGuideList').default)) - app.post( - `/tenant/:tenantId/quickstart-guide/settings`, - safeWrap(require('./quickstartGuideSettingsUpdate').default), - ) -} diff --git a/backend/src/api/quickstart-guide/quickstartGuideList.ts b/backend/src/api/quickstart-guide/quickstartGuideList.ts deleted file mode 100644 index 28cad5e964..0000000000 --- a/backend/src/api/quickstart-guide/quickstartGuideList.ts +++ /dev/null @@ -1,11 +0,0 @@ -import PermissionChecker from '../../services/user/permissionChecker' -import Permissions from '../../security/permissions' -import QuickstartGuideService from '../../services/quickstartGuideService' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.quickstartGuideRead) - - const payload = await new QuickstartGuideService(req).find() - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/quickstart-guide/quickstartGuideSettingsUpdate.ts b/backend/src/api/quickstart-guide/quickstartGuideSettingsUpdate.ts deleted file mode 100644 index 18a527ce65..0000000000 --- a/backend/src/api/quickstart-guide/quickstartGuideSettingsUpdate.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Permissions from '../../security/permissions' -import QuickstartGuideService from '../../services/quickstartGuideService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.quickstartGuideSettingsUpdate) - - const payload = await new QuickstartGuideService(req).updateSettings(req.body) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/report/index.ts b/backend/src/api/report/index.ts deleted file mode 100644 index ca2c05460c..0000000000 --- a/backend/src/api/report/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { safeWrap } from '../../middlewares/errorMiddleware' - -export default (app) => { - app.post(`/tenant/:tenantId/report`, safeWrap(require('./reportCreate').default)) - app.post(`/tenant/:tenantId/report/query`, safeWrap(require('./reportQuery').default)) - app.put(`/tenant/:tenantId/report/:id`, safeWrap(require('./reportUpdate').default)) - app.post(`/tenant/:tenantId/report/:id/duplicate`, safeWrap(require('./reportDuplicate').default)) - app.post(`/tenant/:tenantId/report/import`, safeWrap(require('./reportImport').default)) - app.delete(`/tenant/:tenantId/report`, safeWrap(require('./reportDestroy').default)) - app.get( - `/tenant/:tenantId/report/autocomplete`, - safeWrap(require('./reportAutocomplete').default), - ) - app.get(`/tenant/:tenantId/report`, safeWrap(require('./reportList').default)) - app.get(`/tenant/:tenantId/report/:id`, safeWrap(require('./reportFind').default)) -} diff --git a/backend/src/api/report/reportAutocomplete.ts b/backend/src/api/report/reportAutocomplete.ts deleted file mode 100644 index abff1f75b6..0000000000 --- a/backend/src/api/report/reportAutocomplete.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Permissions from '../../security/permissions' -import ReportService from '../../services/reportService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.reportAutocomplete) - - const payload = await new ReportService(req).findAllAutocomplete(req.query.query, req.query.limit) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/report/reportCreate.ts b/backend/src/api/report/reportCreate.ts deleted file mode 100644 index 2899236029..0000000000 --- a/backend/src/api/report/reportCreate.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Error403 from '../../errors/Error403' -import Permissions from '../../security/permissions' -import track from '../../segment/track' -import ReportService from '../../services/reportService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.reportCreate) - - if (req.body.isTemplate) { - await req.responseHandler.error( - req, - res, - new Error403(req.language, 'errors.report.templateReportsCreateNotAllowed'), - ) - return - } - - const payload = await new ReportService(req).create(req.body) - - track( - 'Report Created', - { id: payload.id, name: payload.name, public: payload.public }, - { ...req }, - ) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/report/reportDestroy.ts b/backend/src/api/report/reportDestroy.ts deleted file mode 100644 index c79c53c562..0000000000 --- a/backend/src/api/report/reportDestroy.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Permissions from '../../security/permissions' -import ReportService from '../../services/reportService' -import PermissionChecker from '../../services/user/permissionChecker' -import track from '../../segment/track' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.reportDestroy) - - await new ReportService(req).destroyAll(req.query.ids) - - track('Report Deleted', { ids: req.query.ids }, { ...req }) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/report/reportDuplicate.ts b/backend/src/api/report/reportDuplicate.ts deleted file mode 100644 index 0a31ca2ec8..0000000000 --- a/backend/src/api/report/reportDuplicate.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Permissions from '../../security/permissions' -import track from '../../segment/track' -import ReportService from '../../services/reportService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.reportCreate) - - const payload = await new ReportService(req).duplicate(req.params.id) - - track( - 'Report Duplicated', - { id: payload.id, name: payload.name, public: payload.public }, - { ...req }, - ) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/report/reportFind.ts b/backend/src/api/report/reportFind.ts deleted file mode 100644 index 22eb258c18..0000000000 --- a/backend/src/api/report/reportFind.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Permissions from '../../security/permissions' -import ReportService from '../../services/reportService' -import PermissionChecker from '../../services/user/permissionChecker' -import track from '../../segment/track' - -export default async (req, res) => { - const reportService = new ReportService(req) - const payload = await reportService.findById(req.params.id) - - if (!payload.public) { - new PermissionChecker(req).validateHas(Permissions.values.reportRead) - } - - if (req.currentUser && req.currentUser.id) { - const viewedBy = new Set(payload.viewedBy).add(req.currentUser.id) - await reportService.update(payload.id, { viewedBy: Array.from(viewedBy) }) - } - - track('Report Viewed', { id: payload.id, name: payload.name, public: payload.public }, { ...req }) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/report/reportImport.ts b/backend/src/api/report/reportImport.ts deleted file mode 100644 index 8345ca2a28..0000000000 --- a/backend/src/api/report/reportImport.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Permissions from '../../security/permissions' -import ReportService from '../../services/reportService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.reportImport) - - await new ReportService(req).import(req.body, req.body.importHash) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/report/reportList.ts b/backend/src/api/report/reportList.ts deleted file mode 100644 index 70d5ad5978..0000000000 --- a/backend/src/api/report/reportList.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Permissions from '../../security/permissions' -import ReportService from '../../services/reportService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.reportRead) - - const payload = await new ReportService(req).findAndCountAll(req.query) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/report/reportQuery.ts b/backend/src/api/report/reportQuery.ts deleted file mode 100644 index 90ce69331e..0000000000 --- a/backend/src/api/report/reportQuery.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Permissions from '../../security/permissions' -import track from '../../segment/track' -import ReportService from '../../services/reportService' -import PermissionChecker from '../../services/user/permissionChecker' - -// /** -// * POST /tenant/{tenantId}/report -// * @summary Create or update an report -// * @tag Activities -// * @security Bearer -// * @description Create or update an report. Existence is checked by sourceId and tenantId. -// * @pathParam {string} tenantId - Your workspace/tenant ID -// * @bodyContent {ReportUpsertInput} application/json -// * @response 200 - Ok -// * @responseContent {Report} 200.application/json -// * @responseExample {ReportUpsert} 200.application/json.Report -// * @response 401 - Unauthorized -// * @response 404 - Not found -// * @response 429 - Too many requests -// */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.reportRead) - - const payload = await new ReportService(req).query(req.body) - - if (req.query.filter && Object.keys(req.query.filter).length > 0) { - track('Reports Advanced Filter', { ...payload }, { ...req }) - } - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/report/reportUpdate.ts b/backend/src/api/report/reportUpdate.ts deleted file mode 100644 index cd6e88fc2a..0000000000 --- a/backend/src/api/report/reportUpdate.ts +++ /dev/null @@ -1,27 +0,0 @@ -import Error403 from '../../errors/Error403' -import Permissions from '../../security/permissions' -import track from '../../segment/track' -import ReportService from '../../services/reportService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.reportEdit) - if (req.body.isTemplate) { - await req.responseHandler.error( - req, - res, - new Error403(req.language, 'errors.report.templateReportsUpdateNotAllowed'), - ) - return - } - - const payload = await new ReportService(req).update(req.params.id, req.body) - - track( - 'Report Updated', - { id: payload.id, name: payload.name, public: payload.public }, - { ...req }, - ) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/segment/index.ts b/backend/src/api/segment/index.ts index aa797b2f81..a9d0f0b04b 100644 --- a/backend/src/api/segment/index.ts +++ b/backend/src/api/segment/index.ts @@ -1,39 +1,28 @@ import { safeWrap } from '../../middlewares/errorMiddleware' export default (app) => { - app.post( - `/tenant/:tenantId/segment/projectGroup`, - safeWrap(require('./segmentCreateProjectGroup').default), - ) - app.post(`/tenant/:tenantId/segment/project`, safeWrap(require('./segmentCreateProject').default)) - app.post( - `/tenant/:tenantId/segment/subproject`, - safeWrap(require('./segmentCreateSubproject').default), - ) + app.post(`/segment/projectGroup`, safeWrap(require('./segmentCreateProjectGroup').default)) + app.post(`/segment/project`, safeWrap(require('./segmentCreateProject').default)) + app.post(`/segment/subproject`, safeWrap(require('./segmentCreateSubproject').default)) // query all project groups - app.post( - `/tenant/:tenantId/segment/projectGroup/query`, - safeWrap(require('./segmentProjectGroupQuery').default), - ) + app.post(`/segment/projectGroup/query`, safeWrap(require('./segmentProjectGroupQuery').default)) // query all projects - app.post( - `/tenant/:tenantId/segment/project/query`, - safeWrap(require('./segmentProjectQuery').default), - ) + app.post(`/segment/project/query`, safeWrap(require('./segmentProjectQuery').default)) // query all subprojects + app.post(`/segment/subproject/query`, safeWrap(require('./segmentSubprojectQuery').default)) + + // query all subprojects lite app.post( - `/tenant/:tenantId/segment/subproject/query`, - safeWrap(require('./segmentSubprojectQuery').default), + `/segment/subproject/query-lite`, + safeWrap(require('./segmentSubprojectQueryLite').default), ) // get segment by id - app.get(`/tenant/:tenantId/segment/:id`, safeWrap(require('./segmentFind').default)) - app.put(`/tenant/:tenantId/segment/:id`, safeWrap(require('./segmentUpdate').default)) - - // app.get(`/tenant/:tenantId/segment/projectGroup/:id`, safeWrap(require('./segmentFind').default)) - // app.get(`/tenant/:tenantId/segment/project/:id`, safeWrap(require('./segmentFind').default)) - // app.get(`/tenant/:tenantId/segment/subproject/:id`, safeWrap(require('./segmentFind').default)) + app.get(`/segment/:segmentId`, safeWrap(require('./segmentFind').default)) + app.put(`/segment/:segmentId`, safeWrap(require('./segmentUpdate').default)) + // Multiple ids + app.post(`/segment/id`, safeWrap(require('./segmentByIds').default)) } diff --git a/backend/src/api/segment/segmentByIds.ts b/backend/src/api/segment/segmentByIds.ts new file mode 100644 index 0000000000..4bdef55c5b --- /dev/null +++ b/backend/src/api/segment/segmentByIds.ts @@ -0,0 +1,12 @@ +import Permissions from '../../security/permissions' +import SegmentService from '../../services/segmentService' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.segmentRead) + + const segmentService = new SegmentService(req) + const payload = await segmentService.findByIds(req.body.ids) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/segment/segmentCreateProjectGroup.ts b/backend/src/api/segment/segmentCreateProjectGroup.ts index 9304dd60e3..d7489347eb 100644 --- a/backend/src/api/segment/segmentCreateProjectGroup.ts +++ b/backend/src/api/segment/segmentCreateProjectGroup.ts @@ -3,7 +3,7 @@ import SegmentService from '../../services/segmentService' import PermissionChecker from '../../services/user/permissionChecker' export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.segmentCreate) + new PermissionChecker(req).validateHas(Permissions.values.projectGroupCreate) const payload = await new SegmentService(req).createProjectGroup(req.body) diff --git a/backend/src/api/segment/segmentFind.ts b/backend/src/api/segment/segmentFind.ts index d9c9c92cea..86dffe1637 100644 --- a/backend/src/api/segment/segmentFind.ts +++ b/backend/src/api/segment/segmentFind.ts @@ -6,7 +6,7 @@ export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.segmentRead) const segmentService = new SegmentService(req) - const payload = await segmentService.findById(req.params.id) + const payload = await segmentService.findById(req.params.segmentId) await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/segment/segmentSubprojectQueryLite.ts b/backend/src/api/segment/segmentSubprojectQueryLite.ts new file mode 100644 index 0000000000..2de163b34b --- /dev/null +++ b/backend/src/api/segment/segmentSubprojectQueryLite.ts @@ -0,0 +1,11 @@ +import Permissions from '../../security/permissions' +import SegmentService from '../../services/segmentService' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.segmentRead) + + const payload = await new SegmentService(req).querySubprojectsLite(req.body) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/segment/segmentUpdate.ts b/backend/src/api/segment/segmentUpdate.ts index c97ab87123..a16dd1a9b3 100644 --- a/backend/src/api/segment/segmentUpdate.ts +++ b/backend/src/api/segment/segmentUpdate.ts @@ -5,7 +5,7 @@ import PermissionChecker from '../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.segmentEdit) - const payload = await new SegmentService(req).update(req.params.id, req.body) + const payload = await new SegmentService(req).update(req.params.segmentId, req.body) await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/settings/activityTypeCreate.ts b/backend/src/api/settings/activityTypeCreate.ts index 0fb72d0c46..de69d468db 100644 --- a/backend/src/api/settings/activityTypeCreate.ts +++ b/backend/src/api/settings/activityTypeCreate.ts @@ -3,12 +3,11 @@ import SegmentService from '../../services/segmentService' import PermissionChecker from '../../services/user/permissionChecker' /** - * POST /tenant/{tenantId}/settings/activity/types + * POST /settings/activity/types * @summary Create an activity type * @tag Activities * @security Bearer * @description Create a custom activity type - * @pathParam {string} tenantId - Your workspace/tenant ID * @bodyContent {ActivityTypesCreateInput} application/json * @response 200 - Ok * @responseContent {ActivityTypes} 200.application/json diff --git a/backend/src/api/settings/activityTypeList.ts b/backend/src/api/settings/activityTypeList.ts index 74c9a7fc85..b4b26814ec 100644 --- a/backend/src/api/settings/activityTypeList.ts +++ b/backend/src/api/settings/activityTypeList.ts @@ -1,12 +1,11 @@ import SegmentService from '../../services/segmentService' /** - * GET /tenant/{tenantId}/settings/activity/types + * GET /settings/activity/types * @summary List all activity types * @tag Activities * @security Bearer * @description List all activity types - * @pathParam {string} tenantId - Your workspace/tenant ID * @response 200 - Ok * @responseContent {ActivityTypes} 200.application/json * @responseExample {ActivityTypes} 200.application/json.ActivityTypes diff --git a/backend/src/api/settings/activityTypeUpdate.ts b/backend/src/api/settings/activityTypeUpdate.ts index a52cd26ed1..689a6316d7 100644 --- a/backend/src/api/settings/activityTypeUpdate.ts +++ b/backend/src/api/settings/activityTypeUpdate.ts @@ -3,12 +3,11 @@ import SegmentService from '../../services/segmentService' import PermissionChecker from '../../services/user/permissionChecker' /** - * PUT /tenant/{tenantId}/settings/activity/types/{key} + * PUT /settings/activity/types/{key} * @summary Update an activity type * @tag Activities * @security Bearer * @description Update a custom activity type - * @pathParam {string} tenantId - Your workspace/tenant ID * @pathParam {string} key - The key of the activity type * @bodyContent {ActivityTypesUpdateInput} application/json * @response 200 - Ok diff --git a/backend/src/api/settings/index.ts b/backend/src/api/settings/index.ts index 9ea669dbb5..65b137749c 100644 --- a/backend/src/api/settings/index.ts +++ b/backend/src/api/settings/index.ts @@ -1,47 +1,20 @@ import { safeWrap } from '../../middlewares/errorMiddleware' export default (app) => { - app.put(`/tenant/:tenantId/settings`, safeWrap(require('./settingsSave').default)) - app.get(`/tenant/:tenantId/settings`, safeWrap(require('./settingsFind').default)) + app.put(`/settings`, safeWrap(require('./settingsSave').default)) + app.get(`/settings`, safeWrap(require('./settingsFind').default)) - app.get( - '/tenant/:tenantId/settings/activity/types', - safeWrap(require('./activityTypeList').default), - ) + app.get('/settings/activity/types', safeWrap(require('./activityTypeList').default)) - app.post( - '/tenant/:tenantId/settings/activity/types', - safeWrap(require('./activityTypeCreate').default), - ) + app.post('/settings/activity/types', safeWrap(require('./activityTypeCreate').default)) - app.put( - '/tenant/:tenantId/settings/activity/types/:key', - safeWrap(require('./activityTypeUpdate').default), - ) + app.put('/settings/activity/types/:key', safeWrap(require('./activityTypeUpdate').default)) - app.delete( - '/tenant/:tenantId/settings/activity/types/:key', - safeWrap(require('./activityTypeDestroy').default), - ) + app.delete('/settings/activity/types/:key', safeWrap(require('./activityTypeDestroy').default)) - app.post( - '/tenant/:tenantId/settings/members/attributes', - safeWrap(require('./memberAttributeCreate').default), - ) - app.delete( - `/tenant/:tenantId/settings/members/attributes`, - safeWrap(require('./memberAttributeDestroy').default), - ) - app.put( - `/tenant/:tenantId/settings/members/attributes/:id`, - safeWrap(require('./memberAttributeUpdate').default), - ) - app.get( - `/tenant/:tenantId/settings/members/attributes`, - safeWrap(require('./memberAttributeList').default), - ) - app.get( - `/tenant/:tenantId/settings/members/attributes/:id`, - safeWrap(require('./memberAttributeFind').default), - ) + app.post('/settings/members/attributes', safeWrap(require('./memberAttributeCreate').default)) + app.delete(`/settings/members/attributes`, safeWrap(require('./memberAttributeDestroy').default)) + app.put(`/settings/members/attributes/:id`, safeWrap(require('./memberAttributeUpdate').default)) + app.get(`/settings/members/attributes`, safeWrap(require('./memberAttributeList').default)) + app.get(`/settings/members/attributes/:id`, safeWrap(require('./memberAttributeFind').default)) } diff --git a/backend/src/api/settings/memberAttributeCreate.ts b/backend/src/api/settings/memberAttributeCreate.ts index 36670f817e..3b55fc5545 100644 --- a/backend/src/api/settings/memberAttributeCreate.ts +++ b/backend/src/api/settings/memberAttributeCreate.ts @@ -3,12 +3,11 @@ import MemberAttributeSettingsService from '../../services/memberAttributeSettin import PermissionChecker from '../../services/user/permissionChecker' /** - * POST /tenant/{tenantId}/settings/members/attributes + * POST /settings/members/attributes * @summary Attribute settings: create * @tag Members * @security Bearer * @description Create a members' attribute setting - * @pathParam {string} tenantId - Your workspace/tenant ID * @bodyContent {MemberAttributeSettingsCreateInput} application/json * @response 200 - Ok * @responseContent {MemberAttributeSettings} 200.application/json diff --git a/backend/src/api/settings/memberAttributeDestroy.ts b/backend/src/api/settings/memberAttributeDestroy.ts index 76a3fa8eb7..926385b361 100644 --- a/backend/src/api/settings/memberAttributeDestroy.ts +++ b/backend/src/api/settings/memberAttributeDestroy.ts @@ -3,12 +3,11 @@ import MemberAttributeSettingsService from '../../services/memberAttributeSettin import PermissionChecker from '../../services/user/permissionChecker' /** - * DELETE /tenant/{tenantId}/settings/members/attributes + * DELETE /settings/members/attributes * @summary Attribute settings: delete * @tag Members * @security Bearer * @description Delete a members' attribute setting - * @pathParam {string} tenantId - Your workspace/tenant ID * @queryParam {string} id - Id to destroy * @response 200 - Ok * @response 401 - Unauthorized diff --git a/backend/src/api/settings/memberAttributeFind.ts b/backend/src/api/settings/memberAttributeFind.ts index 00982604b9..f33122ff7f 100644 --- a/backend/src/api/settings/memberAttributeFind.ts +++ b/backend/src/api/settings/memberAttributeFind.ts @@ -3,12 +3,11 @@ import MemberAttributeSettingsService from '../../services/memberAttributeSettin import PermissionChecker from '../../services/user/permissionChecker' /** - * GET /tenant/{tenantId}/settings/members/attributes/{id} + * GET /settings/members/attributes/{id} * @summary Attributes settings: find * @tag Members * @security Bearer * @description Find a single members' attribute setting by ID - * @pathParam {string} tenantId - Your workspace/tenant ID * @pathParam {string} id - The ID of the member attribute's settings * @response 200 - Ok * @responseContent {MemberAttributeSettings} 200.application/json diff --git a/backend/src/api/settings/memberAttributeList.ts b/backend/src/api/settings/memberAttributeList.ts index 7996937e07..bfb37b8971 100644 --- a/backend/src/api/settings/memberAttributeList.ts +++ b/backend/src/api/settings/memberAttributeList.ts @@ -3,12 +3,11 @@ import MemberAttributeSettingsService from '../../services/memberAttributeSettin import PermissionChecker from '../../services/user/permissionChecker' /** - * GET /tenant/{tenantId}/settings/members/attributes + * GET /settings/members/attributes * @summary Attributes settings: list * @tag Members * @security Bearer * @description Get a list of members' attribute settings - * @pathParam {string} tenantId - Your workspace/tenant ID * @queryParam {string} [filter[label]] - Filter by label of member attribute settings * @queryParam {string} [filter[name]] - Filter by name of member attribute settings * @queryParam {string} [filter[type]] - Filter by type of member attribute settings diff --git a/backend/src/api/settings/memberAttributeUpdate.ts b/backend/src/api/settings/memberAttributeUpdate.ts index 37c61372f8..2b39147a82 100644 --- a/backend/src/api/settings/memberAttributeUpdate.ts +++ b/backend/src/api/settings/memberAttributeUpdate.ts @@ -3,12 +3,11 @@ import MemberAttributeSettingsService from '../../services/memberAttributeSettin import PermissionChecker from '../../services/user/permissionChecker' /** - * PUT /tenant/{tenantId}/settings/members/attributes/{id} + * PUT /settings/members/attributes/{id} * @summary Attribute settings: update * @tag Members * @security Bearer * @description Update a members' attribute setting - * @pathParam {string} tenantId - Your workspace/tenant ID * @pathParam {string} id - The ID of the member attribute settings * @bodyContent {MemberAttributeSettingsUpdateInput} application/json * @response 200 - Ok diff --git a/backend/src/api/systemStatus/index.ts b/backend/src/api/systemStatus/index.ts new file mode 100644 index 0000000000..1bae32c7c0 --- /dev/null +++ b/backend/src/api/systemStatus/index.ts @@ -0,0 +1,5 @@ +import { safeWrap } from '../../middlewares/errorMiddleware' + +export default (app) => { + app.get(`/system-status`, safeWrap(require('./systemStatus').default)) +} diff --git a/backend/src/api/systemStatus/systemStatus.ts b/backend/src/api/systemStatus/systemStatus.ts new file mode 100644 index 0000000000..0ffe64fb13 --- /dev/null +++ b/backend/src/api/systemStatus/systemStatus.ts @@ -0,0 +1,9 @@ +import Axios from 'axios' + +import { OPEN_STATUS_API_CONFIG } from '../../conf' + +export default async (req, res) => { + const response = await Axios.get(OPEN_STATUS_API_CONFIG.baseUrl) + + return req.responseHandler.success(req, res, response.data) +} diff --git a/backend/src/api/tag/index.ts b/backend/src/api/tag/index.ts deleted file mode 100644 index 9366f253b8..0000000000 --- a/backend/src/api/tag/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { safeWrap } from '../../middlewares/errorMiddleware' - -export default (app) => { - app.post(`/tenant/:tenantId/tag`, safeWrap(require('./tagCreate').default)) - app.post(`/tenant/:tenantId/tag/query`, safeWrap(require('./tagQuery').default)) - app.put(`/tenant/:tenantId/tag/:id`, safeWrap(require('./tagUpdate').default)) - app.post(`/tenant/:tenantId/tag/import`, safeWrap(require('./tagImport').default)) - app.delete(`/tenant/:tenantId/tag`, safeWrap(require('./tagDestroy').default)) - app.get(`/tenant/:tenantId/tag/autocomplete`, safeWrap(require('./tagAutocomplete').default)) - app.get(`/tenant/:tenantId/tag`, safeWrap(require('./tagList').default)) - app.get(`/tenant/:tenantId/tag/:id`, safeWrap(require('./tagFind').default)) -} diff --git a/backend/src/api/tag/tagAutocomplete.ts b/backend/src/api/tag/tagAutocomplete.ts deleted file mode 100644 index aae671342b..0000000000 --- a/backend/src/api/tag/tagAutocomplete.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Permissions from '../../security/permissions' -import TagService from '../../services/tagService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.tagAutocomplete) - - const payload = await new TagService(req).findAllAutocomplete(req.query.query, req.query.limit) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/tag/tagCreate.ts b/backend/src/api/tag/tagCreate.ts deleted file mode 100644 index 5a0aa26741..0000000000 --- a/backend/src/api/tag/tagCreate.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Permissions from '../../security/permissions' -import TagService from '../../services/tagService' -import PermissionChecker from '../../services/user/permissionChecker' - -/** - * POST /tenant/{tenantId}/tag - * @summary Create a tag - * @tag Tags - * @security Bearer - * @description Create a tag - * @pathParam {string} tenantId - Your workspace/tenant ID - * @bodyContent {TagNoId} application/json - * @response 200 - Ok - * @responseContent {Tag} 200.application/json - * @responseExample {Tag} 200.application/json.Tag - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.tagCreate) - - const payload = await new TagService(req).create(req.body) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/tag/tagDestroy.ts b/backend/src/api/tag/tagDestroy.ts deleted file mode 100644 index f21ea895d0..0000000000 --- a/backend/src/api/tag/tagDestroy.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Permissions from '../../security/permissions' -import TagService from '../../services/tagService' -import PermissionChecker from '../../services/user/permissionChecker' - -/** - * DELETE /tenant/{tenantId}/tag/{id} - * @summary Delete a tag - * @tag Tags - * @security Bearer - * @description Delete a tag. - * @pathParam {string} tenantId - Your workspace/tenant ID - * @pathParam {string} id - The ID of the tag - * @response 200 - Ok - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.tagDestroy) - - await new TagService(req).destroyAll(req.query.ids) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/tag/tagFind.ts b/backend/src/api/tag/tagFind.ts deleted file mode 100644 index 1cfd2bd543..0000000000 --- a/backend/src/api/tag/tagFind.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Permissions from '../../security/permissions' -import TagService from '../../services/tagService' -import PermissionChecker from '../../services/user/permissionChecker' - -/** - * GET /tenant/{tenantId}/tag/{id} - * @summary Find a tag - * @tag Tags - * @security Bearer - * @description Find a tag by ID - * @pathParam {string} tenantId - Your workspace/tenant ID - * @pathParam {string} id - The ID of the tag - * @response 200 - Ok - * @responseContent {Tag} 200.application/json - * @responseExample {Tag} 200.application/json.Tag - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.tagRead) - - const payload = await new TagService(req).findById(req.params.id) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/tag/tagImport.ts b/backend/src/api/tag/tagImport.ts deleted file mode 100644 index 6dcdf68d03..0000000000 --- a/backend/src/api/tag/tagImport.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Permissions from '../../security/permissions' -import TagService from '../../services/tagService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.tagImport) - - await new TagService(req).import(req.body, req.body.importHash) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/tag/tagList.ts b/backend/src/api/tag/tagList.ts deleted file mode 100644 index 81a04238aa..0000000000 --- a/backend/src/api/tag/tagList.ts +++ /dev/null @@ -1,29 +0,0 @@ -import Permissions from '../../security/permissions' -import TagService from '../../services/tagService' -import PermissionChecker from '../../services/user/permissionChecker' - -/** - * GET /tenant/{tenantId}/tag - * @summary List tags - * @tag Tags - * @security Bearer - * @description Get a list of tags with filtering, sorting and offsetting. - * @pathParam {string} tenantId - Your workspace/tenant ID - * @queryParam {string} [filter[name]] - Filter by the name of the tag. - * @queryParam {string} [filter[createdAtRange]] - Created at lower bound. If you want a range, send this parameter twice with [min] and [max]. If you send it once it will be interpreted as a lower bound. - * @queryParam {TagSort} [orderBy] - Sort the results. Default timestamp_DESC. - * @queryParam {number} [offset] - Skip the first n results. Default 0. - * @queryParam {number} [limit] - Limit the number of results. Default 50. - * @response 200 - Ok - * @responseContent {TagList} 200.application/json - * @responseExample {TagList} 200.application/json.Tags - * @response 401 - Unauthorized - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.tagRead) - - const payload = await new TagService(req).findAndCountAll(req.query) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/tag/tagQuery.ts b/backend/src/api/tag/tagQuery.ts deleted file mode 100644 index ef26cd23a6..0000000000 --- a/backend/src/api/tag/tagQuery.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Permissions from '../../security/permissions' -import track from '../../segment/track' -import TagService from '../../services/tagService' -import PermissionChecker from '../../services/user/permissionChecker' - -// /** -// * POST /tenant/{tenantId}/tag -// * @summary Create or update an tag -// * @tag Activities -// * @security Bearer -// * @description Create or update an tag. Existence is checked by sourceId and tenantId. -// * @pathParam {string} tenantId - Your workspace/tenant ID -// * @bodyContent {TagUpsertInput} application/json -// * @response 200 - Ok -// * @responseContent {Tag} 200.application/json -// * @responseExample {TagUpsert} 200.application/json.Tag -// * @response 401 - Unauthorized -// * @response 404 - Not found -// * @response 429 - Too many requests -// */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.tagRead) - - const payload = await new TagService(req).query(req.body) - - if (req.query.filter && Object.keys(req.query.filter).length > 0) { - track('Tags Advanced Filter', { ...payload }, { ...req }) - } - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/tag/tagUpdate.ts b/backend/src/api/tag/tagUpdate.ts deleted file mode 100644 index 2c5022f38d..0000000000 --- a/backend/src/api/tag/tagUpdate.ts +++ /dev/null @@ -1,27 +0,0 @@ -import Permissions from '../../security/permissions' -import TagService from '../../services/tagService' -import PermissionChecker from '../../services/user/permissionChecker' - -/** - * PUT /tenant/{tenantId}/tag/{id} - * @summary Update an tag - * @tag Tags - * @security Bearer - * @description Update a tag - * @pathParam {string} tenantId - Your workspace/tenant ID - * @pathParam {string} id - The ID of the tag - * @bodyContent {TagNoId} application/json - * @response 200 - Ok - * @responseContent {Tag} 200.application/json - * @responseExample {Tag} 200.application/json.Tag - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.tagEdit) - - const payload = await new TagService(req).update(req.params.id, req.body) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/task/index.ts b/backend/src/api/task/index.ts deleted file mode 100644 index 45cc7c6b5e..0000000000 --- a/backend/src/api/task/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { safeWrap } from '../../middlewares/errorMiddleware' - -export default (app) => { - app.post(`/tenant/:tenantId/task/query`, safeWrap(require('./taskQuery').default)) - app.post(`/tenant/:tenantId/task`, safeWrap(require('./taskCreate').default)) - app.put(`/tenant/:tenantId/task/:id`, safeWrap(require('./taskUpdate').default)) - app.put(`/tenant/:tenantId/task/:id`, safeWrap(require('./taskUpdate').default)) - app.post(`/tenant/:tenantId/task/import`, safeWrap(require('./taskImport').default)) - app.delete(`/tenant/:tenantId/task`, safeWrap(require('./taskDestroy').default)) - app.get(`/tenant/:tenantId/task/autocomplete`, safeWrap(require('./taskAutocomplete').default)) - app.get(`/tenant/:tenantId/task`, safeWrap(require('./taskList').default)) - app.get(`/tenant/:tenantId/task/:id`, safeWrap(require('./taskFind').default)) - app.post(`/tenant/:tenantId/task/batch`, safeWrap(require('./taskBatchOperations').default)) -} diff --git a/backend/src/api/task/taskAutocomplete.ts b/backend/src/api/task/taskAutocomplete.ts deleted file mode 100644 index 17ff10de69..0000000000 --- a/backend/src/api/task/taskAutocomplete.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Permissions from '../../security/permissions' -import TaskService from '../../services/taskService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.taskAutocomplete) - - const payload = await new TaskService(req).findAllAutocomplete(req.query.query, req.query.limit) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/task/taskBatchOperations.ts b/backend/src/api/task/taskBatchOperations.ts deleted file mode 100644 index 0f8f33b5d3..0000000000 --- a/backend/src/api/task/taskBatchOperations.ts +++ /dev/null @@ -1,37 +0,0 @@ -import PermissionChecker from '../../services/user/permissionChecker' -import Permissions from '../../security/permissions' -import TaskService from '../../services/taskService' -import Error400 from '../../errors/Error400' - -/** - * POST /tenant/{tenantId}/task/batch - * @summary Make batch operations on tasks - * @tag Tasks - * @security Bearer - * @description Make batch operations on tasks - * @pathParam {string} tenantId - Your workspace/tenant ID - * @bodyContent {TaskBatchInput} application/json - * @response 200 - Ok - * @responseContent {TaskFindAndUpdateAll} 200.application/json - * @responseExample {TaskFindAndUpdateAll} 200.application/json.Task - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.taskBatch) - - let payload - switch (req.body.operation) { - case 'findAndUpdateAll': - payload = await new TaskService(req).findAndUpdateAll(req.body.payload) - break - case 'findAndDeleteAll': - payload = await new TaskService(req).findAndDeleteAll(req.body.payload) - break - default: - throw new Error400('en', 'tasks.errors.unknownBatchOperation', req.body.operation) - } - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/task/taskCreate.ts b/backend/src/api/task/taskCreate.ts deleted file mode 100644 index c5680cabc7..0000000000 --- a/backend/src/api/task/taskCreate.ts +++ /dev/null @@ -1,33 +0,0 @@ -import Permissions from '../../security/permissions' -import TaskService from '../../services/taskService' -import PermissionChecker from '../../services/user/permissionChecker' -import track from '../../segment/track' - -/** - * POST /tenant/{tenantId}/task - * @summary Create a task - * @tag Tasks - * @security Bearer - * @description Create a task - * @pathParam {string} tenantId - Your workspace/tenant ID - * @bodyContent {TaskInput} application/json - * @response 200 - Ok - * @responseContent {Task} 200.application/json - * @responseExample {Task} 200.application/json.Task - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.taskCreate) - - const payload = await new TaskService(req).create(req.body) - - track( - 'Task Created', - { id: payload.id, dueDate: payload.dueDate, members: payload.members }, - { ...req }, - ) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/task/taskDestroy.ts b/backend/src/api/task/taskDestroy.ts deleted file mode 100644 index 67be2d58dd..0000000000 --- a/backend/src/api/task/taskDestroy.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Permissions from '../../security/permissions' -import TaskService from '../../services/taskService' -import PermissionChecker from '../../services/user/permissionChecker' - -/** - * DELETE /tenant/{tenantId}/task/{id} - * @summary Delete a task - * @tag Tasks - * @security Bearer - * @description Delete a task. - * @pathParam {string} tenantId - Your workspace/tenant ID - * @pathParam {string} id - The ID of the task - * @response 200 - Ok - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.taskDestroy) - - await new TaskService(req).destroyAll(req.query.ids) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/task/taskFind.ts b/backend/src/api/task/taskFind.ts deleted file mode 100644 index b66012d739..0000000000 --- a/backend/src/api/task/taskFind.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Permissions from '../../security/permissions' -import TaskService from '../../services/taskService' -import PermissionChecker from '../../services/user/permissionChecker' - -/** - * GET /tenant/{tenantId}/task/{id} - * @summary Find a task - * @tag Tasks - * @security Bearer - * @description Find a task by ID - * @pathParam {string} tenantId - Your workspace/tenant ID - * @pathParam {string} id - The ID of the task - * @response 200 - Ok - * @responseContent {TaskResponse} 200.application/json - * @responseExample {Task} 200.application/json.Task - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.taskRead) - - const payload = await new TaskService(req).findById(req.params.id) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/task/taskImport.ts b/backend/src/api/task/taskImport.ts deleted file mode 100644 index b2b9ec5f18..0000000000 --- a/backend/src/api/task/taskImport.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Permissions from '../../security/permissions' -import TaskService from '../../services/taskService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.taskImport) - - await new TaskService(req).import(req.body, req.body.importHash) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/task/taskList.ts b/backend/src/api/task/taskList.ts deleted file mode 100644 index 95daf332cf..0000000000 --- a/backend/src/api/task/taskList.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Permissions from '../../security/permissions' -import TaskService from '../../services/taskService' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.taskRead) - - const payload = await new TaskService(req).findAndCountAll(req.query) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/task/taskQuery.ts b/backend/src/api/task/taskQuery.ts deleted file mode 100644 index 41d6f25e79..0000000000 --- a/backend/src/api/task/taskQuery.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Permissions from '../../security/permissions' -import track from '../../segment/track' -import TaskService from '../../services/taskService' -import PermissionChecker from '../../services/user/permissionChecker' - -/** - * POST /tenant/{tenantId}/task/query - * @summary Query tasks - * @tag Tasks - * @security Bearer - * @description Query tasks. It accepts filters, sorting options and pagination. - * @pathParam {string} tenantId - Your workspace/tenant ID - * @bodyContent {TaskQuery} application/json - * @response 200 - Ok - * @responseContent {TaskList} 200.application/json - * @responseExample {TaskList} 200.application/json.Task - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.taskRead) - - const payload = await new TaskService(req).query(req.body) - - if (req.query.filter && Object.keys(req.query.filter).length > 0) { - track('Tasks Advanced Filter', { ...payload }, { ...req }) - } - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/task/taskUpdate.ts b/backend/src/api/task/taskUpdate.ts deleted file mode 100644 index 8422122e47..0000000000 --- a/backend/src/api/task/taskUpdate.ts +++ /dev/null @@ -1,60 +0,0 @@ -import Permissions from '../../security/permissions' -import TaskService from '../../services/taskService' -import PermissionChecker from '../../services/user/permissionChecker' -import track from '../../segment/track' - -/** - * PUT /tenant/{tenantId}/task/{id} - * @summary Update an task - * @tag Tasks - * @security Bearer - * @description Update a task - * @pathParam {string} tenantId - Your workspace/tenant ID - * @pathParam {string} id - The ID of the task - * @bodyContent {TaskInput} application/json - * @response 200 - Ok - * @responseContent {Task} 200.application/json - * @responseExample {Task} 200.application/json.Task - * @response 401 - Unauthorized - * @response 404 - Not found - * @response 429 - Too many requests - */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.taskEdit) - - const taskBeforeUpdate = await new TaskService(req).findById(req.params.id) - const payload = await new TaskService(req).update(req.params.id, req.body) - - if (taskBeforeUpdate.type === 'suggested') { - track( - 'Task Created (from suggestion)', - { id: payload.id, dueDate: payload.dueDate, members: payload.members }, - { ...req }, - ) - } - if (taskBeforeUpdate.status === 'in-progress' && payload.status === 'done') { - track( - 'Task Completed', - { - id: payload.id, - dueDate: payload.dueDate, - members: payload.members, - status: payload.status, - }, - { ...req }, - ) - } else { - track( - 'Task Updated', - { - id: payload.id, - dueDate: payload.dueDate, - members: payload.members, - status: payload.status, - }, - { ...req }, - ) - } - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/tenant/index.ts b/backend/src/api/tenant/index.ts deleted file mode 100644 index acee2917f8..0000000000 --- a/backend/src/api/tenant/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { safeWrap } from '../../middlewares/errorMiddleware' - -export default (app) => { - app.post( - `/tenant/invitation/:token/accept`, - safeWrap(require('./tenantInvitationAccept').default), - ) - app.delete( - `/tenant/invitation/:token/decline`, - safeWrap(require('./tenantInvitationDecline').default), - ) - app.post(`/tenant`, safeWrap(require('./tenantCreate').default)) - app.put(`/tenant/:id`, safeWrap(require('./tenantUpdate').default)) - app.delete(`/tenant`, safeWrap(require('./tenantDestroy').default)) - app.get(`/tenant`, safeWrap(require('./tenantList').default)) - app.get(`/tenant/url`, safeWrap(require('./tenantFind').default)) - app.get(`/tenant/:id`, safeWrap(require('./tenantFind').default)) - app.get(`/tenant/:tenantId/membersToMerge`, safeWrap(require('./tenantMembersToMerge').default)) - app.get( - `/tenant/:tenantId/organizationsToMerge`, - safeWrap(require('./tenantOrganizationsToMerge').default), - ) - app.post(`/tenant/:tenantId/sampleData`, safeWrap(require('./tenantGenerateSampleData').default)) - app.delete(`/tenant/:tenantId/sampleData`, safeWrap(require('./tenantDeleteSampleData').default)) -} diff --git a/backend/src/api/tenant/tenantCreate.ts b/backend/src/api/tenant/tenantCreate.ts deleted file mode 100644 index 4cc6ed6317..0000000000 --- a/backend/src/api/tenant/tenantCreate.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Error403 from '../../errors/Error403' -import identifyTenant from '../../segment/identifyTenant' -import telemetryTrack from '../../segment/telemetryTrack' -import track from '../../segment/track' -import TenantService from '../../services/tenantService' - -export default async (req, res) => { - if (!req.currentUser || !req.currentUser.id) { - throw new Error403(req.language) - } - - const payload = await new TenantService(req).create(req.body) - - track( - 'Tenant Created', - { - id: payload.id, - name: payload.name, - onboard: !!payload.onboard, - }, - { ...req }, - ) - identifyTenant({ ...req, currentTenant: payload }) - - telemetryTrack('Tenant created', {}, { ...req }) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/tenant/tenantDeleteSampleData.ts b/backend/src/api/tenant/tenantDeleteSampleData.ts deleted file mode 100644 index 9067fb1941..0000000000 --- a/backend/src/api/tenant/tenantDeleteSampleData.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Error403 from '../../errors/Error403' -import { i18n } from '../../i18n' -import track from '../../segment/track' -import SampleDataService from '../../services/sampleDataService' -import Permissions from '../../security/permissions' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - if (!req.currentUser || !req.currentUser.id) { - throw new Error403(req.language) - } - - new PermissionChecker(req).validateHas(Permissions.values.memberDestroy) - - await new SampleDataService(req).deleteSampleData() - - track('Delete sample data', {}, { ...req }) - - req.responseHandler.success(req, res, { - message: i18n(req.language, 'tenant.sampleDataDeletionCompleted'), - }) -} diff --git a/backend/src/api/tenant/tenantDestroy.ts b/backend/src/api/tenant/tenantDestroy.ts deleted file mode 100644 index 1b14f34083..0000000000 --- a/backend/src/api/tenant/tenantDestroy.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Error403 from '../../errors/Error403' -import TenantService from '../../services/tenantService' - -export default async (req, res) => { - if (!req.currentUser || !req.currentUser.id) { - throw new Error403(req.language) - } - - // In the case of the Tenant, specific permissions like tenantDestroy and tenantEdit are - // checked inside the service - await new TenantService(req).destroyAll(req.query.ids) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/tenant/tenantFind.ts b/backend/src/api/tenant/tenantFind.ts deleted file mode 100644 index 6f27d6c1b2..0000000000 --- a/backend/src/api/tenant/tenantFind.ts +++ /dev/null @@ -1,23 +0,0 @@ -import identifyTenant from '../../segment/identifyTenant' -import TenantService from '../../services/tenantService' -import Error404 from '../../errors/Error404' - -export default async (req, res) => { - let payload - - if (req.params.id) { - payload = await new TenantService(req).findById(req.params.id) - } else { - payload = await new TenantService(req).findByUrl(req.query.url) - } - - if (payload) { - if (req.currentUser) { - identifyTenant({ ...req, currentTenant: payload }) - } - - await req.responseHandler.success(req, res, payload) - } else { - throw new Error404() - } -} diff --git a/backend/src/api/tenant/tenantGenerateSampleData.ts b/backend/src/api/tenant/tenantGenerateSampleData.ts deleted file mode 100644 index 623998a251..0000000000 --- a/backend/src/api/tenant/tenantGenerateSampleData.ts +++ /dev/null @@ -1,32 +0,0 @@ -import Error403 from '../../errors/Error403' -import { i18n } from '../../i18n' -import SampleDataService from '../../services/sampleDataService' -import track from '../../segment/track' -import Permissions from '../../security/permissions' -import PermissionChecker from '../../services/user/permissionChecker' - -const fs = require('fs') -const path = require('path') - -export default async (req, res) => { - if (!req.currentUser || !req.currentUser.id) { - throw new Error403(req.language) - } - - new PermissionChecker(req).validateHas(Permissions.values.memberCreate) - - const sampleMembersActivities = JSON.parse( - fs.readFileSync( - path.resolve(__dirname, '../../database/initializers/sample-data.json'), - 'utf8', - ), - ) - - track('Generate sample data', {}, { ...req }) - - await req.responseHandler.success(req, res, { - message: i18n(req.language, 'tenant.sampleDataCreationStarted'), - }) - - await new SampleDataService(req).generateSampleData(sampleMembersActivities) -} diff --git a/backend/src/api/tenant/tenantInvitationAccept.ts b/backend/src/api/tenant/tenantInvitationAccept.ts deleted file mode 100644 index 5e6c428378..0000000000 --- a/backend/src/api/tenant/tenantInvitationAccept.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Error403 from '../../errors/Error403' -import TenantService from '../../services/tenantService' - -export default async (req, res) => { - if (!req.currentUser || !req.currentUser.id) { - throw new Error403(req.language) - } - - const payload = await new TenantService(req).acceptInvitation( - req.params.token, - Boolean(req.body.forceAcceptOtherEmail), - ) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/tenant/tenantInvitationDecline.ts b/backend/src/api/tenant/tenantInvitationDecline.ts deleted file mode 100644 index d2d808a314..0000000000 --- a/backend/src/api/tenant/tenantInvitationDecline.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Error403 from '../../errors/Error403' -import TenantService from '../../services/tenantService' - -export default async (req, res) => { - if (!req.currentUser || !req.currentUser.id) { - throw new Error403(req.language) - } - - const payload = await new TenantService(req).declineInvitation(req.params.token) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/tenant/tenantList.ts b/backend/src/api/tenant/tenantList.ts deleted file mode 100644 index 3b886311dd..0000000000 --- a/backend/src/api/tenant/tenantList.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Error403 from '../../errors/Error403' -import TenantService from '../../services/tenantService' - -export default async (req, res) => { - if (!req.currentUser || !req.currentUser.id) { - throw new Error403(req.language) - } - - const payload = await new TenantService(req).findAndCountAll(req.query) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/tenant/tenantMembersToMerge.ts b/backend/src/api/tenant/tenantMembersToMerge.ts deleted file mode 100644 index afc3934091..0000000000 --- a/backend/src/api/tenant/tenantMembersToMerge.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Error403 from '../../errors/Error403' -import TenantService from '../../services/tenantService' - -export default async (req, res) => { - if (!req.currentUser || !req.currentUser.id) { - throw new Error403(req.language) - } - - const payload = await new TenantService(req).findMembersToMerge(req.query) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/tenant/tenantOrganizationsToMerge.ts b/backend/src/api/tenant/tenantOrganizationsToMerge.ts deleted file mode 100644 index fa5498ca1d..0000000000 --- a/backend/src/api/tenant/tenantOrganizationsToMerge.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Error403 from '../../errors/Error403' -import TenantService from '../../services/tenantService' - -export default async (req, res) => { - if (!req.currentUser || !req.currentUser.id) { - throw new Error403(req.language) - } - - const payload = await new TenantService(req).findOrganizationsToMerge(req.query) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/tenant/tenantUpdate.ts b/backend/src/api/tenant/tenantUpdate.ts deleted file mode 100644 index 1f09fd3749..0000000000 --- a/backend/src/api/tenant/tenantUpdate.ts +++ /dev/null @@ -1,14 +0,0 @@ -import Error403 from '../../errors/Error403' -import TenantService from '../../services/tenantService' - -export default async (req, res) => { - if (!req.currentUser || !req.currentUser.id) { - throw new Error403(req.language) - } - - // In the case of the Tenant, specific permissions like tenantDestroy and tenantEdit are - // checked inside the service - const payload = await new TenantService(req).update(req.params.id, req.body) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/user/index.ts b/backend/src/api/user/index.ts index c21a3639cb..8fbb9a948d 100644 --- a/backend/src/api/user/index.ts +++ b/backend/src/api/user/index.ts @@ -1,11 +1,7 @@ import { safeWrap } from '../../middlewares/errorMiddleware' export default (app) => { - app.post(`/tenant/:tenantId/user`, safeWrap(require('./userCreate').default)) - app.put(`/tenant/:tenantId/user`, safeWrap(require('./userEdit').default)) - app.post(`/tenant/:tenantId/user/import`, safeWrap(require('./userImport').default)) - app.delete(`/tenant/:tenantId/user`, safeWrap(require('./userDestroy').default)) - app.get(`/tenant/:tenantId/user`, safeWrap(require('./userList').default)) - app.get(`/tenant/:tenantId/user/autocomplete`, safeWrap(require('./userAutocomplete').default)) - app.get(`/tenant/:tenantId/user/:id`, safeWrap(require('./userFind').default)) + app.get(`/user`, safeWrap(require('./userList').default)) + app.get(`/user/autocomplete`, safeWrap(require('./userAutocomplete').default)) + app.get(`/user/:id`, safeWrap(require('./userFind').default)) } diff --git a/backend/src/api/user/userCreate.ts b/backend/src/api/user/userCreate.ts deleted file mode 100644 index 4a8bf84f56..0000000000 --- a/backend/src/api/user/userCreate.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Permissions from '../../security/permissions' -import track from '../../segment/track' -import UserCreator from '../../services/user/userCreator' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.userCreate) - - const creator = new UserCreator(req) - - const payload = await creator.execute(req.body) - - track('User Invited', { ...req.body }, { ...req }) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/user/userDestroy.ts b/backend/src/api/user/userDestroy.ts deleted file mode 100644 index eb65ef0527..0000000000 --- a/backend/src/api/user/userDestroy.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Permissions from '../../security/permissions' -import UserDestroyer from '../../services/user/userDestroyer' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.userDestroy) - - const remover = new UserDestroyer(req) - - await remover.destroyAll(req.query) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/user/userEdit.ts b/backend/src/api/user/userEdit.ts deleted file mode 100644 index ae8bd20c24..0000000000 --- a/backend/src/api/user/userEdit.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Permissions from '../../security/permissions' -import UserEditor from '../../services/user/userEditor' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.userEdit) - - const editor = new UserEditor(req) - - await editor.update(req.body) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/user/userImport.ts b/backend/src/api/user/userImport.ts deleted file mode 100644 index 2b3ee8ed54..0000000000 --- a/backend/src/api/user/userImport.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Permissions from '../../security/permissions' -import UserImporter from '../../services/user/userImporter' -import PermissionChecker from '../../services/user/permissionChecker' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.userImport) - - await new UserImporter(req).import(req.body, req.body.importHash) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/webhooks/discourse.ts b/backend/src/api/webhooks/discourse.ts deleted file mode 100644 index 0ba0b71dfb..0000000000 --- a/backend/src/api/webhooks/discourse.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { PlatformType } from '@crowd/types' -import IntegrationRepository from '../../database/repositories/integrationRepository' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import IncomingWebhookRepository from '../../database/repositories/incomingWebhookRepository' -import { WebhookType } from '../../types/webhooks' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import { NodeWorkerProcessWebhookMessage } from '../../types/mq/nodeWorkerProcessWebhookMessage' -import { verifyWebhookSignature } from '../../utils/crypto' - -export default async (req, res) => { - const signature = req.headers['x-discourse-event-signature'] - const eventId = req.headers['x-discourse-event-id'] - const eventType = req.headers['x-discourse-event-type'] - const event = req.headers['x-discourse-event'] - const data = req.body - - let integration - - try { - integration = await IntegrationRepository.findActiveIntegrationByPlatform( - PlatformType.DISCOURSE, - req.params.tenantId, - ) - } catch (error) { - req.log.error({ error }, 'Internal error when verifying Discourse webhook') - await req.responseHandler.success( - req, - res, - 'Internal error when verifying Discourse webhook', - 200, - ) - return - } - - if (integration) { - try { - if (!signature) { - req.log.error({ signature }, 'Discourse Webhook signature header missing!') - await req.responseHandler.success( - req, - res, - 'Discourse Webhook signature header missing!', - 200, - ) - return - } - - if ( - !verifyWebhookSignature(JSON.stringify(data), integration.settings.webhookSecret, signature) - ) { - req.log.error({ signature }, 'Discourse Webhook signature verification failed!') - await req.responseHandler.success( - req, - res, - 'Discourse Webhook signature verification failed!', - 200, - ) - return - } - } catch (error) { - req.log.error({ signature, error }, 'Internal error when verifying discourse webhook') - await req.responseHandler.success( - req, - res, - 'Internal error when verifying discourse webhook', - 200, - ) - return - } - - req.log.info({ integrationId: integration.id }, 'Incoming Discourse Webhook!') - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - const repo = new IncomingWebhookRepository(options) - - const result = await repo.create({ - tenantId: integration.tenantId, - integrationId: integration.id, - type: WebhookType.DISCOURSE, - payload: { - signature, - eventId, - eventType, - event, - data, - }, - }) - - await sendNodeWorkerMessage( - integration.tenantId, - new NodeWorkerProcessWebhookMessage(integration.tenantId, result.id), - ) - - await req.responseHandler.success(req, res, {}, 204) - } else { - req.log.error( - { tenantId: req?.params?.tenantId }, - 'No integration found for incoming Discourse Webhook!', - ) - await req.responseHandler.success(req, res, {}, 200) - } -} diff --git a/backend/src/api/webhooks/github.ts b/backend/src/api/webhooks/github.ts deleted file mode 100644 index 0fb00425b1..0000000000 --- a/backend/src/api/webhooks/github.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { PlatformType } from '@crowd/types' -import IntegrationRepository from '../../database/repositories/integrationRepository' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import IncomingWebhookRepository from '../../database/repositories/incomingWebhookRepository' -import { WebhookType } from '../../types/webhooks' -import { getIntegrationStreamWorkerEmitter } from '@/serverless/utils/serviceSQS' - -export default async (req, res) => { - const signature = req.headers['x-hub-signature'] - const event = req.headers['x-github-event'] - const data = req.body - - const identifier = data.installation.id.toString() - const integration = (await IntegrationRepository.findByIdentifier( - identifier, - PlatformType.GITHUB, - )) as any - - if (integration) { - req.log.info({ integrationId: integration.id }, 'Incoming GitHub Webhook!') - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - const repo = new IncomingWebhookRepository(options) - - const result = await repo.create({ - tenantId: integration.tenantId, - integrationId: integration.id, - type: WebhookType.GITHUB, - payload: { - signature, - event, - data, - }, - }) - - const streamEmitter = await getIntegrationStreamWorkerEmitter() - - await streamEmitter.triggerWebhookProcessing( - integration.tenantId, - integration.platform, - result.id, - ) - - await req.responseHandler.success(req, res, {}, 204) - } else { - req.log.error({ identifier }, 'No integration found for incoming GitHub Webhook!') - await req.responseHandler.success(req, res, {}, 200) - } -} diff --git a/backend/src/api/webhooks/index.ts b/backend/src/api/webhooks/index.ts deleted file mode 100644 index 46bc282433..0000000000 --- a/backend/src/api/webhooks/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { safeWrap } from '../../middlewares/errorMiddleware' - -export default (app) => { - // app.post(`/github`, safeWrap(require('./github').default)) - app.post(`/stripe`, safeWrap(require('./stripe').default)) - app.post(`/sendgrid`, safeWrap(require('./sendgrid').default)) - app.post(`/discourse/:tenantId`, safeWrap(require('./discourse').default)) -} diff --git a/backend/src/api/webhooks/sendgrid.ts b/backend/src/api/webhooks/sendgrid.ts deleted file mode 100644 index 3c06690bcd..0000000000 --- a/backend/src/api/webhooks/sendgrid.ts +++ /dev/null @@ -1,10 +0,0 @@ -import sendgridWebhookWorker from '../../serverless/integrations/workers/sendgridWebhookWorker' - -export default async (req, res) => { - const out = await sendgridWebhookWorker(req) - let status = 200 - if (out.status === 204) { - status = 204 - } - await req.responseHandler.success(req, res, out, status) -} diff --git a/backend/src/api/webhooks/stripe.ts b/backend/src/api/webhooks/stripe.ts deleted file mode 100644 index dbce9b44c0..0000000000 --- a/backend/src/api/webhooks/stripe.ts +++ /dev/null @@ -1,10 +0,0 @@ -import stripeWebhookWorker from '../../serverless/integrations/workers/stripeWebhookWorker' - -export default async (req, res) => { - const out = await stripeWebhookWorker(req) - let status = 200 - if (out.status === 204) { - status = 204 - } - await req.responseHandler.success(req, res, out, status) -} diff --git a/backend/src/api/websockets/index.ts b/backend/src/api/websockets/index.ts index 00cfcc4316..a047aaa640 100644 --- a/backend/src/api/websockets/index.ts +++ b/backend/src/api/websockets/index.ts @@ -1,6 +1,8 @@ -import { Server as SocketServer } from 'socket.io' import { Server } from 'http' +import { Server as SocketServer } from 'socket.io' + import { Logger, getServiceChildLogger } from '@crowd/logging' + import WebSocketNamespace from './namespace' import { IAuthenticatedSocket } from './types' diff --git a/backend/src/api/websockets/namespace.ts b/backend/src/api/websockets/namespace.ts index 1cbd334d5c..4e0e2e5ac1 100644 --- a/backend/src/api/websockets/namespace.ts +++ b/backend/src/api/websockets/namespace.ts @@ -1,10 +1,13 @@ -import { Logger, getServiceChildLogger } from '@crowd/logging' import { NextFunction } from 'express' import { Namespace, Server } from 'socket.io' + +import { Logger, getServiceChildLogger } from '@crowd/logging' + import { databaseInit } from '../../database/databaseConnection' import SequelizeRepository from '../../database/repositories/sequelizeRepository' import TenantUserRepository from '../../database/repositories/tenantUserRepository' import AuthService from '../../services/auth/authService' + import { IAuthenticatedSocket, ISocket, ISocketHandler } from './types' const logger = getServiceChildLogger('websockets/namespaces') diff --git a/backend/src/api/websockets/types.ts b/backend/src/api/websockets/types.ts index 14433692ac..b28ebebf20 100644 --- a/backend/src/api/websockets/types.ts +++ b/backend/src/api/websockets/types.ts @@ -1,5 +1,5 @@ -import { Socket } from 'socket.io' import { NextFunction } from 'express' +import { Socket } from 'socket.io' export interface ISocket extends Socket { database: any diff --git a/backend/src/api/widget/index.ts b/backend/src/api/widget/index.ts deleted file mode 100644 index 1d89edee0f..0000000000 --- a/backend/src/api/widget/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { safeWrap } from '../../middlewares/errorMiddleware' - -export default (app) => { - app.post(`/tenant/:tenantId/widget`, safeWrap(require('./widgetCreate').default)) - app.post(`/tenant/:tenantId/widget/query`, safeWrap(require('./widgetQuery').default)) - app.put(`/tenant/:tenantId/widget/:id`, safeWrap(require('./widgetUpdate').default)) - app.post(`/tenant/:tenantId/widget/import`, safeWrap(require('./widgetImport').default)) - app.delete(`/tenant/:tenantId/widget`, safeWrap(require('./widgetDestroy').default)) - app.get( - `/tenant/:tenantId/widget/autocomplete`, - safeWrap(require('./widgetAutocomplete').default), - ) - app.get(`/tenant/:tenantId/widget`, safeWrap(require('./widgetList').default)) - app.get(`/tenant/:tenantId/widget/:id`, safeWrap(require('./widgetFind').default)) -} diff --git a/backend/src/api/widget/widgetAutocomplete.ts b/backend/src/api/widget/widgetAutocomplete.ts deleted file mode 100644 index 51491914f3..0000000000 --- a/backend/src/api/widget/widgetAutocomplete.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Permissions from '../../security/permissions' -import PermissionChecker from '../../services/user/permissionChecker' -import WidgetService from '../../services/widgetService' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.widgetAutocomplete) - - const payload = await new WidgetService(req).findAllAutocomplete(req.query.query, req.query.limit) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/widget/widgetCreate.ts b/backend/src/api/widget/widgetCreate.ts deleted file mode 100644 index b1f81d2c5b..0000000000 --- a/backend/src/api/widget/widgetCreate.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Permissions from '../../security/permissions' -import track from '../../segment/track' -import PermissionChecker from '../../services/user/permissionChecker' -import WidgetService from '../../services/widgetService' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.widgetCreate) - - const payload = await new WidgetService(req).create(req.body) - - track( - 'Widget Created', - { - id: payload.id, - reportId: payload.report ? payload.report.id : undefined, - }, - { ...req }, - ) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/widget/widgetDestroy.ts b/backend/src/api/widget/widgetDestroy.ts deleted file mode 100644 index 162a26ac6e..0000000000 --- a/backend/src/api/widget/widgetDestroy.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Permissions from '../../security/permissions' -import PermissionChecker from '../../services/user/permissionChecker' -import WidgetService from '../../services/widgetService' -import track from '../../segment/track' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.widgetDestroy) - - await new WidgetService(req).destroyAll(req.query.ids) - - const payload = true - - track('Widget Deleted', { ids: req.query.ids }, { ...req }) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/widget/widgetFind.ts b/backend/src/api/widget/widgetFind.ts deleted file mode 100644 index 2486f1e94e..0000000000 --- a/backend/src/api/widget/widgetFind.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Permissions from '../../security/permissions' -import PermissionChecker from '../../services/user/permissionChecker' -import WidgetService from '../../services/widgetService' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.widgetRead) - - const payload = await new WidgetService(req).findById(req.params.id) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/widget/widgetImport.ts b/backend/src/api/widget/widgetImport.ts deleted file mode 100644 index e138930116..0000000000 --- a/backend/src/api/widget/widgetImport.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Permissions from '../../security/permissions' -import PermissionChecker from '../../services/user/permissionChecker' -import WidgetService from '../../services/widgetService' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.widgetImport) - - await new WidgetService(req).import(req.body, req.body.importHash) - - const payload = true - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/widget/widgetList.ts b/backend/src/api/widget/widgetList.ts deleted file mode 100644 index 0223c655d4..0000000000 --- a/backend/src/api/widget/widgetList.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Permissions from '../../security/permissions' -import PermissionChecker from '../../services/user/permissionChecker' -import WidgetService from '../../services/widgetService' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.widgetRead) - - const payload = await new WidgetService(req).findAndCountAll(req.query) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/widget/widgetQuery.ts b/backend/src/api/widget/widgetQuery.ts deleted file mode 100644 index d65334bd45..0000000000 --- a/backend/src/api/widget/widgetQuery.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Permissions from '../../security/permissions' -import track from '../../segment/track' -import PermissionChecker from '../../services/user/permissionChecker' -import WidgetService from '../../services/widgetService' - -// /** -// * POST /tenant/{tenantId}/widget -// * @summary Create or update an widget -// * @tag Activities -// * @security Bearer -// * @description Create or update an widget. Existence is checked by sourceId and tenantId. -// * @pathParam {string} tenantId - Your workspace/tenant ID -// * @bodyContent {WidgetUpsertInput} application/json -// * @response 200 - Ok -// * @responseContent {Widget} 200.application/json -// * @responseExample {WidgetUpsert} 200.application/json.Widget -// * @response 401 - Unauthorized -// * @response 404 - Not found -// * @response 429 - Too many requests -// */ -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.widgetRead) - - const payload = await new WidgetService(req).query(req.body) - - if (req.query.filter && Object.keys(req.query.filter).length > 0) { - track('Widgets Advanced Filter', { ...payload }, { ...req }) - } - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/api/widget/widgetUpdate.ts b/backend/src/api/widget/widgetUpdate.ts deleted file mode 100644 index 823efc07e5..0000000000 --- a/backend/src/api/widget/widgetUpdate.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Permissions from '../../security/permissions' -import track from '../../segment/track' -import PermissionChecker from '../../services/user/permissionChecker' -import WidgetService from '../../services/widgetService' - -export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.widgetEdit) - - const payload = await new WidgetService(req).update(req.params.id, req.body) - - track( - 'Widget Updated', - { - id: payload.id, - reportId: payload.report ? payload.report.id : undefined, - }, - { ...req }, - ) - - await req.responseHandler.success(req, res, payload) -} diff --git a/backend/src/bin/api.ts b/backend/src/bin/api.ts index 94c3362c38..0e116d6c1b 100644 --- a/backend/src/bin/api.ts +++ b/backend/src/bin/api.ts @@ -1,5 +1,6 @@ -import { getServiceLogger } from '@crowd/logging' import { timeout } from '@crowd/common' +import { getServiceLogger } from '@crowd/logging' + import server from '../api' import { API_CONFIG } from '../conf' diff --git a/backend/src/bin/discord-ws.ts b/backend/src/bin/discord-ws.ts deleted file mode 100644 index 997e670e3c..0000000000 --- a/backend/src/bin/discord-ws.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { Client, Events, GatewayIntentBits, MessageType } from 'discord.js' -import moment from 'moment' -import { processPaginated, timeout } from '@crowd/common' -import { RedisCache, getRedisClient } from '@crowd/redis' -import { getChildLogger, getServiceLogger } from '@crowd/logging' -import { PlatformType } from '@crowd/types' -import { SpanStatusCode, getServiceTracer } from '@crowd/tracing' -import { DISCORD_CONFIG, REDIS_CONFIG } from '../conf' -import SequelizeRepository from '../database/repositories/sequelizeRepository' -import IntegrationRepository from '../database/repositories/integrationRepository' -import IncomingWebhookRepository from '../database/repositories/incomingWebhookRepository' -import { DiscordWebsocketEvent, DiscordWebsocketPayload, WebhookType } from '../types/webhooks' -import { - getIntegrationRunWorkerEmitter, - getIntegrationStreamWorkerEmitter, -} from '@/serverless/utils/serviceSQS' - -const tracer = getServiceTracer() -const log = getServiceLogger() - -async function executeIfNotExists( - key: string, - cache: RedisCache, - fn: () => Promise, - delayMilliseconds?: number, -) { - if (delayMilliseconds) { - await timeout(delayMilliseconds) - } - - const exists = await cache.get(key) - if (!exists) { - await fn() - await cache.set(key, '1', 2 * 60 * 60) - } -} - -async function spawnClient( - name: string, - token: string, - cache: RedisCache, - delayMilliseconds?: number, -) { - const logger = getChildLogger('discord-ws', log, { clientName: name }) - - const repoOptions = await SequelizeRepository.getDefaultIRepositoryOptions() - const repo = new IncomingWebhookRepository(repoOptions) - - const processPayload = async ( - event: DiscordWebsocketEvent, - data: any, - guildId: string, - ): Promise => { - const payload: DiscordWebsocketPayload = { - event, - data, - } - - logger.info({ payload }, 'Processing Discord WS Message!') - - await tracer.startActiveSpan('ProcessDiscordWSMessage', async (span) => { - try { - const integration = (await IntegrationRepository.findByIdentifier( - guildId, - PlatformType.DISCORD, - )) as any - - const result = await repo.create({ - tenantId: integration.tenantId, - integrationId: integration.id, - type: WebhookType.DISCORD, - payload, - }) - - const streamEmitter = await getIntegrationStreamWorkerEmitter() - - await streamEmitter.triggerWebhookProcessing( - integration.tenantId, - integration.platform, - result.id, - ) - span.setStatus({ - code: SpanStatusCode.OK, - }) - } catch (err) { - if (err.code === 404) { - logger.warn({ guildId }, 'No integration found for incoming Discord WS Message!') - } else { - span.setStatus({ - code: SpanStatusCode.ERROR, - message: err, - }) - logger.error( - err, - { - discordPayload: JSON.stringify(payload), - guildId, - }, - 'Error processing Discord WS Message!', - ) - } - } finally { - span.end() - } - }) - } - - const client = new Client({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMembers, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.GuildMessageReactions, - GatewayIntentBits.DirectMessages, - GatewayIntentBits.DirectMessageReactions, - GatewayIntentBits.MessageContent, - ], - }) - - // listen to client events - client.on(Events.ClientReady, () => { - logger.info('Discord WS client is ready!') - }) - - client.on(Events.Error, (err) => { - logger.error(err, 'Discord WS client error! Exiting...') - process.exit(1) - }) - - client.on(Events.Debug, (message) => { - logger.debug({ debugMsg: message }, 'Discord WS client debug message!') - }) - - client.on(Events.Warn, (message) => { - logger.warn({ warning: message }, 'Discord WS client warning!') - }) - - // listen to discord events - client.on(Events.GuildMemberAdd, async (m) => { - const member = m as any - await executeIfNotExists( - `member-${member.userId}`, - cache, - async () => { - logger.debug( - { - member: member.displayName, - guildId: member.guildId ?? member.guild.id, - userId: member.userId, - }, - 'Member joined guild!', - ) - await processPayload( - DiscordWebsocketEvent.MEMBER_ADDED, - member, - member.guildId ?? member.guild.id, - ) - }, - delayMilliseconds, - ) - }) - - client.on(Events.MessageCreate, async (message) => { - if (message.type === MessageType.Default || message.type === MessageType.Reply) { - await executeIfNotExists( - `msg-${message.id}`, - cache, - async () => { - logger.debug( - { - guildId: message.guildId, - channelId: message.channelId, - message: message.cleanContent, - authorId: message.author, - }, - 'Message created!', - ) - await processPayload(DiscordWebsocketEvent.MESSAGE_CREATED, message, message.guildId) - }, - delayMilliseconds, - ) - } - }) - - client.on(Events.MessageUpdate, async (oldMessage, newMessage) => { - if (newMessage.type === MessageType.Default && newMessage.editedTimestamp) { - await executeIfNotExists( - `msg-modified-${newMessage.id}-${newMessage.editedTimestamp}`, - cache, - async () => { - logger.debug( - { - guildId: newMessage.guildId, - channelId: newMessage.channelId, - oldMessageId: oldMessage.id, - newMessage: newMessage.cleanContent, - authorId: newMessage.author, - }, - 'Message updated!', - ) - await processPayload( - DiscordWebsocketEvent.MESSAGE_UPDATED, - { - message: newMessage, - oldMessage, - }, - newMessage.guildId, - ) - }, - delayMilliseconds, - ) - } - }) - - await client.login(token) - logger.info('Discord WS client logged in!') -} - -setImmediate(async () => { - // we are saving heartbeat timestamps in redis every 2 seconds - // on boot if we detect that there has been a downtime we should trigger discord integration checks - // so we don't miss anything - const redis = await getRedisClient(REDIS_CONFIG, true) - const cache = new RedisCache('discord-ws', redis, log) - - const lastHeartbeat = await cache.get('heartbeat') - let triggerCheck = false - if (!lastHeartbeat) { - log.info('No heartbeat found, triggering check!') - triggerCheck = true - } else { - const diff = moment().diff(lastHeartbeat, 'seconds') - // if we do rolling update deploys (kubernetes default) - // we might catch a heartbeat without the need to trigger a check - if (diff > 5) { - log.warn('Heartbeat is stale, triggering check!') - triggerCheck = true - } - } - - if (triggerCheck) { - const emitter = await getIntegrationRunWorkerEmitter() - - await processPaginated( - async (page) => IntegrationRepository.findAllActive(PlatformType.DISCORD, page, 10), - async (integrations) => { - log.warn(`Found ${integrations.length} integrations to trigger check for!`) - for (const integration of integrations) { - await emitter.triggerIntegrationRun( - integration.tenantId, - integration.platform, - integration.id, - false, - ) - } - }, - ) - } - - await spawnClient( - 'first-app', - DISCORD_CONFIG.token, - cache, - DISCORD_CONFIG.token2 ? 1000 : undefined, - ) - - if (DISCORD_CONFIG.token2) { - await spawnClient('second-app', DISCORD_CONFIG.token2, cache) - } - - setInterval(async () => { - await cache.set('heartbeat', new Date().toISOString()) - }, 2 * 1000) -}) diff --git a/backend/src/bin/job-generator.ts b/backend/src/bin/job-generator.ts index a16c78af0a..c5c4c333fc 100644 --- a/backend/src/bin/job-generator.ts +++ b/backend/src/bin/job-generator.ts @@ -1,32 +1,29 @@ import { CronJob } from 'cron' +import fs from 'fs' +import path from 'path' +import { QueryTypes, Sequelize } from 'sequelize' + import { getServiceLogger } from '@crowd/logging' -import { SpanStatusCode, getServiceTracer } from '@crowd/tracing' +import { RedisClient, getRedisClient } from '@crowd/redis' + +import { databaseInit } from '@/database/databaseConnection' + +import { REDIS_CONFIG } from '../conf' + import jobs from './jobs' -const tracer = getServiceTracer() const log = getServiceLogger() for (const job of jobs) { const cronJob = new CronJob( job.cronTime, async () => { - await tracer.startActiveSpan(`ProcessingJob:${job.name}`, async (span) => { - log.info({ job: job.name }, 'Triggering job.') - try { - await job.onTrigger(log) - span.setStatus({ - code: SpanStatusCode.OK, - }) - } catch (err) { - span.setStatus({ - code: SpanStatusCode.ERROR, - message: err, - }) - log.error(err, { job: job.name }, 'Error while executing a job!') - } finally { - span.end() - } - }) + log.info({ job: job.name }, 'Triggering job.') + try { + await job.onTrigger(log) + } catch (err) { + log.error(err, { job: job.name }, 'Error while executing a job!') + } }, null, true, @@ -36,3 +33,39 @@ for (const job of jobs) { log.info({ job: job.name }, 'Scheduled a job.') } } + +const liveFilePath = path.join(__dirname, 'tmp/job-generator-live.tmp') +const readyFilePath = path.join(__dirname, 'tmp/job-generator-ready.tmp') + +let seq: Sequelize +let redis: RedisClient +const initRedisSeq = async () => { + if (!seq) { + seq = (await databaseInit()).sequelize as Sequelize + } + + if (!redis) { + redis = await getRedisClient(REDIS_CONFIG, true) + } +} + +setInterval(async () => { + try { + await initRedisSeq() + log.debug('Checking liveness and readiness for job generator.') + const [redisPingRes, dbPingRes] = await Promise.all([ + // ping redis, + redis.ping().then((res) => res === 'PONG'), + // ping database + seq.query('select 1', { type: QueryTypes.SELECT }).then((rows) => rows.length === 1), + ]) + if (redisPingRes && dbPingRes) { + await Promise.all([ + fs.promises.open(liveFilePath, 'a').then((file) => file.close()), + fs.promises.open(readyFilePath, 'a').then((file) => file.close()), + ]) + } + } catch (err) { + log.error(`Error checking liveness and readiness for job generator: ${err}`) + } +}, 5000) diff --git a/backend/src/bin/jobs/autoImportGroupsioGroups.ts b/backend/src/bin/jobs/autoImportGroupsioGroups.ts new file mode 100644 index 0000000000..e977a0f53b --- /dev/null +++ b/backend/src/bin/jobs/autoImportGroupsioGroups.ts @@ -0,0 +1,90 @@ +import cronGenerator from 'cron-time-generator' + +import { getServiceChildLogger } from '@crowd/logging' + +import { getUserSubscriptions } from '@/serverless/integrations/usecases/groupsio/getUserSubscriptions' + +import SequelizeRepository from '../../database/repositories/sequelizeRepository' +import { CrowdJob } from '../../types/jobTypes' + +const log = getServiceChildLogger('autoImportGroupsioGroupsCronJob') + +interface SetttingsObj { + email: string + token: string + groups: Array<{ + id: number + name: string + slug: string + groupAddedOn?: Date + }> + autoImports?: { + mainGroup: string + isAllowed: boolean + }[] + password: string + tokenError: string + tokenExpiry: string + updateMemberAttributes: boolean +} + +const job: CrowdJob = { + name: 'Auto Import Groups IO Groups', + // every 2 days + cronTime: cronGenerator.every(2).days(), + onTrigger: async () => { + log.info('Checking for new groups to auto import.') + const dbOptions = await SequelizeRepository.getDefaultIRepositoryOptions() + + const integrations = await dbOptions.database.sequelize.query( + `select id, settings from integrations + where platform = 'groupsio' + and "deletedAt" is null + `, + ) + + log.info(`Found ${integrations[0].length} integrations to check for auto imports.`) + + for (const integration of integrations[0]) { + const settings = integration.settings as SetttingsObj + if (settings.autoImports) { + const allGroups = await getUserSubscriptions(settings.token) + log.info(`Found ${allGroups.length} available groups in users's account.`) + const existingGroupIds = new Set(settings.groups.map((group) => group.id)) + + for (const autoImport of settings.autoImports) { + if (autoImport.isAllowed) { + const newGroups = allGroups.filter( + (group) => + !existingGroupIds.has(group.id) && + group.group_name.startsWith(autoImport.mainGroup), + ) + + for (const newGroup of newGroups) { + log.info(`Adding new group ${newGroup.nice_group_name} to auto-import.`) + settings.groups.push({ + id: newGroup.id, + name: newGroup.nice_group_name, + slug: newGroup.group_name, + groupAddedOn: new Date(), + }) + } + + if (newGroups.length > 0) { + log.info( + `Added ${newGroups.length} new groups for auto-import in integration ${integration.id}`, + ) + } else { + log.info(`No new groups found for auto-import in integration ${integration.id}.`) + } + } + } + + // Update the integration settings in the database + await dbOptions.database.integration.update({ settings }, { where: { id: integration.id } }) + } + } + }, +} + +export default job diff --git a/backend/src/bin/jobs/checkSqsQueues.ts b/backend/src/bin/jobs/checkSqsQueues.ts deleted file mode 100644 index 9ee66b1870..0000000000 --- a/backend/src/bin/jobs/checkSqsQueues.ts +++ /dev/null @@ -1,60 +0,0 @@ -import cronGenerator from 'cron-time-generator' -import { sendSlackAlert } from '../../utils/slack' -import { SQS_CONFIG } from '../../conf' -import { sqs } from '../../services/aws' -import { CrowdJob } from '../../types/jobTypes' - -interface IQueueCount { - lastCount: number - increaseCount: number -} - -const queues = [SQS_CONFIG.nodejsWorkerQueue, SQS_CONFIG.pythonWorkerQueue] - -const messageCounts: Map = new Map() - -const job: CrowdJob = { - name: 'Check SQS Queues', - cronTime: cronGenerator.every(10).minutes(), - onTrigger: async () => { - for (const queue of queues) { - const result = await sqs - .getQueueAttributes({ - QueueUrl: queue, - AttributeNames: ['ApproximateNumberOfMessages'], - }) - .promise() - - if (result.Attributes) { - const value = parseInt(result.Attributes.ApproximateNumberOfMessages, 10) - if (messageCounts.has(queue)) { - const previousValue = messageCounts.get(queue) - if (previousValue.lastCount < value) { - if (previousValue.increaseCount > 2) { - await sendSlackAlert( - `*Warning*: Queue ${queue} messages have *increasted #${previousValue.increaseCount} times* - last increase was *from ${previousValue.lastCount} to ${value}*!`, - ) - } - - messageCounts.set(queue, { - lastCount: value, - increaseCount: previousValue.increaseCount + 1, - }) - } else { - messageCounts.set(queue, { - lastCount: value, - increaseCount: 0, - }) - } - } else { - messageCounts.set(queue, { - lastCount: value, - increaseCount: value > 0 ? 1 : 0, - }) - } - } - } - }, -} - -export default job diff --git a/backend/src/bin/jobs/checkStuckIntegrationRuns.ts b/backend/src/bin/jobs/checkStuckIntegrationRuns.ts index 3c859c29ec..54c36e1661 100644 --- a/backend/src/bin/jobs/checkStuckIntegrationRuns.ts +++ b/backend/src/bin/jobs/checkStuckIntegrationRuns.ts @@ -1,20 +1,17 @@ -import { processPaginated } from '@crowd/common' -import { Logger, getChildLogger, getServiceChildLogger } from '@crowd/logging' import cronGenerator from 'cron-time-generator' import moment from 'moment' + +import { Logger, getChildLogger, getServiceChildLogger } from '@crowd/logging' import { IntegrationRunState } from '@crowd/types' + import { INTEGRATION_PROCESSING_CONFIG } from '../../conf' -import IncomingWebhookRepository from '../../database/repositories/incomingWebhookRepository' import IntegrationRepository from '../../database/repositories/integrationRepository' import IntegrationRunRepository from '../../database/repositories/integrationRunRepository' import IntegrationStreamRepository from '../../database/repositories/integrationStreamRepository' import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' import { IntegrationRun } from '../../types/integrationRunTypes' import { IntegrationStreamState } from '../../types/integrationStreamTypes' import { CrowdJob } from '../../types/jobTypes' -import { NodeWorkerProcessWebhookMessage } from '../../types/mq/nodeWorkerProcessWebhookMessage' -import { WebhookProcessor } from '../../serverless/integrations/services/webhookProcessor' const log = getServiceChildLogger('checkStuckIntegrationRuns') @@ -221,32 +218,6 @@ export const checkRuns = async (): Promise => { } } -export const checkStuckWebhooks = async (): Promise => { - const dbOptions = await SequelizeRepository.getDefaultIRepositoryOptions() - const repo = new IncomingWebhookRepository(dbOptions) - - // update retryable error state webhooks to pending state - let errorWebhooks = await repo.findError(1, 20, WebhookProcessor.MAX_RETRY_LIMIT) - - while (errorWebhooks.length > 0) { - await repo.markAllPending(errorWebhooks.map((w) => w.id)) - errorWebhooks = await repo.findError(1, 20, WebhookProcessor.MAX_RETRY_LIMIT) - } - - await processPaginated( - async (page) => repo.findPending(page, 20), - async (webhooks) => { - for (const webhook of webhooks) { - log.warn({ id: webhook.id }, 'Found stuck webhook! Restarting it!') - await sendNodeWorkerMessage( - webhook.tenantId, - new NodeWorkerProcessWebhookMessage(webhook.tenantId, webhook.id), - ) - } - }, - ) -} - const job: CrowdJob = { name: 'Detect & Fix Stuck Integration Runs', cronTime: cronGenerator.every(90).minutes(), @@ -254,7 +225,7 @@ const job: CrowdJob = { if (!running) { running = true try { - await Promise.all([checkRuns(), checkStuckIntegrations(), checkStuckWebhooks()]) + await Promise.all([checkRuns(), checkStuckIntegrations()]) } finally { running = false } diff --git a/backend/src/bin/jobs/cleanUp.ts b/backend/src/bin/jobs/cleanUp.ts index cac6cf2021..38b2a6e8df 100644 --- a/backend/src/bin/jobs/cleanUp.ts +++ b/backend/src/bin/jobs/cleanUp.ts @@ -1,9 +1,9 @@ import { getServiceChildLogger } from '@crowd/logging' + import IncomingWebhookRepository from '../../database/repositories/incomingWebhookRepository' import IntegrationRunRepository from '../../database/repositories/integrationRunRepository' import SequelizeRepository from '../../database/repositories/sequelizeRepository' import { CrowdJob } from '../../types/jobTypes' -import AuditLogRepository from '../../database/repositories/auditLogRepository' const MAX_MONTHS_TO_KEEP = 3 @@ -39,13 +39,6 @@ export const cleanUpOldWebhooks = async () => { await repo.cleanUpOldWebhooks(MAX_MONTHS_TO_KEEP) } -export const cleanUpOldAuditLogs = async () => { - const dbOptions = await SequelizeRepository.getDefaultIRepositoryOptions() - - log.info(`Cleaning up audit logs that are older than 1 month!`) - await AuditLogRepository.cleanUpOldAuditLogs(1, dbOptions) -} - const job: CrowdJob = { name: 'Clean up old data', // run once every week on Sunday at 1AM diff --git a/backend/src/bin/jobs/downgradeExpiredPlans.ts b/backend/src/bin/jobs/downgradeExpiredPlans.ts deleted file mode 100644 index cf2058175a..0000000000 --- a/backend/src/bin/jobs/downgradeExpiredPlans.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { getServiceChildLogger } from '@crowd/logging' -import cronGenerator from 'cron-time-generator' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import Plans from '../../security/plans' -import { CrowdJob } from '../../types/jobTypes' - -const log = getServiceChildLogger('downgradeExpiredPlansCronJob') - -const job: CrowdJob = { - name: 'Downgrade Expired Trial Plans', - // every day - cronTime: cronGenerator.every(1).days(), - onTrigger: async () => { - log.info('Downgrading expired trial plans.') - const dbOptions = await SequelizeRepository.getDefaultIRepositoryOptions() - - const expiredTrialTenants = await dbOptions.database.sequelize.query( - `select t.id, t.name from tenants t - where t."isTrialPlan" and t."trialEndsAt" < now()`, - ) - - for (const tenant of expiredTrialTenants[0]) { - await dbOptions.database.tenant.update( - { isTrialPlan: false, trialEndsAt: null, plan: Plans.values.essential }, - { returning: true, raw: true, where: { id: tenant.id } }, - ) - } - - log.info('Downgrading expired non-trial plans') - const expiredNonTrialTenants = await dbOptions.database.sequelize.query( - `select t.id, t.name from tenants t - where (t.plan = ${Plans.values.growth} or t.plan = ${Plans.values.eagleEye} or t.plan = ${Plans.values.scale}) and t."planSubscriptionEndsAt" is not null and t."planSubscriptionEndsAt" + interval '3 days' < now()`, - ) - - for (const tenant of expiredNonTrialTenants[0]) { - await dbOptions.database.tenant.update( - { plan: Plans.values.essential }, - { returning: true, raw: true, where: { id: tenant.id } }, - ) - } - }, -} - -export default job diff --git a/backend/src/bin/jobs/eagleEyeEmailDigestTicks.ts b/backend/src/bin/jobs/eagleEyeEmailDigestTicks.ts deleted file mode 100644 index b7af547801..0000000000 --- a/backend/src/bin/jobs/eagleEyeEmailDigestTicks.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Op } from 'sequelize' -import moment from 'moment' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import { CrowdJob } from '../../types/jobTypes' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import { NodeWorkerMessageType } from '../../serverless/types/workerTypes' -import { NodeWorkerMessageBase } from '../../types/mq/nodeWorkerMessageBase' - -const job: CrowdJob = { - name: 'Eagle Eye Email Digest Ticker', - // every half hour - cronTime: '*/30 * * * *', - onTrigger: async () => { - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - const tenantUsers = ( - await options.database.tenantUser.findAll({ - where: { - [Op.and]: [ - { - 'settings.eagleEye.emailDigestActive': { - [Op.ne]: null, - }, - }, - { - 'settings.eagleEye.emailDigestActive': { - [Op.eq]: true, - }, - }, - ], - }, - }) - ).filter( - (tenantUser) => - tenantUser.settings.eagleEye && - tenantUser.settings.eagleEye.emailDigestActive && - moment() > moment(tenantUser.settings.eagleEye.emailDigest.nextEmailAt), - ) - - for (const tenantUser of tenantUsers) { - await sendNodeWorkerMessage(tenantUser.tenantId, { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - user: tenantUser.userId, - tenant: tenantUser.tenantId, - service: 'eagle-eye-email-digest', - } as NodeWorkerMessageBase) - } - }, -} - -export default job diff --git a/backend/src/bin/jobs/index.ts b/backend/src/bin/jobs/index.ts index f0b41bf638..df42e775e2 100644 --- a/backend/src/bin/jobs/index.ts +++ b/backend/src/bin/jobs/index.ts @@ -1,40 +1,21 @@ import { CrowdJob } from '../../types/jobTypes' -import integrationTicks from './integrationTicks' -import weeklyAnalyticsEmailsCoordinator from './weeklyAnalyticsEmailsCoordinator' -import memberScoreCoordinator from './memberScoreCoordinator' -import checkSqsQueues from './checkSqsQueues' -import refreshMaterializedViews from './refreshMaterializedViews' -import refreshMaterializedViewsForCube from './refreshMaterializedViewsForCube' -import downgradeExpiredPlans from './downgradeExpiredPlans' -import eagleEyeEmailDigestTicks from './eagleEyeEmailDigestTicks' -import integrationDataChecker from './integrationDataChecker' -import mergeSuggestions from './mergeSuggestions' -import refreshSampleData from './refreshSampleData' -import cleanUp from './cleanUp' -import checkStuckIntegrationRuns from './checkStuckIntegrationRuns' -import enrichOrganizations from './organizationEnricher' -import { WEEKLY_EMAILS_CONFIG } from '../../conf' -const EMAILS_ENABLED = WEEKLY_EMAILS_CONFIG.enabled === 'true' +import autoImportGroups from './autoImportGroupsioGroups' +import checkStuckIntegrationRuns from './checkStuckIntegrationRuns' +import cleanUp from './cleanUp' +import integrationTicks from './integrationTicks' +import refreshGithubRepoSettingsJob from './refreshGithubRepoSettings' +import refreshGitlabToken from './refreshGitlabToken' +import refreshGroupsioToken from './refreshGroupsioToken' const jobs: CrowdJob[] = [ integrationTicks, - memberScoreCoordinator, - checkSqsQueues, - refreshMaterializedViews, - refreshMaterializedViewsForCube, - downgradeExpiredPlans, - eagleEyeEmailDigestTicks, - integrationDataChecker, - mergeSuggestions, - refreshSampleData, cleanUp, checkStuckIntegrationRuns, - enrichOrganizations, + refreshGroupsioToken, + refreshGitlabToken, + refreshGithubRepoSettingsJob, + autoImportGroups, ] -if (EMAILS_ENABLED) { - jobs.push(weeklyAnalyticsEmailsCoordinator) -} - export default jobs diff --git a/backend/src/bin/jobs/integrationDataChecker.ts b/backend/src/bin/jobs/integrationDataChecker.ts deleted file mode 100644 index 8568b2e1fe..0000000000 --- a/backend/src/bin/jobs/integrationDataChecker.ts +++ /dev/null @@ -1,31 +0,0 @@ -import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import { CrowdJob } from '../../types/jobTypes' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import { NodeWorkerMessageType } from '../../serverless/types/workerTypes' -import { NodeWorkerMessageBase } from '../../types/mq/nodeWorkerMessageBase' - -const job: CrowdJob = { - name: 'Integration Data Checker', - // every hour on weekdays - cronTime: '0 * * * 1-5', - onTrigger: async () => { - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - - const integrations = await options.database.integration.findAll({ - where: { - status: 'done', - }, - }) - - for (const integration of integrations) { - await sendNodeWorkerMessage(integration.id, { - tenantId: integration.tenantId, - type: NodeWorkerMessageType.NODE_MICROSERVICE, - integrationId: integration.id, - service: 'integration-data-checker', - } as NodeWorkerMessageBase) - } - }, -} - -export default job diff --git a/backend/src/bin/jobs/integrationTicks.ts b/backend/src/bin/jobs/integrationTicks.ts index cdae0e306b..daa5f197e2 100644 --- a/backend/src/bin/jobs/integrationTicks.ts +++ b/backend/src/bin/jobs/integrationTicks.ts @@ -1,5 +1,7 @@ import cronGenerator from 'cron-time-generator' + import { getServiceLogger } from '@crowd/logging' + import SequelizeRepository from '../../database/repositories/sequelizeRepository' import { IntegrationProcessor } from '../../serverless/integrations/services/integrationProcessor' import { IServiceOptions } from '../../services/IServiceOptions' diff --git a/backend/src/bin/jobs/memberScoreCoordinator.ts b/backend/src/bin/jobs/memberScoreCoordinator.ts deleted file mode 100644 index 85f5c60497..0000000000 --- a/backend/src/bin/jobs/memberScoreCoordinator.ts +++ /dev/null @@ -1,16 +0,0 @@ -import cronGenerator from 'cron-time-generator' -import { CrowdJob } from '../../types/jobTypes' -import { sendPythonWorkerMessage } from '../../serverless/utils/pythonWorkerSQS' -import { PythonWorkerMessageType } from '../../serverless/types/workerTypes' - -const job: CrowdJob = { - name: 'Member Score Coordinator', - cronTime: cronGenerator.every(90).minutes(), - onTrigger: async () => { - await sendPythonWorkerMessage('global', { - type: PythonWorkerMessageType.MEMBERS_SCORE, - }) - }, -} - -export default job diff --git a/backend/src/bin/jobs/mergeSuggestions.ts b/backend/src/bin/jobs/mergeSuggestions.ts deleted file mode 100644 index 8cbc69b1cf..0000000000 --- a/backend/src/bin/jobs/mergeSuggestions.ts +++ /dev/null @@ -1,27 +0,0 @@ -import cronGenerator from 'cron-time-generator' -import { timeout } from '@crowd/common' -import TenantService from '../../services/tenantService' -import { CrowdJob } from '../../types/jobTypes' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import { NodeWorkerMessageType } from '../../serverless/types/workerTypes' -import { NodeWorkerMessageBase } from '../../types/mq/nodeWorkerMessageBase' - -const job: CrowdJob = { - name: 'Merge suggestions', - // every 12 hours - cronTime: cronGenerator.every(12).hours(), - onTrigger: async () => { - const tenants = await TenantService._findAndCountAllForEveryUser({}) - for (const tenant of tenants.rows) { - await sendNodeWorkerMessage(tenant.id, { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - tenant: tenant.id, - service: 'merge-suggestions', - } as NodeWorkerMessageBase) - - await timeout(300) - } - }, -} - -export default job diff --git a/backend/src/bin/jobs/organizationEnricher.ts b/backend/src/bin/jobs/organizationEnricher.ts deleted file mode 100644 index 4d0eb48ffa..0000000000 --- a/backend/src/bin/jobs/organizationEnricher.ts +++ /dev/null @@ -1,32 +0,0 @@ -import cronGenerator from 'cron-time-generator' -import { getServiceLogger } from '@crowd/logging' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import { CrowdJob } from '../../types/jobTypes' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import { NodeWorkerMessageBase } from '../../types/mq/nodeWorkerMessageBase' -import { NodeWorkerMessageType } from '../../serverless/types/workerTypes' -import TenantRepository from '../../database/repositories/tenantRepository' - -const job: CrowdJob = { - name: 'organization enricher', - cronTime: cronGenerator.everyDay(), - onTrigger: sendWorkerMessage, -} - -async function sendWorkerMessage() { - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - const log = getServiceLogger() - const tenants = await TenantRepository.getPayingTenantIds(options) - log.info(tenants) - for (const { id } of tenants) { - const payload = { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - service: 'enrich-organizations', - tenantId: id, - } as NodeWorkerMessageBase - log.info({ payload }, 'enricher worker payload') - await sendNodeWorkerMessage(id, payload) - } -} - -export default job diff --git a/backend/src/bin/jobs/refreshGithubRepoSettings.ts b/backend/src/bin/jobs/refreshGithubRepoSettings.ts new file mode 100644 index 0000000000..20a385c56e --- /dev/null +++ b/backend/src/bin/jobs/refreshGithubRepoSettings.ts @@ -0,0 +1,68 @@ +/* eslint-disable no-continue */ +import cronGenerator from 'cron-time-generator' + +import { IS_DEV_ENV, timeout } from '@crowd/common' +import { getServiceChildLogger } from '@crowd/logging' + +import SequelizeRepository from '../../database/repositories/sequelizeRepository' +import IntegrationService from '../../services/integrationService' +import { CrowdJob } from '../../types/jobTypes' + +const log = getServiceChildLogger('refreshGithubRepoSettings') + +const refreshForGitHub = async () => { + log.info('Updating Github repo settings.') + const dbOptions = await SequelizeRepository.getDefaultIRepositoryOptions() + + interface Integration { + id: string + tenantId: string + integrationIdentifier: string + } + + const githubIntegrations = await dbOptions.database.sequelize.query( + `SELECT id, "tenantId", "integrationIdentifier" FROM integrations + WHERE platform = 'github' AND "deletedAt" IS NULL + AND "createdAt" < NOW() - INTERVAL '1 minute' AND "integrationIdentifier" IS NOT NULL`, + ) + + for (const integration of githubIntegrations[0] as Integration[]) { + log.info(`Updating repo settings for Github integration: ${integration.id}`) + + try { + const options = await SequelizeRepository.getDefaultIRepositoryOptions() + options.currentTenant = { id: integration.tenantId } + + const integrationService = new IntegrationService(options) + // newly discovered repos will be mapped to default segment of the integration + await integrationService.updateGithubIntegrationSettings(integration.integrationIdentifier) + + log.info(`Successfully updated repo settings for Github integration: ${integration.id}`) + } catch (err) { + log.error( + `Error updating repo settings for Github integration ${integration.id}: ${err.message}`, + ) + } finally { + await timeout(1000) + } + } + + log.info('Finished updating Github repo settings.') +} + +export const refreshGithubRepoSettings = async () => { + log.info('Updating Github repo settings.') + + await refreshForGitHub() +} + +const job: CrowdJob = { + name: 'Refresh Github repo settings', + // every day + cronTime: IS_DEV_ENV ? cronGenerator.every(5).minutes() : cronGenerator.every(1).days(), + onTrigger: async () => { + await refreshGithubRepoSettings() + }, +} + +export default job diff --git a/backend/src/bin/jobs/refreshGitlabToken.ts b/backend/src/bin/jobs/refreshGitlabToken.ts new file mode 100644 index 0000000000..74e46fbd40 --- /dev/null +++ b/backend/src/bin/jobs/refreshGitlabToken.ts @@ -0,0 +1,75 @@ +import axios, { AxiosRequestConfig, AxiosResponse } from 'axios' +import cronGenerator from 'cron-time-generator' + +import { timeout } from '@crowd/common' +import { getServiceChildLogger } from '@crowd/logging' + +import { GITLAB_CONFIG } from '@/conf' + +import SequelizeRepository from '../../database/repositories/sequelizeRepository' +import { CrowdJob } from '../../types/jobTypes' + +const log = getServiceChildLogger('refreshGitlabTokenCronJob') + +const job: CrowdJob = { + name: 'Refresh Gitlab token', + // every hour + cronTime: cronGenerator.every(1).hours(), + onTrigger: async () => { + log.info('Checking Gitlab tokens for refresh.') + const dbOptions = await SequelizeRepository.getDefaultIRepositoryOptions() + + interface Integration { + id: string + token: string + refreshToken: string + } + + const gitlabTokens = await dbOptions.database.sequelize.query( + `SELECT id, token, "refreshToken" FROM integrations + WHERE platform = 'gitlab' AND "deletedAt" IS NULL + AND "createdAt" < NOW() - INTERVAL '1 hour'`, + ) + + for (const integration of gitlabTokens[0] as Integration[]) { + log.info(`Refreshing token for Gitlab integration: ${integration.id}`) + + try { + const config: AxiosRequestConfig = { + method: 'post', + url: 'https://gitlab.com/oauth/token', + data: { + grant_type: 'refresh_token', + refresh_token: integration.refreshToken, + client_id: GITLAB_CONFIG.clientId, + client_secret: GITLAB_CONFIG.clientSecret, + }, + headers: { + 'Content-Type': 'application/json', + }, + } + + const response: AxiosResponse = await axios(config) + + const newToken = response.data.access_token + const newRefreshToken = response.data.refresh_token + + await dbOptions.database.integration.update( + { + token: newToken, + refreshToken: newRefreshToken, + }, + { where: { id: integration.id } }, + ) + + log.info(`Successfully refreshed token for Gitlab integration: ${integration.id}`) + } catch (err) { + log.error(`Error refreshing token for Gitlab integration ${integration.id}: ${err.message}`) + } finally { + await timeout(1000) + } + } + }, +} + +export default job diff --git a/backend/src/bin/jobs/refreshGroupsioToken.ts b/backend/src/bin/jobs/refreshGroupsioToken.ts new file mode 100644 index 0000000000..6c0fd0b5d8 --- /dev/null +++ b/backend/src/bin/jobs/refreshGroupsioToken.ts @@ -0,0 +1,83 @@ +import axios, { AxiosRequestConfig, AxiosResponse } from 'axios' +import cronGenerator from 'cron-time-generator' +import moment from 'moment' + +import { decryptData } from '@crowd/common' +import { getServiceChildLogger } from '@crowd/logging' + +import SequelizeRepository from '../../database/repositories/sequelizeRepository' +import { CrowdJob } from '../../types/jobTypes' + +const log = getServiceChildLogger('refreshgroupsioTokenCronJob') + +const job: CrowdJob = { + name: 'Refresh Groups IO token', + // every day + cronTime: cronGenerator.every(1).days(), + onTrigger: async () => { + log.info('Checking expiry for current groups io token.') + const dbOptions = await SequelizeRepository.getDefaultIRepositoryOptions() + + interface SetttingsObj { + email: string + token: string + groups: string[] + password: string + tokenError: string + tokenExpiry: string + updateMemberAttributes: boolean + } + + const expiredGroupsIOTokens = await dbOptions.database.sequelize.query( + `select id, settings from integrations + where platform = 'groupsio' + and "deletedAt" is null + and DATE_PART('day', to_date( settings ->> 'tokenExpiry', 'YYYY-MM-DD') - now() ) < 2`, + ) + + for (const integration of expiredGroupsIOTokens[0]) { + const thisSetting: SetttingsObj = integration.settings + thisSetting.tokenError = '' + + log.info('Refreshing token for groups: ', thisSetting.groups) + + try { + const decryptedPassword = decryptData(thisSetting.password) + + const config: AxiosRequestConfig = { + method: 'post', + url: 'https://groups.io/api/v1/login', + params: { + email: thisSetting.email, + password: decryptedPassword, + }, + headers: { + 'Content-Type': 'application/json', + }, + } + + const response: AxiosResponse = await axios(config) + + // we need to get cookie from the response and it's expiry + const cookie = response.headers['set-cookie'][0].split(';')[0] + const cookieExpiryString: string = response.headers['set-cookie'][0] + .split(';')[3] + .split('=')[1] + const cookieExpiry = moment(cookieExpiryString).format('YYYY-MM-DD HH:mm:ss.sss Z') + + thisSetting.token = cookie + thisSetting.tokenExpiry = cookieExpiry + } catch (err) { + thisSetting.tokenError = err.message + log.error(err.message) + } finally { + await dbOptions.database.integration.update( + { settings: thisSetting }, + { where: { id: integration.id } }, + ) + } + } + }, +} + +export default job diff --git a/backend/src/bin/jobs/refreshMaterializedViews.ts b/backend/src/bin/jobs/refreshMaterializedViews.ts deleted file mode 100644 index a042cc3dad..0000000000 --- a/backend/src/bin/jobs/refreshMaterializedViews.ts +++ /dev/null @@ -1,27 +0,0 @@ -import cronGenerator from 'cron-time-generator' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import { CrowdJob } from '../../types/jobTypes' - -let processing = false - -const job: CrowdJob = { - name: 'Refresh Materialized View', - // every two hours - cronTime: cronGenerator.every(2).minutes(), - onTrigger: async () => { - if (!processing) { - processing = true - } else { - return - } - const dbOptions = await SequelizeRepository.getDefaultIRepositoryOptions() - - await dbOptions.database.sequelize.query( - 'refresh materialized view concurrently "memberActivityAggregatesMVs"', - ) - - processing = false - }, -} - -export default job diff --git a/backend/src/bin/jobs/refreshMaterializedViewsForCube.ts b/backend/src/bin/jobs/refreshMaterializedViewsForCube.ts deleted file mode 100644 index 1b9a2aab10..0000000000 --- a/backend/src/bin/jobs/refreshMaterializedViewsForCube.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Logger, logExecutionTimeV2 } from '@crowd/logging' -import { CrowdJob } from '../../types/jobTypes' -import { databaseInit } from '../../database/databaseConnection' - -let processing = false - -const job: CrowdJob = { - name: 'Refresh Materialized View For Cube', - cronTime: '1,31 * * * *', - onTrigger: async (log: Logger) => { - if (!processing) { - processing = true - } else { - return - } - - // initialize database with 15 minutes query timeout - const database = await databaseInit(1000 * 60 * 15) - - const materializedViews = [ - 'mv_members_cube', - 'mv_activities_cube', - 'mv_organizations_cube', - 'mv_segments_cube', - ] - - for (const view of materializedViews) { - await logExecutionTimeV2( - () => - database.sequelize.query(`REFRESH MATERIALIZED VIEW CONCURRENTLY "${view}"`, { - useMaster: true, - }), - log, - `Refresh Materialized View ${view}`, - ) - } - - processing = false - }, -} - -export default job diff --git a/backend/src/bin/jobs/refreshSampleData.ts b/backend/src/bin/jobs/refreshSampleData.ts deleted file mode 100644 index efed37c975..0000000000 --- a/backend/src/bin/jobs/refreshSampleData.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { CrowdJob } from '../../types/jobTypes' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import { NodeWorkerMessageType } from '../../serverless/types/workerTypes' -import { NodeWorkerMessageBase } from '../../types/mq/nodeWorkerMessageBase' - -const job: CrowdJob = { - name: 'Refresh sample data', - // every day - cronTime: '0 0 * * *', - onTrigger: async () => { - await sendNodeWorkerMessage('refresh-sample-data', { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - service: 'refresh-sample-data', - } as NodeWorkerMessageBase) - }, -} - -export default job diff --git a/backend/src/bin/jobs/weeklyAnalyticsEmailsCoordinator.ts b/backend/src/bin/jobs/weeklyAnalyticsEmailsCoordinator.ts deleted file mode 100644 index e1b9954617..0000000000 --- a/backend/src/bin/jobs/weeklyAnalyticsEmailsCoordinator.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { timeout } from '@crowd/common' -import { CrowdJob } from '../../types/jobTypes' -import TenantService from '../../services/tenantService' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import { NodeWorkerMessageBase } from '../../types/mq/nodeWorkerMessageBase' -import { NodeWorkerMessageType } from '../../serverless/types/workerTypes' - -const job: CrowdJob = { - name: 'Weekly Analytics Emails coordinator', - cronTime: '0 8 * * MON', - onTrigger: async () => { - const tenants = await TenantService._findAndCountAllForEveryUser({}) - - for (const tenant of tenants.rows) { - await sendNodeWorkerMessage(tenant.id, { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - tenant: tenant.id, - service: 'weekly-analytics-emails', - } as NodeWorkerMessageBase) - - // Wait 1 second between messages to potentially reduce spike load on cube between each tenant runs - await timeout(1000) - } - }, -} - -export default job diff --git a/backend/src/bin/nodejs-worker.ts b/backend/src/bin/nodejs-worker.ts deleted file mode 100644 index e1fae5e069..0000000000 --- a/backend/src/bin/nodejs-worker.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { timeout } from '@crowd/common' -import { Logger, getChildLogger, getServiceLogger, logExecutionTimeV2 } from '@crowd/logging' -import { SpanStatusCode, getServiceTracer } from '@crowd/tracing' -import { DeleteMessageRequest, Message, ReceiveMessageRequest } from 'aws-sdk/clients/sqs' -import moment from 'moment' -import { SQS_CONFIG } from '../conf' -import { processDbOperationsMessage } from '../serverless/dbOperations/workDispatcher' -import { processNodeMicroserviceMessage } from '../serverless/microservices/nodejs/workDispatcher' -import { NodeWorkerMessageType } from '../serverless/types/workerTypes' -import { sendNodeWorkerMessage } from '../serverless/utils/nodeWorkerSQS' -import { NodeWorkerMessageBase } from '../types/mq/nodeWorkerMessageBase' -import { deleteMessage, receiveMessage, sendMessage } from '../utils/sqs' -import { processIntegration, processWebhook } from './worker/integrations' - -/* eslint-disable no-constant-condition */ - -const tracer = getServiceTracer() -const serviceLogger = getServiceLogger() - -let exiting = false - -const messagesInProgress = new Map() - -process.on('SIGTERM', async () => { - serviceLogger.warn('Detected SIGTERM signal, started exiting!') - exiting = true -}) - -const receive = (delayed?: boolean): Promise => { - const params: ReceiveMessageRequest = { - QueueUrl: delayed ? SQS_CONFIG.nodejsWorkerDelayableQueue : SQS_CONFIG.nodejsWorkerQueue, - MessageAttributeNames: !delayed - ? undefined - : ['remainingDelaySeconds', 'tenantId', 'targetQueueUrl'], - } - - return receiveMessage(params) -} - -const removeFromQueue = (receiptHandle: string, delayed?: boolean): Promise => { - const params: DeleteMessageRequest = { - QueueUrl: delayed ? SQS_CONFIG.nodejsWorkerDelayableQueue : SQS_CONFIG.nodejsWorkerQueue, - ReceiptHandle: receiptHandle, - } - - return deleteMessage(params) -} - -async function handleDelayedMessages() { - const delayedHandlerLogger = getChildLogger('delayedMessages', serviceLogger, { - queue: SQS_CONFIG.nodejsWorkerDelayableQueue, - }) - delayedHandlerLogger.info('Listing for delayed messages!') - - // noinspection InfiniteLoopJS - while (!exiting) { - const message = await receive(true) - - if (message) { - await tracer.startActiveSpan('ProcessDelayedMessage', async (span) => { - try { - const msg: NodeWorkerMessageBase = JSON.parse(message.Body) - const messageLogger = getChildLogger('messageHandler', serviceLogger, { - messageId: message.MessageId, - type: msg.type, - }) - - if (message.MessageAttributes && message.MessageAttributes.remainingDelaySeconds) { - // re-delay - const newDelay = parseInt( - message.MessageAttributes.remainingDelaySeconds.StringValue, - 10, - ) - const tenantId = message.MessageAttributes.tenantId.StringValue - messageLogger.debug({ newDelay, tenantId }, 'Re-delaying message!') - await sendNodeWorkerMessage(tenantId, msg, newDelay) - } else { - // just emit to the normal queue for processing - const tenantId = message.MessageAttributes.tenantId.StringValue - - if (message.MessageAttributes.targetQueueUrl) { - const targetQueueUrl = message.MessageAttributes.targetQueueUrl.StringValue - messageLogger.debug({ tenantId, targetQueueUrl }, 'Successfully delayed a message!') - await sendMessage({ - QueueUrl: targetQueueUrl, - MessageGroupId: tenantId, - MessageDeduplicationId: `${tenantId}-${moment().valueOf()}`, - MessageBody: JSON.stringify(msg), - }) - } else { - messageLogger.debug({ tenantId }, 'Successfully delayed a message!') - await sendNodeWorkerMessage(tenantId, msg) - } - } - - await removeFromQueue(message.ReceiptHandle, true) - span.setStatus({ - code: SpanStatusCode.OK, - }) - } catch (err) { - span.setStatus({ - code: SpanStatusCode.ERROR, - message: err, - }) - } finally { - span.end() - } - }) - } else { - delayedHandlerLogger.trace('No message received!') - } - } - - delayedHandlerLogger.warn('Exiting!') -} - -let processingMessages = 0 -const isWorkerAvailable = (): boolean => processingMessages <= 3 -const addWorkerJob = (): void => { - processingMessages++ -} -const removeWorkerJob = (): void => { - processingMessages-- -} - -async function handleMessages() { - const handlerLogger = getChildLogger('messages', serviceLogger, { - queue: SQS_CONFIG.nodejsWorkerQueue, - }) - handlerLogger.info('Listening for messages!') - - const processSingleMessage = async (message: Message): Promise => { - await tracer.startActiveSpan('ProcessMessage', async (span) => { - const msg: NodeWorkerMessageBase = JSON.parse(message.Body) - - const messageLogger = getChildLogger('messageHandler', serviceLogger, { - messageId: message.MessageId, - type: msg.type, - }) - - try { - if ( - msg.type === NodeWorkerMessageType.NODE_MICROSERVICE && - (msg as any).service === 'enrich_member_organizations' - ) { - messageLogger.warn( - 'Skipping enrich_member_organizations message! Purging the queue because they are not needed anymore!', - ) - await removeFromQueue(message.ReceiptHandle) - return - } - - messageLogger.info( - { messageType: msg.type, messagePayload: JSON.stringify(msg) }, - 'Received a new queue message!', - ) - - let processFunction: (msg: NodeWorkerMessageBase, logger?: Logger) => Promise - - switch (msg.type) { - case NodeWorkerMessageType.INTEGRATION_PROCESS: - processFunction = processIntegration - break - case NodeWorkerMessageType.NODE_MICROSERVICE: - processFunction = processNodeMicroserviceMessage - break - case NodeWorkerMessageType.DB_OPERATIONS: - processFunction = processDbOperationsMessage - break - case NodeWorkerMessageType.PROCESS_WEBHOOK: - processFunction = processWebhook - break - - default: - messageLogger.error('Error while parsing queue message! Invalid type.') - } - - if (processFunction) { - await logExecutionTimeV2( - async () => { - // remove the message from the queue as it's about to be processed - await removeFromQueue(message.ReceiptHandle) - messagesInProgress.set(message.MessageId, msg) - try { - await processFunction(msg, messageLogger) - } catch (err) { - messageLogger.error(err, 'Error while processing queue message!') - } finally { - messagesInProgress.delete(message.MessageId) - } - }, - messageLogger, - 'Processing queue message!', - ) - } - - span.setStatus({ - code: SpanStatusCode.OK, - }) - } catch (err) { - span.setStatus({ - code: SpanStatusCode.ERROR, - message: err, - }) - messageLogger.error(err, { payload: msg }, 'Error while processing queue message!') - } finally { - span.end() - } - }) - } - - // noinspection InfiniteLoopJS - while (!exiting) { - if (isWorkerAvailable()) { - const message = await receive() - - if (message) { - addWorkerJob() - processSingleMessage(message).then(removeWorkerJob).catch(removeWorkerJob) - } else { - serviceLogger.trace('No message received!') - } - } else { - await timeout(200) - } - } - - // mark in flight messages as exiting - for (const msg of messagesInProgress.values()) { - ;(msg as any).exiting = true - } - - while (messagesInProgress.size !== 0) { - handlerLogger.warn(`Waiting for ${messagesInProgress.size} messages to finish!`) - await timeout(500) - } - - handlerLogger.warn('Exiting!') -} - -setImmediate(async () => { - const promises = [handleMessages(), handleDelayedMessages()] - await Promise.all(promises) -}) diff --git a/backend/src/bin/scripts/backfill-email-domain-member-organization-dates.ts b/backend/src/bin/scripts/backfill-email-domain-member-organization-dates.ts new file mode 100644 index 0000000000..aa423fa5ff --- /dev/null +++ b/backend/src/bin/scripts/backfill-email-domain-member-organization-dates.ts @@ -0,0 +1,179 @@ +import commandLineArgs from 'command-line-args' + +import { inferMemberOrganizationStintChanges } from '@crowd/common_services' +import { + changeMemberOrganizationAffiliationOverrides, + createMemberOrganization, + fetchEmailDomainMemberOrganizationActivityDates, + fetchEmailDomainMemberOrganizationsWithoutDates, + fetchManyOrganizationAffiliationPolicies, + fetchMemberOrganizationsBySource, + pgpQx, + updateMemberOrganization, +} from '@crowd/data-access-layer' +import { getDbConnection } from '@crowd/data-access-layer/src/database' +import { deleteMemberSegmentAffiliations } from '@crowd/data-access-layer/src/member_segment_affiliations' +import { chunkArray } from '@crowd/data-access-layer/src/old/apps/merge_suggestions_worker/utils' +import { getServiceLogger } from '@crowd/logging' +import { getRedisClient } from '@crowd/redis' +import { OrganizationSource } from '@crowd/types' + +import { DB_CONFIG, REDIS_CONFIG } from '@/conf' + +const log = getServiceLogger() + +const options = [ + { + name: 'testRun', + alias: 't', + type: Boolean, + description: 'Run in test mode (limit to 1 batch and 10 members).', + }, + { + name: 'afterMemberId', + alias: 'a', + type: String, + description: 'The member ID to start processing after.', + }, + { + name: 'batchSize', + alias: 'b', + type: Number, + description: 'The number of members to fetch in each batch.', + }, + { + name: 'help', + alias: 'h', + type: Boolean, + description: 'Print this usage guide.', + }, +] + +const parameters = commandLineArgs(options) + +setImmediate(async () => { + const testRun = parameters.testRun ?? false + const BATCH_SIZE = parameters.batchSize ?? (testRun ? 10 : 500) + let afterMemberId = parameters.afterMemberId ?? undefined + + const db = await getDbConnection({ + host: DB_CONFIG.writeHost, + port: DB_CONFIG.port, + database: DB_CONFIG.database, + user: DB_CONFIG.username, + password: DB_CONFIG.password, + }) + + const qx = pgpQx(db) + const redis = await getRedisClient(REDIS_CONFIG, true) + + log.info({ testRun, BATCH_SIZE, afterMemberId }, 'Running script with the following parameters!') + + let hasMore = true + + while (hasMore) { + const memberIds = await fetchEmailDomainMemberOrganizationsWithoutDates( + qx, + BATCH_SIZE, + afterMemberId, + ) + + if (memberIds.length > 0) { + for (const chunk of chunkArray(memberIds, 50)) { + await Promise.all( + chunk.map(async (memberId) => { + if (testRun) { + log.info({ memberId }, 'Processing member!') + } + + try { + const [existingMemberOrganizations, activityDates] = await Promise.all([ + fetchMemberOrganizationsBySource(qx, memberId, OrganizationSource.EMAIL_DOMAIN), + fetchEmailDomainMemberOrganizationActivityDates(qx, memberId), + ]) + + const changes = inferMemberOrganizationStintChanges( + memberId, + existingMemberOrganizations, + activityDates, + ) + + if (testRun) { + log.info( + { existingMemberOrganizations, activityDates, changes }, + 'Previewing changes for member.', + ) + } + + if (changes.length > 0) { + await qx.tx(async (tx) => { + for (const change of changes) { + if (change.type === 'insert') { + const memberOrganizationId = await createMemberOrganization(tx, memberId, { + organizationId: change.organizationId, + dateStart: change.dateStart, + dateEnd: change.dateEnd, + source: OrganizationSource.EMAIL_DOMAIN, + }) + + const orgAffiliationPolicyById = + await fetchManyOrganizationAffiliationPolicies(tx, [change.organizationId]) + + if ( + memberOrganizationId && + orgAffiliationPolicyById.get(change.organizationId) + ) { + await changeMemberOrganizationAffiliationOverrides(tx, [ + { + memberId, + memberOrganizationId, + allowAffiliation: false, + }, + ]) + await deleteMemberSegmentAffiliations(tx, { + memberId, + organizationId: change.organizationId, + }) + } + } else if (change.type === 'update') { + await updateMemberOrganization(tx, memberId, change.id, { + dateStart: change.dateStart, + dateEnd: change.dateEnd, + }) + } + + if (testRun) { + log.info( + { memberId, orgId: change.organizationId, type: change.type }, + 'Member organization updated.', + ) + } + } + }) + await redis.sAdd('recalculate-member-affiliations', [memberId]) + } else if (testRun) { + log.info({ memberId }, 'No changes found for member!') + } + } catch (err) { + log.error({ memberId, err }, 'Failed to process for member!') + throw err + } + }), + ) + } + + const lastMemberId = memberIds[memberIds.length - 1] + afterMemberId = lastMemberId + + log.info({ lastMemberId, count: memberIds.length }, 'Batch processed!') + + if (testRun || memberIds.length < BATCH_SIZE) { + hasMore = false + } + } else { + hasMore = false + } + } + + process.exit(0) +}) diff --git a/backend/src/bin/scripts/cache-dashboard.ts b/backend/src/bin/scripts/cache-dashboard.ts new file mode 100644 index 0000000000..de97c18f32 --- /dev/null +++ b/backend/src/bin/scripts/cache-dashboard.ts @@ -0,0 +1,73 @@ +import commandLineArgs from 'command-line-args' +import commandLineUsage from 'command-line-usage' +import { randomUUID } from 'crypto' +import * as fs from 'fs' +import path from 'path' + +import { getTemporalClient } from '@crowd/temporal' + +import { TEMPORAL_CONFIG } from '@/conf' + +/* eslint-disable no-console */ + +const banner = fs.readFileSync(path.join(__dirname, 'banner.txt'), 'utf8') + +const options = [ + { + name: 'help', + alias: 'h', + type: Boolean, + description: 'Print this usage guide.', + }, + { + name: 'tenantId', + alias: 't', + type: String, + description: 'Tenant ID', + }, + { + name: 'allTenants', + alias: 'a', + type: Boolean, + description: 'all tenants', + }, +] +const sections = [ + { + content: banner, + raw: true, + }, + { + header: `Cache dashboard to redis for given tenants`, + content: 'Cache dashboard to redis for given tenants', + }, + { + header: 'Options', + optionList: options, + }, +] + +const usage = commandLineUsage(sections) +const parameters = commandLineArgs(options) + +if (parameters.help) { + console.log(usage) +} else { + setImmediate(async () => { + const temporal = await getTemporalClient(TEMPORAL_CONFIG) + + const uuid = randomUUID() + + await temporal.workflow.start('spawnDashboardCacheRefreshForAllTenants', { + taskQueue: 'cache', + workflowId: `spawnDashboardCacheRefreshForAllTenants/${uuid}`, + retry: { + maximumAttempts: 10, + }, + args: [], + searchAttributes: {}, + }) + + process.exit(0) + }) +} diff --git a/backend/src/bin/scripts/change-tenant-plan.ts b/backend/src/bin/scripts/change-tenant-plan.ts deleted file mode 100644 index 296e9db74e..0000000000 --- a/backend/src/bin/scripts/change-tenant-plan.ts +++ /dev/null @@ -1,92 +0,0 @@ -import commandLineArgs from 'command-line-args' -import commandLineUsage from 'command-line-usage' -import * as fs from 'fs' -import path from 'path' -import { getServiceLogger } from '@crowd/logging' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' - -/* eslint-disable no-console */ - -const banner = fs.readFileSync(path.join(__dirname, 'banner.txt'), 'utf8') - -const log = getServiceLogger() - -const options = [ - { - name: 'tenant', - alias: 't', - type: String, - description: 'The unique ID of tenant that you would like to update.', - }, - { - name: 'plan', - alias: 'p', - type: String, - description: `Plan that will be applied to the tenant. Accepted values are 'Growth' and 'Essential'.`, - }, - { - name: 'trialEndsAt', - alias: 'x', - description: - 'YYYY-MM-dd format trial end date. If this value is ommited, isTrial will be set to false.', - type: String, - defaultValue: null, - }, - { - name: 'help', - alias: 'h', - type: Boolean, - description: 'Print this usage guide.', - }, -] -const sections = [ - { - content: banner, - raw: true, - }, - { - header: 'Update tenant plan', - content: 'Updates tenant plan.', - }, - { - header: 'Options', - optionList: options, - }, -] - -const usage = commandLineUsage(sections) -const parameters = commandLineArgs(options) - -if (parameters.help || !parameters.tenant || !parameters.plan) { - console.log(usage) -} else if (parameters.plan !== 'Growth' && parameters.plan !== 'Essential') { - console.log(usage) - console.log(`Invalid plan ${parameters.plan}`) -} else { - setImmediate(async () => { - const plan = parameters.plan - const isTrial = parameters.trialEndsAt !== null - const trialEndsAt = parameters.trialEndsAt - - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - const tenantIds = parameters.tenant.split(',') - - for (const tenantId of tenantIds) { - const tenant = await options.database.tenant.findByPk(tenantId) - - if (!tenant) { - log.error({ tenantId }, 'Tenant not found!') - process.exit(1) - } else { - log.info({ tenantId, isTrial }, `Tenant found - updating tenant plan to ${plan}!`) - await tenant.update({ - plan, - isTrialPlan: isTrial, - trialEndsAt, - }) - } - } - - process.exit(0) - }) -} diff --git a/backend/src/bin/scripts/continue-run.ts b/backend/src/bin/scripts/continue-run.ts deleted file mode 100644 index af5dd5be49..0000000000 --- a/backend/src/bin/scripts/continue-run.ts +++ /dev/null @@ -1,103 +0,0 @@ -import commandLineArgs from 'command-line-args' -import commandLineUsage from 'command-line-usage' -import * as fs from 'fs' -import path from 'path' -import { getServiceLogger } from '@crowd/logging' -import { IntegrationRunState } from '@crowd/types' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import { NodeWorkerIntegrationProcessMessage } from '../../types/mq/nodeWorkerIntegrationProcessMessage' -import IntegrationRunRepository from '../../database/repositories/integrationRunRepository' - -/* eslint-disable no-console */ - -const banner = fs.readFileSync(path.join(__dirname, 'banner.txt'), 'utf8') - -const log = getServiceLogger() - -const options = [ - { - name: 'run', - alias: 'r', - typeLabel: '{underline runId}', - type: String, - description: - 'The unique ID of integration run that you would like to continue processing. Use comma delimiter when sending multiple integration runs.', - }, - { - name: 'disableFiringCrowdWebhooks', - alias: 'd', - typeLabel: '{underline disableFiringCrowdWebhooks}', - type: Boolean, - defaultOption: false, - description: 'Should it disable firing outgoing crowd webhooks?', - }, - { - name: 'help', - alias: 'h', - type: Boolean, - description: 'Print this usage guide.', - }, -] -const sections = [ - { - content: banner, - raw: true, - }, - { - header: 'Continue Processing Integration Run', - content: 'Trigger processing of integration run.', - }, - { - header: 'Options', - optionList: options, - }, -] - -const usage = commandLineUsage(sections) -const parameters = commandLineArgs(options) - -if (parameters.help && !parameters.run) { - console.log(usage) -} else { - setImmediate(async () => { - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - - const fireCrowdWebhooks = !parameters.disableFiringCrowdWebhooks - - const runRepo = new IntegrationRunRepository(options) - - const runIds = parameters.run.split(',') - for (const runId of runIds) { - const run = await runRepo.findById(runId) - - if (!run) { - log.error({ runId }, 'Integration run not found!') - process.exit(1) - } else { - await log.info({ runId }, 'Integration run found - triggering SQS message!') - - if (run.state !== IntegrationRunState.PENDING) { - log.warn( - { currentState: run.state }, - `Setting integration state to ${IntegrationRunState.PENDING}!`, - ) - await runRepo.restart(run.id) - } - - if (!fireCrowdWebhooks) { - log.info( - 'fireCrowdWebhooks is false - This continue-run will not trigger outgoing crowd webhooks!', - ) - } - - await sendNodeWorkerMessage( - run.tenantId, - new NodeWorkerIntegrationProcessMessage(run.id, null, fireCrowdWebhooks), - ) - } - } - - process.exit(0) - }) -} diff --git a/backend/src/bin/scripts/enrich-members-and-organizations.ts b/backend/src/bin/scripts/enrich-members-and-organizations.ts deleted file mode 100644 index 38e049afec..0000000000 --- a/backend/src/bin/scripts/enrich-members-and-organizations.ts +++ /dev/null @@ -1,154 +0,0 @@ -import commandLineArgs from 'command-line-args' -import commandLineUsage from 'command-line-usage' -import * as fs from 'fs' -import path from 'path' -import { getServiceLogger } from '@crowd/logging' -import SequelizeRepository from '@/database/repositories/sequelizeRepository' -import MemberRepository from '@/database/repositories/memberRepository' -import { sendBulkEnrichMessage, sendNodeWorkerMessage } from '@/serverless/utils/nodeWorkerSQS' -import OrganizationRepository from '@/database/repositories/organizationRepository' -import { NodeWorkerMessageType } from '@/serverless/types/workerTypes' -import { NodeWorkerMessageBase } from '@/types/mq/nodeWorkerMessageBase' -import getUserContext from '@/database/utils/getUserContext' -import { IRepositoryOptions } from '@/database/repositories/IRepositoryOptions' -import SegmentService from '@/services/segmentService' - -/* eslint-disable no-console */ - -const banner = fs.readFileSync(path.join(__dirname, 'banner.txt'), 'utf8') - -const log = getServiceLogger() - -const options = [ - { - name: 'tenant', - alias: 't', - type: String, - description: 'The unique ID of tenant that you would like to enrich.', - }, - { - name: 'help', - alias: 'h', - type: Boolean, - description: 'Print this usage guide.', - }, - { - name: 'organization', - alias: 'o', - type: Boolean, - defaultValue: false, - description: 'Enrich organizations of the tenant', - }, - { - name: 'member', - alias: 'm', - type: Boolean, - defaultValue: false, - description: 'Enrich members of the tenant', - }, -] -const sections = [ - { - content: banner, - raw: true, - }, - { - header: 'Enrich members, organizations or both of the tenant', - content: 'Enrich all enrichable members, organizations or both of the tenant', - }, - { - header: 'Options', - optionList: options, - }, -] - -const usage = commandLineUsage(sections) -const parameters = commandLineArgs(options) - -if (parameters.help || (!parameters.tenant && (!parameters.organization || !parameters.member))) { - console.log(usage) -} else { - setImmediate(async () => { - const tenantIds = parameters.tenant.split(',') - const enrichMembers = parameters.member - const enrichOrganizations = parameters.organization - const limit = 1000 - - for (const tenantId of tenantIds) { - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - const tenant = await options.database.tenant.findByPk(tenantId) - - if (!tenant) { - log.error({ tenantId }, 'Tenant not found!') - process.exit(1) - } else { - log.info( - { tenantId }, - `Tenant found - starting enrichment operation for tenant ${tenantId}`, - ) - - const userContext: IRepositoryOptions = await getUserContext(tenantId) - const segmentService = new SegmentService(userContext) - const { rows: segments } = await segmentService.querySubprojects({}) - - log.info({ tenantId }, `Total segments found in the tenant: ${segments.length}`) - - // get all segment ids for the tenant - const segmentIds = segments.map((segment) => segment.id) - - const optionsWithTenant = await SequelizeRepository.getDefaultIRepositoryOptions( - userContext, - tenant, - segments, - ) - - if (enrichMembers) { - let offset = 0 - let totalMembers = 0 - - do { - const { ids: memberIds, count: membersCount } = - await MemberRepository.getMemberIdsandCount( - { limit, offset, countOnly: false }, - optionsWithTenant, - ) - - totalMembers = membersCount - log.info({ tenantId }, `Total members found in the tenant: ${membersCount}`) - - await sendBulkEnrichMessage(tenantId, memberIds, segmentIds, false, true) - - offset += limit - } while (totalMembers > offset) - - log.info({ tenantId }, `Members enrichment operation finished for tenant ${tenantId}`) - } - - if (enrichOrganizations) { - const organizations = await OrganizationRepository.findAndCountAll({}, optionsWithTenant) - - const totalOrganizations = organizations.count - - log.info({ tenantId }, `Total organizations found in the tenant: ${totalOrganizations}`) - - const payload = { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - service: 'enrich-organizations', - tenantId, - // Since there is no pagination implemented for the organizations enrichment, - // we set a limit of 10,000 to ensure all organizations are included when enriched in bulk. - maxEnrichLimit: 10000, - } as NodeWorkerMessageBase - - await sendNodeWorkerMessage(tenantId, payload) - log.info( - { tenantId }, - `Organizations enrichment operation finished for tenant ${tenantId}`, - ) - } - } - } - - process.exit(0) - }) -} diff --git a/backend/src/bin/scripts/enrich-organizations-synchronous.ts b/backend/src/bin/scripts/enrich-organizations-synchronous.ts deleted file mode 100644 index 6f9f51e744..0000000000 --- a/backend/src/bin/scripts/enrich-organizations-synchronous.ts +++ /dev/null @@ -1,66 +0,0 @@ -import commandLineArgs from 'command-line-args' -import commandLineUsage from 'command-line-usage' -import * as fs from 'fs' -import path from 'path' -import { getServiceLogger } from '@crowd/logging' -import { BulkorganizationEnrichmentWorker } from '@/serverless/microservices/nodejs/bulk-enrichment/bulkOrganizationEnrichmentWorker' - -/* eslint-disable no-console */ - -const banner = fs.readFileSync(path.join(__dirname, 'banner.txt'), 'utf8') - -const log = getServiceLogger() - -const options = [ - { - name: 'tenant', - alias: 't', - type: String, - description: 'The unique ID of tenant that you would like to enrich.', - }, - { - name: 'limit', - alias: 'l', - type: Number, - description: 'The maximum number of organizations to enrich.', - }, - { - name: 'help', - alias: 'h', - type: Boolean, - description: 'Print this usage guide.', - }, -] -const sections = [ - { - content: banner, - raw: true, - }, - { - header: 'Enrich organizations of the tenant synchronously', - content: 'Enrich all enrichable organizations of the tenant', - }, - { - header: 'Options', - optionList: options, - }, -] - -const usage = commandLineUsage(sections) -const parameters = commandLineArgs(options) - -if (parameters.help || !parameters.tenant || !parameters.limit) { - console.log(usage) -} else { - setImmediate(async () => { - const tenantIds = parameters.tenant.split(',') - const limit = parameters.limit - - for (const tenantId of tenantIds) { - await BulkorganizationEnrichmentWorker(tenantId, limit, true, true) - log.info(`Done for tenant ${tenantId}`) - } - - process.exit(0) - }) -} diff --git a/backend/src/bin/scripts/fix-members-activities-after-unaffilation.ts b/backend/src/bin/scripts/fix-members-activities-after-unaffilation.ts new file mode 100644 index 0000000000..dbb66ca9fc --- /dev/null +++ b/backend/src/bin/scripts/fix-members-activities-after-unaffilation.ts @@ -0,0 +1,120 @@ +import commandLineArgs from 'command-line-args' + +import { signalMemberUpdate } from '@crowd/common_services' +import { getDbConnection } from '@crowd/data-access-layer/src/database' +import { getServiceLogger } from '@crowd/logging' +import { getTemporalClient } from '@crowd/temporal' + +import { DB_CONFIG, TEMPORAL_CONFIG } from '@/conf' + +const log = getServiceLogger() + +const options = [ + { + name: 'organizationId', + alias: 'o', + typeLabel: '{underline organizationId}', + type: String, + description: 'The organization ID to process members for.', + }, + { + name: 'dryRun', + alias: 'd', + type: Boolean, + description: 'Run in dry-run mode (show what would be processed).', + }, + { + name: 'help', + alias: 'h', + type: Boolean, + description: 'Print this usage guide.', + }, +] + +const parameters = commandLineArgs(options) + +setImmediate(async () => { + const organizationId = parameters.organizationId + const dryRun = parameters.dryRun ?? false + + log.info({ organizationId, dryRun }, 'Running script with the following parameters!') + + const db = await getDbConnection({ + host: DB_CONFIG.readHost, + port: DB_CONFIG.port, + database: DB_CONFIG.database, + user: DB_CONFIG.username, + password: DB_CONFIG.password, + }) + const temporal = await getTemporalClient(TEMPORAL_CONFIG) + + try { + const memberIds = await db.any( + ` + SELECT DISTINCT ar."memberId" AS id + FROM "activityRelations" ar + JOIN "memberOrganizations" mo + ON ar."memberId" = mo."memberId" + AND ar."organizationId" = mo."organizationId" + LEFT JOIN "memberOrganizationAffiliationOverrides" moao + ON mo."id" = moao."memberOrganizationId" + WHERE ar."organizationId" = $1 + AND ( + ( + mo."deletedAt" IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM "memberOrganizations" mo2 + WHERE mo2."memberId" = mo."memberId" + AND mo2."organizationId" = mo."organizationId" + AND mo2."deletedAt" IS NULL + ) + ) + OR ( + mo."deletedAt" IS NULL + AND moao."allowAffiliation" = false + ) + ); + `, + [organizationId], + ) + + log.info(`Found ${memberIds.length} members to process`) + + if (memberIds.length === 0) { + log.info('No members found. Implement the query to get actual memberIds.') + return + } + + if (dryRun) { + log.info('DRY RUN - Would update affiliations for the following members:') + memberIds.forEach((member: { id: string }) => { + log.info(` - Member ID: ${member.id}`) + }) + return + } + + let processedCount = 0 + for (const member of memberIds) { + try { + log.info(`Processing member: ${member.id}`) + + await signalMemberUpdate(temporal, member.id, { + memberOrganizationIds: [organizationId], + }) + + processedCount++ + log.info(`Successfully triggered workflow for member: ${member.id}`) + } catch (error) { + log.error(`Failed to process member ${member.id}:`, error) + } + } + + log.info(`Script completed. Processed ${processedCount}/${memberIds.length} members.`) + } catch (error) { + log.error('Script failed:', error) + throw error + } + + process.exit(0) +}) diff --git a/backend/src/bin/scripts/fix-missing-org-displayName.ts b/backend/src/bin/scripts/fix-missing-org-displayName.ts new file mode 100644 index 0000000000..d2ffb5f7f2 --- /dev/null +++ b/backend/src/bin/scripts/fix-missing-org-displayName.ts @@ -0,0 +1,166 @@ +/* eslint-disable @typescript-eslint/dot-notation */ + +/* eslint-disable no-console */ + +/* eslint-disable import/no-extraneous-dependencies */ +import commandLineArgs from 'command-line-args' +import commandLineUsage from 'command-line-usage' + +import { QueryExecutor } from '@crowd/data-access-layer/src/queryExecutor' + +import { databaseInit } from '@/database/databaseConnection' +import { IRepositoryOptions } from '@/database/repositories/IRepositoryOptions' +import OrganizationRepository from '@/database/repositories/organizationRepository' +import SequelizeRepository from '@/database/repositories/sequelizeRepository' + +const options = [ + { + name: 'help', + alias: 'h', + type: Boolean, + description: 'Print this usage guide.', + }, + { + name: 'tenantId', + alias: 't', + type: String, + description: 'Tenant Id', + }, +] +const sections = [ + { + header: `Fix empty displayName in organizations`, + content: 'Script will fix organizations with empty displayName', + }, + { + header: 'Options', + optionList: options, + }, +] + +const usage = commandLineUsage(sections) +const parameters = commandLineArgs(options) + +function getOrgsWithoutDisplayName( + qx: QueryExecutor, + tenantId: string, + { limit = 50, countOnly = false }, +) { + return qx.select( + ` + SELECT + ${countOnly ? 'COUNT(*)' : 'o.id'} + FROM organizations o + WHERE o."tenantId" = $(tenantId) + AND o."displayName" IS NULL + ${countOnly ? '' : 'LIMIT $(limit)'} + `, + { tenantId, limit }, + ) +} + +async function getOrgIdentities(qx: QueryExecutor, orgId: string, tenantId: string) { + return qx.select( + ` + SELECT value + FROM "organizationIdentities" + WHERE "organizationId" = $(orgId) + AND "tenantId" = $(tenantId) + LIMIT 1 + `, + { orgId, tenantId }, + ) +} + +async function getOrgAttributes(qx: QueryExecutor, orgId: string) { + return qx.select( + ` + SELECT value + FROM "orgAttributes" + WHERE "organizationId" = $(orgId) + AND name = 'name' + LIMIT 1 + `, + { orgId }, + ) +} + +async function updateOrgDisplayName(qx: QueryExecutor, orgId: string, displayName: string) { + await qx.result( + ` + UPDATE organizations + SET "displayName" = $(displayName) + WHERE id = $(id) + `, + { id: orgId, displayName }, + ) +} + +if (parameters.help || !parameters.tenantId) { + console.log(usage) +} else { + setImmediate(async () => { + const prodDb = await databaseInit() + const tenantId = parameters.tenantId + const qx = SequelizeRepository.getQueryExecutor({ + database: prodDb, + } as IRepositoryOptions) + + const options = await SequelizeRepository.getDefaultIRepositoryOptions() + + const BATCH_SIZE = 50 + let processed = 0 + + const totalOrgs = await getOrgsWithoutDisplayName(qx, tenantId, { countOnly: true }) + + console.log(`Total organizations without displayName: ${totalOrgs[0].count}`) + + let orgs = await getOrgsWithoutDisplayName(qx, tenantId, { limit: BATCH_SIZE }) + + while (totalOrgs[0].count > processed) { + for (const org of orgs) { + let displayName + let updateAttributes = false + + const attributes = await getOrgAttributes(qx, org.id) + + if (attributes.length > 0) { + displayName = attributes[0]?.value + } else { + const identities = await getOrgIdentities(qx, org.id, tenantId) + displayName = identities && identities[0]?.value + updateAttributes = true + } + + if (displayName) { + await updateOrgDisplayName(qx, org.id, displayName) + + if (updateAttributes) { + await OrganizationRepository.updateOrgAttributes( + org.id, + { + attributes: { + name: { + custom: [displayName], + default: displayName, + }, + }, + }, + options, + ) + } + } else { + console.log(`Organization ${org.id} does not have displayName`) + } + + processed++ + } + + console.log(`Processed ${processed}/${totalOrgs[0].count} organizations`) + + orgs = await getOrgsWithoutDisplayName(qx, tenantId, { limit: BATCH_SIZE }) + } + + process.exit(0) + }) +} diff --git a/backend/src/bin/scripts/generate-merge-suggestions.ts b/backend/src/bin/scripts/generate-merge-suggestions.ts deleted file mode 100644 index 911f5fc184..0000000000 --- a/backend/src/bin/scripts/generate-merge-suggestions.ts +++ /dev/null @@ -1,62 +0,0 @@ -import commandLineArgs from 'command-line-args' -import commandLineUsage from 'command-line-usage' -import * as fs from 'fs' -import path from 'path' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import { NodeWorkerMessageType } from '../../serverless/types/workerTypes' -import { NodeWorkerMessageBase } from '@/types/mq/nodeWorkerMessageBase' - -/* eslint-disable no-console */ - -const banner = fs.readFileSync(path.join(__dirname, 'banner.txt'), 'utf8') - -const options = [ - { - name: 'tenant', - alias: 't', - type: String, - description: - 'The unique ID of that tenant that you would like to generate merge suggestions for.', - }, - { - name: 'help', - alias: 'h', - type: Boolean, - description: 'Print this usage guide.', - }, -] -const sections = [ - { - content: banner, - raw: true, - }, - { - header: 'Generate merge suggestions for a tenant', - content: 'Generate merge suggestions for a tenant', - }, - { - header: 'Options', - optionList: options, - }, -] - -const usage = commandLineUsage(sections) -const parameters = commandLineArgs(options) - -if (parameters.help || !parameters.tenant) { - console.log(usage) -} else { - setImmediate(async () => { - const tenantIds = parameters.tenant.split(',') - - for (const tenantId of tenantIds) { - await sendNodeWorkerMessage(tenantId, { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - tenant: tenantId, - service: 'merge-suggestions', - } as NodeWorkerMessageBase) - } - - process.exit(0) - }) -} diff --git a/backend/src/bin/scripts/import-lfx-memberships.ts b/backend/src/bin/scripts/import-lfx-memberships.ts new file mode 100644 index 0000000000..5d9fb167cf --- /dev/null +++ b/backend/src/bin/scripts/import-lfx-memberships.ts @@ -0,0 +1,165 @@ +/* eslint-disable @typescript-eslint/dot-notation */ + +/* eslint-disable no-console */ + +/* eslint-disable import/no-extraneous-dependencies */ +import commandLineArgs from 'command-line-args' +import commandLineUsage from 'command-line-usage' +import { parse } from 'csv-parse/sync' +import * as fs from 'fs' +import uniq from 'lodash/uniq' +import moment from 'moment' +import path from 'path' + +import { LfxMembership, insertLfxMembership } from '@crowd/data-access-layer/src/lfx_memberships' +import { + findOrgIdByDisplayName, + findOrgIdByDomain, +} from '@crowd/data-access-layer/src/organizations' +import { findProjectGroupByName } from '@crowd/data-access-layer/src/segments' + +import { databaseInit } from '@/database/databaseConnection' +import { IRepositoryOptions } from '@/database/repositories/IRepositoryOptions' +import SequelizeRepository from '@/database/repositories/sequelizeRepository' + +const options = [ + { + name: 'help', + alias: 'h', + type: Boolean, + description: 'Print this usage guide.', + }, + { + name: 'file', + alias: 'f', + type: String, + description: 'Path to CSV file to import', + }, + { + name: 'tenantId', + alias: 't', + type: String, + description: 'Tenant Id. Hint: what you probably need is 875c38bd-2b1b-4e91-ad07-0cfbabb4c49f', + }, +] +const sections = [ + { + header: `Import LFX Membership `, + content: + 'Merges two members, then unmerges these and cross checks unmerge result with original data.', + }, + { + header: 'Options', + optionList: options, + }, +] + +const usage = commandLineUsage(sections) +const parameters = commandLineArgs(options) + +function parseDomains(domains: string) { + return uniq( + domains + .split(',') + .map((domain) => domain.trim()) + .filter((domain) => domain.length > 0) + + // the rest if for values that look like this: "andesdigital.cl\n\n--- Merged Data:\n\ andesdigital.cl" + .flatMap((domain) => domain.split('\n')) + .filter((domain) => domain.match(/^[a-z0-9.-]+$/)), + ) +} + +async function findOrgId(qx, record) { + let org = await findOrgIdByDomain(qx, [record['Account Domain']]) + if (org) { + return org + } + + org = await findOrgIdByDomain(qx, record['Domain Alias']) + if (org) { + return org + } + + org = await findOrgIdByDisplayName(qx, { orgName: record['Account Name'], exact: true }) + if (org) { + return org + } + + org = await findOrgIdByDisplayName(qx, { + orgName: record['Account Name'], + exact: false, + }) + return org +} + +if (parameters.help || !parameters.file || !parameters.tenantId) { + console.log(usage) +} else { + setImmediate(async () => { + const prodDb = await databaseInit() + const qx = SequelizeRepository.getQueryExecutor({ + database: prodDb, + } as IRepositoryOptions) + + await qx.result(`DELETE FROM "lfxMemberships"`) + + console.log('All records deleted') + + const fileData = fs.readFileSync(path.resolve(parameters.file), 'latin1') + + const records = parse(fileData, { + columns: true, + skip_empty_lines: true, + }) + + console.log('New records:', records.length) + + for (let i = 0; i < records.length; i++) { + const record = records[i] + const orgName = record['Account Name'] + + // Exclude individual no account organizations from LF Members + if ( + ![ + 'Individual - No Account', + 'Individual ? No Account', + 'individual with no account', + ].includes(orgName) + ) { + record['Domain Alias'] = parseDomains(record['Domain Alias']) + + const segment = await findProjectGroupByName(qx, { + name: record['Project'], + }) + const orgId = await findOrgId(qx, record) + const row = { + organizationId: orgId, + segmentId: segment?.id, + accountName: orgName, + parentAccount: record['Parent Account'], + project: record['Project'], + productName: record['Product Name'], + purchaseHistoryName: record['Purchase History Name'], + installDate: moment(record['Install Date'], 'MM/DD/YYYY').toDate(), + usageEndDate: moment(record['Usage End Date'], 'MM/DD/YYYY').toDate(), + status: record['Status'], + priceCurrency: record['Price Currency'], + price: parseInt(record['Price'], 10), + productFamily: record['Product Family'], + tier: record['Tier'], + accountDomain: record['Account Domain'], + domainAlias: record['Domain Alias'], + } as LfxMembership + + await insertLfxMembership(qx, row) + + console.log('Inserted record:', i, orgName) + } else { + console.log('Ignored Individual - No account:', i, orgName) + } + } + + process.exit(0) + }) +} diff --git a/backend/src/bin/scripts/insightsProjects/cleanup-duplicate-insights-projects.ts b/backend/src/bin/scripts/insightsProjects/cleanup-duplicate-insights-projects.ts new file mode 100644 index 0000000000..a1502368c4 --- /dev/null +++ b/backend/src/bin/scripts/insightsProjects/cleanup-duplicate-insights-projects.ts @@ -0,0 +1,189 @@ +/* eslint-disable no-console */ + +/* eslint-disable no-continue */ + +/** + * TBD + */ +import commandLineArgs from 'command-line-args' +import commandLineUsage from 'command-line-usage' +import * as fs from 'fs' +import path from 'path' + +import { databaseInit } from '@/database/databaseConnection' +import { IRepositoryOptions } from '@/database/repositories/IRepositoryOptions' +import SequelizeRepository from '@/database/repositories/sequelizeRepository' + +const options = [ + { + name: 'help', + alias: 'h', + type: Boolean, + description: 'Print this usage guide.', + }, + { + name: 'file', + alias: 'f', + type: String, + description: 'Path to JSON file to consolidate projects from', + }, + { + name: 'dryRun', + alias: 'd', + type: Boolean, + description: + 'Dry run mode. Will not delete any projects. Will print the projects to be deleted.', + }, +] + +const sections = [ + { + header: 'Consolidate Insights Projects', + content: 'Consolidates insights projects based on the main repository URL from the CSV file.', + }, + { + header: 'Options', + optionList: options, + }, +] + +/** + * Parses a JSON file containing project information. + * @param filePath - The path to the JSON file to parse + * @returns An array of project objects containing project information from the JSON + */ +function parseJSON(filePath: string) { + const fileData = fs.readFileSync(path.resolve(filePath), 'utf-8') + return JSON.parse(fileData) +} + +async function cleanUpDuplicateProjects(qx, internalProjects, dryRun: boolean) { + let matchedCount = 0 + let deletedCount = 0 + + // Check for segmentId in related projects + for (const project of internalProjects) { + const projectToDelete = await qx.result( + `SELECT * FROM "insightsProjects" + WHERE "github" = $1 + AND "segmentId" IS NULL + AND "isLF" = false`, + [project], + ) + + if (projectToDelete.rows.length > 0) { + matchedCount++ + console.log(`Project ${projectToDelete.rows[0].name} match`) + } else { + console.log(`No match for ${project}`) + continue + } + + if (!dryRun) { + const replacementProject = await qx.result( + `SELECT * + FROM "insightsProjects" ip + WHERE ip.id != $1 + AND $2 = ANY(ip."repositories") + LIMIT 1`, + [projectToDelete.rows[0].id, project], + ) + + if (replacementProject.rows.length > 0) { + const updatedLinks = await qx.result( + ` + UPDATE "collectionsInsightsProjects" cip + SET + "insightsProjectId" = $1, + "updatedAt" = NOW() + WHERE "insightsProjectId" = $2 + AND NOT EXISTS ( + SELECT 1 + FROM "collectionsInsightsProjects" + WHERE "collectionId" = cip."collectionId" + AND "insightsProjectId" = $1 + ) + RETURNING * + `, + [replacementProject.rows[0].id, projectToDelete.rows[0].id], + ) + + if (updatedLinks.rows.length > 0) { + console.log( + `Updated collection insights project to point to replacement project ${replacementProject.rows[0].id}`, + ) + } else { + console.log(`Skipping to update links for ${projectToDelete.rows[0].name} project`) + } + + const deletedLinks = await qx.result( + `UPDATE "collectionsInsightsProjects" + SET "deletedAt" = NOW() + WHERE "insightsProjectId" = $1 AND "deletedAt" IS NULL + RETURNING *`, + [projectToDelete.rows[0].id], + ) + if (deletedLinks.rows.length > 0) { + console.log(`Deleted ${deletedLinks.rows.length} collection insights project links`) + } else { + console.log(`Skipping to delete links for ${projectToDelete.rows[0].name} project`) + } + + await qx.result( + `DELETE FROM "insightsProjects" + WHERE id = $1`, + [projectToDelete.rows[0].id], + ) + deletedCount++ + console.log(`Deleted ${projectToDelete.rows[0].name} project`) + } else { + console.log( + `Skipping ${projectToDelete.rows[0].name} project because no replacement project found`, + ) + } + } + } + + console.log(`\nSummary:`) + console.log(`- Found ${matchedCount} matching projects`) + if (!dryRun) { + console.log(`- Deleted ${deletedCount} projects`) + } else { + console.log(`- Would delete ${matchedCount} projects (dry run)`) + } +} + +const usage = commandLineUsage(sections) +const parameters = commandLineArgs(options) + +if (parameters.help || !parameters.file) { + console.log(usage) +} else { + setImmediate(async () => { + try { + const prodDb = await databaseInit() + const qx = SequelizeRepository.getQueryExecutor({ + database: prodDb, + } as IRepositoryOptions) + + // Parse JSON file + const projects = parseJSON(parameters.file) + const parsedProjects = Object.keys(projects) + .filter((project) => projects[project].internal) + .map((project) => `https://github.com/${project}`) + + console.log( + `Found ${Object.keys(projects).length} total projects in JSON and ${parsedProjects.length} are internal`, + ) + + // Consolidate projects + await cleanUpDuplicateProjects(qx, parsedProjects, parameters.dryRun || false) + + console.log('Project cleanup completed successfully') + process.exit(0) + } catch (error) { + console.error('Error during project cleanup:', error) + process.exit(1) + } + }) +} diff --git a/backend/src/bin/scripts/insightsProjects/cleanup-duplicate-repos-projects.ts b/backend/src/bin/scripts/insightsProjects/cleanup-duplicate-repos-projects.ts new file mode 100644 index 0000000000..f1aa2c674e --- /dev/null +++ b/backend/src/bin/scripts/insightsProjects/cleanup-duplicate-repos-projects.ts @@ -0,0 +1,202 @@ +/* eslint-disable no-console */ + +/* eslint-disable no-continue */ + +/** + * TBD + */ +import commandLineArgs from 'command-line-args' +import commandLineUsage from 'command-line-usage' + +import { databaseInit } from '@/database/databaseConnection' +import { IRepositoryOptions } from '@/database/repositories/IRepositoryOptions' +import SequelizeRepository from '@/database/repositories/sequelizeRepository' + +const options = [ + { + name: 'help', + alias: 'h', + type: Boolean, + description: 'Print this usage guide.', + }, + { + name: 'dryRun', + alias: 'd', + type: Boolean, + description: + 'Dry run mode. Will not delete any projects. Will print the projects to be deleted.', + }, +] + +const sections = [ + { + header: 'Consolidate Insights Projects', + content: 'Consolidates insights projects based on the main repository URL from the CSV file.', + }, + { + header: 'Options', + optionList: options, + }, +] + +async function getProjectsWithDuplicateRepos(qx) { + const result = await qx.result( + ` + with unnested_repos as ( + select + id as id, + unnest(repositories) as repo_url + from "insightsProjects" + ), + duplicate_repos as ( + select + array_agg(distinct id) as "projectIds" + from unnested_repos + group by repo_url + having count(distinct id) > 1 + and repo_url ilike '%github%' + ) + select + "projectIds" + from duplicate_repos; + `, + ) + + return result.rows +} + +async function cleanUpDuplicateProjects(qx, projects, dryRun: boolean) { + let matchedCount = 0 + let deletedCount = 0 + + // Check for segmentId in related projects + for (const project of projects) { + const result = await qx.result( + ` + WITH proj AS ( + SELECT + id, + "segmentId", + "isLF", + cardinality(repositories) AS repo_count + FROM "insightsProjects" + WHERE id IN ($1, $2) + ), + marked AS ( + SELECT + id, + -- Flag if this project matches the delete criteria: + CASE + WHEN "segmentId" IS NULL + AND "isLF" = false + AND repo_count = 1 THEN true + ELSE false + END AS to_delete + FROM proj + ) + SELECT + -- The id of the project to delete (if any) + (SELECT id FROM marked WHERE to_delete ORDER BY id LIMIT 1) AS "projectToDelete", + -- The id of the project to keep (the other one) + (SELECT id FROM marked WHERE NOT to_delete ORDER BY id LIMIT 1) AS "projectToKeep"; + `, + [project.projectIds[0], project.projectIds[1]], + ) + + if (result.rows.length > 0) { + matchedCount++ + const projectToKeep = result.rows[0].projectToKeep + const projectToDelete = result.rows[0].projectToDelete + + console.log(`Project to delete: ${projectToDelete}`) + console.log(`Project to keep: ${projectToKeep}`) + + if (!dryRun && projectToDelete) { + const updatedLinks = await qx.result( + ` + UPDATE "collectionsInsightsProjects" cip + SET + "insightsProjectId" = $1, + "updatedAt" = NOW() + WHERE "insightsProjectId" = $2 + AND NOT EXISTS ( + SELECT 1 + FROM "collectionsInsightsProjects" + WHERE "collectionId" = cip."collectionId" + AND "insightsProjectId" = $1 + ) + RETURNING * + `, + [projectToKeep, projectToDelete], + ) + + if (updatedLinks.rows.length > 0) { + console.log( + `Updated collection insights project to point to replacement project ${projectToKeep}`, + ) + } else { + console.log(`Skipping to update links for ${projectToDelete} project`) + } + + const deletedLinks = await qx.result( + `UPDATE "collectionsInsightsProjects" + SET "deletedAt" = NOW() + WHERE "insightsProjectId" = $1 AND "deletedAt" IS NULL + RETURNING *`, + [projectToDelete], + ) + if (deletedLinks.rows.length > 0) { + console.log(`Deleted ${deletedLinks.rows.length} collection insights project links`) + } else { + console.log(`Skipping to delete links for ${projectToDelete} project`) + } + + await qx.result( + `DELETE FROM "insightsProjects" + WHERE id = $1`, + [projectToDelete], + ) + deletedCount++ + console.log(`Deleted ${projectToDelete} project`) + } + } else { + console.log(`No match for ${project.projectIds[0]} and ${project.projectIds[1]}`) + continue + } + } + + console.log(`\nSummary:`) + console.log(`- Found ${matchedCount} matching projects`) + if (!dryRun) { + console.log(`- Deleted ${deletedCount} projects`) + } else { + console.log(`- Would delete ${matchedCount} projects (dry run)`) + } +} + +const usage = commandLineUsage(sections) +const parameters = commandLineArgs(options) + +if (parameters.help) { + console.log(usage) +} else { + setImmediate(async () => { + try { + const prodDb = await databaseInit() + const qx = SequelizeRepository.getQueryExecutor({ + database: prodDb, + } as IRepositoryOptions) + + const projects = await getProjectsWithDuplicateRepos(qx) + + // Consolidate projects + await cleanUpDuplicateProjects(qx, projects, parameters.dryRun || false) + + console.log('Project cleanup completed successfully') + process.exit(0) + } catch (error) { + console.error('Error during project cleanup:', error) + process.exit(1) + } + }) +} diff --git a/backend/src/bin/scripts/insightsProjects/import-pcc-projects-data.ts b/backend/src/bin/scripts/insightsProjects/import-pcc-projects-data.ts new file mode 100644 index 0000000000..5c21b6dd40 --- /dev/null +++ b/backend/src/bin/scripts/insightsProjects/import-pcc-projects-data.ts @@ -0,0 +1,155 @@ +/* eslint-disable @typescript-eslint/dot-notation */ + +/* eslint-disable no-console */ + +/* eslint-disable import/no-extraneous-dependencies */ + +/** + * Access Snowflake's instance: https://app.snowflake.com/jnmhvwd/xpb85243 + * Create a new worksheet and run the query below. + * Download the results as a CSV file. + * + * How to run this script: + * 1. Push and deploy your changes on this file if needed + * 2. cd crowd-kube/lf-production + * 3. kubectl config use-context + * 4. kubepods | grep script- + * 5. kubectl cp /local/path/report.csv ://usr/crowd/app/crowd.dev/backend/pcc_projects_data.csv + * 6. kubectl exec -it -- sh + * 7. cd crowd.dev + * 8. git status + * 9. git pull (make sure you are on the right branch) + * 10. cd backend + * 11. Make sure your csv file is in the right location and that you have all changes you need. And then run the script. + * 12. LOG_LEVEL=debug SERVICE=script ./node_modules/.bin/tsx src/bin/scripts/import-pcc-projects-data.ts --file ./pcc_projects_data.csv + * +SELECT + CASE WHEN p.NAME IN ('', 'nil') THEN NULL ELSE p.name END AS NAME, + CASE WHEN p.SLUG__C IN ('', 'nil') THEN NULL ELSE p.slug__c END AS SLUG__C, + CASE WHEN p.REPOSITORYURL__C IN ('', 'nil') THEN NULL ELSE p.REPOSITORYURL__C END AS REPOSITORYURL__C, + CASE WHEN p.DESCRIPTION__C IN ('', 'nil') THEN NULL ELSE p.DESCRIPTION__C END AS DESCRIPTION__C, + CASE WHEN p.WEBSITE__C IN ('', 'nil') THEN NULL ELSE p.WEBSITE__C END AS WEBSITE__C, + CASE WHEN p.TWITTER__C IN ('', 'nil') THEN NULL ELSE p.TWITTER__C END AS TWITTER__C, + CASE WHEN p.LINKEDIN__C IN ('', 'nil') THEN NULL ELSE p.LINKEDIN__C END AS LINKEDIN__C +FROM + FIVETRAN_INGEST.SFDC_CONNECTOR_PROD_SALESFORCE.PROJECT__C p +JOIN + FIVETRAN_INGEST.crowd_prod_public.segments s + ON p.slug__c = s.slug +WHERE + s.parentslug IS NOT NULL + AND s.grandparentslug IS NOT NULL; + +*/ +import commandLineArgs from 'command-line-args' +import commandLineUsage from 'command-line-usage' +import { parse } from 'csv-parse/sync' +import * as fs from 'fs' +import path from 'path' + +import { databaseInit } from '@/database/databaseConnection' +import { IRepositoryOptions } from '@/database/repositories/IRepositoryOptions' +import SequelizeRepository from '@/database/repositories/sequelizeRepository' + +const options = [ + { + name: 'help', + alias: 'h', + type: Boolean, + description: 'Print this usage guide.', + }, + { + name: 'file', + alias: 'f', + type: String, + description: 'Path to CSV file to import', + }, +] + +const sections = [ + { + header: 'Update Insights Projects with PCC Data', + content: 'Updates insights projects with data from PCC projects CSV export.', + }, + { + header: 'Options', + optionList: options, + }, +] + +const usage = commandLineUsage(sections) +const parameters = commandLineArgs(options) + +if (parameters.help || !parameters.file) { + console.log(usage) +} else { + setImmediate(async () => { + const prodDb = await databaseInit() + const qx = SequelizeRepository.getQueryExecutor({ + database: prodDb, + } as IRepositoryOptions) + + const fileData = fs.readFileSync(path.resolve(parameters.file), 'utf-8') + + const records = parse(fileData, { + columns: true, + skip_empty_lines: true, + }) + + console.log('Processing records:', records.length) + + let updatedCount = 0 + let notFoundCount = 0 + + for (let i = 0; i < records.length; i++) { + const record = records[i] + const slug = record['SLUG__C'] + + console.log(`Processing record ${i + 1}/${records.length}:`, slug) + + try { + // Find matching insights project + const result = await qx.result( + `UPDATE "insightsProjects" + SET + description = CASE WHEN $1 IS NOT NULL THEN $1 ELSE description END, + github = CASE WHEN $2 IS NOT NULL THEN $2 ELSE github END, + twitter = CASE WHEN $3 IS NOT NULL THEN $3 ELSE twitter END, + linkedin = CASE WHEN $4 IS NOT NULL THEN $4 ELSE linkedin END, + website = CASE WHEN $5 IS NOT NULL THEN $5 ELSE website END, + "updatedAt" = NOW() + WHERE slug = $6 + RETURNING *`, + [ + record['DESCRIPTION__C'] || null, + record['REPOSITORYURL__C'] || null, + record['TWITTER__C'] || null, + record['LINKEDIN__C'] || null, + record['WEBSITE__C'] || null, + slug, + ], + ) + + if (result > 0) { + console.log('Updated project:', slug) + updatedCount++ + } else { + console.log('No matching project found for slug:', slug) + notFoundCount++ + } + } catch (error) { + console.error('Error updating project:', slug, error) + notFoundCount++ + } + } + + console.log('\nFinal Summary:') + console.log('Total projects processed:', records.length) + console.log('Successfully updated:', updatedCount) + console.log('Not found or failed:', notFoundCount) + console.log(`Success rate: ${((updatedCount / records.length) * 100).toFixed(2)}%`) + + console.log('Processing complete') + process.exit(0) + }) +} diff --git a/backend/src/bin/scripts/merge-duplicated-members.ts b/backend/src/bin/scripts/merge-duplicated-members.ts deleted file mode 100644 index a1599e1dbe..0000000000 --- a/backend/src/bin/scripts/merge-duplicated-members.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { QueryTypes } from 'sequelize' -import { Logger, getChildLogger, getServiceLogger } from '@crowd/logging' -import { generateUUIDv1, timeout } from '@crowd/common' -import MemberRepository from '../../database/repositories/memberRepository' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import MemberService from '../../services/memberService' - -/* eslint-disable no-continue */ -/* eslint-disable @typescript-eslint/no-loop-func */ - -const log = getServiceLogger() - -function checkUsernames(allUsernames: any[]): boolean { - for (let i = 0; i < allUsernames.length; i++) { - const usernames = allUsernames[i] - for (const [platform, username] of Object.entries(usernames)) { - for (let j = i; j < allUsernames.length; j++) { - if (allUsernames[j][platform] && allUsernames[j][platform] !== username) { - return false - } - } - } - } - - return true -} - -function checkEmails(allEmails: string[][]): boolean { - for (let i = 0; i < allEmails.length; i++) { - const emails = allEmails[i] - for (let j = i; j < allEmails.length; j++) { - const emails2 = allEmails[j] - for (let k = 0; k < emails.length; k++) { - if (emails[k] !== emails2[k]) { - return false - } - } - } - } - - return true -} - -async function doMerge(data, logger: Logger) { - // merge all instances to the first one - const firstId = data.all_ids[0] - const tenantOptions = await SequelizeRepository.getDefaultIRepositoryOptions(undefined, { - id: data.tenantId, - }) - tenantOptions.log = logger - const service = new MemberService(tenantOptions) - - for (let i = 1; i < data.all_ids.length; i++) { - logger.info(`Merging ${data.all_ids[i]} into ${firstId}...`) - const id = data.all_ids[i] - await service.merge(firstId, id) - } -} - -async function check(): Promise { - let count = 0 - const dbOptions = await SequelizeRepository.getDefaultIRepositoryOptions() - - const seq = SequelizeRepository.getSequelize(dbOptions) - - log.info('Querying database for duplicated members...') - - const results = await seq.query( - ` with activity_counts as (select count(id) as count, "memberId" - from activities - group by "memberId") - select keys.platform, - m.username ->> keys.platform as username, - m."tenantId", - count(*) as duplicate_count, - coalesce(sum(ac.count), 0) as total_activitites, - json_agg(m.id) as all_ids, - jsonb_agg(m.emails) as all_emails, - jsonb_agg(m.username) as all_usernames - from members m - left join activity_counts ac on ac."memberId" = m.id, - lateral jsonb_object_keys(m.username) as keys(platform) - group by keys.platform, - m.username ->> keys.platform, - m."tenantId" - having count(*) > 1 - order by duplicate_count desc, - keys.platform, - m."tenantId";`, - { - type: QueryTypes.SELECT, - }, - ) - - log.info(`Found ${results.length} duplicated members.`) - - let jobs = 0 - - const promises = [] - - for (const [i, data] of results.entries() as any) { - log.info(`Processing ${i + 1}/${results.length}...`) - - if (data.username.toLowerCase().includes('deleted')) { - log.warn('Skipping deleted member...') - continue - } - - if (checkUsernames(data.all_usernames) && checkEmails(data.all_emails)) { - const logger = getChildLogger('merger', log, { - requestId: generateUUIDv1(), - platform: data.platform, - tenantId: data.tenantId, - username: data.username, - }) - logger.info(`Found ${data.all_ids.length} duplicated members with same usernames and emails.`) - - while (jobs >= 5) { - log.info('Waiting for job opening...') - await timeout(500) - } - - jobs++ - log.info({ jobs }, 'Job started!') - promises.push( - doMerge(data, logger) - .then(() => { - jobs-- - log.info({ jobs }, 'Job done!') - }) - .catch((err) => { - logger.error(err, { ids: data.all_ids }, 'Error while merging members!') - jobs-- - log.info({ jobs }, 'Job done with error!') - }), - ) - count++ - } else { - const logger = getChildLogger('fixer', log, { - requestId: generateUUIDv1(), - platform: data.platform, - tenantId: data.tenantId, - username: data.username, - }) - logger.info( - 'Can not automatically merge - first member in the group by joinedAt will get the identity and the rest will get them as weakIdentities.', - ) - - const options = { ...dbOptions, log: logger, currentTenant: { id: data.tenantId } } - - let transaction - try { - transaction = await SequelizeRepository.createTransaction(options) - const txOptions = { ...options, transaction } - - const allMembers = [] - for (const id of data.all_ids) { - const member = await MemberRepository.findById(id, txOptions) - allMembers.push(member) - } - - // sort so the oldest members by joinedAt are first - allMembers.sort((a, b) => new Date(a.joinedAt).getTime() - new Date(b.joinedAt).getTime()) - - // first member stays the same - it will keep the identity - // these ones will get the duplicated identity as weakIdentities - const otherMembers = allMembers.slice(1) - - for (const member of otherMembers) { - logger.info({ memberId: member.id }, 'Removing identity from member.username column!') - // let's remove this identity from the member.username column - delete member.username[data.platform] - await MemberRepository.update( - member.id, - { - username: member.username, - }, - txOptions, - ) - } - - logger.info('Adding duplicated identity to other members as weakIdentity...') - // finally let's add the duplicated identity as weakIdentity - await MemberRepository.addToWeakIdentities( - otherMembers.map((m) => m.id), - data.username, - data.platform, - txOptions, - ) - - await SequelizeRepository.commitTransaction(transaction) - count++ - } catch (err) { - logger.error(err, 'Error while merging members that can not be automatically merged!') - if (transaction) { - await SequelizeRepository.rollbackTransaction(transaction) - } - } - } - } - - await Promise.all(promises) - return count -} - -setImmediate(async () => { - log.info('Starting merge duplicated members script...') - let count = await check() - - while (count > 0) { - count = await check() - } -}) diff --git a/backend/src/bin/scripts/merge-duplicated-organizations.ts b/backend/src/bin/scripts/merge-duplicated-organizations.ts index 19902c3d09..29646d2b35 100644 --- a/backend/src/bin/scripts/merge-duplicated-organizations.ts +++ b/backend/src/bin/scripts/merge-duplicated-organizations.ts @@ -1,8 +1,11 @@ import { QueryTypes } from 'sequelize' + import { getServiceLogger } from '@crowd/logging' + +import SegmentRepository from '@/database/repositories/segmentRepository' + import SequelizeRepository from '../../database/repositories/sequelizeRepository' import OrganizationService from '../../services/organizationService' -import SegmentRepository from '@/database/repositories/segmentRepository' /* eslint-disable no-continue */ /* eslint-disable @typescript-eslint/no-loop-func */ @@ -63,7 +66,7 @@ async function mergeOrganizationsWithSameWebsite(): Promise { const primaryOrganizationId = orgInfo.organizationIds.shift() for (const orgId of orgInfo.organizationIds) { log.info(`Merging organization ${orgId} into ${primaryOrganizationId}!`) - await service.merge(primaryOrganizationId, orgId) + await service.mergeSync(primaryOrganizationId, orgId, null) } } } while (mergeableOrganizations.length > 0) diff --git a/backend/src/bin/scripts/merge-members.ts b/backend/src/bin/scripts/merge-members.ts index 00dd232dec..bb7cec12bc 100644 --- a/backend/src/bin/scripts/merge-members.ts +++ b/backend/src/bin/scripts/merge-members.ts @@ -2,10 +2,13 @@ import commandLineArgs from 'command-line-args' import commandLineUsage from 'command-line-usage' import * as fs from 'fs' import path from 'path' + +import { CommonMemberService } from '@crowd/common_services' +import { optionsQx } from '@crowd/data-access-layer' +import { MemberField, findMemberById } from '@crowd/data-access-layer/src/members' import { getServiceLogger } from '@crowd/logging' -import MemberRepository from '../../database/repositories/memberRepository' + import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import MemberService from '../../services/memberService' /* eslint-disable no-console */ @@ -63,12 +66,20 @@ if (parameters.help || !parameters.originalId || !parameters.targetId) { const targetIds = parameters.targetId.split(',') const options = await SequelizeRepository.getDefaultIRepositoryOptions() + const qx = SequelizeRepository.getQueryExecutor(options) + + const originalMember = await findMemberById(qx, originalId, [ + MemberField.ID, + MemberField.TENANT_ID, + ]) - const originalMember = await MemberRepository.findById(originalId, options, true, true, true) options.currentTenant = { id: originalMember.tenantId } for (const targetId of targetIds) { - const targetMember = await MemberRepository.findById(targetId, options, true, true, true) + const targetMember = await findMemberById(qx, targetId, [ + MemberField.ID, + MemberField.TENANT_ID, + ]) if (originalMember.tenantId !== targetMember.tenantId) { log.error( @@ -76,7 +87,7 @@ if (parameters.help || !parameters.originalId || !parameters.targetId) { ) } else { log.info(`Merging ${targetId} into ${originalId}...`) - const service = new MemberService(options) + const service = new CommonMemberService(optionsQx(options), options.temporal, log) try { await service.merge(originalId, targetId) } catch (err) { diff --git a/backend/src/bin/scripts/merge-organizations.ts b/backend/src/bin/scripts/merge-organizations.ts index a3513af201..c154199362 100644 --- a/backend/src/bin/scripts/merge-organizations.ts +++ b/backend/src/bin/scripts/merge-organizations.ts @@ -2,11 +2,14 @@ import commandLineArgs from 'command-line-args' import commandLineUsage from 'command-line-usage' import * as fs from 'fs' import path from 'path' + import { getServiceLogger } from '@crowd/logging' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' + import OrganizationRepository from '@/database/repositories/organizationRepository' import OrganizationService from '@/services/organizationService' +import SequelizeRepository from '../../database/repositories/sequelizeRepository' + /* eslint-disable no-console */ const banner = fs.readFileSync(path.join(__dirname, 'banner.txt'), 'utf8') @@ -87,7 +90,7 @@ if (parameters.help || !parameters.originalId || !parameters.toMergeId || !param log.info(`Merging ${toMergeId} into ${originalId}...`) const service = new OrganizationService(options) try { - await service.merge(originalId, toMergeId) + await service.mergeSync(originalId, toMergeId, null) } catch (err) { log.error(`Error merging organizations: ${err.message}`) process.exit(1) diff --git a/backend/src/bin/scripts/merge-similar-organizations.ts b/backend/src/bin/scripts/merge-similar-organizations.ts new file mode 100644 index 0000000000..6be6f7b54e --- /dev/null +++ b/backend/src/bin/scripts/merge-similar-organizations.ts @@ -0,0 +1,181 @@ +import commandLineArgs from 'command-line-args' +import commandLineUsage from 'command-line-usage' +import * as fs from 'fs' +import path from 'path' +import { QueryTypes } from 'sequelize' + +import { optionsQx } from '@crowd/data-access-layer' +import { addMergeAction, setMergeAction } from '@crowd/data-access-layer/src/mergeActions/repo' +import { MergeActionState, MergeActionType } from '@crowd/types' + +import { IRepositoryOptions } from '@/database/repositories/IRepositoryOptions' +import getUserContext from '@/database/utils/getUserContext' +import OrganizationService from '@/services/organizationService' +import TenantService from '@/services/tenantService' + +import SequelizeRepository from '../../database/repositories/sequelizeRepository' + +/* eslint-disable no-console */ + +const banner = fs.readFileSync(path.join(__dirname, 'banner.txt'), 'utf8') + +const options = [ + { + name: 'tenant', + alias: 't', + type: String, + description: 'The unique ID of tenant', + }, + { + name: 'allTenants', + alias: 'a', + type: Boolean, + defaultValue: false, + description: 'Set this flag to merge similar organizations for all tenants.', + }, + { + name: 'similarityThreshold', + alias: 's', + type: String, + defaultValue: false, + description: + 'Similarity threshold of organization merge suggestions. Suggestions lower than this value will not be merged. Defaults to 0.95', + }, + { + name: 'hardLimit', + alias: 'l', + type: String, + defaultValue: false, + description: `Hard limit for # of organizations that'll be merged. Mostly a flag for testing purposes.`, + }, + { + name: 'help', + alias: 'h', + type: Boolean, + description: 'Print this usage guide.', + }, +] +const sections = [ + { + content: banner, + raw: true, + }, + { + header: 'Merge organizations with similarity higher than given threshold.', + content: 'Merge organizations with similarity higher than given threshold.', + }, + { + header: 'Options', + optionList: options, + }, +] + +const usage = commandLineUsage(sections) +const parameters = commandLineArgs(options) + +if (parameters.help || (!parameters.tenant && !parameters.allTenants)) { + console.log(usage) +} else { + setImmediate(async () => { + const options = await SequelizeRepository.getDefaultIRepositoryOptions() + + let tenantIds + + if (parameters.allTenants) { + tenantIds = (await TenantService._findAndCountAllForEveryUser({})).rows.map((t) => t.id) + } else if (parameters.tenant) { + tenantIds = parameters.tenant.split(',') + } else { + tenantIds = [] + } + + for (const tenantId of tenantIds) { + const userContext: IRepositoryOptions = await getUserContext(tenantId) + const orgService = new OrganizationService(userContext) + + let hasMoreData = true + let counter = 0 + + while (hasMoreData) { + // find organization merge suggestions of tenant + const result = await options.database.sequelize.query( + ` + SELECT + "ot"."organizationId", + "ot"."toMergeId", + "ot".similarity, + "ot".status, + "org1"."displayName" AS "orgDisplayName", + "org2"."displayName" AS "mergeDisplayName" + FROM + "organizationToMerge" "ot" + LEFT JOIN + "organizations" "org1" + ON + "ot"."organizationId" = "org1"."id" + LEFT JOIN + "organizations" "org2" + ON + "ot"."toMergeId" = "org2"."id" + WHERE + ("ot".similarity > :similarityThreshold) AND + ("org1"."displayName" ilike "org2"."displayName") AND + ("org1"."tenantId" = :tenantId) AND + ("org2"."tenantId" = :tenantId) + ORDER BY + "ot".similarity DESC + LIMIT 100 + OFFSET :offset;`, + { + replacements: { + similarityThreshold: parameters.similarityThreshold || 0.95, + offset: 0, + tenantId, + }, + type: QueryTypes.SELECT, + }, + ) + + if (result.length === 0) { + hasMoreData = false + } else { + for (const row of result) { + try { + console.log( + `Merging [${row.organizationId}] "${row.orgDisplayName}" into ${row.toMergeId} "${row.mergeDisplayName}"...`, + ) + await addMergeAction( + optionsQx(userContext), + MergeActionType.ORG, + row.organizationId, + row.toMergeId, + undefined, + ) + await orgService.mergeSync(row.organizationId, row.toMergeId, null) + } catch (err) { + console.log('Error merging organizations - continuing with the rest', err) + await setMergeAction( + optionsQx(userContext), + MergeActionType.ORG, + row.organizationId, + row.toMergeId, + { + state: MergeActionState.ERROR, + }, + ) + } + + if (parameters.hardLimit && counter >= parameters.hardLimit) { + console.log(`Hard limit of ${parameters.hardLimit} reached. Exiting...`) + process.exit(0) + } + + counter += 1 + } + } + } + } + + process.exit(0) + }) +} diff --git a/backend/src/bin/scripts/process-bot-members.ts b/backend/src/bin/scripts/process-bot-members.ts new file mode 100644 index 0000000000..68cf47e862 --- /dev/null +++ b/backend/src/bin/scripts/process-bot-members.ts @@ -0,0 +1,93 @@ +import commandLineArgs from 'command-line-args' + +import { DEFAULT_TENANT_ID } from '@crowd/common' +import { fetchBotCandidateMembers, pgpQx } from '@crowd/data-access-layer' +import { getDbConnection } from '@crowd/data-access-layer/src/database' +import { chunkArray } from '@crowd/data-access-layer/src/old/apps/merge_suggestions_worker/utils' +import { getServiceLogger } from '@crowd/logging' +import { getTemporalClient } from '@crowd/temporal' + +import { DB_CONFIG, TEMPORAL_CONFIG } from '@/conf' + +const log = getServiceLogger() + +const options = [ + { + name: 'testRun', + alias: 't', + type: Boolean, + description: 'Run in test mode (limit to 10 members).', + }, + { + name: 'help', + alias: 'h', + type: Boolean, + description: 'Print this usage guide.', + }, +] + +const parameters = commandLineArgs(options) + +setImmediate(async () => { + const testRun = parameters.testRun ?? false + const BATCH_SIZE = testRun ? 10 : 100 + + const db = await getDbConnection({ + host: DB_CONFIG.readHost, + port: DB_CONFIG.port, + database: DB_CONFIG.database, + user: DB_CONFIG.username, + password: DB_CONFIG.password, + }) + + const qx = pgpQx(db) + const temporal = await getTemporalClient(TEMPORAL_CONFIG) + + log.info({ testRun, BATCH_SIZE }, 'Running script with the following parameters!') + + let botLikeMembers = [] + + do { + botLikeMembers = await fetchBotCandidateMembers(qx, BATCH_SIZE) + + const chunks = chunkArray(botLikeMembers, 10) + + for (const chunk of chunks) { + // parallel processing + await Promise.all( + chunk.map(async (memberId) => { + if (testRun) { + log.info({ memberId }, 'Triggering workflow for member!') + } + + try { + await temporal.workflow.start('processMemberBotAnalysisWithLLM', { + taskQueue: 'profiles', + workflowId: `member-bot-analysis-with-llm/${memberId}`, + retry: { + maximumAttempts: 10, + }, + args: [{ memberId }], + searchAttributes: { + TenantId: [DEFAULT_TENANT_ID], + }, + }) + + // wait till the workflow is finished + await temporal.workflow.result(`member-bot-analysis-with-llm/${memberId}`) + } catch (err) { + log.error({ memberId, err }, 'Failed to trigger workflow for member!') + throw err + } + }), + ) + } + + if (testRun) { + log.info('Test run - stopping after first batch!') + break + } + } while (botLikeMembers.length > 0) + + process.exit(0) +}) diff --git a/backend/src/bin/scripts/process-integration.ts b/backend/src/bin/scripts/process-integration.ts deleted file mode 100644 index 499571d6bf..0000000000 --- a/backend/src/bin/scripts/process-integration.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { processPaginated, singleOrDefault } from '@crowd/common' -import { INTEGRATION_SERVICES } from '@crowd/integrations' -import { getServiceLogger } from '@crowd/logging' -import commandLineArgs from 'command-line-args' -import commandLineUsage from 'command-line-usage' -import * as fs from 'fs' -import path from 'path' -import { IntegrationRunState } from '@crowd/types' -import IntegrationRepository from '../../database/repositories/integrationRepository' -import IntegrationRunRepository from '../../database/repositories/integrationRunRepository' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import { getIntegrationRunWorkerEmitter } from '../../serverless/utils/serviceSQS' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import { NodeWorkerIntegrationProcessMessage } from '../../types/mq/nodeWorkerIntegrationProcessMessage' - -/* eslint-disable no-console */ - -const banner = fs.readFileSync(path.join(__dirname, 'banner.txt'), 'utf8') - -const log = getServiceLogger() - -const options = [ - { - name: 'integration', - alias: 'i', - typeLabel: '{underline integrationId}', - type: String, - description: - 'The unique ID of integration that you would like to process. Use comma delimiter when sending multiple integrations.', - }, - { - name: 'onboarding', - alias: 'o', - description: 'Process integration as if it was onboarding.', - type: Boolean, - defaultValue: false, - }, - { - name: 'disableFiringCrowdWebhooks', - alias: 'd', - typeLabel: '{underline disableFiringCrowdWebhooks}', - type: Boolean, - defaultOption: false, - description: 'Should it disable firing outgoing crowd webhooks?', - }, - { - name: 'platform', - alias: 'p', - description: 'The platform for which we should run all integrations.', - }, - { - name: 'help', - alias: 'h', - type: Boolean, - description: 'Print this usage guide.', - }, -] -const sections = [ - { - content: banner, - raw: true, - }, - { - header: 'Process Integration', - content: 'Trigger processing of integrations.', - }, - { - header: 'Options', - optionList: options, - }, -] - -const usage = commandLineUsage(sections) -const parameters = commandLineArgs(options) - -const triggerIntegrationRun = async ( - runRepo: IntegrationRunRepository, - tenantId: string, - integrationId: string, - onboarding: boolean, - fireCrowdWebhooks: boolean, -) => { - const existingRun = await runRepo.findLastProcessingRun(integrationId) - - if (existingRun && existingRun.onboarding) { - log.error('Integration is already processing, skipping!') - return - } - - log.info( - { integrationId, onboarding }, - 'Integration found - creating a new run in the old framework!', - ) - const run = await runRepo.create({ - integrationId, - tenantId, - onboarding, - state: IntegrationRunState.PENDING, - }) - - log.info( - { integrationId, onboarding }, - 'Triggering SQS message for the old framework integration!', - ) - await sendNodeWorkerMessage( - tenantId, - new NodeWorkerIntegrationProcessMessage(run.id, null, fireCrowdWebhooks), - ) -} - -const triggerNewIntegrationRun = async ( - tenantId: string, - integrationId: string, - platform: string, - onboarding: boolean, -) => { - log.info( - { integrationId, onboarding }, - 'Triggering SQS message for the new framework integration!', - ) - - const emitter = await getIntegrationRunWorkerEmitter() - await emitter.triggerIntegrationRun(tenantId, platform, integrationId, onboarding) -} - -if (parameters.help || (!parameters.integration && !parameters.platform)) { - console.log(usage) -} else { - setImmediate(async () => { - const onboarding = parameters.onboarding - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - - const fireCrowdWebhooks = !parameters.disableFiringCrowdWebhooks - - const runRepo = new IntegrationRunRepository(options) - - if (parameters.platform) { - let inNewFramework = false - - if (singleOrDefault(INTEGRATION_SERVICES, (s) => s.type === parameters.platform)) { - inNewFramework = true - } - - await processPaginated( - async (page) => IntegrationRepository.findAllActive(parameters.platform, page, 10), - async (integrations) => { - for (const i of integrations) { - const integration = i as any - - if (inNewFramework) { - await triggerNewIntegrationRun( - integration.tenantId, - integration.id, - integration.platform, - onboarding, - ) - } else { - await triggerIntegrationRun( - runRepo, - integration.tenantId, - integration.id, - onboarding, - fireCrowdWebhooks, - ) - } - } - }, - ) - } else { - const integrationIds = parameters.integration.split(',') - for (const integrationId of integrationIds) { - const integration = await options.database.integration.findOne({ - where: { id: integrationId }, - }) - - if (!integration) { - log.error({ integrationId }, 'Integration not found!') - process.exit(1) - } else { - log.info({ integrationId, onboarding }, 'Integration found - triggering SQS message!') - - let inNewFramework = false - - if (singleOrDefault(INTEGRATION_SERVICES, (s) => s.type === integration.platform)) { - inNewFramework = true - } - - if (inNewFramework) { - await triggerNewIntegrationRun( - integration.tenantId, - integration.id, - integration.platform, - onboarding, - ) - } else { - await triggerIntegrationRun( - runRepo, - integration.tenantId, - integration.id, - onboarding, - fireCrowdWebhooks, - ) - } - } - } - } - - process.exit(0) - }) -} diff --git a/backend/src/bin/scripts/process-stream.ts b/backend/src/bin/scripts/process-stream.ts deleted file mode 100644 index a207c14e6f..0000000000 --- a/backend/src/bin/scripts/process-stream.ts +++ /dev/null @@ -1,82 +0,0 @@ -import commandLineArgs from 'command-line-args' -import commandLineUsage from 'command-line-usage' -import * as fs from 'fs' -import path from 'path' -import { getServiceLogger } from '@crowd/logging' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import { NodeWorkerIntegrationProcessMessage } from '../../types/mq/nodeWorkerIntegrationProcessMessage' -import IntegrationRunRepository from '../../database/repositories/integrationRunRepository' -import IntegrationStreamRepository from '../../database/repositories/integrationStreamRepository' - -/* eslint-disable no-console */ - -const banner = fs.readFileSync(path.join(__dirname, 'banner.txt'), 'utf8') - -const log = getServiceLogger() - -const options = [ - { - name: 'stream', - alias: 's', - typeLabel: '{underline streamId}', - type: String, - description: - 'The unique ID of integration stream that you would like to process. Use comma delimiter when sending multiple integration streams.', - }, - { - name: 'help', - alias: 'h', - type: Boolean, - description: 'Print this usage guide.', - }, -] -const sections = [ - { - content: banner, - raw: true, - }, - { - header: 'Process integration stream', - content: 'Trigger processing of integration stream.', - }, - { - header: 'Options', - optionList: options, - }, -] - -const usage = commandLineUsage(sections) -const parameters = commandLineArgs(options) - -if (parameters.help && !parameters.stream) { - console.log(usage) -} else { - setImmediate(async () => { - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - - const streamRepo = new IntegrationStreamRepository(options) - const runRepo = new IntegrationRunRepository(options) - - const streamIds = parameters.stream.split(',') - for (const streamId of streamIds) { - const stream = await streamRepo.findById(streamId) - - if (!stream) { - log.error({ streamId }, 'Integration stream not found!') - process.exit(1) - } else { - log.info({ streamId }, 'Integration stream found! Triggering SQS message!') - - const run = await runRepo.findById(stream.runId) - - await sendNodeWorkerMessage( - run.tenantId, - new NodeWorkerIntegrationProcessMessage(run.id, stream.id), - ) - } - } - - process.exit(0) - }) -} diff --git a/backend/src/bin/scripts/process-webhook.ts b/backend/src/bin/scripts/process-webhook.ts deleted file mode 100644 index 8026b15b9a..0000000000 --- a/backend/src/bin/scripts/process-webhook.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { getServiceLogger } from '@crowd/logging' -import { getRedisClient } from '@crowd/redis' -import commandLineArgs from 'command-line-args' -import commandLineUsage from 'command-line-usage' -import * as fs from 'fs' -import path from 'path' -import { QueryTypes } from 'sequelize' -import { IntegrationProcessor } from '@/serverless/integrations/services/integrationProcessor' -import { REDIS_CONFIG } from '../../conf' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' - -/* eslint-disable no-console */ - -const banner = fs.readFileSync(path.join(__dirname, 'banner.txt'), 'utf8') - -const log = getServiceLogger() - -const options = [ - { - name: 'webhook', - alias: 'w', - typeLabel: '{underline webhookId}', - type: String, - description: - 'The unique ID of webhook that you would like to process. Use comma delimiter when sending multiple webhooks.', - }, - { - name: 'tenant', - alias: 't', - typeLabel: '{underline tenantId}', - type: String, - description: - 'The unique ID of tenant that you would like to process. Use in combination with type.', - }, - { - name: 'type', - alias: 'p', - typeLabel: '{underline type}', - type: String, - description: 'The webhook type to process. Use in combination with tenant.', - }, - { - name: 'help', - alias: 'h', - type: Boolean, - description: 'Print this usage guide.', - }, -] -const sections = [ - { - content: banner, - raw: true, - }, - { - header: 'Process Webhook', - content: 'Trigger processing of webhooks.', - }, - { - header: 'Options', - optionList: options, - }, -] - -const usage = commandLineUsage(sections) -const parameters = commandLineArgs(options) - -if (parameters.help || (!parameters.webhook && (!parameters.tenant || !parameters.type))) { - console.log(usage) -} else { - setImmediate(async () => { - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - const redisEmitter = await getRedisClient(REDIS_CONFIG) - const integrationProcessorInstance = new IntegrationProcessor(options, redisEmitter) - - if (parameters.webhook) { - const webhookIds = parameters.webhook.split(',') - - for (const webhookId of webhookIds) { - log.info({ webhookId }, 'Webhook found - processing!') - await integrationProcessorInstance.processWebhook(webhookId, true, true) - } - } else if (parameters.tenant && parameters.type) { - const seq = SequelizeRepository.getSequelize(options) - - let ids = ( - await seq.query( - ` - select id from "incomingWebhooks" - where state in ('PENDING', 'ERROR') - and "tenantId" = :tenantId and type = :type - order by id - limit 100 - `, - { - type: QueryTypes.SELECT, - replacements: { - tenantId: parameters.tenant, - type: parameters.type, - }, - }, - ) - ).map((r) => (r as any).id) - - while (ids.length > 0) { - for (const webhookId of ids) { - log.info({ webhookId }, 'Webhook found - processing!') - await integrationProcessorInstance.processWebhook(webhookId, true, true) - } - - ids = ( - await seq.query( - ` - select id from "incomingWebhooks" - where state in ('PENDING', 'ERROR') - and "tenantId" = :tenantId and type = :type - and id > :id - order by id - limit 100 - `, - { - type: QueryTypes.SELECT, - replacements: { - tenantId: parameters.tenant, - type: parameters.type, - id: ids[ids.length - 1], - }, - }, - ) - ).map((r) => (r as any).id) - } - } - - process.exit(0) - }) -} diff --git a/backend/src/bin/scripts/refresh-github-repo-settings.ts b/backend/src/bin/scripts/refresh-github-repo-settings.ts new file mode 100644 index 0000000000..97e729c3dd --- /dev/null +++ b/backend/src/bin/scripts/refresh-github-repo-settings.ts @@ -0,0 +1,22 @@ +import { getServiceChildLogger } from '@crowd/logging' + +import { refreshGithubRepoSettings } from '../jobs/refreshGithubRepoSettings' + +const logger = getServiceChildLogger('refreshGithubRepoSettings') + +setImmediate(async () => { + try { + const startTime = Date.now() + logger.info('Starting refresh of Github repo settings') + + await refreshGithubRepoSettings() + + const duration = Date.now() - startTime + logger.info(`Completed refresh of Github repo settings in ${duration}ms`) + + process.exit(0) + } catch (error) { + logger.error(`Error refreshing Github repo settings: ${error.message}`) + process.exit(1) + } +}) diff --git a/backend/src/bin/scripts/send-weekly-analytics-email.ts b/backend/src/bin/scripts/send-weekly-analytics-email.ts deleted file mode 100644 index 3ba2d50bf0..0000000000 --- a/backend/src/bin/scripts/send-weekly-analytics-email.ts +++ /dev/null @@ -1,114 +0,0 @@ -import commandLineArgs from 'command-line-args' -import commandLineUsage from 'command-line-usage' -import * as fs from 'fs' -import moment from 'moment' -import path from 'path' -import { getServiceLogger } from '@crowd/logging' -import { timeout } from '@crowd/common' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import { NodeWorkerMessageType } from '../../serverless/types/workerTypes' -import { NodeWorkerMessageBase } from '../../types/mq/nodeWorkerMessageBase' -import RecurringEmailsHistoryRepository from '../../database/repositories/recurringEmailsHistoryRepository' -import { RecurringEmailType } from '../../types/recurringEmailsHistoryTypes' -import TenantService from '@/services/tenantService' - -/* eslint-disable no-console */ - -const banner = fs.readFileSync(path.join(__dirname, 'banner.txt'), 'utf8') - -const log = getServiceLogger() - -const options = [ - { - name: 'tenant', - alias: 't', - type: String, - description: 'The unique ID of tenant that you would like to send weekly emails to.', - }, - { - name: 'sendToAllTenants', - alias: 'a', - type: Boolean, - defaultValue: false, - description: - 'Set this flag to send the analytics e-mails to all tenants. Tenants that already got a weekly analytics e-mail for the previous week will be discarded.', - }, - { - name: 'help', - alias: 'h', - type: Boolean, - description: 'Print this usage guide.', - }, -] -const sections = [ - { - content: banner, - raw: true, - }, - { - header: 'Send weekly analytics email to given tenant.', - content: - 'Sends weekly analytics email to given tenant. The daterange will be from previous week.', - }, - { - header: 'Options', - optionList: options, - }, -] - -const usage = commandLineUsage(sections) -const parameters = commandLineArgs(options) - -if (parameters.help || (!parameters.tenant && !parameters.sendToAllTenants)) { - console.log(usage) -} else { - setImmediate(async () => { - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - - let tenantIds - - if (parameters.sendToAllTenants) { - tenantIds = (await TenantService._findAndCountAllForEveryUser({})).rows.map((t) => t.id) - } else if (parameters.tenant) { - tenantIds = parameters.tenant.split(',') - } else { - tenantIds = [] - } - - const weekOfYear = moment().utc().startOf('isoWeek').subtract(7, 'days').isoWeek().toString() - const rehRepository = new RecurringEmailsHistoryRepository(options) - - for (const tenantId of tenantIds) { - const tenant = await options.database.tenant.findByPk(tenantId) - const isEmailAlreadySent = - (await rehRepository.findByWeekOfYear( - tenantId, - weekOfYear, - RecurringEmailType.WEEKLY_ANALYTICS, - )) !== null - - if (!tenant) { - log.error({ tenantId }, 'Tenant not found! Skipping.') - } else if (isEmailAlreadySent) { - log.info( - { tenantId }, - 'Analytics email for this week is already sent to this tenant. Skipping.', - ) - } else { - log.info({ tenantId }, `Tenant found - sending weekly email message!`) - await sendNodeWorkerMessage(tenant.id, { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - tenant: tenant.id, - service: 'weekly-analytics-emails', - } as NodeWorkerMessageBase) - - if (tenantIds.length > 1) { - await timeout(1000) - } - } - } - - process.exit(0) - }) -} diff --git a/backend/src/bin/scripts/trigger-webhook.ts b/backend/src/bin/scripts/trigger-webhook.ts deleted file mode 100644 index 8490dec005..0000000000 --- a/backend/src/bin/scripts/trigger-webhook.ts +++ /dev/null @@ -1,167 +0,0 @@ -import commandLineArgs from 'command-line-args' -import commandLineUsage from 'command-line-usage' -import * as fs from 'fs' -import path from 'path' -import { getServiceLogger } from '@crowd/logging' -import { timeout } from '@crowd/common' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import IncomingWebhookRepository from '../../database/repositories/incomingWebhookRepository' -import { WebhookState, WebhookType } from '../../types/webhooks' -import { NodeWorkerProcessWebhookMessage } from '../../types/mq/nodeWorkerProcessWebhookMessage' -import { sqs, getCurrentQueueSize } from '../../services/aws' -import { WebhookProcessor } from '../../serverless/integrations/services/webhookProcessor' -import { SQS_CONFIG } from '../../conf' - -/* eslint-disable no-console */ - -const banner = fs.readFileSync(path.join(__dirname, 'banner.txt'), 'utf8') - -const log = getServiceLogger() - -const options = [ - { - name: 'webhook', - alias: 'w', - typeLabel: '{underline webhookId}', - type: String, - description: - 'The unique ID of webhook that you would like to process. Use comma delimiter when sending multiple webhooks.', - }, - { - name: 'force', - alias: 'f', - typeLabel: '{underline force}', - type: Boolean, - defaultOption: false, - description: 'Force processing of webhooks.', - }, - { - name: 'processPlatformErrors', - alias: 'p', - typeLabel: '{underline processPlatformErrors}', - type: String, - defaultOption: false, - description: `Retry error state webhooks in specified platform. Currently supported: ['github', 'discord'].`, - }, - { - name: 'help', - alias: 'h', - type: Boolean, - description: 'Print this usage guide.', - }, -] -const sections = [ - { - content: banner, - raw: true, - }, - { - header: 'Process Webhook', - content: 'Trigger processing of webhooks.', - }, - { - header: 'Options', - optionList: options, - }, -] - -const usage = commandLineUsage(sections) -const parameters = commandLineArgs(options) - -if (parameters.help || (!parameters.webhook && !parameters.processPlatformErrors)) { - console.log(usage) -} else { - setImmediate(async () => { - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - const repo = new IncomingWebhookRepository(options) - let currentPage = 1 - const PROCESS_QUEUE_THRESHOLD = 100 - const PAGE_SIZE = 1000 - - if (parameters.processPlatformErrors) { - let webhookType: WebhookType - - if (parameters.processPlatformErrors === 'github') { - webhookType = WebhookType.GITHUB - } else if (parameters.processPlatformErrors === 'discord') { - webhookType = WebhookType.DISCORD - } else { - console.log(usage) - process.exit(0) - } - - log.debug('Processing error state webhooks!') - let webhooks = await repo.findError( - currentPage, - PAGE_SIZE, - WebhookProcessor.MAX_RETRY_LIMIT, - webhookType, - ) - - log.info(webhooks.map((w) => w.id)) - - while (webhooks.length > 0) { - for (const webhook of webhooks) { - log.info({ webhook }, 'Webhook found - triggering SQS message!') - await sendNodeWorkerMessage( - webhook.tenantId, - new NodeWorkerProcessWebhookMessage(webhook.tenantId, webhook.id, true, false), - ) - } - - // no need to process further if result isn't same as page size - if (webhooks.length < PAGE_SIZE) { - log.info('Finished processing.') - break - } - - let queueSize = await getCurrentQueueSize(sqs, SQS_CONFIG.nodejsWorkerQueue) - - // ensure queueSize before sending new page message - while (queueSize > PROCESS_QUEUE_THRESHOLD) { - log.info( - `Queue size(${queueSize}) is bigger than threshold(${PROCESS_QUEUE_THRESHOLD}), waiting 30 seconds before retrying.`, - ) - await timeout(30000) - queueSize = await getCurrentQueueSize(sqs, SQS_CONFIG.nodejsWorkerQueue) - } - - currentPage += 1 - - log.info( - `Queue size(${queueSize}) below threshold(${PROCESS_QUEUE_THRESHOLD}) - Continuing with page${currentPage}`, - ) - - webhooks = await repo.findError( - currentPage, - PAGE_SIZE, - WebhookProcessor.MAX_RETRY_LIMIT, - webhookType, - ) - } - } else { - const webhookIds = parameters.webhook.split(',') - - for (const webhookId of webhookIds) { - const webhook = await repo.findById(webhookId) - - if (!webhook) { - log.error({ webhookId }, 'Webhook not found!') - process.exit(1) - } else if (!parameters.force && webhook.state !== WebhookState.PENDING) { - log.error({ webhookId }, 'Webhook is not in pending state!') - process.exit(1) - } else { - log.info({ webhookId }, 'Webhook found - triggering SQS message!') - await sendNodeWorkerMessage( - webhook.tenantId, - new NodeWorkerProcessWebhookMessage(webhook.tenantId, webhook.id, parameters.force), - ) - } - } - } - - process.exit(0) - }) -} diff --git a/backend/src/bin/scripts/unleash-init.ts b/backend/src/bin/scripts/unleash-init.ts deleted file mode 100644 index 61af560df7..0000000000 --- a/backend/src/bin/scripts/unleash-init.ts +++ /dev/null @@ -1,392 +0,0 @@ -import Sequelize, { QueryTypes } from 'sequelize' -import { getServiceLogger } from '@crowd/logging' -import { generateUUIDv1 } from '@crowd/common' -import { UnleashContextField } from '../../types/unleashContext' -import { UNLEASH_CONFIG } from '../../conf' -import Plans from '../../security/plans' -import { FeatureFlag } from '../../types/common' -import { PLAN_LIMITS } from '../../feature-flags/isFeatureEnabled' - -/* eslint-disable no-console */ - -const log = getServiceLogger() - -const constaintConfiguration = { - [FeatureFlag.AUTOMATIONS]: [ - [ - { - values: [Plans.values.scale], - inverted: false, - operator: 'IN', - contextName: 'plan', - caseInsensitive: false, - }, - { - value: PLAN_LIMITS[Plans.values.scale][FeatureFlag.AUTOMATIONS].toString(), - values: [], - inverted: false, - operator: 'NUM_LT', - contextName: 'automationCount', - caseInsensitive: false, - }, - ], - [ - { - values: [Plans.values.growth], - inverted: false, - operator: 'IN', - contextName: 'plan', - caseInsensitive: false, - }, - { - value: PLAN_LIMITS[Plans.values.growth][FeatureFlag.AUTOMATIONS].toString(), - values: [], - inverted: false, - operator: 'NUM_LT', - contextName: 'automationCount', - caseInsensitive: false, - }, - ], - [ - { - values: [Plans.values.essential], - inverted: false, - operator: 'IN', - contextName: 'plan', - caseInsensitive: false, - }, - { - value: PLAN_LIMITS[Plans.values.essential][FeatureFlag.AUTOMATIONS].toString(), - values: [], - inverted: false, - operator: 'NUM_LT', - contextName: 'automationCount', - caseInsensitive: false, - }, - ], - ], - [FeatureFlag.CSV_EXPORT]: [ - [ - { - values: [Plans.values.scale], - inverted: false, - operator: 'IN', - contextName: 'plan', - caseInsensitive: false, - }, - { - value: PLAN_LIMITS[Plans.values.scale][FeatureFlag.CSV_EXPORT].toString(), - values: [], - inverted: false, - operator: 'NUM_LT', - contextName: 'csvExportCount', - caseInsensitive: false, - }, - ], - [ - { - values: [Plans.values.growth], - inverted: false, - operator: 'IN', - contextName: 'plan', - caseInsensitive: false, - }, - { - value: PLAN_LIMITS[Plans.values.growth][FeatureFlag.CSV_EXPORT].toString(), - values: [], - inverted: false, - operator: 'NUM_LT', - contextName: 'csvExportCount', - caseInsensitive: false, - }, - ], - [ - { - values: [Plans.values.essential], - inverted: false, - operator: 'IN', - contextName: 'plan', - caseInsensitive: false, - }, - { - value: PLAN_LIMITS[Plans.values.essential][FeatureFlag.CSV_EXPORT].toString(), - values: [], - inverted: false, - operator: 'NUM_LT', - contextName: 'csvExportCount', - caseInsensitive: false, - }, - ], - ], - [FeatureFlag.EAGLE_EYE]: [ - [ - { - values: [Plans.values.growth, Plans.values.eagleEye, Plans.values.scale], - inverted: false, - operator: 'IN', - contextName: 'plan', - caseInsensitive: false, - }, - ], - ], - [FeatureFlag.LINKEDIN]: [ - [ - { - values: [Plans.values.growth, Plans.values.scale], - inverted: false, - operator: 'IN', - contextName: 'plan', - caseInsensitive: false, - }, - ], - ], - [FeatureFlag.HUBSPOT]: [ - [ - { - values: [Plans.values.scale], - inverted: false, - operator: 'IN', - contextName: 'plan', - caseInsensitive: false, - }, - ], - ], - [FeatureFlag.MEMBER_ENRICHMENT]: [ - [ - { - values: [Plans.values.scale], - inverted: false, - operator: 'IN', - contextName: 'plan', - caseInsensitive: false, - }, - { - value: PLAN_LIMITS[Plans.values.scale][FeatureFlag.MEMBER_ENRICHMENT].toString(), - values: [], - inverted: false, - operator: 'NUM_LT', - contextName: 'memberEnrichmentCount', - caseInsensitive: false, - }, - ], - [ - { - values: [Plans.values.growth], - inverted: false, - operator: 'IN', - contextName: 'plan', - caseInsensitive: false, - }, - { - value: PLAN_LIMITS[Plans.values.growth][FeatureFlag.MEMBER_ENRICHMENT].toString(), - values: [], - inverted: false, - operator: 'NUM_LT', - contextName: 'memberEnrichmentCount', - caseInsensitive: false, - }, - ], - ], - [FeatureFlag.ORGANIZATION_ENRICHMENT]: [ - [ - { - values: [Plans.values.scale], - inverted: false, - operator: 'IN', - contextName: 'plan', - caseInsensitive: false, - }, - { - value: PLAN_LIMITS[Plans.values.scale][FeatureFlag.ORGANIZATION_ENRICHMENT].toString(), - values: [], - inverted: false, - operator: 'NUM_LT', - contextName: 'organizationEnrichmentCount', - caseInsensitive: false, - }, - ], - [ - { - values: [Plans.values.growth], - inverted: false, - operator: 'IN', - contextName: 'plan', - caseInsensitive: false, - }, - { - value: PLAN_LIMITS[Plans.values.growth][FeatureFlag.ORGANIZATION_ENRICHMENT].toString(), - values: [], - inverted: false, - operator: 'NUM_LT', - contextName: 'organizationEnrichmentCount', - caseInsensitive: false, - }, - ], - ], - [FeatureFlag.SEGMENTS]: [], -} - -let seq: any - -setImmediate(async () => { - seq = new (Sequelize)( - UNLEASH_CONFIG.db.database, - UNLEASH_CONFIG.db.username, - UNLEASH_CONFIG.db.password, - { - dialect: 'postgres', - port: UNLEASH_CONFIG.db.port, - replication: { - read: [{ host: UNLEASH_CONFIG.db.host }], - write: { host: UNLEASH_CONFIG.db.host }, - }, - logging: false, - }, - ) - - await createApiToken(UNLEASH_CONFIG.adminApiKey, 'admin-token', 'admin') - await createApiToken(UNLEASH_CONFIG.frontendApiKey, 'frontend-token', 'frontend') - await createApiToken(UNLEASH_CONFIG.backendApiKey, 'backend-token', 'client') - - const allContextFields = Object.values(UnleashContextField) - for (const field of allContextFields) { - await createContextField(field) - } - - const allFeatureFlags = Object.values(FeatureFlag) - for (const flag of allFeatureFlags) { - await createFeatureFlag(flag) - await createStrategy(flag, constaintConfiguration[flag]) - } - - process.exit(0) -}) - -async function createApiToken(token: string, name: string, type: string): Promise { - const results = await seq.query( - 'select * from api_tokens where secret = :token and type = :type and username = :name;', - { - replacements: { - token, - name, - type, - }, - type: QueryTypes.SELECT, - }, - ) - if (results.length === 0) { - log.info(`${name} token not found - creating...`) - await seq.query( - `insert into api_tokens(secret, username, type, environment) values (:token, :name, :type, 'production')`, - { - replacements: { - token, - name, - type, - }, - type: QueryTypes.INSERT, - }, - ) - } else { - log.info(`${name} token found!`) - } -} - -async function createContextField(field: string): Promise { - const results = await seq.query(`select * from context_fields where name = :field`, { - replacements: { - field, - }, - type: QueryTypes.SELECT, - }) - - if (results.length === 0) { - log.info(`Context field ${field} not found - creating...`) - await seq.query(`insert into context_fields(name, stickiness) values (:field, true)`, { - replacements: { - field, - }, - type: QueryTypes.INSERT, - }) - } else { - log.info(`Context field ${field} found!`) - } -} - -async function createFeatureFlag(flag: FeatureFlag): Promise { - const results = await seq.query( - `select * from features where name = :flag and type = 'permission'`, - { - replacements: { - flag, - }, - type: QueryTypes.SELECT, - }, - ) - - if (results.length === 0) { - log.info(`Feature flag ${flag} not found - creating...`) - await seq.query( - `insert into features(name, description, type) values (:flag, '', 'permission')`, - { - replacements: { - flag, - }, - type: QueryTypes.INSERT, - }, - ) - await seq.query( - `insert into feature_environments(environment, feature_name, enabled) values ('production', :flag, true)`, - { - replacements: { - flag, - }, - type: QueryTypes.INSERT, - }, - ) - } else { - log.info(`Feature flag ${flag} found!`) - } -} - -async function createStrategy(flag: FeatureFlag, constraints: any[]): Promise { - const results = await seq.query( - `select * from feature_strategies where feature_name = :flag and project_name = 'default' and environment = 'production' and strategy_name = 'default'`, - { - replacements: { - flag, - }, - type: QueryTypes.SELECT, - }, - ) - - if (results.length > 0) { - log.warn(`Feature flag ${flag} constraints found - re-creating...`) - await seq.query( - `delete from feature_strategies where feature_name = :flag and project_name = 'default' and environment = 'production' and strategy_name = 'default'`, - { - replacements: { - flag, - }, - type: QueryTypes.DELETE, - }, - ) - } - - log.info(`Feature flag ${flag} constraints not found - creating...`) - - for (const constraint of constraints) { - const id = generateUUIDv1() - await seq.query( - `insert into feature_strategies(id, feature_name, project_name, environment, strategy_name, constraints) values (:id, :flag, 'default', 'production', 'default', :constraint)`, - { - replacements: { - flag, - id, - constraint: JSON.stringify(constraint), - }, - type: QueryTypes.INSERT, - }, - ) - } -} diff --git a/backend/src/database/migrations/U1670239828__tenantPlanUpdates.sql b/backend/src/bin/tmp/.gitkeep similarity index 100% rename from backend/src/database/migrations/U1670239828__tenantPlanUpdates.sql rename to backend/src/bin/tmp/.gitkeep diff --git a/backend/src/bin/worker/integrations.ts b/backend/src/bin/worker/integrations.ts deleted file mode 100644 index 387de96fa8..0000000000 --- a/backend/src/bin/worker/integrations.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { getRedisClient } from '@crowd/redis' -import { Logger } from '@crowd/logging' -import { REDIS_CONFIG } from '../../conf' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import { IntegrationProcessor } from '../../serverless/integrations/services/integrationProcessor' -import { IServiceOptions } from '../../services/IServiceOptions' -import { NodeWorkerIntegrationProcessMessage } from '../../types/mq/nodeWorkerIntegrationProcessMessage' -import { NodeWorkerProcessWebhookMessage } from '../../types/mq/nodeWorkerProcessWebhookMessage' - -let integrationProcessorInstance: IntegrationProcessor - -async function getIntegrationProcessor(logger: Logger): Promise { - if (integrationProcessorInstance) return integrationProcessorInstance - - const options: IServiceOptions = { - ...(await SequelizeRepository.getDefaultIRepositoryOptions()), - log: logger, - } - - const redisEmitter = await getRedisClient(REDIS_CONFIG) - - integrationProcessorInstance = new IntegrationProcessor(options, redisEmitter) - - return integrationProcessorInstance -} - -export const processIntegration = async ( - msg: NodeWorkerIntegrationProcessMessage, - messageLogger: Logger, -): Promise => { - const processor = await getIntegrationProcessor(messageLogger) - await processor.process(msg) -} - -export const processWebhook = async ( - msg: NodeWorkerProcessWebhookMessage, - messageLogger: Logger, -): Promise => { - const processor = await getIntegrationProcessor(messageLogger) - await processor.processWebhook(msg.webhookId, msg.force, msg.fireCrowdWebhooks) -} diff --git a/backend/src/conf/configTypes.ts b/backend/src/conf/configTypes.ts index 8b4dd7deaa..68df3b898a 100644 --- a/backend/src/conf/configTypes.ts +++ b/backend/src/conf/configTypes.ts @@ -1,6 +1,5 @@ export enum ServiceType { API = 'api', - NODEJS_WORKER = 'nodejs-worker', JOB_GENERATOR = 'job-generator', } @@ -11,22 +10,12 @@ export enum TenantMode { } export interface AwsCredentials { - accountId: string + endpoint: string accessKeyId: string secretAccessKey: string region: string } -export interface SQSConfiguration { - host?: string - port?: number - nodejsWorkerQueue: string - nodejsWorkerDelayableQueue: string - integrationRunWorkerQueue: string - pythonWorkerQueue: string - aws: AwsCredentials -} - export interface S3Configuration { host?: string port?: number @@ -52,8 +41,6 @@ export interface DbConfiguration { password?: string apiUsername?: string apiPassword?: string - nodejsWorkerUsername?: string - nodejsWorkerPassword?: string jobGeneratorUsername?: string jobGeneratorPassword?: string @@ -81,17 +68,14 @@ export interface ApiConfiguration { export interface Auth0Configuration { clientId: string jwks: string + issuerBaseURLs: string + audience: string } -export interface PlansConfiguration { - stripePricePremium: string - stripePriceEnterprise: string - stripeSecretKey: string - stripWebhookSigningSecret: string - stripeEagleEyePlanProductId: string - stripeGrowthPlanProductId: string +export interface SSOConfiguration { + crowdTenantId: string + lfTenantId: string } - export interface DevtoConfiguration { globalLimit?: number } @@ -117,11 +101,6 @@ export interface SlackConfiguration { appToken?: string } -export interface SlackNotifierConfiguration { - clientId: string - clientSecret: string -} - export interface GoogleConfiguration { clientId: string clientSecret: string @@ -143,36 +122,11 @@ export interface GithubConfiguration { privateKey: string webhookSecret: string isCommitDataEnabled: string + isSnowflakeEnabled: string globalLimit?: number callbackUrl: string } -export interface SendgridConfiguration { - key: string - webhookSigningSecret: string - emailFrom: string - nameFrom: string - templateEmailAddressVerification: string - templateInvitation: string - templatePasswordReset: string - templateWeeklyAnalytics: string - templateIntegrationDone: string - templateCsvExport: string - templateEagleEyeDigest: string - weeklyAnalyticsUnsubscribeGroupId: string -} - -export interface NetlifyConfiguration { - apiKey: string - siteDomain: string -} - -export interface CubeJSConfiguration { - url: string - jwtSecret: string - jwtExpiry: string -} - export interface NangoConfiguration { url: string secretKey: string @@ -192,46 +146,24 @@ export interface EagleEyeConfiguration { apiKey: string } -export interface UnleashConfiguration { - url: string - adminApiKey: string - frontendApiKey: string - backendApiKey: string - - db: { - host: string - port: number - username: string - password: string - database: string - } +export interface GithubTokenConfiguration { + clientId: string + installationId: string + privateKey: string } export interface StackExchangeConfiguration { key: string } -export interface SlackAlertingConfiguration { - url: string -} - -export interface SampleDataConfiguration { - tenantId: string -} - export interface IntegrationProcessingConfiguration { maxRetries: number } -export interface WeeklyEmailsConfiguration { - enabled: string // true - enabled, anything else - disabled -} - export interface IOpenSearchConfig { node: string - region?: string - accessKeyId?: string - secretAccessKey?: string + username: string + password: string } export interface CrowdAnalyticsConfiguration { @@ -240,3 +172,37 @@ export interface CrowdAnalyticsConfiguration { baseUrl: string apiToken: string } + +export interface EncryptionConfiguration { + secretKey: string + initVector: string +} + +export interface IOpenStatusApiConfig { + baseUrl: string +} + +export interface GitlabConfiguration { + clientId: string + clientSecret: string + callbackUrl: string + webhookToken: string +} + +export interface IRedditConfig { + clientId: string + clientSecret: string +} + +export interface SnowflakeConfiguration { + privateKey: string + account: string + username: string + database: string + warehouse: string + role: string +} + +export interface LinuxFoundationConfiguration { + collectionId: string +} diff --git a/backend/src/conf/index.ts b/backend/src/conf/index.ts index 41220ae8ba..553167cde5 100644 --- a/backend/src/conf/index.ts +++ b/backend/src/conf/index.ts @@ -1,43 +1,51 @@ import config from 'config' + +import { IDatabaseConfig } from '@crowd/data-access-layer/src/database' +import { ISearchSyncApiConfig } from '@crowd/opensearch' +import { IQueueClientConfig } from '@crowd/queue' import { IRedisConfiguration } from '@crowd/redis' +import { ITemporalConfig } from '@crowd/temporal' +import { IGithubIssueReporterConfiguration, IJiraIssueReporterConfiguration } from '@crowd/types' + import { - SQSConfiguration, - S3Configuration, - DbConfiguration, - PlansConfiguration, - TwitterConfiguration, ApiConfiguration, - SlackConfiguration, - GoogleConfiguration, - DiscordConfiguration, - ServiceType, - SegmentConfiguration, - GithubConfiguration, - SendgridConfiguration, - NetlifyConfiguration, - TenantMode, - CubeJSConfiguration, - ComprehendConfiguration, + Auth0Configuration, ClearbitConfiguration, - DevtoConfiguration, - NangoConfiguration, - EnrichmentConfiguration, + ComprehendConfiguration, + CrowdAnalyticsConfiguration, + DbConfiguration, + DiscordConfiguration, EagleEyeConfiguration, - UnleashConfiguration, - StackExchangeConfiguration, - SlackAlertingConfiguration, - SampleDataConfiguration, + EncryptionConfiguration, + EnrichmentConfiguration, + GithubConfiguration, + GithubTokenConfiguration, + GitlabConfiguration, + GoogleConfiguration, + IOpenSearchConfig, + IOpenStatusApiConfig, + IRedditConfig, IntegrationProcessingConfiguration, - SlackNotifierConfiguration, + LinuxFoundationConfiguration, + NangoConfiguration, OrganizationEnrichmentConfiguration, - IOpenSearchConfig, - Auth0Configuration, - WeeklyEmailsConfiguration, - CrowdAnalyticsConfiguration, + S3Configuration, + SSOConfiguration, + SegmentConfiguration, + ServiceType, + SlackConfiguration, + SnowflakeConfiguration, + StackExchangeConfiguration, + TenantMode, + TwitterConfiguration, } from './configTypes' // TODO-kube +export const ENCRYPTION_SECRET_KEY = process.env.ENCRYPTION_SECRET_KEY + +export const ENCRYPTION_INIT_VECTOR = process.env.ENCRYPTION_INIT_VECTOR + export const KUBE_MODE: boolean = process.env.KUBE_MODE !== undefined export const SERVICE: ServiceType = process.env.SERVICE as ServiceType @@ -59,7 +67,10 @@ export const LOG_LEVEL: string = process.env.LOG_LEVEL || 'info' export const IS_CLOUD_ENV: boolean = IS_PROD_ENV || IS_STAGING_ENV -export const SQS_CONFIG: SQSConfiguration = config.get('sqs') +export const ENCRYPTION_CONFIG: EncryptionConfiguration = + config.get('encryption') + +export const QUEUE_CONFIG: IQueueClientConfig = config.get('queue') export const REDIS_CONFIG: IRedisConfiguration = config.get('redis') @@ -67,6 +78,14 @@ export const S3_CONFIG: S3Configuration = config.get('s3') export const DB_CONFIG: DbConfiguration = config.get('db') +export const PRODUCT_DB_CONFIG: IDatabaseConfig = config.has('productDb') + ? config.get('productDb') + : undefined + +export const PACKAGES_DB_CONFIG: IDatabaseConfig | undefined = config.has('packagesDb') + ? config.get('packagesDb') + : undefined + export const SEGMENT_CONFIG: SegmentConfiguration = config.get('segment') export const COMPREHEND_CONFIG: ComprehendConfiguration = @@ -78,28 +97,23 @@ export const API_CONFIG: ApiConfiguration = config.get('api') export const AUTH0_CONFIG: Auth0Configuration = config.get('auth0') -export const PLANS_CONFIG: PlansConfiguration = config.get('plans') - -export const DEVTO_CONFIG: DevtoConfiguration = config.get('devto') +export const SSO_CONFIG: SSOConfiguration = config.get('sso') export const TWITTER_CONFIG: TwitterConfiguration = config.get('twitter') export const SLACK_CONFIG: SlackConfiguration = config.get('slack') -export const SLACK_NOTIFIER_CONFIG: SlackNotifierConfiguration = - config.get('slackNotifier') - export const GOOGLE_CONFIG: GoogleConfiguration = config.get('google') export const DISCORD_CONFIG: DiscordConfiguration = config.get('discord') export const GITHUB_CONFIG: GithubConfiguration = config.get('github') -export const SENDGRID_CONFIG: SendgridConfiguration = config.get('sendgrid') - -export const NETLIFY_CONFIG: NetlifyConfiguration = config.get('netlify') +export const GITHUB_ISSUE_REPORTER_CONFIG: IGithubIssueReporterConfiguration = + config.get('githubIssueReporter') -export const CUBEJS_CONFIG: CubeJSConfiguration = config.get('cubejs') +export const JIRA_ISSUE_REPORTER_CONFIG: IJiraIssueReporterConfiguration = + config.get('jiraIssueReporter') export const NANGO_CONFIG: NangoConfiguration = config.get('nango') @@ -111,7 +125,8 @@ export const ORGANIZATION_ENRICHMENT_CONFIG: OrganizationEnrichmentConfiguration export const EAGLE_EYE_CONFIG: EagleEyeConfiguration = config.get('eagleEye') -export const UNLEASH_CONFIG: UnleashConfiguration = config.get('unleash') +export const GITHUB_TOKEN_CONFIG: GithubTokenConfiguration = + config.get('githubToken') export const OPENSEARCH_CONFIG: IOpenSearchConfig = config.get('opensearch') @@ -120,17 +135,29 @@ export const STACKEXCHANGE_CONFIG: StackExchangeConfiguration = key: process.env.STACKEXCHANGE_KEY, } -export const SLACK_ALERTING_CONFIG: SlackAlertingConfiguration = - config.get('slackAlerting') - -export const SAMPLE_DATA_CONFIG: SampleDataConfiguration = - config.get('sampleData') - export const INTEGRATION_PROCESSING_CONFIG: IntegrationProcessingConfiguration = config.get('integrationProcessing') -export const WEEKLY_EMAILS_CONFIG: WeeklyEmailsConfiguration = - config.get('weeklyEmails') - export const CROWD_ANALYTICS_CONFIG: CrowdAnalyticsConfiguration = config.get('crowdAnalytics') + +export const TEMPORAL_CONFIG: ITemporalConfig = config.get('temporal') + +export const SEARCH_SYNC_API_CONFIG: ISearchSyncApiConfig = + config.get('searchSyncApi') + +export const OPEN_STATUS_API_CONFIG: IOpenStatusApiConfig = + config.get('openStatusApi') + +export const GITLAB_CONFIG: GitlabConfiguration = config.get('gitlab') + +export const REDDIT_CONFIG: IRedditConfig = config.get('reddit') + +export const SNOWFLAKE_CONFIG: SnowflakeConfiguration = + config.get('snowflake') + +export const LINUX_FOUNDATION_CONFIG: LinuxFoundationConfiguration = + config.get('linuxFoundation') + +export const ENABLE_LF_COLLECTION_MANAGEMENT: boolean = + process.env.ENABLE_LF_COLLECTION_MANAGEMENT === 'true' diff --git a/backend/src/cubejs/.dockerignore b/backend/src/cubejs/.dockerignore deleted file mode 100644 index 35ac4ada57..0000000000 --- a/backend/src/cubejs/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -**/.cubestore \ No newline at end of file diff --git a/backend/src/cubejs/.eslintrc.js b/backend/src/cubejs/.eslintrc.js deleted file mode 100644 index 6680d7e49e..0000000000 --- a/backend/src/cubejs/.eslintrc.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - rules: { - semi: ['error', 'never'], - 'no-undef': 0, - }, -} diff --git a/backend/src/cubejs/.gitignore b/backend/src/cubejs/.gitignore deleted file mode 100644 index ed93299e91..0000000000 --- a/backend/src/cubejs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/.cubecloud diff --git a/backend/src/cubejs/Dockerfile b/backend/src/cubejs/Dockerfile deleted file mode 100644 index 264a3125cb..0000000000 --- a/backend/src/cubejs/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM cubejs/cube:v0.33.55 - -COPY ./ /cube/conf/ diff --git a/backend/src/cubejs/cube.js b/backend/src/cubejs/cube.js deleted file mode 100644 index b3475230a9..0000000000 --- a/backend/src/cubejs/cube.js +++ /dev/null @@ -1,69 +0,0 @@ -// Cube.js configuration options: https://cube.dev/docs/config - -// NOTE: third-party dependencies and the use of require(...) are disabled for -// CubeCloud users by default. Please contact support if you need them -// enabled for your account. You are still allowed to require -// @cubejs-backend/*-driver packages. - -module.exports = { - queryRewrite: (query, { securityContext }) => { - // Ensure `securityContext` has an `id` property - if (!securityContext.tenantId) { - throw new Error('No id found in Security Context!') - } - if (!securityContext.segments) { - throw new Error('No segments found in Security Context!') - } - const measureCube = query.measures[0].split('.') - - if ( - query.timeDimensions && - query.timeDimensions[0] && - !('granularity' in query.timeDimensions[0]) && - (!('dateRange' in query.timeDimensions[0]) || - ('dateRange' in query.timeDimensions[0] && query.timeDimensions[0].dateRange === undefined)) - ) { - query.timeDimensions = [] - } - - // If member score is selected as a dimension, filter -1's out - if (query.dimensions && query.dimensions[0] && query.dimensions[0] === 'Members.score') { - query.filters.push({ - member: 'Members.score', - operator: 'notEquals', - values: ['-1'], - }) - } - - // Cubejs doesn't support all time dateranges with cumulative measures yet. - // If a cumulative measure is selected - // without time dimension daterange(all time), - // send a long daterange - if ( - query.measures[0] === 'Members.cumulativeCount' && - query.timeDimensions[0] && - !query.timeDimensions[0].dateRange - ) { - query.timeDimensions[0].dateRange = ['2020-01-01', new Date().toISOString()] - } - - query.filters.push({ - member: `Members.isBot`, - operator: 'equals', - values: ['0'], - }) - - query.filters.push({ - member: `${measureCube[0]}.tenantId`, - operator: 'equals', - values: [securityContext.tenantId], - }) - query.filters.push({ - member: 'Segments.id', - operator: 'equals', - values: securityContext.segments, - }) - - return query - }, -} diff --git a/backend/src/cubejs/cubeJsRepository.ts b/backend/src/cubejs/cubeJsRepository.ts deleted file mode 100644 index 4629693b68..0000000000 --- a/backend/src/cubejs/cubeJsRepository.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as metrics from './metrics/index' - -class CubeJsRepository { - static getActiveMembers = metrics.activeMembers - - static getNewActivities = metrics.newActivities - - static getNewConversations = metrics.newConversations - - static getNewMembers = metrics.newMembers - - static getNewOrganizations = metrics.newOrganizations - - static getActiveOrganizations = metrics.activeOrganizations -} - -export default CubeJsRepository diff --git a/backend/src/cubejs/metrics/activeMembers.ts b/backend/src/cubejs/metrics/activeMembers.ts deleted file mode 100644 index e068732ec3..0000000000 --- a/backend/src/cubejs/metrics/activeMembers.ts +++ /dev/null @@ -1,37 +0,0 @@ -import CubeJsService from '../../services/cubejs/cubeJsService' -import CubeDimensions from '../../services/cubejs/cubeDimensions' -import CubeMeasures from '../../services/cubejs/cubeMeasures' - -/** - * Gets `active members` count for a given date range. - * Members are active when they have an activity in given date range. - * @param cjs cubejs service instance - * @param startDate - * @param endDate - * @returns - */ -export default async (cjs: CubeJsService, startDate: moment.Moment, endDate: moment.Moment) => { - const activeMembers = - ( - await cjs.load({ - measures: [CubeMeasures.MEMBER_COUNT], - timeDimensions: [ - { - dimension: CubeDimensions.ACTIVITY_DATE, - dateRange: [startDate.format('YYYY-MM-DD'), endDate.format('YYYY-MM-DD')], - }, - ], - limit: 1, - order: { [CubeDimensions.MEMBER_JOINED_AT]: 'asc' }, - filters: [ - { - member: CubeDimensions.IS_TEAM_MEMBER, - operator: 'equals', - values: ['false'], - }, - ], - }) - )[0][CubeMeasures.MEMBER_COUNT] ?? 0 - - return parseInt(activeMembers, 10) -} diff --git a/backend/src/cubejs/metrics/activeOrganizations.ts b/backend/src/cubejs/metrics/activeOrganizations.ts deleted file mode 100644 index a18194b8bc..0000000000 --- a/backend/src/cubejs/metrics/activeOrganizations.ts +++ /dev/null @@ -1,37 +0,0 @@ -import CubeJsService from '../../services/cubejs/cubeJsService' -import CubeDimensions from '../../services/cubejs/cubeDimensions' -import CubeMeasures from '../../services/cubejs/cubeMeasures' - -/** - * Gets `active organizations` count for a given date range. - * Organizations are active when they have an activity in given date range. - * @param cjs cubejs service instance - * @param startDate - * @param endDate - * @returns - */ -export default async (cjs: CubeJsService, startDate: moment.Moment, endDate: moment.Moment) => { - const activeOrganizations = - ( - await cjs.load({ - measures: [CubeMeasures.ORGANIZATION_COUNT], - timeDimensions: [ - { - dimension: CubeDimensions.ACTIVITY_DATE, - dateRange: [startDate.format('YYYY-MM-DD'), endDate.format('YYYY-MM-DD')], - }, - ], - limit: 1, - order: { [CubeDimensions.ORGANIZATIONS_JOINED_AT]: 'asc' }, - filters: [ - { - member: CubeDimensions.IS_TEAM_MEMBER, - operator: 'equals', - values: ['false'], - }, - ], - }) - )[0][CubeMeasures.ORGANIZATION_COUNT] ?? 0 - - return parseInt(activeOrganizations, 10) -} diff --git a/backend/src/cubejs/metrics/index.ts b/backend/src/cubejs/metrics/index.ts deleted file mode 100644 index f05376e28a..0000000000 --- a/backend/src/cubejs/metrics/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { default as newActivities } from './newActivities' -export { default as newConversations } from './newConversations' -export { default as activeMembers } from './activeMembers' -export { default as newMembers } from './newMembers' -export { default as newOrganizations } from './newOrganizations' -export { default as activeOrganizations } from './activeOrganizations' diff --git a/backend/src/cubejs/metrics/newActivities.ts b/backend/src/cubejs/metrics/newActivities.ts deleted file mode 100644 index 5e6d439f25..0000000000 --- a/backend/src/cubejs/metrics/newActivities.ts +++ /dev/null @@ -1,37 +0,0 @@ -import CubeJsService from '../../services/cubejs/cubeJsService' -import CubeDimensions from '../../services/cubejs/cubeDimensions' -import CubeMeasures from '../../services/cubejs/cubeMeasures' - -/** - * Gets `new activities` count for a given date range. - * Activities are new when activity.timestamp is in between given date range. - * @param cjs cubejs service instance - * @param startDate - * @param endDate - * @returns - */ -export default async (cjs: CubeJsService, startDate: moment.Moment, endDate: moment.Moment) => { - const newActivities = - ( - await cjs.load({ - measures: [CubeMeasures.ACTIVITY_COUNT], - timeDimensions: [ - { - dimension: CubeDimensions.ACTIVITY_DATE, - dateRange: [startDate.format('YYYY-MM-DD'), endDate.format('YYYY-MM-DD')], - }, - ], - limit: 1, - order: { [CubeDimensions.ACTIVITY_DATE]: 'asc' }, - filters: [ - { - member: CubeDimensions.IS_TEAM_MEMBER, - operator: 'equals', - values: ['false'], - }, - ], - }) - )[0][CubeMeasures.ACTIVITY_COUNT] ?? 0 - - return parseInt(newActivities, 10) -} diff --git a/backend/src/cubejs/metrics/newConversations.ts b/backend/src/cubejs/metrics/newConversations.ts deleted file mode 100644 index a5304cecf9..0000000000 --- a/backend/src/cubejs/metrics/newConversations.ts +++ /dev/null @@ -1,29 +0,0 @@ -import CubeJsService from '../../services/cubejs/cubeJsService' -import CubeDimensions from '../../services/cubejs/cubeDimensions' -import CubeMeasures from '../../services/cubejs/cubeMeasures' - -/** - * Gets `new conversations` count for a given date range. - * Conversations are new when conversation.firstActivityTime is in between given date range. - * @param cjs cubejs service instance - * @param startDate - * @param endDate - * @returns - */ -export default async (cjs: CubeJsService, startDate: moment.Moment, endDate: moment.Moment) => { - const newConversations = - ( - await cjs.load({ - measures: [CubeMeasures.CONVERSATION_COUNT], - timeDimensions: [ - { - dimension: CubeDimensions.CONVERSATION_FIRST_ACTIVITY_TIME, - dateRange: [startDate.format('YYYY-MM-DD'), endDate.format('YYYY-MM-DD')], - }, - ], - limit: 1, - }) - )[0][CubeMeasures.CONVERSATION_COUNT] ?? 0 - - return parseInt(newConversations, 10) -} diff --git a/backend/src/cubejs/metrics/newMembers.ts b/backend/src/cubejs/metrics/newMembers.ts deleted file mode 100644 index 8f08985537..0000000000 --- a/backend/src/cubejs/metrics/newMembers.ts +++ /dev/null @@ -1,37 +0,0 @@ -import CubeJsService from '../../services/cubejs/cubeJsService' -import CubeDimensions from '../../services/cubejs/cubeDimensions' -import CubeMeasures from '../../services/cubejs/cubeMeasures' - -/** - * Gets `new members` count for a given date range. - * Members are new when member.joinedAt is in between given date range. - * @param cjs cubejs service instance - * @param startDate - * @param endDate - * @returns - */ -export default async (cjs: CubeJsService, startDate: moment.Moment, endDate: moment.Moment) => { - const newMembers = - ( - await cjs.load({ - measures: [CubeMeasures.MEMBER_COUNT], - timeDimensions: [ - { - dimension: CubeDimensions.MEMBER_JOINED_AT, - dateRange: [startDate.format('YYYY-MM-DD'), endDate.format('YYYY-MM-DD')], - }, - ], - limit: 1, - order: { [CubeDimensions.MEMBER_JOINED_AT]: 'asc' }, - filters: [ - { - member: CubeDimensions.IS_TEAM_MEMBER, - operator: 'equals', - values: ['false'], - }, - ], - }) - )[0][CubeMeasures.MEMBER_COUNT] ?? 0 - - return parseInt(newMembers, 10) -} diff --git a/backend/src/cubejs/metrics/newOrganizations.ts b/backend/src/cubejs/metrics/newOrganizations.ts deleted file mode 100644 index 3a2eec1921..0000000000 --- a/backend/src/cubejs/metrics/newOrganizations.ts +++ /dev/null @@ -1,37 +0,0 @@ -import CubeJsService from '../../services/cubejs/cubeJsService' -import CubeDimensions from '../../services/cubejs/cubeDimensions' -import CubeMeasures from '../../services/cubejs/cubeMeasures' - -/** - * Gets `new organizations` count for a given date range. - * Organizations are new when organization.joinedAt is in between given date range. - * @param cjs cubejs service instance - * @param startDate - * @param endDate - * @returns - */ -export default async (cjs: CubeJsService, startDate: moment.Moment, endDate: moment.Moment) => { - const newOrganizations = - ( - await cjs.load({ - measures: [CubeMeasures.ORGANIZATION_COUNT], - timeDimensions: [ - { - dimension: CubeDimensions.ORGANIZATIONS_JOINED_AT, - dateRange: [startDate.format('YYYY-MM-DD'), endDate.format('YYYY-MM-DD')], - }, - ], - limit: 1, - order: { [CubeDimensions.ORGANIZATIONS_JOINED_AT]: 'asc' }, - filters: [ - { - member: CubeDimensions.IS_TEAM_MEMBER, - operator: 'equals', - values: ['false'], - }, - ], - }) - )[0][CubeMeasures.ORGANIZATION_COUNT] ?? 0 - - return parseInt(newOrganizations, 10) -} diff --git a/backend/src/cubejs/package-lock.json b/backend/src/cubejs/package-lock.json deleted file mode 100644 index 4bc24bc01c..0000000000 --- a/backend/src/cubejs/package-lock.json +++ /dev/null @@ -1,10782 +0,0 @@ -{ - "name": "cubejs", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "cubejs", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@cubejs-backend/server-core": "^0.32.27" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", - "dependencies": { - "@babel/highlight": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.4.tgz", - "integrity": "sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.4.tgz", - "integrity": "sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.4", - "@babel/helper-compilation-targets": "^7.21.4", - "@babel/helper-module-transforms": "^7.21.2", - "@babel/helpers": "^7.21.0", - "@babel/parser": "^7.21.4", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.4", - "@babel/types": "^7.21.4", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz", - "integrity": "sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA==", - "dependencies": { - "@babel/types": "^7.21.4", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", - "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", - "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", - "dependencies": { - "@babel/helper-explode-assignable-expression": "^7.18.6", - "@babel/types": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.4.tgz", - "integrity": "sha512-Fa0tTuOXZ1iL8IeDFUWCzjZcn+sJGd9RZdH9esYVjEejGmzf+FFYQpMi/kZUk2kPy/q1H3/GPw7np8qar/stfg==", - "dependencies": { - "@babel/compat-data": "^7.21.4", - "@babel/helper-validator-option": "^7.21.0", - "browserslist": "^4.21.3", - "lru-cache": "^5.1.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.4.tgz", - "integrity": "sha512-46QrX2CQlaFRF4TkwfTt6nJD7IHq8539cCL7SDpqWSDeJKY1xylKKY5F/33mJhLZ3mFvKv2gGrVS6NkyF6qs+Q==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-member-expression-to-functions": "^7.21.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-replace-supers": "^7.20.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/helper-split-export-declaration": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.4.tgz", - "integrity": "sha512-M00OuhU+0GyZ5iBBN9czjugzWrEq2vDpf/zCYHxxf93ul/Q5rv+a5h+/+0WnI1AebHNVtl5bFV0qsJoH23DbfA==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "regexpu-core": "^5.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", - "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", - "dependencies": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0-0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-explode-assignable-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", - "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", - "dependencies": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz", - "integrity": "sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==", - "dependencies": { - "@babel/types": "^7.21.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz", - "integrity": "sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==", - "dependencies": { - "@babel/types": "^7.21.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz", - "integrity": "sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.20.2", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.2", - "@babel/types": "^7.21.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", - "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", - "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-wrap-function": "^7.18.9", - "@babel/types": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz", - "integrity": "sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-member-expression-to-functions": "^7.20.7", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.7", - "@babel/types": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", - "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", - "dependencies": { - "@babel/types": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", - "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", - "dependencies": { - "@babel/types": "^7.20.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", - "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", - "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", - "dependencies": { - "@babel/helper-function-name": "^7.19.0", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.5", - "@babel/types": "^7.20.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz", - "integrity": "sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==", - "dependencies": { - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.0", - "@babel/types": "^7.21.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", - "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", - "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz", - "integrity": "sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-proposal-optional-chaining": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", - "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-static-block": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", - "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-proposal-dynamic-import": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", - "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", - "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-json-strings": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", - "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", - "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", - "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-numeric-separator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", - "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", - "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", - "dependencies": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-catch-binding": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", - "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", - "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-methods": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", - "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz", - "integrity": "sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", - "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz", - "integrity": "sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", - "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", - "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-remap-async-to-generator": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", - "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz", - "integrity": "sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz", - "integrity": "sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-replace-supers": "^7.20.7", - "@babel/helper-split-export-declaration": "^7.18.6", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz", - "integrity": "sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/template": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz", - "integrity": "sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", - "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", - "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", - "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", - "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz", - "integrity": "sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", - "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", - "dependencies": { - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", - "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", - "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz", - "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==", - "dependencies": { - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz", - "integrity": "sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA==", - "dependencies": { - "@babel/helper-module-transforms": "^7.21.2", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-simple-access": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz", - "integrity": "sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==", - "dependencies": { - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-identifier": "^7.19.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", - "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", - "dependencies": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz", - "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.20.5", - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", - "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", - "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz", - "integrity": "sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", - "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz", - "integrity": "sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "regenerator-transform": "^0.15.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", - "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", - "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz", - "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", - "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", - "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", - "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", - "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", - "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.21.4.tgz", - "integrity": "sha512-2W57zHs2yDLm6GD5ZpvNn71lZ0B/iypSdIeq25OurDKji6AdzV07qp4s3n1/x5BqtiGaTrPN3nerlSCaC5qNTw==", - "dependencies": { - "@babel/compat-data": "^7.21.4", - "@babel/helper-compilation-targets": "^7.21.4", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-option": "^7.21.0", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.20.7", - "@babel/plugin-proposal-async-generator-functions": "^7.20.7", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-class-static-block": "^7.21.0", - "@babel/plugin-proposal-dynamic-import": "^7.18.6", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", - "@babel/plugin-proposal-json-strings": "^7.18.6", - "@babel/plugin-proposal-logical-assignment-operators": "^7.20.7", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", - "@babel/plugin-proposal-numeric-separator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.20.7", - "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", - "@babel/plugin-proposal-optional-chaining": "^7.21.0", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-private-property-in-object": "^7.21.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.20.0", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.20.7", - "@babel/plugin-transform-async-to-generator": "^7.20.7", - "@babel/plugin-transform-block-scoped-functions": "^7.18.6", - "@babel/plugin-transform-block-scoping": "^7.21.0", - "@babel/plugin-transform-classes": "^7.21.0", - "@babel/plugin-transform-computed-properties": "^7.20.7", - "@babel/plugin-transform-destructuring": "^7.21.3", - "@babel/plugin-transform-dotall-regex": "^7.18.6", - "@babel/plugin-transform-duplicate-keys": "^7.18.9", - "@babel/plugin-transform-exponentiation-operator": "^7.18.6", - "@babel/plugin-transform-for-of": "^7.21.0", - "@babel/plugin-transform-function-name": "^7.18.9", - "@babel/plugin-transform-literals": "^7.18.9", - "@babel/plugin-transform-member-expression-literals": "^7.18.6", - "@babel/plugin-transform-modules-amd": "^7.20.11", - "@babel/plugin-transform-modules-commonjs": "^7.21.2", - "@babel/plugin-transform-modules-systemjs": "^7.20.11", - "@babel/plugin-transform-modules-umd": "^7.18.6", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.20.5", - "@babel/plugin-transform-new-target": "^7.18.6", - "@babel/plugin-transform-object-super": "^7.18.6", - "@babel/plugin-transform-parameters": "^7.21.3", - "@babel/plugin-transform-property-literals": "^7.18.6", - "@babel/plugin-transform-regenerator": "^7.20.5", - "@babel/plugin-transform-reserved-words": "^7.18.6", - "@babel/plugin-transform-shorthand-properties": "^7.18.6", - "@babel/plugin-transform-spread": "^7.20.7", - "@babel/plugin-transform-sticky-regex": "^7.18.6", - "@babel/plugin-transform-template-literals": "^7.18.9", - "@babel/plugin-transform-typeof-symbol": "^7.18.9", - "@babel/plugin-transform-unicode-escapes": "^7.18.10", - "@babel/plugin-transform-unicode-regex": "^7.18.6", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.21.4", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "core-js-compat": "^3.25.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" - }, - "node_modules/@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", - "dependencies": { - "regenerator-runtime": "^0.13.11" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/standalone": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.21.4.tgz", - "integrity": "sha512-Rw4nGqH/iyVeYxARKcz7iGP+njkPsVqJ45TmXMONoGoxooWjXCAs+CUcLeAZdBGCLqgaPvHKCYvIaDT2Iq+KfA==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.4.tgz", - "integrity": "sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q==", - "dependencies": { - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.4", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.4", - "@babel/types": "^7.21.4", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", - "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@cubejs-backend/api-gateway": { - "version": "0.32.27", - "resolved": "https://registry.npmjs.org/@cubejs-backend/api-gateway/-/api-gateway-0.32.27.tgz", - "integrity": "sha512-l4joKt9f7e5VImaw4G+tVZrvpO2G67J/0fjvIj2fCwk3tRlNqTqrjADnofykkCeR1LntB8miipj2bdZMrECGog==", - "dependencies": { - "@cubejs-backend/native": "^0.32.27", - "@cubejs-backend/shared": "^0.32.25", - "@ungap/structured-clone": "^0.3.4", - "body-parser": "^1.19.0", - "chrono-node": "^2.6.2", - "express-graphql": "^0.12.0", - "graphql": "^15.8.0", - "graphql-scalars": "^1.10.0", - "joi": "^17.8.3", - "jsonwebtoken": "^8.3.0", - "jwk-to-pem": "^2.0.4", - "moment": "^2.24.0", - "moment-timezone": "^0.5.27", - "nexus": "^1.1.0", - "node-fetch": "^2.6.1", - "querystring": "^0.2.1", - "ramda": "^0.27.0", - "uuid": "^8.3.2" - }, - "engines": { - "node": "^14.0.0 || ^16.0.0 || >=17.0.0" - } - }, - "node_modules/@cubejs-backend/base-driver": { - "version": "0.32.26", - "resolved": "https://registry.npmjs.org/@cubejs-backend/base-driver/-/base-driver-0.32.26.tgz", - "integrity": "sha512-OVplhEEdSGHYxgGbNEmN9IXieZbaEjfLgi8mBmlr9OTC1b3cecFHrV8tvRRqi6Emi8ZLtsMjmQx4LOcv7fVn/A==", - "dependencies": { - "@cubejs-backend/shared": "^0.32.25", - "ramda": "^0.27.0" - }, - "engines": { - "node": "^14.0.0 || ^16.0.0 || >=17.0.0" - } - }, - "node_modules/@cubejs-backend/cloud": { - "version": "0.32.25", - "resolved": "https://registry.npmjs.org/@cubejs-backend/cloud/-/cloud-0.32.25.tgz", - "integrity": "sha512-bk+3kAE4PKIBI7by8utyKTpA9zJJwBZLvRe8Ica0V+z+pFSEooPu4Pnp2hcoalqZ0GMKQkyRDZzbGu7zTVUK7w==", - "dependencies": { - "@cubejs-backend/dotenv": "^9.0.2", - "@cubejs-backend/shared": "^0.32.25", - "chokidar": "^3.5.1", - "env-var": "^6.3.0", - "fs-extra": "^9.1.0", - "jsonwebtoken": "^8.5.1", - "request": "^2.88.2", - "request-promise": "^4.2.5" - }, - "engines": { - "node": "^14.0.0 || ^16.0.0 || >=17.0.0" - } - }, - "node_modules/@cubejs-backend/cloud/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@cubejs-backend/cloud/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@cubejs-backend/cloud/node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@cubejs-backend/cubesql": { - "version": "0.32.19", - "resolved": "https://registry.npmjs.org/@cubejs-backend/cubesql/-/cubesql-0.32.19.tgz", - "integrity": "sha512-b605Vw2OeFkn0dQmHd9FR2uEHzFCLlduTIez/4WpWw7MilcX1T810AMXIdwxbn9DPl/ILc5MvHn9hZ74SqE4dw==", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@cubejs-backend/cubestore": { - "version": "0.32.25", - "resolved": "https://registry.npmjs.org/@cubejs-backend/cubestore/-/cubestore-0.32.25.tgz", - "integrity": "sha512-k22AR9znDL8vcKDe6ieIg9cbSRzfnW+lSAd+v+0R0nJ8Z/8SU0lXOzTecWkOmihJ4U+Gxqd1tueazzEO2BRfWQ==", - "hasInstallScript": true, - "dependencies": { - "@cubejs-backend/shared": "^0.32.25", - "@octokit/core": "^3.2.5", - "source-map-support": "^0.5.19" - }, - "bin": { - "cubestore-dev": "bin/cubestore-dev" - }, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@cubejs-backend/cubestore-driver": { - "version": "0.32.26", - "resolved": "https://registry.npmjs.org/@cubejs-backend/cubestore-driver/-/cubestore-driver-0.32.26.tgz", - "integrity": "sha512-WIBE8NUAdCQds2Rue4wL27cIO3esK5n+Y3DP43M72iw6/BP9f09VlbgQJORWtY9mF0g1P0ZEwwFhHvAR0yEfKA==", - "dependencies": { - "@cubejs-backend/base-driver": "^0.32.26", - "@cubejs-backend/cubestore": "^0.32.25", - "@cubejs-backend/shared": "^0.32.25", - "csv-write-stream": "^2.0.0", - "flatbuffers": "23.3.3", - "fs-extra": "^9.1.0", - "generic-pool": "^3.6.0", - "moment-timezone": "^0.5.31", - "node-fetch": "^2.6.1", - "sqlstring": "^2.3.3", - "tempy": "^1.0.1", - "uuid": "^8.3.2", - "ws": "^7.4.3" - }, - "engines": { - "node": "^14.0.0 || ^16.0.0 || >=17.0.0" - } - }, - "node_modules/@cubejs-backend/cubestore-driver/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@cubejs-backend/cubestore-driver/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@cubejs-backend/cubestore-driver/node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@cubejs-backend/dotenv": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@cubejs-backend/dotenv/-/dotenv-9.0.2.tgz", - "integrity": "sha512-yC1juhXEjM7K97KfXubDm7WGipd4Lpxe+AT8XeTRE9meRULrKlw0wtE2E8AQkGOfTBn+P1SCkePQ/BzIbOh1VA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@cubejs-backend/native": { - "version": "0.32.27", - "resolved": "https://registry.npmjs.org/@cubejs-backend/native/-/native-0.32.27.tgz", - "integrity": "sha512-LIqsSH6sprAqHbQdLcw6bqGtYKyaCf1TiqPx8OmFQp7GgNudkyk5ZfeXPWUhCnCnZX+q+A/K9EcDzxgY6GvLuQ==", - "hasInstallScript": true, - "dependencies": { - "@cubejs-backend/cubesql": "^0.32.19", - "@cubejs-backend/shared": "^0.32.25", - "@mapbox/node-pre-gyp": "^1" - }, - "engines": { - "node": "^14.0.0 || ^16.0.0 || >=17.0.0" - } - }, - "node_modules/@cubejs-backend/query-orchestrator": { - "version": "0.32.27", - "resolved": "https://registry.npmjs.org/@cubejs-backend/query-orchestrator/-/query-orchestrator-0.32.27.tgz", - "integrity": "sha512-+FQt2bm5trfpmsXY8sLT0fYKqSfStNGSzsf3RFOdWZ4vofiBg8psNMQlKF8DlFlVtuKcNPSk/hcmPqce+heagg==", - "dependencies": { - "@cubejs-backend/base-driver": "^0.32.26", - "@cubejs-backend/cubestore-driver": "^0.32.26", - "@cubejs-backend/shared": "^0.32.25", - "csv-write-stream": "^2.0.0", - "es5-ext": "0.10.53", - "generic-pool": "^3.7.1", - "ioredis": "^4.27.8", - "lru-cache": "^6.0.0", - "moment-range": "^4.0.2", - "moment-timezone": "^0.5.33", - "ramda": "^0.27.2", - "redis": "^3.0.2" - }, - "engines": { - "node": "^14.0.0 || ^16.0.0 || >=17.0.0" - } - }, - "node_modules/@cubejs-backend/query-orchestrator/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@cubejs-backend/query-orchestrator/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/@cubejs-backend/schema-compiler": { - "version": "0.32.27", - "resolved": "https://registry.npmjs.org/@cubejs-backend/schema-compiler/-/schema-compiler-0.32.27.tgz", - "integrity": "sha512-klV7r6uR6+7HJaINBHBdKaxlMGc/Hz10VMhFG8acYSz2zvFpQ+vDbcvnNbi0dBLpjQfCoWeJfpbxjrVL5XXvsQ==", - "dependencies": { - "@babel/code-frame": "^7.12.11", - "@babel/core": "^7.12.10", - "@babel/generator": "^7.12.10", - "@babel/parser": "^7.12.10", - "@babel/preset-env": "^7.12.10", - "@babel/standalone": "^7.12.10", - "@babel/traverse": "^7.12.10", - "@babel/types": "^7.12.12", - "@cubejs-backend/shared": "^0.32.25", - "antlr4ts": "0.5.0-alpha.4", - "camelcase": "^6.2.0", - "cron-parser": "^3.5.0", - "humps": "^2.0.1", - "inflection": "^1.12.0", - "joi": "^17.8.3", - "js-yaml": "^4.1.0", - "lru-cache": "^5.1.1", - "moment-range": "^4.0.1", - "moment-timezone": "^0.5.33", - "node-dijkstra": "^2.5.0", - "ramda": "^0.27.2", - "syntax-error": "^1.3.0" - }, - "engines": { - "node": "^14.0.0 || ^16.0.0 || >=17.0.0" - } - }, - "node_modules/@cubejs-backend/server-core": { - "version": "0.32.27", - "resolved": "https://registry.npmjs.org/@cubejs-backend/server-core/-/server-core-0.32.27.tgz", - "integrity": "sha512-vlAhRFScey4tlDU5mR9jZEitod1A5Um/+8dx/sD4zJ5YrZn3o34AneC8vD8ERpuM1ZGhuiEix3sW0hrJ7isc6w==", - "dependencies": { - "@cubejs-backend/api-gateway": "^0.32.27", - "@cubejs-backend/cloud": "^0.32.25", - "@cubejs-backend/dotenv": "^9.0.2", - "@cubejs-backend/query-orchestrator": "^0.32.27", - "@cubejs-backend/schema-compiler": "^0.32.27", - "@cubejs-backend/shared": "^0.32.25", - "@cubejs-backend/templates": "^0.32.25", - "codesandbox-import-utils": "^2.1.12", - "cross-spawn": "^7.0.1", - "fs-extra": "^8.1.0", - "is-docker": "^2.1.1", - "joi": "^17.8.3", - "jsonwebtoken": "^8.4.0", - "lodash.clonedeep": "^4.5.0", - "lru-cache": "^5.1.1", - "moment": "^2.29.1", - "node-fetch": "^2.6.0", - "p-limit": "^3.1.0", - "promise-timeout": "^1.3.0", - "ramda": "^0.27.0", - "semver": "^6.3.0", - "serve-static": "^1.13.2", - "sqlstring": "^2.3.1", - "uuid": "^8.3.2", - "ws": "^7.5.3" - }, - "engines": { - "node": "^14.0.0 || ^16.0.0 || >=17.0.0" - } - }, - "node_modules/@cubejs-backend/shared": { - "version": "0.32.25", - "resolved": "https://registry.npmjs.org/@cubejs-backend/shared/-/shared-0.32.25.tgz", - "integrity": "sha512-C7FJYHf3COPVRa/EUAkw3csqndUOrhg7AEBbqBmZvW3RNFfV/I7IJhpd4GXJqe/hlWt5Af0KzNu+oN4qttEXKQ==", - "dependencies": { - "@oclif/color": "^0.1.2", - "bytes": "^3.1.0", - "cli-progress": "^3.9.0", - "cross-spawn": "^7.0.3", - "decompress": "^4.2.1", - "env-var": "^6.3.0", - "fs-extra": "^9.1.0", - "http-proxy-agent": "^4.0.1", - "moment-range": "^4.0.1", - "moment-timezone": "^0.5.33", - "node-fetch": "^2.6.1", - "shelljs": "^0.8.5", - "throttle-debounce": "^3.0.1", - "uuid": "^8.3.2" - }, - "engines": { - "node": "^14.0.0 || ^16.0.0 || >=17.0.0" - } - }, - "node_modules/@cubejs-backend/shared/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@cubejs-backend/shared/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@cubejs-backend/shared/node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@cubejs-backend/templates": { - "version": "0.32.25", - "resolved": "https://registry.npmjs.org/@cubejs-backend/templates/-/templates-0.32.25.tgz", - "integrity": "sha512-MH5b/gtw0cWSmVDFd7BkoryjYjNaqfKSVThO8l0zy7quOote+gEN0BMYP9UGv1qEP/G2zrosrqZ9juISzk8hrQ==", - "dependencies": { - "@cubejs-backend/shared": "^0.32.25", - "cross-spawn": "^7.0.3", - "decompress": "^4.2.1", - "decompress-targz": "^4.1.1", - "fs-extra": "^9.1.0", - "node-fetch": "^2.6.1", - "ramda": "^0.27.2", - "source-map-support": "^0.5.19" - }, - "engines": { - "node": "^14.0.0 || ^16.0.0 || >=17.0.0" - } - }, - "node_modules/@cubejs-backend/templates/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@cubejs-backend/templates/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@cubejs-backend/templates/node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" - }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", - "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@oclif/color": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@oclif/color/-/color-0.1.2.tgz", - "integrity": "sha512-M9o+DOrb8l603qvgz1FogJBUGLqcMFL1aFg2ZEL0FbXJofiNTLOWIeB4faeZTLwE6dt0xH9GpCVpzksMMzGbmA==", - "dependencies": { - "ansi-styles": "^3.2.1", - "chalk": "^3.0.0", - "strip-ansi": "^5.2.0", - "supports-color": "^5.4.0", - "tslib": "^1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@oclif/color/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@oclif/color/node_modules/chalk/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@oclif/color/node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@oclif/color/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@oclif/color/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/@oclif/color/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@octokit/auth-token": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", - "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", - "dependencies": { - "@octokit/types": "^6.0.3" - } - }, - "node_modules/@octokit/core": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz", - "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", - "dependencies": { - "@octokit/auth-token": "^2.4.4", - "@octokit/graphql": "^4.5.8", - "@octokit/request": "^5.6.3", - "@octokit/request-error": "^2.0.5", - "@octokit/types": "^6.0.3", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" - } - }, - "node_modules/@octokit/endpoint": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", - "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", - "dependencies": { - "@octokit/types": "^6.0.3", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - } - }, - "node_modules/@octokit/graphql": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", - "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", - "dependencies": { - "@octokit/request": "^5.6.0", - "@octokit/types": "^6.0.3", - "universal-user-agent": "^6.0.0" - } - }, - "node_modules/@octokit/openapi-types": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", - "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" - }, - "node_modules/@octokit/request": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", - "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", - "dependencies": { - "@octokit/endpoint": "^6.0.1", - "@octokit/request-error": "^2.1.0", - "@octokit/types": "^6.16.1", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" - } - }, - "node_modules/@octokit/request-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", - "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", - "dependencies": { - "@octokit/types": "^6.0.3", - "deprecation": "^2.0.0", - "once": "^1.4.0" - } - }, - "node_modules/@octokit/types": { - "version": "6.41.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", - "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", - "dependencies": { - "@octokit/openapi-types": "^12.11.0" - } - }, - "node_modules/@sideway/address": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", - "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" - }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-0.3.4.tgz", - "integrity": "sha512-TSVh8CpnwNAsPC5wXcIyh92Bv1gq6E9cNDeeLu7Z4h8V4/qWtXJp7y42qljRkqcpmsve1iozwv1wr+3BNdILCg==" - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "dependencies": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - } - }, - "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/antlr4ts": { - "version": "0.5.0-alpha.4", - "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", - "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", - "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", - "dependencies": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.3.3", - "semver": "^6.1.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", - "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3", - "core-js-compat": "^3.25.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", - "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/before-after-hook": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", - "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/binaryextensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-2.3.0.tgz", - "integrity": "sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==", - "engines": { - "node": ">=0.8" - }, - "funding": { - "url": "https://bevry.me/fund" - } - }, - "node_modules/bl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", - "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", - "dependencies": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/bl/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/bl/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/bl/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, - "node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - }, - "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" - }, - "node_modules/browserslist": { - "version": "4.21.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", - "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "dependencies": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "node_modules/buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "node_modules/buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==" - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001480", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001480.tgz", - "integrity": "sha512-q7cpoPPvZYgtyC4VaBSN0Bt+PJ4c4EYRf0DrduInOz2SkFpHD5p3LnvEpqBp7UnJn+8x1Ogl1s38saUxe+ihQQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/chrono-node": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/chrono-node/-/chrono-node-2.6.3.tgz", - "integrity": "sha512-VkWaaZnNulqzNH9i4XCdyI05OX6MFEnCMNKdZOR4w//wS5/E2qkwAss/O5sj6SfTZK84fX4SSOG4pzqjqIseiA==", - "dependencies": { - "dayjs": "^1.10.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "engines": { - "node": ">=6" - } - }, - "node_modules/cli-progress": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", - "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", - "dependencies": { - "string-width": "^4.2.3" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/codesandbox-import-util-types": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/codesandbox-import-util-types/-/codesandbox-import-util-types-2.2.3.tgz", - "integrity": "sha512-Qj00p60oNExthP2oR3vvXmUGjukij+rxJGuiaKM6tyUmSyimdZsqHI/TUvFFClAffk9s7hxGnQgWQ8KCce27qQ==" - }, - "node_modules/codesandbox-import-utils": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/codesandbox-import-utils/-/codesandbox-import-utils-2.2.3.tgz", - "integrity": "sha512-ymtmcgZKU27U+nM2qUb21aO8Ut/u2S9s6KorOgG81weP+NA0UZkaHKlaRqbLJ9h4i/4FLvwmEXYAnTjNmp6ogg==", - "dependencies": { - "codesandbox-import-util-types": "^2.2.3", - "istextorbinary": "^2.2.1", - "lz-string": "^1.4.4" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" - }, - "node_modules/core-js-compat": { - "version": "3.30.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.30.1.tgz", - "integrity": "sha512-d690npR7MC6P0gq4npTl5n2VQeNAmUrJ90n+MHiKS7W2+xno4o3F5GDEuylSdi6EJ3VssibSGXOa1r3YXD3Mhw==", - "dependencies": { - "browserslist": "^4.21.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" - }, - "node_modules/cron-parser": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-3.5.0.tgz", - "integrity": "sha512-wyVZtbRs6qDfFd8ap457w3XVntdvqcwBGxBoTvJQH9KGVKL/fB+h2k3C8AqiVxvUQKN1Ps/Ns46CNViOpVDhfQ==", - "dependencies": { - "is-nan": "^1.3.2", - "luxon": "^1.26.0" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/csv-write-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/csv-write-stream/-/csv-write-stream-2.0.0.tgz", - "integrity": "sha512-QTraH6FOYfM5f+YGwx71hW1nR9ZjlWri67/D4CWtiBkdce0UAa91Vc0yyHg0CjC0NeEGnvO/tBSJkA1XF9D9GQ==", - "dependencies": { - "argparse": "^1.0.7", - "generate-object-property": "^1.0.0", - "ndjson": "^1.3.0" - }, - "bin": { - "csv-write": "cli.js" - } - }, - "node_modules/d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "dependencies": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/dayjs": { - "version": "1.11.7", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", - "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", - "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", - "dependencies": { - "decompress-tar": "^4.0.0", - "decompress-tarbz2": "^4.0.0", - "decompress-targz": "^4.0.0", - "decompress-unzip": "^4.0.1", - "graceful-fs": "^4.1.10", - "make-dir": "^1.0.0", - "pify": "^2.3.0", - "strip-dirs": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-tar": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", - "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", - "dependencies": { - "file-type": "^5.2.0", - "is-stream": "^1.1.0", - "tar-stream": "^1.5.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-tarbz2": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", - "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", - "dependencies": { - "decompress-tar": "^4.1.0", - "file-type": "^6.1.0", - "is-stream": "^1.1.0", - "seek-bzip": "^1.0.5", - "unbzip2-stream": "^1.0.9" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-tarbz2/node_modules/file-type": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", - "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-targz": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", - "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", - "dependencies": { - "decompress-tar": "^4.1.1", - "file-type": "^5.2.0", - "is-stream": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-unzip": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", - "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", - "dependencies": { - "file-type": "^3.8.0", - "get-stream": "^2.2.0", - "pify": "^2.3.0", - "yauzl": "^2.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress-unzip/node_modules/file-type": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", - "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decompress/node_modules/make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/decompress/node_modules/make-dir/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "engines": { - "node": ">=4" - } - }, - "node_modules/define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/del": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", - "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", - "dependencies": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/del/node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" - }, - "node_modules/denque": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", - "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/editions": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/editions/-/editions-2.3.1.tgz", - "integrity": "sha512-ptGvkwTvGdGfC0hfhKg0MT+TRLRKGtUiWGBInxOm5pz7ssADezahjCUaYuZ8Dr+C05FW0AECIIPt4WBxVINEhA==", - "dependencies": { - "errlop": "^2.0.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=0.8" - }, - "funding": { - "url": "https://bevry.me/fund" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/electron-to-chromium": { - "version": "1.4.365", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.365.tgz", - "integrity": "sha512-FRHZO+1tUNO4TOPXmlxetkoaIY8uwHzd1kKopK/Gx2SKn1L47wJXWD44wxP5CGRyyP98z/c8e1eBzJrgPeiBOg==" - }, - "node_modules/elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-var": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/env-var/-/env-var-6.3.0.tgz", - "integrity": "sha512-gaNzDZuVaJQJlP2SigAZLu/FieZN5MzdN7lgHNehESwlRanHwGQ/WUtJ7q//dhrj3aGBZM45yEaKOuvSJaf4mA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/errlop": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/errlop/-/errlop-2.2.0.tgz", - "integrity": "sha512-e64Qj9+4aZzjzzFpZC7p5kmm/ccCrbLhAJplhsDXQFs87XTsXwOpH4s1Io2s90Tau/8r2j9f4l/thhDevRjzxw==", - "engines": { - "node": ">=0.8" - }, - "funding": { - "url": "https://bevry.me/fund" - } - }, - "node_modules/es5-ext": { - "version": "0.10.53", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", - "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", - "dependencies": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.3", - "next-tick": "~1.0.0" - } - }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", - "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "node_modules/es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", - "dependencies": { - "d": "^1.0.1", - "ext": "^1.1.2" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express-graphql": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/express-graphql/-/express-graphql-0.12.0.tgz", - "integrity": "sha512-DwYaJQy0amdy3pgNtiTDuGGM2BLdj+YO2SgbKoLliCfuHv3VVTt7vNG/ZqK2hRYjtYHE2t2KB705EU94mE64zg==", - "deprecated": "This package is no longer maintained. We recommend using `graphql-http` instead. Please consult the migration document https://github.com/graphql/graphql-http#migrating-express-grpahql.", - "dependencies": { - "accepts": "^1.3.7", - "content-type": "^1.0.4", - "http-errors": "1.8.0", - "raw-body": "^2.4.1" - }, - "engines": { - "node": ">= 10.x" - }, - "peerDependencies": { - "graphql": "^14.7.0 || ^15.3.0" - } - }, - "node_modules/express-graphql/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express-graphql/node_modules/http-errors": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", - "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express-graphql/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express-graphql/node_modules/toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/ext": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", - "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", - "dependencies": { - "type": "^2.7.2" - } - }, - "node_modules/ext/node_modules/type": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", - "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "engines": [ - "node >=0.6.0" - ] - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/file-type": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/flatbuffers": { - "version": "23.3.3", - "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-23.3.3.tgz", - "integrity": "sha512-jmreOaAT1t55keaf+Z259Tvh8tR/Srry9K8dgCgvizhKSEr6gLGgaOJI2WFL5fkOpGOGRZwxUrlFn0GCmXUy6g==" - }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "engines": { - "node": "*" - } - }, - "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gauge/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/gauge/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha512-TuOwZWgJ2VAMEGJvAyPWvpqxSANF0LDpmyHauMjFYzaACvn+QTT/AZomvPCzVBV7yDN3OmwHQ5OvHaeLKre3JQ==", - "dependencies": { - "is-property": "^1.0.0" - } - }, - "node_modules/generic-pool": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", - "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-stream": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", - "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", - "dependencies": { - "object-assign": "^4.0.1", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/graphql": { - "version": "15.8.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz", - "integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/graphql-scalars": { - "version": "1.21.3", - "resolved": "https://registry.npmjs.org/graphql-scalars/-/graphql-scalars-1.21.3.tgz", - "integrity": "sha512-QLWw3BHmqHZMp9JeYmPpjq7JT9aw/H8TpwmWKJEuMSE3+O7Xe7TduQbOLFzbs1q9UxX6CVkc0O1JO/YfkP/pAw==", - "dependencies": { - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/graphql-scalars/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" - }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "dependencies": { - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" - }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/humps": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz", - "integrity": "sha512-E0eIbrFWUhwfXJmsbdjRQFQPrl5pTEoKlz163j1mTqqUnU9PgR4AgB8AIITzuB3vLBdxZXyZ9TDIrwB2OASz4g==" - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/inflection": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", - "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", - "engines": [ - "node >= 0.4.0" - ] - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/ioredis": { - "version": "4.28.5", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz", - "integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==", - "dependencies": { - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.1", - "denque": "^1.1.0", - "lodash.defaults": "^4.2.0", - "lodash.flatten": "^4.4.0", - "lodash.isarguments": "^3.1.0", - "p-map": "^2.1.0", - "redis-commands": "1.7.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", - "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-natural-number": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", - "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==" - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" - }, - "node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" - }, - "node_modules/istextorbinary": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-2.6.0.tgz", - "integrity": "sha512-+XRlFseT8B3L9KyjxxLjfXSLMuErKDsd8DBNrsaxoViABMEZlOSCstwmw0qpoFX3+U6yWU1yhLudAe6/lETGGA==", - "dependencies": { - "binaryextensions": "^2.1.2", - "editions": "^2.2.0", - "textextensions": "^2.5.0" - }, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://bevry.me/fund" - } - }, - "node_modules/iterall": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", - "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==" - }, - "node_modules/joi": { - "version": "17.9.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.9.1.tgz", - "integrity": "sha512-FariIi9j6QODKATGBrEX7HZcja8Bsh3rfdGYy/Sb65sGlZWK/QWesU1ghk7aJWDj95knjXlQfSmzFSPPkLVsfw==", - "dependencies": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/js-yaml/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonwebtoken": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", - "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", - "dependencies": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=4", - "npm": ">=1.4.28" - } - }, - "node_modules/jsonwebtoken/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jwk-to-pem": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz", - "integrity": "sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A==", - "dependencies": { - "asn1.js": "^5.3.0", - "elliptic": "^6.5.4", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" - }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" - }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/luxon": { - "version": "1.28.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.1.tgz", - "integrity": "sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==", - "engines": { - "node": "*" - } - }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", - "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", - "engines": { - "node": "*" - } - }, - "node_modules/moment-range": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/moment-range/-/moment-range-4.0.2.tgz", - "integrity": "sha512-n8sceWwSTjmz++nFHzeNEUsYtDqjgXgcOBzsHi+BoXQU2FW+eU92LUaK8gqOiSu5PG57Q9sYj1Fz4LRDj4FtKA==", - "dependencies": { - "es6-symbol": "^3.1.0" - }, - "engines": { - "node": "*" - }, - "peerDependencies": { - "moment": ">= 2" - } - }, - "node_modules/moment-timezone": { - "version": "0.5.43", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.43.tgz", - "integrity": "sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==", - "dependencies": { - "moment": "^2.29.4" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/ndjson": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/ndjson/-/ndjson-1.5.0.tgz", - "integrity": "sha512-hUPLuaziboGjNF7wHngkgVc0FOclR8dDk/HfEvTtDr/iUrqBWiRcRSTK3/nLOqKH33th714BrMmTPtObI9gZxQ==", - "dependencies": { - "json-stringify-safe": "^5.0.1", - "minimist": "^1.2.0", - "split2": "^2.1.0", - "through2": "^2.0.3" - }, - "bin": { - "ndjson": "cli.js" - } - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha512-mc/caHeUcdjnC/boPWJefDr4KUIWQNv+tlnFnJd38QMou86QtxQzBJfxgGRzvx8jazYRqrVlaHarfO72uNxPOg==" - }, - "node_modules/nexus": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/nexus/-/nexus-1.3.0.tgz", - "integrity": "sha512-w/s19OiNOs0LrtP7pBmD9/FqJHvZLmCipVRt6v1PM8cRUYIbhEswyNKGHVoC4eHZGPSnD+bOf5A3+gnbt0A5/A==", - "dependencies": { - "iterall": "^1.3.0", - "tslib": "^2.0.3" - }, - "peerDependencies": { - "graphql": "15.x || 16.x" - } - }, - "node_modules/nexus/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" - }, - "node_modules/node-dijkstra": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/node-dijkstra/-/node-dijkstra-2.5.0.tgz", - "integrity": "sha512-2REYb1lo8yDRAY1gsdhjwQdGVSh47VI5Z5wS8sCqydO/P1OVAKOwLuV1fDxNLvbrtMspW7k2UZ2JKIKP06hl+A==" - }, - "node_modules/node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-releases": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", - "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==" - }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "engines": { - "node": "*" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" - }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", - "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/promise-timeout": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/promise-timeout/-/promise-timeout-1.3.0.tgz", - "integrity": "sha512-5yANTE0tmi5++POym6OgtFmwfDvOXABD9oj/jLQr5GPEyuNEb7jH4wbbANJceJid49jwhi1RddxnhnEAb/doqg==" - }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" - }, - "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/querystring": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", - "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/ramda": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.2.tgz", - "integrity": "sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", - "dependencies": { - "resolve": "^1.1.6" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/redis": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz", - "integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==", - "dependencies": { - "denque": "^1.5.0", - "redis-commands": "^1.7.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-redis" - } - }, - "node_modules/redis-commands": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", - "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==" - }, - "node_modules/redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", - "engines": { - "node": ">=4" - } - }, - "node_modules/redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "dependencies": { - "redis-errors": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" - }, - "node_modules/regenerator-transform": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", - "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, - "node_modules/regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", - "dependencies": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "dependencies": { - "jsesc": "~0.5.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "bin": { - "jsesc": "bin/jsesc" - } - }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/request-promise": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz", - "integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==", - "deprecated": "request-promise has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142", - "dependencies": { - "bluebird": "^3.5.0", - "request-promise-core": "1.1.4", - "stealthy-require": "^1.1.1", - "tough-cookie": "^2.3.3" - }, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "request": "^2.34" - } - }, - "node_modules/request-promise-core": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", - "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", - "dependencies": { - "lodash": "^4.17.19" - }, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "request": "^2.34" - } - }, - "node_modules/request/node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/request/node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/resolve": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.3.tgz", - "integrity": "sha512-P8ur/gp/AmbEzjr729bZnLjXK5Z+4P0zhIJgBgzqRih7hL7BOukHGtSTA3ACMY467GRFz3duQsi0bDZdR7DKdw==", - "dependencies": { - "is-core-module": "^2.12.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/seek-bzip": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", - "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", - "dependencies": { - "commander": "^2.8.1" - }, - "bin": { - "seek-bunzip": "bin/seek-bunzip", - "seek-table": "bin/seek-bzip-table" - } - }, - "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "engines": { - "node": ">=8" - } - }, - "node_modules/shelljs": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", - "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", - "dependencies": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - }, - "bin": { - "shjs": "bin/shjs" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/split2": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", - "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", - "dependencies": { - "through2": "^2.0.2" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, - "node_modules/sqlstring": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", - "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/standard-as-callback": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-dirs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", - "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", - "dependencies": { - "is-natural-number": "^4.0.1" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/syntax-error": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", - "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", - "dependencies": { - "acorn-node": "^1.2.0" - } - }, - "node_modules/tar": { - "version": "6.1.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", - "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^4.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", - "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", - "dependencies": { - "bl": "^1.0.0", - "buffer-alloc": "^1.2.0", - "end-of-stream": "^1.0.0", - "fs-constants": "^1.0.0", - "readable-stream": "^2.3.0", - "to-buffer": "^1.1.1", - "xtend": "^4.0.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/tar-stream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/tar-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/tar-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/temp-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", - "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/tempy": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-1.0.1.tgz", - "integrity": "sha512-biM9brNqxSc04Ee71hzFbryD11nX7VPhQQY32AdDmjFvodsRFz/3ufeoTZ6uYkRFfGo188tENcASNs3vTdsM0w==", - "dependencies": { - "del": "^6.0.0", - "is-stream": "^2.0.0", - "temp-dir": "^2.0.0", - "type-fest": "^0.16.0", - "unique-string": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/textextensions": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-2.6.0.tgz", - "integrity": "sha512-49WtAWS+tcsy93dRt6P0P3AMD2m5PvXRhuEA0kaXos5ZLlujtYmpmFsB+QvWUSxE1ZsstmYXfQ7L40+EcQgpAQ==", - "engines": { - "node": ">=0.8" - }, - "funding": { - "url": "https://bevry.me/fund" - } - }, - "node_modules/throttle-debounce": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", - "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", - "engines": { - "node": ">=10" - } - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" - }, - "node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/through2/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/through2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/through2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" - }, - "node_modules/type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" - }, - "node_modules/type-fest": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", - "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unbzip2-stream": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", - "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", - "dependencies": { - "buffer": "^5.2.1", - "through": "^2.3.8" - } - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "engines": { - "node": ">=4" - } - }, - "node_modules/unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "dependencies": { - "crypto-random-string": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - }, - "dependencies": { - "@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", - "requires": { - "@babel/highlight": "^7.18.6" - } - }, - "@babel/compat-data": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.4.tgz", - "integrity": "sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g==" - }, - "@babel/core": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.4.tgz", - "integrity": "sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==", - "requires": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.4", - "@babel/helper-compilation-targets": "^7.21.4", - "@babel/helper-module-transforms": "^7.21.2", - "@babel/helpers": "^7.21.0", - "@babel/parser": "^7.21.4", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.4", - "@babel/types": "^7.21.4", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" - } - }, - "@babel/generator": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz", - "integrity": "sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA==", - "requires": { - "@babel/types": "^7.21.4", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", - "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", - "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", - "requires": { - "@babel/helper-explode-assignable-expression": "^7.18.6", - "@babel/types": "^7.18.9" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.4.tgz", - "integrity": "sha512-Fa0tTuOXZ1iL8IeDFUWCzjZcn+sJGd9RZdH9esYVjEejGmzf+FFYQpMi/kZUk2kPy/q1H3/GPw7np8qar/stfg==", - "requires": { - "@babel/compat-data": "^7.21.4", - "@babel/helper-validator-option": "^7.21.0", - "browserslist": "^4.21.3", - "lru-cache": "^5.1.1", - "semver": "^6.3.0" - } - }, - "@babel/helper-create-class-features-plugin": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.4.tgz", - "integrity": "sha512-46QrX2CQlaFRF4TkwfTt6nJD7IHq8539cCL7SDpqWSDeJKY1xylKKY5F/33mJhLZ3mFvKv2gGrVS6NkyF6qs+Q==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-member-expression-to-functions": "^7.21.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-replace-supers": "^7.20.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/helper-split-export-declaration": "^7.18.6" - } - }, - "@babel/helper-create-regexp-features-plugin": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.4.tgz", - "integrity": "sha512-M00OuhU+0GyZ5iBBN9czjugzWrEq2vDpf/zCYHxxf93ul/Q5rv+a5h+/+0WnI1AebHNVtl5bFV0qsJoH23DbfA==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "regexpu-core": "^5.3.1" - } - }, - "@babel/helper-define-polyfill-provider": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", - "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", - "requires": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - } - }, - "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==" - }, - "@babel/helper-explode-assignable-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", - "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", - "requires": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz", - "integrity": "sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==", - "requires": { - "@babel/types": "^7.21.0" - } - }, - "@babel/helper-module-imports": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz", - "integrity": "sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==", - "requires": { - "@babel/types": "^7.21.4" - } - }, - "@babel/helper-module-transforms": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz", - "integrity": "sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==", - "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.20.2", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.2", - "@babel/types": "^7.21.2" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", - "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==" - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", - "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-wrap-function": "^7.18.9", - "@babel/types": "^7.18.9" - } - }, - "@babel/helper-replace-supers": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz", - "integrity": "sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==", - "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-member-expression-to-functions": "^7.20.7", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.7", - "@babel/types": "^7.20.7" - } - }, - "@babel/helper-simple-access": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", - "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", - "requires": { - "@babel/types": "^7.20.2" - } - }, - "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", - "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", - "requires": { - "@babel/types": "^7.20.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==" - }, - "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" - }, - "@babel/helper-validator-option": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", - "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==" - }, - "@babel/helper-wrap-function": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", - "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", - "requires": { - "@babel/helper-function-name": "^7.19.0", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.5", - "@babel/types": "^7.20.5" - } - }, - "@babel/helpers": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz", - "integrity": "sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==", - "requires": { - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.0", - "@babel/types": "^7.21.0" - } - }, - "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", - "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==" - }, - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", - "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz", - "integrity": "sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-proposal-optional-chaining": "^7.20.7" - } - }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", - "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", - "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" - } - }, - "@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "requires": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-proposal-class-static-block": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", - "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", - "requires": { - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - } - }, - "@babel/plugin-proposal-dynamic-import": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", - "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - } - }, - "@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", - "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - } - }, - "@babel/plugin-proposal-json-strings": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", - "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-json-strings": "^7.8.3" - } - }, - "@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", - "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", - "requires": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - } - }, - "@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", - "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - } - }, - "@babel/plugin-proposal-numeric-separator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", - "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - } - }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", - "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", - "requires": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.20.7" - } - }, - "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", - "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - } - }, - "@babel/plugin-proposal-optional-chaining": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", - "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", - "requires": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - } - }, - "@babel/plugin-proposal-private-methods": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", - "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", - "requires": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz", - "integrity": "sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - } - }, - "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-syntax-import-assertions": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", - "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.19.0" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz", - "integrity": "sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.20.2" - } - }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", - "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", - "requires": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-remap-async-to-generator": "^7.18.9" - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", - "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz", - "integrity": "sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.20.2" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz", - "integrity": "sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-replace-supers": "^7.20.7", - "@babel/helper-split-export-declaration": "^7.18.6", - "globals": "^11.1.0" - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz", - "integrity": "sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/template": "^7.20.7" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz", - "integrity": "sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==", - "requires": { - "@babel/helper-plugin-utils": "^7.20.2" - } - }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", - "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", - "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", - "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz", - "integrity": "sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.20.2" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", - "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", - "requires": { - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", - "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-member-expression-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", - "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz", - "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==", - "requires": { - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helper-plugin-utils": "^7.20.2" - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz", - "integrity": "sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA==", - "requires": { - "@babel/helper-module-transforms": "^7.21.2", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-simple-access": "^7.20.2" - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz", - "integrity": "sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==", - "requires": { - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-identifier": "^7.19.1" - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", - "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", - "requires": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz", - "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.20.5", - "@babel/helper-plugin-utils": "^7.20.2" - } - }, - "@babel/plugin-transform-new-target": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", - "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", - "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.6" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz", - "integrity": "sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.20.2" - } - }, - "@babel/plugin-transform-property-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", - "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz", - "integrity": "sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.20.2", - "regenerator-transform": "^0.15.1" - } - }, - "@babel/plugin-transform-reserved-words": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", - "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", - "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz", - "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==", - "requires": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", - "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", - "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", - "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-unicode-escapes": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", - "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", - "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/preset-env": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.21.4.tgz", - "integrity": "sha512-2W57zHs2yDLm6GD5ZpvNn71lZ0B/iypSdIeq25OurDKji6AdzV07qp4s3n1/x5BqtiGaTrPN3nerlSCaC5qNTw==", - "requires": { - "@babel/compat-data": "^7.21.4", - "@babel/helper-compilation-targets": "^7.21.4", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-option": "^7.21.0", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.20.7", - "@babel/plugin-proposal-async-generator-functions": "^7.20.7", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-class-static-block": "^7.21.0", - "@babel/plugin-proposal-dynamic-import": "^7.18.6", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", - "@babel/plugin-proposal-json-strings": "^7.18.6", - "@babel/plugin-proposal-logical-assignment-operators": "^7.20.7", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", - "@babel/plugin-proposal-numeric-separator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.20.7", - "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", - "@babel/plugin-proposal-optional-chaining": "^7.21.0", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-private-property-in-object": "^7.21.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.20.0", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.20.7", - "@babel/plugin-transform-async-to-generator": "^7.20.7", - "@babel/plugin-transform-block-scoped-functions": "^7.18.6", - "@babel/plugin-transform-block-scoping": "^7.21.0", - "@babel/plugin-transform-classes": "^7.21.0", - "@babel/plugin-transform-computed-properties": "^7.20.7", - "@babel/plugin-transform-destructuring": "^7.21.3", - "@babel/plugin-transform-dotall-regex": "^7.18.6", - "@babel/plugin-transform-duplicate-keys": "^7.18.9", - "@babel/plugin-transform-exponentiation-operator": "^7.18.6", - "@babel/plugin-transform-for-of": "^7.21.0", - "@babel/plugin-transform-function-name": "^7.18.9", - "@babel/plugin-transform-literals": "^7.18.9", - "@babel/plugin-transform-member-expression-literals": "^7.18.6", - "@babel/plugin-transform-modules-amd": "^7.20.11", - "@babel/plugin-transform-modules-commonjs": "^7.21.2", - "@babel/plugin-transform-modules-systemjs": "^7.20.11", - "@babel/plugin-transform-modules-umd": "^7.18.6", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.20.5", - "@babel/plugin-transform-new-target": "^7.18.6", - "@babel/plugin-transform-object-super": "^7.18.6", - "@babel/plugin-transform-parameters": "^7.21.3", - "@babel/plugin-transform-property-literals": "^7.18.6", - "@babel/plugin-transform-regenerator": "^7.20.5", - "@babel/plugin-transform-reserved-words": "^7.18.6", - "@babel/plugin-transform-shorthand-properties": "^7.18.6", - "@babel/plugin-transform-spread": "^7.20.7", - "@babel/plugin-transform-sticky-regex": "^7.18.6", - "@babel/plugin-transform-template-literals": "^7.18.9", - "@babel/plugin-transform-typeof-symbol": "^7.18.9", - "@babel/plugin-transform-unicode-escapes": "^7.18.10", - "@babel/plugin-transform-unicode-regex": "^7.18.6", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.21.4", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "core-js-compat": "^3.25.1", - "semver": "^6.3.0" - } - }, - "@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - } - }, - "@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" - }, - "@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", - "requires": { - "regenerator-runtime": "^0.13.11" - } - }, - "@babel/standalone": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.21.4.tgz", - "integrity": "sha512-Rw4nGqH/iyVeYxARKcz7iGP+njkPsVqJ45TmXMONoGoxooWjXCAs+CUcLeAZdBGCLqgaPvHKCYvIaDT2Iq+KfA==" - }, - "@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" - } - }, - "@babel/traverse": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.4.tgz", - "integrity": "sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q==", - "requires": { - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.4", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.4", - "@babel/types": "^7.21.4", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", - "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", - "to-fast-properties": "^2.0.0" - } - }, - "@cubejs-backend/api-gateway": { - "version": "0.32.27", - "resolved": "https://registry.npmjs.org/@cubejs-backend/api-gateway/-/api-gateway-0.32.27.tgz", - "integrity": "sha512-l4joKt9f7e5VImaw4G+tVZrvpO2G67J/0fjvIj2fCwk3tRlNqTqrjADnofykkCeR1LntB8miipj2bdZMrECGog==", - "requires": { - "@cubejs-backend/native": "^0.32.27", - "@cubejs-backend/shared": "^0.32.25", - "@ungap/structured-clone": "^0.3.4", - "body-parser": "^1.19.0", - "chrono-node": "^2.6.2", - "express-graphql": "^0.12.0", - "graphql": "^15.8.0", - "graphql-scalars": "^1.10.0", - "joi": "^17.8.3", - "jsonwebtoken": "^8.3.0", - "jwk-to-pem": "^2.0.4", - "moment": "^2.24.0", - "moment-timezone": "^0.5.27", - "nexus": "^1.1.0", - "node-fetch": "^2.6.1", - "querystring": "^0.2.1", - "ramda": "^0.27.0", - "uuid": "^8.3.2" - } - }, - "@cubejs-backend/base-driver": { - "version": "0.32.26", - "resolved": "https://registry.npmjs.org/@cubejs-backend/base-driver/-/base-driver-0.32.26.tgz", - "integrity": "sha512-OVplhEEdSGHYxgGbNEmN9IXieZbaEjfLgi8mBmlr9OTC1b3cecFHrV8tvRRqi6Emi8ZLtsMjmQx4LOcv7fVn/A==", - "requires": { - "@cubejs-backend/shared": "^0.32.25", - "ramda": "^0.27.0" - } - }, - "@cubejs-backend/cloud": { - "version": "0.32.25", - "resolved": "https://registry.npmjs.org/@cubejs-backend/cloud/-/cloud-0.32.25.tgz", - "integrity": "sha512-bk+3kAE4PKIBI7by8utyKTpA9zJJwBZLvRe8Ica0V+z+pFSEooPu4Pnp2hcoalqZ0GMKQkyRDZzbGu7zTVUK7w==", - "requires": { - "@cubejs-backend/dotenv": "^9.0.2", - "@cubejs-backend/shared": "^0.32.25", - "chokidar": "^3.5.1", - "env-var": "^6.3.0", - "fs-extra": "^9.1.0", - "jsonwebtoken": "^8.5.1", - "request": "^2.88.2", - "request-promise": "^4.2.5" - }, - "dependencies": { - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" - } - } - }, - "@cubejs-backend/cubesql": { - "version": "0.32.19", - "resolved": "https://registry.npmjs.org/@cubejs-backend/cubesql/-/cubesql-0.32.19.tgz", - "integrity": "sha512-b605Vw2OeFkn0dQmHd9FR2uEHzFCLlduTIez/4WpWw7MilcX1T810AMXIdwxbn9DPl/ILc5MvHn9hZ74SqE4dw==" - }, - "@cubejs-backend/cubestore": { - "version": "0.32.25", - "resolved": "https://registry.npmjs.org/@cubejs-backend/cubestore/-/cubestore-0.32.25.tgz", - "integrity": "sha512-k22AR9znDL8vcKDe6ieIg9cbSRzfnW+lSAd+v+0R0nJ8Z/8SU0lXOzTecWkOmihJ4U+Gxqd1tueazzEO2BRfWQ==", - "requires": { - "@cubejs-backend/shared": "^0.32.25", - "@octokit/core": "^3.2.5", - "source-map-support": "^0.5.19" - } - }, - "@cubejs-backend/cubestore-driver": { - "version": "0.32.26", - "resolved": "https://registry.npmjs.org/@cubejs-backend/cubestore-driver/-/cubestore-driver-0.32.26.tgz", - "integrity": "sha512-WIBE8NUAdCQds2Rue4wL27cIO3esK5n+Y3DP43M72iw6/BP9f09VlbgQJORWtY9mF0g1P0ZEwwFhHvAR0yEfKA==", - "requires": { - "@cubejs-backend/base-driver": "^0.32.26", - "@cubejs-backend/cubestore": "^0.32.25", - "@cubejs-backend/shared": "^0.32.25", - "csv-write-stream": "^2.0.0", - "flatbuffers": "23.3.3", - "fs-extra": "^9.1.0", - "generic-pool": "^3.6.0", - "moment-timezone": "^0.5.31", - "node-fetch": "^2.6.1", - "sqlstring": "^2.3.3", - "tempy": "^1.0.1", - "uuid": "^8.3.2", - "ws": "^7.4.3" - }, - "dependencies": { - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" - } - } - }, - "@cubejs-backend/dotenv": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@cubejs-backend/dotenv/-/dotenv-9.0.2.tgz", - "integrity": "sha512-yC1juhXEjM7K97KfXubDm7WGipd4Lpxe+AT8XeTRE9meRULrKlw0wtE2E8AQkGOfTBn+P1SCkePQ/BzIbOh1VA==" - }, - "@cubejs-backend/native": { - "version": "0.32.27", - "resolved": "https://registry.npmjs.org/@cubejs-backend/native/-/native-0.32.27.tgz", - "integrity": "sha512-LIqsSH6sprAqHbQdLcw6bqGtYKyaCf1TiqPx8OmFQp7GgNudkyk5ZfeXPWUhCnCnZX+q+A/K9EcDzxgY6GvLuQ==", - "requires": { - "@cubejs-backend/cubesql": "^0.32.19", - "@cubejs-backend/shared": "^0.32.25", - "@mapbox/node-pre-gyp": "^1" - } - }, - "@cubejs-backend/query-orchestrator": { - "version": "0.32.27", - "resolved": "https://registry.npmjs.org/@cubejs-backend/query-orchestrator/-/query-orchestrator-0.32.27.tgz", - "integrity": "sha512-+FQt2bm5trfpmsXY8sLT0fYKqSfStNGSzsf3RFOdWZ4vofiBg8psNMQlKF8DlFlVtuKcNPSk/hcmPqce+heagg==", - "requires": { - "@cubejs-backend/base-driver": "^0.32.26", - "@cubejs-backend/cubestore-driver": "^0.32.26", - "@cubejs-backend/shared": "^0.32.25", - "csv-write-stream": "^2.0.0", - "es5-ext": "0.10.53", - "generic-pool": "^3.7.1", - "ioredis": "^4.27.8", - "lru-cache": "^6.0.0", - "moment-range": "^4.0.2", - "moment-timezone": "^0.5.33", - "ramda": "^0.27.2", - "redis": "^3.0.2" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } - }, - "@cubejs-backend/schema-compiler": { - "version": "0.32.27", - "resolved": "https://registry.npmjs.org/@cubejs-backend/schema-compiler/-/schema-compiler-0.32.27.tgz", - "integrity": "sha512-klV7r6uR6+7HJaINBHBdKaxlMGc/Hz10VMhFG8acYSz2zvFpQ+vDbcvnNbi0dBLpjQfCoWeJfpbxjrVL5XXvsQ==", - "requires": { - "@babel/code-frame": "^7.12.11", - "@babel/core": "^7.12.10", - "@babel/generator": "^7.12.10", - "@babel/parser": "^7.12.10", - "@babel/preset-env": "^7.12.10", - "@babel/standalone": "^7.12.10", - "@babel/traverse": "^7.12.10", - "@babel/types": "^7.12.12", - "@cubejs-backend/shared": "^0.32.25", - "antlr4ts": "0.5.0-alpha.4", - "camelcase": "^6.2.0", - "cron-parser": "^3.5.0", - "humps": "^2.0.1", - "inflection": "^1.12.0", - "joi": "^17.8.3", - "js-yaml": "^4.1.0", - "lru-cache": "^5.1.1", - "moment-range": "^4.0.1", - "moment-timezone": "^0.5.33", - "node-dijkstra": "^2.5.0", - "ramda": "^0.27.2", - "syntax-error": "^1.3.0" - } - }, - "@cubejs-backend/server-core": { - "version": "0.32.27", - "resolved": "https://registry.npmjs.org/@cubejs-backend/server-core/-/server-core-0.32.27.tgz", - "integrity": "sha512-vlAhRFScey4tlDU5mR9jZEitod1A5Um/+8dx/sD4zJ5YrZn3o34AneC8vD8ERpuM1ZGhuiEix3sW0hrJ7isc6w==", - "requires": { - "@cubejs-backend/api-gateway": "^0.32.27", - "@cubejs-backend/cloud": "^0.32.25", - "@cubejs-backend/dotenv": "^9.0.2", - "@cubejs-backend/query-orchestrator": "^0.32.27", - "@cubejs-backend/schema-compiler": "^0.32.27", - "@cubejs-backend/shared": "^0.32.25", - "@cubejs-backend/templates": "^0.32.25", - "codesandbox-import-utils": "^2.1.12", - "cross-spawn": "^7.0.1", - "fs-extra": "^8.1.0", - "is-docker": "^2.1.1", - "joi": "^17.8.3", - "jsonwebtoken": "^8.4.0", - "lodash.clonedeep": "^4.5.0", - "lru-cache": "^5.1.1", - "moment": "^2.29.1", - "node-fetch": "^2.6.0", - "p-limit": "^3.1.0", - "promise-timeout": "^1.3.0", - "ramda": "^0.27.0", - "semver": "^6.3.0", - "serve-static": "^1.13.2", - "sqlstring": "^2.3.1", - "uuid": "^8.3.2", - "ws": "^7.5.3" - } - }, - "@cubejs-backend/shared": { - "version": "0.32.25", - "resolved": "https://registry.npmjs.org/@cubejs-backend/shared/-/shared-0.32.25.tgz", - "integrity": "sha512-C7FJYHf3COPVRa/EUAkw3csqndUOrhg7AEBbqBmZvW3RNFfV/I7IJhpd4GXJqe/hlWt5Af0KzNu+oN4qttEXKQ==", - "requires": { - "@oclif/color": "^0.1.2", - "bytes": "^3.1.0", - "cli-progress": "^3.9.0", - "cross-spawn": "^7.0.3", - "decompress": "^4.2.1", - "env-var": "^6.3.0", - "fs-extra": "^9.1.0", - "http-proxy-agent": "^4.0.1", - "moment-range": "^4.0.1", - "moment-timezone": "^0.5.33", - "node-fetch": "^2.6.1", - "shelljs": "^0.8.5", - "throttle-debounce": "^3.0.1", - "uuid": "^8.3.2" - }, - "dependencies": { - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" - } - } - }, - "@cubejs-backend/templates": { - "version": "0.32.25", - "resolved": "https://registry.npmjs.org/@cubejs-backend/templates/-/templates-0.32.25.tgz", - "integrity": "sha512-MH5b/gtw0cWSmVDFd7BkoryjYjNaqfKSVThO8l0zy7quOote+gEN0BMYP9UGv1qEP/G2zrosrqZ9juISzk8hrQ==", - "requires": { - "@cubejs-backend/shared": "^0.32.25", - "cross-spawn": "^7.0.3", - "decompress": "^4.2.1", - "decompress-targz": "^4.1.1", - "fs-extra": "^9.1.0", - "node-fetch": "^2.6.1", - "ramda": "^0.27.2", - "source-map-support": "^0.5.19" - }, - "dependencies": { - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" - } - } - }, - "@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" - }, - "@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "requires": { - "@hapi/hoek": "^9.0.0" - } - }, - "@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" - }, - "@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - }, - "dependencies": { - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" - } - } - }, - "@mapbox/node-pre-gyp": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", - "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", - "requires": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@oclif/color": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@oclif/color/-/color-0.1.2.tgz", - "integrity": "sha512-M9o+DOrb8l603qvgz1FogJBUGLqcMFL1aFg2ZEL0FbXJofiNTLOWIeB4faeZTLwE6dt0xH9GpCVpzksMMzGbmA==", - "requires": { - "ansi-styles": "^3.2.1", - "chalk": "^3.0.0", - "strip-ansi": "^5.2.0", - "supports-color": "^5.4.0", - "tslib": "^1" - }, - "dependencies": { - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - } - } - }, - "@octokit/auth-token": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", - "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", - "requires": { - "@octokit/types": "^6.0.3" - } - }, - "@octokit/core": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz", - "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", - "requires": { - "@octokit/auth-token": "^2.4.4", - "@octokit/graphql": "^4.5.8", - "@octokit/request": "^5.6.3", - "@octokit/request-error": "^2.0.5", - "@octokit/types": "^6.0.3", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/endpoint": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", - "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", - "requires": { - "@octokit/types": "^6.0.3", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/graphql": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", - "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", - "requires": { - "@octokit/request": "^5.6.0", - "@octokit/types": "^6.0.3", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/openapi-types": { - "version": "12.11.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", - "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" - }, - "@octokit/request": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", - "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", - "requires": { - "@octokit/endpoint": "^6.0.1", - "@octokit/request-error": "^2.1.0", - "@octokit/types": "^6.16.1", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/request-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", - "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", - "requires": { - "@octokit/types": "^6.0.3", - "deprecation": "^2.0.0", - "once": "^1.4.0" - } - }, - "@octokit/types": { - "version": "6.41.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", - "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", - "requires": { - "@octokit/openapi-types": "^12.11.0" - } - }, - "@sideway/address": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", - "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", - "requires": { - "@hapi/hoek": "^9.0.0" - } - }, - "@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" - }, - "@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" - }, - "@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" - }, - "@ungap/structured-clone": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-0.3.4.tgz", - "integrity": "sha512-TSVh8CpnwNAsPC5wXcIyh92Bv1gq6E9cNDeeLu7Z4h8V4/qWtXJp7y42qljRkqcpmsve1iozwv1wr+3BNdILCg==" - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - } - }, - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" - }, - "acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "requires": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - } - }, - "acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "requires": { - "debug": "4" - } - }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "antlr4ts": { - "version": "0.5.0-alpha.4", - "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", - "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==" - }, - "anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" - }, - "are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" - }, - "asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==" - }, - "aws4": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" - }, - "babel-plugin-polyfill-corejs2": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", - "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", - "requires": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.3.3", - "semver": "^6.1.1" - } - }, - "babel-plugin-polyfill-corejs3": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", - "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", - "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.3", - "core-js-compat": "^3.25.1" - } - }, - "babel-plugin-polyfill-regenerator": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", - "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", - "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.3" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "before-after-hook": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", - "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" - }, - "binaryextensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-2.3.0.tgz", - "integrity": "sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==" - }, - "bl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", - "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", - "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - }, - "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { - "fill-range": "^7.0.1" - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" - }, - "browserslist": { - "version": "4.21.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", - "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", - "requires": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" - } - }, - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "requires": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" - }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==" - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" - }, - "caniuse-lite": { - "version": "1.0.30001480", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001480.tgz", - "integrity": "sha512-q7cpoPPvZYgtyC4VaBSN0Bt+PJ4c4EYRf0DrduInOz2SkFpHD5p3LnvEpqBp7UnJn+8x1Ogl1s38saUxe+ihQQ==" - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" - }, - "chrono-node": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/chrono-node/-/chrono-node-2.6.3.tgz", - "integrity": "sha512-VkWaaZnNulqzNH9i4XCdyI05OX6MFEnCMNKdZOR4w//wS5/E2qkwAss/O5sj6SfTZK84fX4SSOG4pzqjqIseiA==", - "requires": { - "dayjs": "^1.10.0" - } - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" - }, - "cli-progress": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", - "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", - "requires": { - "string-width": "^4.2.3" - } - }, - "cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==" - }, - "codesandbox-import-util-types": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/codesandbox-import-util-types/-/codesandbox-import-util-types-2.2.3.tgz", - "integrity": "sha512-Qj00p60oNExthP2oR3vvXmUGjukij+rxJGuiaKM6tyUmSyimdZsqHI/TUvFFClAffk9s7hxGnQgWQ8KCce27qQ==" - }, - "codesandbox-import-utils": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/codesandbox-import-utils/-/codesandbox-import-utils-2.2.3.tgz", - "integrity": "sha512-ymtmcgZKU27U+nM2qUb21aO8Ut/u2S9s6KorOgG81weP+NA0UZkaHKlaRqbLJ9h4i/4FLvwmEXYAnTjNmp6ogg==", - "requires": { - "codesandbox-import-util-types": "^2.2.3", - "istextorbinary": "^2.2.1", - "lz-string": "^1.4.4" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" - }, - "content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" - }, - "convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" - }, - "core-js-compat": { - "version": "3.30.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.30.1.tgz", - "integrity": "sha512-d690npR7MC6P0gq4npTl5n2VQeNAmUrJ90n+MHiKS7W2+xno4o3F5GDEuylSdi6EJ3VssibSGXOa1r3YXD3Mhw==", - "requires": { - "browserslist": "^4.21.5" - } - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" - }, - "cron-parser": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-3.5.0.tgz", - "integrity": "sha512-wyVZtbRs6qDfFd8ap457w3XVntdvqcwBGxBoTvJQH9KGVKL/fB+h2k3C8AqiVxvUQKN1Ps/Ns46CNViOpVDhfQ==", - "requires": { - "is-nan": "^1.3.2", - "luxon": "^1.26.0" - } - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" - }, - "csv-write-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/csv-write-stream/-/csv-write-stream-2.0.0.tgz", - "integrity": "sha512-QTraH6FOYfM5f+YGwx71hW1nR9ZjlWri67/D4CWtiBkdce0UAa91Vc0yyHg0CjC0NeEGnvO/tBSJkA1XF9D9GQ==", - "requires": { - "argparse": "^1.0.7", - "generate-object-property": "^1.0.0", - "ndjson": "^1.3.0" - } - }, - "d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "requires": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "dayjs": { - "version": "1.11.7", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", - "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "decompress": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", - "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", - "requires": { - "decompress-tar": "^4.0.0", - "decompress-tarbz2": "^4.0.0", - "decompress-targz": "^4.0.0", - "decompress-unzip": "^4.0.1", - "graceful-fs": "^4.1.10", - "make-dir": "^1.0.0", - "pify": "^2.3.0", - "strip-dirs": "^2.0.0" - }, - "dependencies": { - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "requires": { - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==" - } - } - } - } - }, - "decompress-tar": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", - "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", - "requires": { - "file-type": "^5.2.0", - "is-stream": "^1.1.0", - "tar-stream": "^1.5.2" - } - }, - "decompress-tarbz2": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", - "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", - "requires": { - "decompress-tar": "^4.1.0", - "file-type": "^6.1.0", - "is-stream": "^1.1.0", - "seek-bzip": "^1.0.5", - "unbzip2-stream": "^1.0.9" - }, - "dependencies": { - "file-type": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", - "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==" - } - } - }, - "decompress-targz": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", - "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", - "requires": { - "decompress-tar": "^4.1.1", - "file-type": "^5.2.0", - "is-stream": "^1.1.0" - } - }, - "decompress-unzip": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", - "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", - "requires": { - "file-type": "^3.8.0", - "get-stream": "^2.2.0", - "pify": "^2.3.0", - "yauzl": "^2.4.2" - }, - "dependencies": { - "file-type": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", - "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==" - } - } - }, - "define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - }, - "del": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", - "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", - "requires": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" - }, - "dependencies": { - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "requires": { - "aggregate-error": "^3.0.0" - } - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" - }, - "denque": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", - "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==" - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" - }, - "destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" - }, - "detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "requires": { - "path-type": "^4.0.0" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "editions": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/editions/-/editions-2.3.1.tgz", - "integrity": "sha512-ptGvkwTvGdGfC0hfhKg0MT+TRLRKGtUiWGBInxOm5pz7ssADezahjCUaYuZ8Dr+C05FW0AECIIPt4WBxVINEhA==", - "requires": { - "errlop": "^2.0.0", - "semver": "^6.3.0" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "electron-to-chromium": { - "version": "1.4.365", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.365.tgz", - "integrity": "sha512-FRHZO+1tUNO4TOPXmlxetkoaIY8uwHzd1kKopK/Gx2SKn1L47wJXWD44wxP5CGRyyP98z/c8e1eBzJrgPeiBOg==" - }, - "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "requires": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { - "once": "^1.4.0" - } - }, - "env-var": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/env-var/-/env-var-6.3.0.tgz", - "integrity": "sha512-gaNzDZuVaJQJlP2SigAZLu/FieZN5MzdN7lgHNehESwlRanHwGQ/WUtJ7q//dhrj3aGBZM45yEaKOuvSJaf4mA==" - }, - "errlop": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/errlop/-/errlop-2.2.0.tgz", - "integrity": "sha512-e64Qj9+4aZzjzzFpZC7p5kmm/ccCrbLhAJplhsDXQFs87XTsXwOpH4s1Io2s90Tau/8r2j9f4l/thhDevRjzxw==" - }, - "es5-ext": { - "version": "0.10.53", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", - "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", - "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.3", - "next-tick": "~1.0.0" - } - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", - "requires": { - "d": "^1.0.1", - "ext": "^1.1.2" - } - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" - }, - "express-graphql": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/express-graphql/-/express-graphql-0.12.0.tgz", - "integrity": "sha512-DwYaJQy0amdy3pgNtiTDuGGM2BLdj+YO2SgbKoLliCfuHv3VVTt7vNG/ZqK2hRYjtYHE2t2KB705EU94mE64zg==", - "requires": { - "accepts": "^1.3.7", - "content-type": "^1.0.4", - "http-errors": "1.8.0", - "raw-body": "^2.4.1" - }, - "dependencies": { - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==" - }, - "http-errors": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", - "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==" - }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" - } - } - }, - "ext": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", - "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", - "requires": { - "type": "^2.7.2" - }, - "dependencies": { - "type": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", - "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "requires": { - "reusify": "^1.0.4" - } - }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "requires": { - "pend": "~1.2.0" - } - }, - "file-type": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==" - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "flatbuffers": { - "version": "23.3.3", - "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-23.3.3.tgz", - "integrity": "sha512-jmreOaAT1t55keaf+Z259Tvh8tR/Srry9K8dgCgvizhKSEr6gLGgaOJI2WFL5fkOpGOGRZwxUrlFn0GCmXUy6g==" - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==" - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "requires": { - "minipass": "^3.0.0" - }, - "dependencies": { - "minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "requires": { - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "requires": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, - "generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha512-TuOwZWgJ2VAMEGJvAyPWvpqxSANF0LDpmyHauMjFYzaACvn+QTT/AZomvPCzVBV7yDN3OmwHQ5OvHaeLKre3JQ==", - "requires": { - "is-property": "^1.0.0" - } - }, - "generic-pool": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", - "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==" - }, - "gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" - }, - "get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - } - }, - "get-stream": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", - "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", - "requires": { - "object-assign": "^4.0.1", - "pinkie-promise": "^2.0.0" - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "requires": { - "is-glob": "^4.0.1" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "graphql": { - "version": "15.8.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz", - "integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==" - }, - "graphql-scalars": { - "version": "1.21.3", - "resolved": "https://registry.npmjs.org/graphql-scalars/-/graphql-scalars-1.21.3.tgz", - "integrity": "sha512-QLWw3BHmqHZMp9JeYmPpjq7JT9aw/H8TpwmWKJEuMSE3+O7Xe7TduQbOLFzbs1q9UxX6CVkc0O1JO/YfkP/pAw==", - "requires": { - "tslib": "^2.5.0" - }, - "dependencies": { - "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==" - }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" - }, - "has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "requires": { - "get-intrinsic": "^1.1.1" - } - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" - }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "humps": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz", - "integrity": "sha512-E0eIbrFWUhwfXJmsbdjRQFQPrl5pTEoKlz163j1mTqqUnU9PgR4AgB8AIITzuB3vLBdxZXyZ9TDIrwB2OASz4g==" - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==" - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" - }, - "inflection": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", - "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==" - }, - "ioredis": { - "version": "4.28.5", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz", - "integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==", - "requires": { - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.1", - "denque": "^1.1.0", - "lodash.defaults": "^4.2.0", - "lodash.flatten": "^4.4.0", - "lodash.isarguments": "^3.1.0", - "p-map": "^2.1.0", - "redis-commands": "1.7.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - } - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-core-module": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", - "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", - "requires": { - "has": "^1.0.3" - } - }, - "is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==" - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - } - }, - "is-natural-number": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", - "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==" - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==" - }, - "is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==" - }, - "is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" - }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==" - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" - }, - "istextorbinary": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-2.6.0.tgz", - "integrity": "sha512-+XRlFseT8B3L9KyjxxLjfXSLMuErKDsd8DBNrsaxoViABMEZlOSCstwmw0qpoFX3+U6yWU1yhLudAe6/lETGGA==", - "requires": { - "binaryextensions": "^2.1.2", - "editions": "^2.2.0", - "textextensions": "^2.5.0" - } - }, - "iterall": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", - "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==" - }, - "joi": { - "version": "17.9.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.9.1.tgz", - "integrity": "sha512-FariIi9j6QODKATGBrEX7HZcja8Bsh3rfdGYy/Sb65sGlZWK/QWesU1ghk7aJWDj95knjXlQfSmzFSPPkLVsfw==", - "requires": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "requires": { - "argparse": "^2.0.1" - }, - "dependencies": { - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - } - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" - }, - "json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" - }, - "json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsonwebtoken": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", - "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", - "requires": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^5.6.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - } - } - }, - "jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - } - }, - "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jwk-to-pem": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz", - "integrity": "sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A==", - "requires": { - "asn1.js": "^5.3.0", - "elliptic": "^6.5.4", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" - }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" - }, - "lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" - }, - "lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" - }, - "lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" - }, - "lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, - "lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" - }, - "lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "requires": { - "yallist": "^3.0.2" - } - }, - "luxon": { - "version": "1.28.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.1.tgz", - "integrity": "sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==" - }, - "lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==" - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "requires": { - "semver": "^6.0.0" - } - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" - }, - "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" - }, - "minipass": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", - "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==" - }, - "minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "dependencies": { - "minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "requires": { - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - }, - "moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" - }, - "moment-range": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/moment-range/-/moment-range-4.0.2.tgz", - "integrity": "sha512-n8sceWwSTjmz++nFHzeNEUsYtDqjgXgcOBzsHi+BoXQU2FW+eU92LUaK8gqOiSu5PG57Q9sYj1Fz4LRDj4FtKA==", - "requires": { - "es6-symbol": "^3.1.0" - } - }, - "moment-timezone": { - "version": "0.5.43", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.43.tgz", - "integrity": "sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==", - "requires": { - "moment": "^2.29.4" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "ndjson": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/ndjson/-/ndjson-1.5.0.tgz", - "integrity": "sha512-hUPLuaziboGjNF7wHngkgVc0FOclR8dDk/HfEvTtDr/iUrqBWiRcRSTK3/nLOqKH33th714BrMmTPtObI9gZxQ==", - "requires": { - "json-stringify-safe": "^5.0.1", - "minimist": "^1.2.0", - "split2": "^2.1.0", - "through2": "^2.0.3" - } - }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" - }, - "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha512-mc/caHeUcdjnC/boPWJefDr4KUIWQNv+tlnFnJd38QMou86QtxQzBJfxgGRzvx8jazYRqrVlaHarfO72uNxPOg==" - }, - "nexus": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/nexus/-/nexus-1.3.0.tgz", - "integrity": "sha512-w/s19OiNOs0LrtP7pBmD9/FqJHvZLmCipVRt6v1PM8cRUYIbhEswyNKGHVoC4eHZGPSnD+bOf5A3+gnbt0A5/A==", - "requires": { - "iterall": "^1.3.0", - "tslib": "^2.0.3" - }, - "dependencies": { - "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" - } - } - }, - "node-dijkstra": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/node-dijkstra/-/node-dijkstra-2.5.0.tgz", - "integrity": "sha512-2REYb1lo8yDRAY1gsdhjwQdGVSh47VI5Z5wS8sCqydO/P1OVAKOwLuV1fDxNLvbrtMspW7k2UZ2JKIKP06hl+A==" - }, - "node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "node-releases": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", - "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==" - }, - "nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "requires": { - "abbrev": "1" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "requires": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" - }, - "object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - }, - "on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "requires": { - "ee-first": "1.1.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "requires": { - "wrappy": "1" - } - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==" - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" - }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==" - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", - "requires": { - "pinkie": "^2.0.0" - } - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "promise-timeout": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/promise-timeout/-/promise-timeout-1.3.0.tgz", - "integrity": "sha512-5yANTE0tmi5++POym6OgtFmwfDvOXABD9oj/jLQr5GPEyuNEb7jH4wbbANJceJid49jwhi1RddxnhnEAb/doqg==" - }, - "psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" - }, - "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" - }, - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "requires": { - "side-channel": "^1.0.4" - } - }, - "querystring": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", - "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==" - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" - }, - "ramda": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.2.tgz", - "integrity": "sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==" - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "requires": { - "picomatch": "^2.2.1" - } - }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", - "requires": { - "resolve": "^1.1.6" - } - }, - "redis": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz", - "integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==", - "requires": { - "denque": "^1.5.0", - "redis-commands": "^1.7.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0" - } - }, - "redis-commands": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", - "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==" - }, - "redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==" - }, - "redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "requires": { - "redis-errors": "^1.0.0" - } - }, - "regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" - }, - "regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", - "requires": { - "regenerate": "^1.4.2" - } - }, - "regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" - }, - "regenerator-transform": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", - "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", - "requires": { - "@babel/runtime": "^7.8.4" - } - }, - "regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", - "requires": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - } - }, - "regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==" - } - } - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" - } - } - }, - "request-promise": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz", - "integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==", - "requires": { - "bluebird": "^3.5.0", - "request-promise-core": "1.1.4", - "stealthy-require": "^1.1.1", - "tough-cookie": "^2.3.3" - } - }, - "request-promise-core": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", - "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", - "requires": { - "lodash": "^4.17.19" - } - }, - "resolve": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.3.tgz", - "integrity": "sha512-P8ur/gp/AmbEzjr729bZnLjXK5Z+4P0zhIJgBgzqRih7hL7BOukHGtSTA3ACMY467GRFz3duQsi0bDZdR7DKdw==", - "requires": { - "is-core-module": "^2.12.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "seek-bzip": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", - "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", - "requires": { - "commander": "^2.8.1" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - }, - "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "requires": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } - } - }, - "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "shelljs": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", - "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", - "requires": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - } - }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "split2": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", - "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", - "requires": { - "through2": "^2.0.2" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, - "sqlstring": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", - "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==" - }, - "sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "standard-as-callback": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==" - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "strip-dirs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", - "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", - "requires": { - "is-natural-number": "^4.0.1" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" - }, - "syntax-error": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", - "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", - "requires": { - "acorn-node": "^1.2.0" - } - }, - "tar": { - "version": "6.1.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", - "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==", - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^4.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "dependencies": { - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } - }, - "tar-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", - "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", - "requires": { - "bl": "^1.0.0", - "buffer-alloc": "^1.2.0", - "end-of-stream": "^1.0.0", - "fs-constants": "^1.0.0", - "readable-stream": "^2.3.0", - "to-buffer": "^1.1.1", - "xtend": "^4.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "temp-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", - "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==" - }, - "tempy": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-1.0.1.tgz", - "integrity": "sha512-biM9brNqxSc04Ee71hzFbryD11nX7VPhQQY32AdDmjFvodsRFz/3ufeoTZ6uYkRFfGo188tENcASNs3vTdsM0w==", - "requires": { - "del": "^6.0.0", - "is-stream": "^2.0.0", - "temp-dir": "^2.0.0", - "type-fest": "^0.16.0", - "unique-string": "^2.0.0" - }, - "dependencies": { - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" - } - } - }, - "textextensions": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-2.6.0.tgz", - "integrity": "sha512-49WtAWS+tcsy93dRt6P0P3AMD2m5PvXRhuEA0kaXos5ZLlujtYmpmFsB+QvWUSxE1ZsstmYXfQ7L40+EcQgpAQ==" - }, - "throttle-debounce": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", - "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==" - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { - "is-number": "^7.0.0" - } - }, - "toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" - }, - "type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" - }, - "type-fest": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", - "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==" - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "unbzip2-stream": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", - "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", - "requires": { - "buffer": "^5.2.1", - "through": "^2.3.8" - } - }, - "unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==" - }, - "unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "requires": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - } - }, - "unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==" - }, - "unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==" - }, - "unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "requires": { - "crypto-random-string": "^2.0.0" - } - }, - "universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" - }, - "update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", - "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - } - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "requires": { - "punycode": "^2.1.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - }, - "wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "requires": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "requires": {} - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - }, - "yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "requires": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" - } - } -} diff --git a/backend/src/cubejs/package.json b/backend/src/cubejs/package.json deleted file mode 100644 index ca9f0e0d5d..0000000000 --- a/backend/src/cubejs/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "cubejs", - "version": "1.0.0", - "description": "", - "main": ".eslintrc.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC", - "dependencies": { - "@cubejs-backend/server-core": "^0.32.27" - } -} diff --git a/backend/src/cubejs/schema/Activities.js b/backend/src/cubejs/schema/Activities.js deleted file mode 100644 index e7e0758adb..0000000000 --- a/backend/src/cubejs/schema/Activities.js +++ /dev/null @@ -1,61 +0,0 @@ -cube('Activities', { - sql_table: 'mv_activities_cube', - - measures: { - count: { - sql: `${CUBE}.id`, - type: 'count_distinct', - drillMembers: [tenantId, date], - }, - cumulativeCount: { - type: 'count', - rollingWindow: { - trailing: 'unbounded', - }, - }, - }, - - dimensions: { - id: { - sql: `${CUBE}.id`, - type: 'string', - primaryKey: true, - }, - - iscontribution: { - sql: `${CUBE}."isContribution"`, - type: 'string', - }, - - sentimentMood: { - sql: `${CUBE}."sentimentMood"`, - type: 'string', - }, - - platform: { - sql: `${CUBE}.platform`, - type: 'string', - }, - - channel: { - sql: `${CUBE}.channel`, - type: 'string', - }, - - tenantId: { - sql: `${CUBE}."tenantId"`, - type: 'string', - shown: false, - }, - - type: { - sql: `${CUBE}.type`, - type: 'string', - }, - - date: { - sql: `${CUBE}.timestamp`, - type: 'time', - }, - }, -}) diff --git a/backend/src/cubejs/schema/Conversations.js b/backend/src/cubejs/schema/Conversations.js deleted file mode 100644 index 3fb8b6286b..0000000000 --- a/backend/src/cubejs/schema/Conversations.js +++ /dev/null @@ -1,61 +0,0 @@ -cube('Conversations', { - sql: `SELECT - con.*, - a.platform AS platform, - a.channel AS category - FROM - conversations con - LEFT JOIN activities a ON con.id = a."conversationId" - GROUP BY - con.id, - a.platform, - a.channel`, - - joins: { - Activities: { - sql: `${CUBE}.id = ${Activities}."conversationId"`, - relationship: 'hasMany', - }, - }, - - measures: { - count: { - type: 'count', - drillMembers: [tenantId, createdat], - }, - }, - - dimensions: { - id: { - sql: `${CUBE}.id`, - type: 'string', - primaryKey: true, - }, - - tenantId: { - sql: `${CUBE}."tenantId"`, - type: 'string', - shown: false, - }, - - published: { - sql: `${CUBE}.published`, - type: 'string', - }, - - createdat: { - sql: `${CUBE}."createdAt"`, - type: 'time', - }, - - platform: { - sql: `${CUBE}."platform"`, - type: 'string', - }, - - category: { - sql: `${CUBE}."category"`, - type: 'string', - }, - }, -}) diff --git a/backend/src/cubejs/schema/MemberOrganizations.js b/backend/src/cubejs/schema/MemberOrganizations.js deleted file mode 100644 index caad050585..0000000000 --- a/backend/src/cubejs/schema/MemberOrganizations.js +++ /dev/null @@ -1,21 +0,0 @@ -cube('MemberOrganizations', { - sql_table: '"memberOrganizations"', - - preAggregations: {}, - - joins: { - Organizations: { - sql: `${CUBE}."organizationId" = ${Organizations}.id`, - relationship: 'belongsTo', - }, - - Activities: { - sql: `${CUBE}."memberId" = ${Activities}."memberId"`, - relationship: 'hasMany', - }, - }, - - measures: {}, - - dimensions: {}, -}) diff --git a/backend/src/cubejs/schema/MemberSegments.js b/backend/src/cubejs/schema/MemberSegments.js deleted file mode 100644 index fab2e41c5f..0000000000 --- a/backend/src/cubejs/schema/MemberSegments.js +++ /dev/null @@ -1,21 +0,0 @@ -cube('MemberSegments', { - sql_table: '"memberSegments"', - - preAggregations: {}, - - joins: { - Members: { - sql: `${CUBE}."memberId" = ${Members}.id`, - relationship: 'belongsTo', - }, - - Segments: { - sql: `${CUBE}."segmentId" = ${Segments}."id"`, - relationship: 'belongsTo', - }, - }, - - measures: {}, - - dimensions: {}, -}) diff --git a/backend/src/cubejs/schema/MemberTags.js b/backend/src/cubejs/schema/MemberTags.js deleted file mode 100644 index 56c368d559..0000000000 --- a/backend/src/cubejs/schema/MemberTags.js +++ /dev/null @@ -1,14 +0,0 @@ -cube('MemberTags', { - sql_table: '"memberTags"', - - joins: { - Tags: { - relationship: 'hasMany', - sql: `${CUBE}."tagId" = ${Tags}."id"`, - }, - }, - - measures: {}, - - dimensions: {}, -}) diff --git a/backend/src/cubejs/schema/Members.js b/backend/src/cubejs/schema/Members.js deleted file mode 100644 index d79531dbf5..0000000000 --- a/backend/src/cubejs/schema/Members.js +++ /dev/null @@ -1,86 +0,0 @@ -cube('Members', { - sql_table: 'mv_members_cube', - - joins: { - Activities: { - sql: `${CUBE}.id = ${Activities}."memberId"`, - relationship: 'hasMany', - }, - - MemberTags: { - sql: `${CUBE}.id = ${MemberTags}."memberId"`, - relationship: 'belongsTo', - }, - - MemberOrganizations: { - sql: `${CUBE}.id = ${MemberOrganizations}."memberId"`, - relationship: 'belongsTo', - }, - - MemberSegments: { - sql: `${CUBE}.id = ${MemberSegments}."memberId"`, - relationship: 'belongsTo', - }, - }, - - measures: { - count: { - sql: `${CUBE}.id`, - type: 'count_distinct', - }, - - cumulativeCount: { - type: 'count', - rollingWindow: { - trailing: 'unbounded', - }, - }, - }, - - dimensions: { - id: { - sql: `${CUBE}.id`, - type: 'string', - primaryKey: true, - }, - - tenantId: { - sql: `${CUBE}."tenantId"`, - type: 'string', - shown: false, - }, - - location: { - sql: `${CUBE}.location`, - type: 'string', - }, - - isTeamMember: { - sql: `${CUBE}."isTeamMember"`, - type: 'boolean', - }, - isBot: { - sql: `${CUBE}."isBot"`, - type: 'boolean', - }, - isOrganization: { - sql: `${CUBE}."isOrganization"`, - type: 'boolean', - }, - - joinedAt: { - sql: `${CUBE}."joinedAt"`, - type: 'time', - }, - - joinedAtUnixTs: { - sql: `${CUBE}."joinedAtUnixTs"`, - type: 'number', - }, - - score: { - sql: `${CUBE}."score"`, - type: 'number', - }, - }, -}) diff --git a/backend/src/cubejs/schema/OrganizationSegments.js b/backend/src/cubejs/schema/OrganizationSegments.js deleted file mode 100644 index ffc5d2570b..0000000000 --- a/backend/src/cubejs/schema/OrganizationSegments.js +++ /dev/null @@ -1,21 +0,0 @@ -cube('OrganizationSegments', { - sql_table: '"organizationSegments"', - - preAggregations: {}, - - joins: { - Organizations: { - sql: `${CUBE}."organizationId" = ${Organizations}.id`, - relationship: 'belongsTo', - }, - - Segments: { - sql: `${CUBE}."segmentId" = ${Segments}."id"`, - relationship: 'belongsTo', - }, - }, - - measures: {}, - - dimensions: {}, -}) diff --git a/backend/src/cubejs/schema/Organizations.js b/backend/src/cubejs/schema/Organizations.js deleted file mode 100644 index 3562a59715..0000000000 --- a/backend/src/cubejs/schema/Organizations.js +++ /dev/null @@ -1,40 +0,0 @@ -cube('Organizations', { - sql_table: 'mv_organizations_cube', - joins: { - MemberOrganizations: { - sql: `${CUBE}.id = ${MemberOrganizations}."organizationId"`, - relationship: 'hasMany', - }, - OrganizationSegments: { - sql: `${CUBE}.id = ${OrganizationSegments}."organizationId"`, - relationship: 'belongsTo', - }, - }, - measures: { - count: { - sql: `${CUBE}.id`, - type: 'count_distinct', - drillMembers: [tenantId], - }, - }, - dimensions: { - id: { - sql: `${CUBE}.id`, - type: 'string', - primaryKey: true, - }, - - tenantId: { - sql: `${CUBE}."tenantId"`, - type: 'string', - }, - createdat: { - sql: `${CUBE}."createdAt"`, - type: 'time', - }, - joinedAt: { - sql: `${CUBE}."earliestJoinedAt"`, - type: 'time', - }, - }, -}) diff --git a/backend/src/cubejs/schema/Segments.js b/backend/src/cubejs/schema/Segments.js deleted file mode 100644 index 903ffb2d70..0000000000 --- a/backend/src/cubejs/schema/Segments.js +++ /dev/null @@ -1,29 +0,0 @@ -cube('Segments', { - sql_table: 'mv_segments_cube', - - preAggregations: {}, - - joins: { - Activities: { - sql: `${CUBE}.id = ${Activities}."segmentId"`, - relationship: 'hasMany', - }, - OrganizationSegments: { - sql: `${CUBE}.id = ${OrganizationSegments}."segmentId"`, - relationship: 'belongsTo', - }, - }, - - dimensions: { - name: { - sql: `${CUBE}."name"`, - type: 'string', - }, - - id: { - sql: `${CUBE}.id`, - type: 'string', - primaryKey: true, - }, - }, -}) diff --git a/backend/src/cubejs/schema/Sentiment.js b/backend/src/cubejs/schema/Sentiment.js deleted file mode 100644 index 4d4d13fdc1..0000000000 --- a/backend/src/cubejs/schema/Sentiment.js +++ /dev/null @@ -1,58 +0,0 @@ -cube('Sentiment', { - sql: `select - a.id, - a."tenantId" , - a."platform" , - a."timestamp" , - a."memberId" , - (a.sentiment->>'sentiment')::integer as sentiment, - case - when (a.sentiment->>'sentiment')::integer < 34 then 'negative' - when (a.sentiment->>'sentiment')::integer > 66 then 'positive' - else 'neutral' - end as mood - from - activities a - where - a.sentiment->>'sentiment' is not null`, - - preAggregations: {}, - - joins: { - Members: { - sql: `${CUBE}."memberId" = ${Members}."id"`, - relationship: 'belongsTo', - }, - }, - - measures: { - averageSentiment: { - type: 'avg', - sql: 'sentiment', - }, - }, - - dimensions: { - id: { - sql: `${CUBE}.id`, - type: 'string', - primaryKey: true, - }, - - tenantId: { - sql: `${CUBE}."tenantId"`, - type: 'string', - shown: false, - }, - - platform: { - sql: `${CUBE}.platform`, - type: 'string', - }, - - date: { - sql: `${CUBE}.timestamp`, - type: 'time', - }, - }, -}) diff --git a/backend/src/cubejs/schema/Tags.js b/backend/src/cubejs/schema/Tags.js deleted file mode 100644 index aaff0f85ed..0000000000 --- a/backend/src/cubejs/schema/Tags.js +++ /dev/null @@ -1,14 +0,0 @@ -cube(`Tags`, { - sql_table: 'tags', - - preAggregations: {}, - - joins: {}, - - dimensions: { - name: { - sql: `${CUBE}.name`, - type: `string`, - }, - }, -}) diff --git a/backend/src/database/databaseConnection.ts b/backend/src/database/databaseConnection.ts index fb7a799e8a..f4094cea2d 100644 --- a/backend/src/database/databaseConnection.ts +++ b/backend/src/database/databaseConnection.ts @@ -5,10 +5,22 @@ let cached /** * Initializes the connection to the Database */ -export async function databaseInit(queryTimeoutMilliseconds: number = 30000) { +export async function databaseInit( + queryTimeoutMilliseconds: number = 60000, + forceNewInstance: boolean = false, + databaseHostnameOverride: string = null, +) { + if (forceNewInstance) { + return models(queryTimeoutMilliseconds, databaseHostnameOverride) + } + if (!cached) { - cached = models(queryTimeoutMilliseconds) + cached = models(queryTimeoutMilliseconds, databaseHostnameOverride) } return cached } + +export async function databaseClose(database) { + await database.sequelize.close() +} diff --git a/backend/src/database/flyway_migrate.sh b/backend/src/database/flyway_migrate.sh index 4bad150f77..b2a4582979 100755 --- a/backend/src/database/flyway_migrate.sh +++ b/backend/src/database/flyway_migrate.sh @@ -10,6 +10,8 @@ flyway \ -password="$PGPASSWORD" \ -connectRetries=60 \ -outOfOrder=true \ - -placeholderReplacement=false\ + -mixed=true \ + -placeholderReplacement=false \ -schemas=public \ - migrate \ No newline at end of file + -X \ + migrate diff --git a/backend/src/database/initializers/activities.json b/backend/src/database/initializers/activities.json deleted file mode 100644 index 4d6b95ef03..0000000000 --- a/backend/src/database/initializers/activities.json +++ /dev/null @@ -1,28268 +0,0 @@ -[ - { - "crowdInfo": { - "body": "yeah same, I'm in the middle of something", - "channel": "random", - "slackId": "1643815623.203779", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643815623203779" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-02-02T15:27:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yeah same, I'm in the middle of something", - "channel": "random", - "slackId": "1643815623.203779", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643815623203779" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-02-02T15:27:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yeah same, I'm in the middle of something", - "channel": "random", - "slackId": "1643815623.203779", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643815623203779" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-02-02T15:27:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yeah same, I'm in the middle of something", - "channel": "random", - "slackId": "1643815623.203779", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643815623203779" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-02-02T15:27:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I’m going to skip too :v:", - "channel": "random", - "slackId": "1643815597.525419", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643815597525419" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-02-02T15:26:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I’m going to skip too :v:", - "channel": "random", - "slackId": "1643815597.525419", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643815597525419" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-02-02T15:26:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I’m going to skip too :v:", - "channel": "random", - "slackId": "1643815597.525419", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643815597525419" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-02-02T15:26:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I’m going to skip too :v:", - "channel": "random", - "slackId": "1643815597.525419", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643815597525419" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-02-02T15:26:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Guys I have to skip coffee break, so sorry!", - "channel": "random", - "slackId": "1643815490.117279", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643815490117279" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 1, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-02-02T15:24:50.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Guys I have to skip coffee break, so sorry!", - "channel": "random", - "slackId": "1643815490.117279", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643815490117279" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-02-02T15:24:50.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Guys I have to skip coffee break, so sorry!", - "channel": "random", - "slackId": "1643815490.117279", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643815490117279" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-02-02T15:24:50.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Guys I have to skip coffee break, so sorry!", - "channel": "random", - "slackId": "1643815490.117279", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643815490117279" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-02-02T15:24:50.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": " something interesting for us? (@Anil Bostanci @Joan Reyero) just had a chat with the founder", - "channel": "dev", - "slackId": "1643803149.117499", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643803149117499" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-02-02T11:59:09.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": " something interesting for us? (@Anil Bostanci @Joan Reyero) just had a chat with the founder", - "channel": "dev", - "slackId": "1643803149.117499", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643803149117499" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-02-02T11:59:09.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": " something interesting for us? (@Anil Bostanci @Joan Reyero) just had a chat with the founder", - "channel": "dev", - "slackId": "1643803149.117499", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643803149117499" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-02-02T11:59:09.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": " something interesting for us? (@Anil Bostanci @Joan Reyero) just had a chat with the founder", - "channel": "dev", - "slackId": "1643803149.117499", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643803149117499" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-02-02T11:59:09.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "In the middle of something. Will have to skip coffee break today", - "channel": "random", - "slackId": "1643729415.005949", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643729415005949" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T15:30:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "In the middle of something. Will have to skip coffee break today", - "channel": "random", - "slackId": "1643729415.005949", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643729415005949" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T15:30:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "In the middle of something. Will have to skip coffee break today", - "channel": "random", - "slackId": "1643729415.005949", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643729415005949" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-02-01T15:30:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "In the middle of something. Will have to skip coffee break today", - "channel": "random", - "slackId": "1643729415.005949", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643729415005949" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-02-01T15:30:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "white_check_mark", - "slackId": "1643728672.002100", - "thread": { - "id": "1643726374.210029", - "body": "Perfect :v:" - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 1, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T15:17:52.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "white_check_mark", - "slackId": "1643728672.002100", - "thread": { - "id": "1643726374.210029", - "body": "Perfect :v:" - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T15:17:52.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "white_check_mark", - "slackId": "1643728672.002100", - "thread": { - "id": "1643726374.210029", - "body": "Perfect :v:" - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T15:17:52.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "white_check_mark", - "slackId": "1643728672.002100", - "thread": { - "id": "1643726374.210029", - "body": "Perfect :v:" - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T15:17:52.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Perfect :v:", - "channel": "dev", - "slackId": "1643726374.210029", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643726374210029?thread_ts=1643725075.827419&cid=C01NBV2BDDK", - "thread": { - "id": "1643725075.827419", - "body": "Hey guys, I was trying to run the frontend of crowd-dev, I ran `npm install` and then `npm start` but I got this error, any ideas? Thanks :)" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:39:34.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Perfect :v:", - "channel": "dev", - "slackId": "1643726374.210029", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643726374210029?thread_ts=1643725075.827419&cid=C01NBV2BDDK", - "thread": { - "id": "1643725075.827419", - "body": "Hey guys, I was trying to run the frontend of crowd-dev, I ran `npm install` and then `npm start` but I got this error, any ideas? Thanks :)" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:39:34.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Perfect :v:", - "channel": "dev", - "slackId": "1643726374.210029", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643726374210029?thread_ts=1643725075.827419&cid=C01NBV2BDDK", - "thread": { - "id": "1643725075.827419", - "body": "Hey guys, I was trying to run the frontend of crowd-dev, I ran `npm install` and then `npm start` but I got this error, any ideas? Thanks :)" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-02-01T14:39:34.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Perfect :v:", - "channel": "dev", - "slackId": "1643726374.210029", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643726374210029?thread_ts=1643725075.827419&cid=C01NBV2BDDK", - "thread": { - "id": "1643725075.827419", - "body": "Hey guys, I was trying to run the frontend of crowd-dev, I ran `npm install` and then `npm start` but I got this error, any ideas? Thanks :)" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:39:34.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Fixed by upgrading my Node version from v10 to v16.13.2!\nThank @Mario Balca for your help :slightly_smiling_face:", - "channel": "dev", - "slackId": "1643726355.920559", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643726355920559?thread_ts=1643725075.827419&cid=C01NBV2BDDK", - "thread": { - "id": "1643725075.827419", - "body": "Hey guys, I was trying to run the frontend of crowd-dev, I ran `npm install` and then `npm start` but I got this error, any ideas? Thanks :)" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 1, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:39:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Fixed by upgrading my Node version from v10 to v16.13.2!\nThank @Mario Balca for your help :slightly_smiling_face:", - "channel": "dev", - "slackId": "1643726355.920559", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643726355920559?thread_ts=1643725075.827419&cid=C01NBV2BDDK", - "thread": { - "id": "1643725075.827419", - "body": "Hey guys, I was trying to run the frontend of crowd-dev, I ran `npm install` and then `npm start` but I got this error, any ideas? Thanks :)" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:39:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Fixed by upgrading my Node version from v10 to v16.13.2!\nThank @Mario Balca for your help :slightly_smiling_face:", - "channel": "dev", - "slackId": "1643726355.920559", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643726355920559?thread_ts=1643725075.827419&cid=C01NBV2BDDK", - "thread": { - "id": "1643725075.827419", - "body": "Hey guys, I was trying to run the frontend of crowd-dev, I ran `npm install` and then `npm start` but I got this error, any ideas? Thanks :)" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:39:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Fixed by upgrading my Node version from v10 to v16.13.2!\nThank @Mario Balca for your help :slightly_smiling_face:", - "channel": "dev", - "slackId": "1643726355.920559", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643726355920559?thread_ts=1643725075.827419&cid=C01NBV2BDDK", - "thread": { - "id": "1643725075.827419", - "body": "Hey guys, I was trying to run the frontend of crowd-dev, I ran `npm install` and then `npm start` but I got this error, any ideas? Thanks :)" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:39:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Hmmm maybe we can quickly jump into a slack huddle and I can to try to help", - "channel": "dev", - "slackId": "1643725255.139779", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643725255139779?thread_ts=1643725075.827419&cid=C01NBV2BDDK", - "thread": { - "id": "1643725075.827419", - "body": "Hey guys, I was trying to run the frontend of crowd-dev, I ran `npm install` and then `npm start` but I got this error, any ideas? Thanks :)" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:20:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Hmmm maybe we can quickly jump into a slack huddle and I can to try to help", - "channel": "dev", - "slackId": "1643725255.139779", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643725255139779?thread_ts=1643725075.827419&cid=C01NBV2BDDK", - "thread": { - "id": "1643725075.827419", - "body": "Hey guys, I was trying to run the frontend of crowd-dev, I ran `npm install` and then `npm start` but I got this error, any ideas? Thanks :)" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:20:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Hmmm maybe we can quickly jump into a slack huddle and I can to try to help", - "channel": "dev", - "slackId": "1643725255.139779", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643725255139779?thread_ts=1643725075.827419&cid=C01NBV2BDDK", - "thread": { - "id": "1643725075.827419", - "body": "Hey guys, I was trying to run the frontend of crowd-dev, I ran `npm install` and then `npm start` but I got this error, any ideas? Thanks :)" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-02-01T14:20:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Hmmm maybe we can quickly jump into a slack huddle and I can to try to help", - "channel": "dev", - "slackId": "1643725255.139779", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643725255139779?thread_ts=1643725075.827419&cid=C01NBV2BDDK", - "thread": { - "id": "1643725075.827419", - "body": "Hey guys, I was trying to run the frontend of crowd-dev, I ran `npm install` and then `npm start` but I got this error, any ideas? Thanks :)" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:20:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "thinking_face", - "slackId": "1643725135.001200", - "thread": { - "id": "1643725075.827419", - "body": "Hey guys, I was trying to run the frontend of crowd-dev, I ran `npm install` and then `npm start` but I got this error, any ideas? Thanks :)" - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:18:55.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "thinking_face", - "slackId": "1643725135.001200", - "thread": { - "id": "1643725075.827419", - "body": "Hey guys, I was trying to run the frontend of crowd-dev, I ran `npm install` and then `npm start` but I got this error, any ideas? Thanks :)" - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:18:55.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "thinking_face", - "slackId": "1643725135.001200", - "thread": { - "id": "1643725075.827419", - "body": "Hey guys, I was trying to run the frontend of crowd-dev, I ran `npm install` and then `npm start` but I got this error, any ideas? Thanks :)" - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-02-01T14:18:55.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "thinking_face", - "slackId": "1643725135.001200", - "thread": { - "id": "1643725075.827419", - "body": "Hey guys, I was trying to run the frontend of crowd-dev, I ran `npm install` and then `npm start` but I got this error, any ideas? Thanks :)" - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:18:55.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "dev", - "slackId": "1643725075.827419", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643725075827419" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 1, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:17:55.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "dev", - "slackId": "1643725075.827419", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643725075827419" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:17:55.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "dev", - "slackId": "1643725075.827419", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643725075827419" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:17:55.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "dev", - "slackId": "1643725075.827419", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643725075827419" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:17:55.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "dev", - "slackId": "1643724836.136399", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643724836136399" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 1, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:13:56.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "dev", - "slackId": "1643724836.136399", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643724836136399" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:13:56.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "dev", - "slackId": "1643724836.136399", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643724836136399" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:13:56.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "dev", - "slackId": "1643724836.136399", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643724836136399" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T14:13:56.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "body": ":joy::joy:", - "channel": "twitter", - "slackId": "1643719471.759179", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1643719471759179" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T12:44:31.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": ":joy::joy:", - "channel": "twitter", - "slackId": "1643719471.759179", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1643719471759179" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T12:44:31.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": ":joy::joy:", - "channel": "twitter", - "slackId": "1643719471.759179", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1643719471759179" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-02-01T12:44:31.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": ":joy::joy:", - "channel": "twitter", - "slackId": "1643719471.759179", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1643719471759179" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-02-01T12:44:31.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "sorry for flexing haha", - "channel": "twitter", - "slackId": "1643719118.782799", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1643719118782799" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T12:38:38.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "sorry for flexing haha", - "channel": "twitter", - "slackId": "1643719118.782799", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1643719118782799" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T12:38:38.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "sorry for flexing haha", - "channel": "twitter", - "slackId": "1643719118.782799", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1643719118782799" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T12:38:38.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "sorry for flexing haha", - "channel": "twitter", - "slackId": "1643719118.782799", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1643719118782799" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-02-01T12:38:38.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "procurement", - "slackId": "1643577153.009819", - "url": "https://crowddevspace.slack.com/archives/C02C2PPMHRB/p1643577153009819" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 1, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-01-30T21:12:33.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "bugs", - "slackId": "1643577153.064039", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643577153064039" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-01-30T21:12:33.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "procurement", - "slackId": "1643577153.009819", - "url": "https://crowddevspace.slack.com/archives/C02C2PPMHRB/p1643577153009819" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-01-30T21:12:33.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "fantasy-football", - "slackId": "1643577153.033869", - "url": "https://crowddevspace.slack.com/archives/C02EF5YGPCL/p1643577153033869" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-01-30T21:12:33.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "general", - "slackId": "1643577152.962369", - "url": "https://crowddevspace.slack.com/archives/C01NTLA4EBT/p1643577152962369" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 1, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-01-30T21:12:32.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "general", - "slackId": "1643577152.962369", - "url": "https://crowddevspace.slack.com/archives/C01NTLA4EBT/p1643577152962369" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-01-30T21:12:32.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "general", - "slackId": "1643577152.962369", - "url": "https://crowddevspace.slack.com/archives/C01NTLA4EBT/p1643577152962369" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-01-30T21:12:32.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "sign-ups", - "slackId": "1643577152.983069", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1643577152983069" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Africa/Algiers" - } - }, - "username": "Mehdi Mabrouki", - "type": "member", - "score": 2, - "email": "mehdi@crowd.dev", - "organisation": "", - "location": "Africa/Algiers (timezone)", - "bio": "" - }, - "timestamp": "2022-01-30T21:12:32.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "fire", - "slackId": "1643392217.000800", - "thread": { - "id": "1643390597.319629", - "body": " this is amazing" - }, - "channel": "random" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T17:50:17.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "fire", - "slackId": "1643392217.000800", - "thread": { - "id": "1643390597.319629", - "body": " this is amazing" - }, - "channel": "random" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T17:50:17.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "fire", - "slackId": "1643392217.000800", - "thread": { - "id": "1643390597.319629", - "body": " this is amazing" - }, - "channel": "random" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-28T17:50:17.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "fire", - "slackId": "1643392217.000800", - "thread": { - "id": "1643390597.319629", - "body": " this is amazing" - }, - "channel": "random" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-28T17:50:17.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "smile", - "slackId": "1643392210.000700", - "thread": { - "id": "1643390597.319629", - "body": " this is amazing" - }, - "channel": "random" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T17:50:10.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "smile", - "slackId": "1643392210.000700", - "thread": { - "id": "1643390597.319629", - "body": " this is amazing" - }, - "channel": "random" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T17:50:10.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "smile", - "slackId": "1643392210.000700", - "thread": { - "id": "1643390597.319629", - "body": " this is amazing" - }, - "channel": "random" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-28T17:50:10.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "smile", - "slackId": "1643392210.000700", - "thread": { - "id": "1643390597.319629", - "body": " this is amazing" - }, - "channel": "random" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-28T17:50:10.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": " this is amazing", - "channel": "random", - "slackId": "1643390597.319629", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643390597319629" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T17:23:17.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": " this is amazing", - "channel": "random", - "slackId": "1643390597.319629", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643390597319629" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T17:23:17.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": " this is amazing", - "channel": "random", - "slackId": "1643390597.319629", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643390597319629" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T17:23:17.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": " this is amazing", - "channel": "random", - "slackId": "1643390597.319629", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643390597319629" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T17:23:17.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "random", - "slackId": "1643385428.154199", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643385428154199" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T15:57:08.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "random", - "slackId": "1643385428.154199", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643385428154199" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T15:57:08.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "random", - "slackId": "1643385428.154199", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643385428154199" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T15:57:08.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "random", - "slackId": "1643385428.154199", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643385428154199" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T15:57:08.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1643371282.003100", - "thread": { - "id": "1643371257.571759", - "body": "I pushed a new version just now, tests were failing" - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T12:01:22.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1643371282.003100", - "thread": { - "id": "1643371257.571759", - "body": "I pushed a new version just now, tests were failing" - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T12:01:22.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1643371282.003100", - "thread": { - "id": "1643371257.571759", - "body": "I pushed a new version just now, tests were failing" - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-28T12:01:22.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1643371282.003100", - "thread": { - "id": "1643371257.571759", - "body": "I pushed a new version just now, tests were failing" - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T12:01:22.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I pushed a new version just now, tests were failing", - "channel": "dev", - "slackId": "1643371257.571759", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643371257571759?thread_ts=1643370668.747659&cid=C01NBV2BDDK", - "thread": { - "id": "1643370668.747659", - "body": "Nice thanks!" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T12:00:57.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I pushed a new version just now, tests were failing", - "channel": "dev", - "slackId": "1643371257.571759", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643371257571759?thread_ts=1643370668.747659&cid=C01NBV2BDDK", - "thread": { - "id": "1643370668.747659", - "body": "Nice thanks!" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T12:00:57.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I pushed a new version just now, tests were failing", - "channel": "dev", - "slackId": "1643371257.571759", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643371257571759?thread_ts=1643370668.747659&cid=C01NBV2BDDK", - "thread": { - "id": "1643370668.747659", - "body": "Nice thanks!" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T12:00:57.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I pushed a new version just now, tests were failing", - "channel": "dev", - "slackId": "1643371257.571759", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643371257571759?thread_ts=1643370668.747659&cid=C01NBV2BDDK", - "thread": { - "id": "1643370668.747659", - "body": "Nice thanks!" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T12:00:57.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1643370832.002900", - "thread": { - "id": "1643370668.747659", - "body": "Nice thanks!" - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:53:52.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1643370832.002900", - "thread": { - "id": "1643370668.747659", - "body": "Nice thanks!" - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:53:52.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1643370832.002900", - "thread": { - "id": "1643370668.747659", - "body": "Nice thanks!" - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:53:52.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1643370832.002900", - "thread": { - "id": "1643370668.747659", - "body": "Nice thanks!" - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:53:52.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Nice thanks!", - "channel": "dev", - "slackId": "1643370668.747659", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370668747659" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:51:08.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Nice thanks!", - "channel": "dev", - "slackId": "1643370668.747659", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370668747659" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:51:08.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Nice thanks!", - "channel": "dev", - "slackId": "1643370668.747659", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370668747659" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-28T11:51:08.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Nice thanks!", - "channel": "dev", - "slackId": "1643370668.747659", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370668747659" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:51:08.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "i merged Joan's changes too to this", - "channel": "dev", - "slackId": "1643370635.602419", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370635602419" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:50:35.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "i merged Joan's changes too to this", - "channel": "dev", - "slackId": "1643370635.602419", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370635602419" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:50:35.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "i merged Joan's changes too to this", - "channel": "dev", - "slackId": "1643370635.602419", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370635602419" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:50:35.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "i merged Joan's changes too to this", - "channel": "dev", - "slackId": "1643370635.602419", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370635602419" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:50:35.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "you can merge my current working branch (members-to-merge-relations) to ur branch. errors should disappear", - "channel": "dev", - "slackId": "1643370608.948779", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370608948779" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:50:08.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "you can merge my current working branch (members-to-merge-relations) to ur branch. errors should disappear", - "channel": "dev", - "slackId": "1643370608.948779", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370608948779" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:50:08.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "you can merge my current working branch (members-to-merge-relations) to ur branch. errors should disappear", - "channel": "dev", - "slackId": "1643370608.948779", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370608948779" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:50:08.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "you can merge my current working branch (members-to-merge-relations) to ur branch. errors should disappear", - "channel": "dev", - "slackId": "1643370608.948779", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370608948779" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:50:08.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yeah those are fixed in the new branches but not merged into main yet", - "channel": "dev", - "slackId": "1643370563.620339", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370563620339" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:49:23.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yeah those are fixed in the new branches but not merged into main yet", - "channel": "dev", - "slackId": "1643370563.620339", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370563620339" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:49:23.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yeah those are fixed in the new branches but not merged into main yet", - "channel": "dev", - "slackId": "1643370563.620339", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370563620339" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:49:23.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yeah those are fixed in the new branches but not merged into main yet", - "channel": "dev", - "slackId": "1643370563.620339", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370563620339" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:49:23.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "dev", - "slackId": "1643370304.105919", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370304105919" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:45:04.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "dev", - "slackId": "1643370304.105919", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370304105919" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:45:04.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "dev", - "slackId": "1643370304.105919", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370304105919" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-28T11:45:04.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "dev", - "slackId": "1643370304.105919", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370304105919" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:45:04.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "dev", - "slackId": "1643370280.206799", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370280206799" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:44:40.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "dev", - "slackId": "1643370280.206799", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370280206799" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:44:40.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "dev", - "slackId": "1643370280.206799", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370280206799" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-28T11:44:40.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "dev", - "slackId": "1643370280.206799", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643370280206799" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-28T11:44:40.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Yeah", - "channel": "dev", - "slackId": "1643304915.001300", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643304915001300?thread_ts=1643301420.000700&cid=C01NBV2BDDK", - "thread": { - "id": "1643301420.000700", - "body": "@Joan Reyero @Anil Bostanci Just noticed that we’re 1 major behind in `Sequilize` , maybe it would be cool to get it to version 7 right from the start" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T17:35:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Yeah", - "channel": "dev", - "slackId": "1643304915.001300", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643304915001300?thread_ts=1643301420.000700&cid=C01NBV2BDDK", - "thread": { - "id": "1643301420.000700", - "body": "@Joan Reyero @Anil Bostanci Just noticed that we’re 1 major behind in `Sequilize` , maybe it would be cool to get it to version 7 right from the start" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T17:35:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Yeah", - "channel": "dev", - "slackId": "1643304915.001300", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643304915001300?thread_ts=1643301420.000700&cid=C01NBV2BDDK", - "thread": { - "id": "1643301420.000700", - "body": "@Joan Reyero @Anil Bostanci Just noticed that we’re 1 major behind in `Sequilize` , maybe it would be cool to get it to version 7 right from the start" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-27T17:35:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Yeah", - "channel": "dev", - "slackId": "1643304915.001300", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643304915001300?thread_ts=1643301420.000700&cid=C01NBV2BDDK", - "thread": { - "id": "1643301420.000700", - "body": "@Joan Reyero @Anil Bostanci Just noticed that we’re 1 major behind in `Sequilize` , maybe it would be cool to get it to version 7 right from the start" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-27T17:35:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1 :thumbsup:", - "channel": "dev", - "slackId": "1643304875.001100", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643304875001100?thread_ts=1643301420.000700&cid=C01NBV2BDDK", - "thread": { - "id": "1643301420.000700", - "body": "@Joan Reyero @Anil Bostanci Just noticed that we’re 1 major behind in `Sequilize` , maybe it would be cool to get it to version 7 right from the start" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T17:34:35.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1 :thumbsup:", - "channel": "dev", - "slackId": "1643304875.001100", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643304875001100?thread_ts=1643301420.000700&cid=C01NBV2BDDK", - "thread": { - "id": "1643301420.000700", - "body": "@Joan Reyero @Anil Bostanci Just noticed that we’re 1 major behind in `Sequilize` , maybe it would be cool to get it to version 7 right from the start" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T17:34:35.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1 :thumbsup:", - "channel": "dev", - "slackId": "1643304875.001100", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643304875001100?thread_ts=1643301420.000700&cid=C01NBV2BDDK", - "thread": { - "id": "1643301420.000700", - "body": "@Joan Reyero @Anil Bostanci Just noticed that we’re 1 major behind in `Sequilize` , maybe it would be cool to get it to version 7 right from the start" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T17:34:35.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1 :thumbsup:", - "channel": "dev", - "slackId": "1643304875.001100", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643304875001100?thread_ts=1643301420.000700&cid=C01NBV2BDDK", - "thread": { - "id": "1643301420.000700", - "body": "@Joan Reyero @Anil Bostanci Just noticed that we’re 1 major behind in `Sequilize` , maybe it would be cool to get it to version 7 right from the start" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T17:34:35.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@Joan Reyero @Anil Bostanci Just noticed that we’re 1 major behind in `Sequilize` , maybe it would be cool to get it to version 7 right from the start", - "channel": "dev", - "slackId": "1643301420.000700", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643301420000700" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T16:37:00.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@Joan Reyero @Anil Bostanci Just noticed that we’re 1 major behind in `Sequilize` , maybe it would be cool to get it to version 7 right from the start", - "channel": "dev", - "slackId": "1643301420.000700", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643301420000700" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T16:37:00.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@Joan Reyero @Anil Bostanci Just noticed that we’re 1 major behind in `Sequilize` , maybe it would be cool to get it to version 7 right from the start", - "channel": "dev", - "slackId": "1643301420.000700", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643301420000700" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-27T16:37:00.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@Joan Reyero @Anil Bostanci Just noticed that we’re 1 major behind in `Sequilize` , maybe it would be cool to get it to version 7 right from the start", - "channel": "dev", - "slackId": "1643301420.000700", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643301420000700" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T16:37:00.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "me too :+1:", - "channel": "random", - "slackId": "1643297543.003400", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643297543003400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T15:32:23.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "me too :+1:", - "channel": "random", - "slackId": "1643297543.003400", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643297543003400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T15:32:23.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "me too :+1:", - "channel": "random", - "slackId": "1643297543.003400", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643297543003400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-27T15:32:23.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "me too :+1:", - "channel": "random", - "slackId": "1643297543.003400", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643297543003400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T15:32:23.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "i will be skipping coffee break today - need to wrap some stuff up", - "channel": "random", - "slackId": "1643297526.003200", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643297526003200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T15:32:06.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "i will be skipping coffee break today - need to wrap some stuff up", - "channel": "random", - "slackId": "1643297526.003200", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643297526003200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T15:32:06.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "i will be skipping coffee break today - need to wrap some stuff up", - "channel": "random", - "slackId": "1643297526.003200", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643297526003200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T15:32:06.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "i will be skipping coffee break today - need to wrap some stuff up", - "channel": "random", - "slackId": "1643297526.003200", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643297526003200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T15:32:06.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Let’s gooo", - "channel": "random", - "slackId": "1643297243.002200", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643297243002200?thread_ts=1643289699.001200&cid=C01NBUP9DAR", - "thread": { - "id": "1643289699.001200", - "body": "getting an intro to the guy who built the Ubuntu community :smile: would that be a cool reference? :stuck_out_tongue:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T15:27:23.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Let’s gooo", - "channel": "random", - "slackId": "1643297243.002200", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643297243002200?thread_ts=1643289699.001200&cid=C01NBUP9DAR", - "thread": { - "id": "1643289699.001200", - "body": "getting an intro to the guy who built the Ubuntu community :smile: would that be a cool reference? :stuck_out_tongue:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T15:27:23.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Let’s gooo", - "channel": "random", - "slackId": "1643297243.002200", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643297243002200?thread_ts=1643289699.001200&cid=C01NBUP9DAR", - "thread": { - "id": "1643289699.001200", - "body": "getting an intro to the guy who built the Ubuntu community :smile: would that be a cool reference? :stuck_out_tongue:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-27T15:27:23.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Let’s gooo", - "channel": "random", - "slackId": "1643297243.002200", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643297243002200?thread_ts=1643289699.001200&cid=C01NBUP9DAR", - "thread": { - "id": "1643289699.001200", - "body": "getting an intro to the guy who built the Ubuntu community :smile: would that be a cool reference? :stuck_out_tongue:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T15:27:23.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yes!! :fire:", - "channel": "random", - "slackId": "1643289757.002000", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643289757002000?thread_ts=1643289699.001200&cid=C01NBUP9DAR", - "thread": { - "id": "1643289699.001200", - "body": "getting an intro to the guy who built the Ubuntu community :smile: would that be a cool reference? :stuck_out_tongue:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T13:22:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yes!! :fire:", - "channel": "random", - "slackId": "1643289757.002000", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643289757002000?thread_ts=1643289699.001200&cid=C01NBUP9DAR", - "thread": { - "id": "1643289699.001200", - "body": "getting an intro to the guy who built the Ubuntu community :smile: would that be a cool reference? :stuck_out_tongue:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T13:22:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yes!! :fire:", - "channel": "random", - "slackId": "1643289757.002000", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643289757002000?thread_ts=1643289699.001200&cid=C01NBUP9DAR", - "thread": { - "id": "1643289699.001200", - "body": "getting an intro to the guy who built the Ubuntu community :smile: would that be a cool reference? :stuck_out_tongue:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T13:22:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yes!! :fire:", - "channel": "random", - "slackId": "1643289757.002000", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643289757002000?thread_ts=1643289699.001200&cid=C01NBUP9DAR", - "thread": { - "id": "1643289699.001200", - "body": "getting an intro to the guy who built the Ubuntu community :smile: would that be a cool reference? :stuck_out_tongue:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T13:22:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Wow! Awesome :sunglasses: :clap: ", - "channel": "random", - "slackId": "1643289751.001800", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643289751001800?thread_ts=1643289699.001200&cid=C01NBUP9DAR", - "thread": { - "id": "1643289699.001200", - "body": "getting an intro to the guy who built the Ubuntu community :smile: would that be a cool reference? :stuck_out_tongue:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T13:22:31.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Wow! Awesome :sunglasses: :clap: ", - "channel": "random", - "slackId": "1643289751.001800", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643289751001800?thread_ts=1643289699.001200&cid=C01NBUP9DAR", - "thread": { - "id": "1643289699.001200", - "body": "getting an intro to the guy who built the Ubuntu community :smile: would that be a cool reference? :stuck_out_tongue:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T13:22:31.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Wow! Awesome :sunglasses: :clap: ", - "channel": "random", - "slackId": "1643289751.001800", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643289751001800?thread_ts=1643289699.001200&cid=C01NBUP9DAR", - "thread": { - "id": "1643289699.001200", - "body": "getting an intro to the guy who built the Ubuntu community :smile: would that be a cool reference? :stuck_out_tongue:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-27T13:22:31.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Wow! Awesome :sunglasses: :clap: ", - "channel": "random", - "slackId": "1643289751.001800", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643289751001800?thread_ts=1643289699.001200&cid=C01NBUP9DAR", - "thread": { - "id": "1643289699.001200", - "body": "getting an intro to the guy who built the Ubuntu community :smile: would that be a cool reference? :stuck_out_tongue:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-27T13:22:31.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "getting an intro to the guy who built the Ubuntu community :smile: would that be a cool reference? :stuck_out_tongue:", - "channel": "random", - "slackId": "1643289699.001200", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643289699001200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T13:21:39.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "getting an intro to the guy who built the Ubuntu community :smile: would that be a cool reference? :stuck_out_tongue:", - "channel": "random", - "slackId": "1643289699.001200", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643289699001200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T13:21:39.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "getting an intro to the guy who built the Ubuntu community :smile: would that be a cool reference? :stuck_out_tongue:", - "channel": "random", - "slackId": "1643289699.001200", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643289699001200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T13:21:39.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "getting an intro to the guy who built the Ubuntu community :smile: would that be a cool reference? :stuck_out_tongue:", - "channel": "random", - "slackId": "1643289699.001200", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643289699001200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T13:21:39.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "\ninteresting take on YC and Stripe =D", - "channel": "random", - "slackId": "1643283741.000300", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643283741000300" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T11:42:21.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "\ninteresting take on YC and Stripe =D", - "channel": "random", - "slackId": "1643283741.000300", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643283741000300" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T11:42:21.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "\ninteresting take on YC and Stripe =D", - "channel": "random", - "slackId": "1643283741.000300", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643283741000300" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T11:42:21.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "\ninteresting take on YC and Stripe =D", - "channel": "random", - "slackId": "1643283741.000300", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643283741000300" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-27T11:42:21.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "pray", - "slackId": "1643201302.000500", - "thread": { - "id": "1643185566.000300", - "body": "active member numbers should be correct now\nthere was a problem in the cache table creation" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-26T12:48:22.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "pray", - "slackId": "1643201302.000500", - "thread": { - "id": "1643185566.000300", - "body": "active member numbers should be correct now\nthere was a problem in the cache table creation" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-26T12:48:22.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "pray", - "slackId": "1643201302.000500", - "thread": { - "id": "1643185566.000300", - "body": "active member numbers should be correct now\nthere was a problem in the cache table creation" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-26T12:48:22.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "pray", - "slackId": "1643201302.000500", - "thread": { - "id": "1643185566.000300", - "body": "active member numbers should be correct now\nthere was a problem in the cache table creation" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-26T12:48:22.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "dev", - "slackId": "1643195797.000200", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643195797000200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-26T11:16:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "dev", - "slackId": "1643195797.000200", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643195797000200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-26T11:16:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "dev", - "slackId": "1643195797.000200", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643195797000200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-26T11:16:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "dev", - "slackId": "1643195797.000200", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1643195797000200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-26T11:16:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "will have to skip stand-up & coffee break today - schedule is insanely full :sweat_smile:", - "channel": "random", - "slackId": "1643190524.000800", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643190524000800" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-26T09:48:44.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "will have to skip stand-up & coffee break today - schedule is insanely full :sweat_smile:", - "channel": "random", - "slackId": "1643190524.000800", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643190524000800" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-26T09:48:44.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "will have to skip stand-up & coffee break today - schedule is insanely full :sweat_smile:", - "channel": "random", - "slackId": "1643190524.000800", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643190524000800" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-26T09:48:44.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "will have to skip stand-up & coffee break today - schedule is insanely full :sweat_smile:", - "channel": "random", - "slackId": "1643190524.000800", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1643190524000800" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-26T09:48:44.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "active member numbers should be correct now\nthere was a problem in the cache table creation", - "channel": "bugs", - "slackId": "1643185566.000300", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643185566000300?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-26T08:26:06.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "active member numbers should be correct now\nthere was a problem in the cache table creation", - "channel": "bugs", - "slackId": "1643185566.000300", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643185566000300?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-26T08:26:06.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "active member numbers should be correct now\nthere was a problem in the cache table creation", - "channel": "bugs", - "slackId": "1643185566.000300", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643185566000300?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-26T08:26:06.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "active member numbers should be correct now\nthere was a problem in the cache table creation", - "channel": "bugs", - "slackId": "1643185566.000300", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643185566000300?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-26T08:26:06.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "morning :sunny:", - "channel": "bugs", - "slackId": "1643185513.000100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643185513000100?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-26T08:25:13.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "morning :sunny:", - "channel": "bugs", - "slackId": "1643185513.000100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643185513000100?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-26T08:25:13.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "morning :sunny:", - "channel": "bugs", - "slackId": "1643185513.000100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643185513000100?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-26T08:25:13.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "morning :sunny:", - "channel": "bugs", - "slackId": "1643185513.000100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643185513000100?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-26T08:25:13.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "i will check the numbers and write back", - "channel": "bugs", - "slackId": "1643129377.002200", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129377002200?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:49:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "i will check the numbers and write back", - "channel": "bugs", - "slackId": "1643129377.002200", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129377002200?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:49:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "i will check the numbers and write back", - "channel": "bugs", - "slackId": "1643129377.002200", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129377002200?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:49:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "i will check the numbers and write back", - "channel": "bugs", - "slackId": "1643129377.002200", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129377002200?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:49:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "It’s already on staging", - "channel": "bugs", - "slackId": "1643129284.001900", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129284001900?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:48:04.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "It’s already on staging", - "channel": "bugs", - "slackId": "1643129284.001900", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129284001900?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:48:04.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "It’s already on staging", - "channel": "bugs", - "slackId": "1643129284.001900", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129284001900?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-25T16:48:04.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "It’s already on staging", - "channel": "bugs", - "slackId": "1643129284.001900", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129284001900?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:48:04.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "ok, that’s correct than… still the “Active Members” seems not to work", - "channel": "bugs", - "slackId": "1643129283.001700", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129283001700?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:48:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "ok, that’s correct than… still the “Active Members” seems not to work", - "channel": "bugs", - "slackId": "1643129283.001700", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129283001700?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:48:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "ok, that’s correct than… still the “Active Members” seems not to work", - "channel": "bugs", - "slackId": "1643129283.001700", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129283001700?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:48:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "ok, that’s correct than… still the “Active Members” seems not to work", - "channel": "bugs", - "slackId": "1643129283.001700", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129283001700?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:48:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1643129277.001600", - "thread": { - "id": "1643129241.001400", - "body": "This tweak will be deployed soon enough" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:47:57.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1643129277.001600", - "thread": { - "id": "1643129241.001400", - "body": "This tweak will be deployed soon enough" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:47:57.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1643129277.001600", - "thread": { - "id": "1643129241.001400", - "body": "This tweak will be deployed soon enough" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-25T16:47:57.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1643129277.001600", - "thread": { - "id": "1643129241.001400", - "body": "This tweak will be deployed soon enough" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-25T16:47:57.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "This tweak will be deployed soon enough", - "channel": "bugs", - "slackId": "1643129241.001400", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129241001400?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:47:21.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "This tweak will be deployed soon enough", - "channel": "bugs", - "slackId": "1643129241.001400", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129241001400?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:47:21.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "This tweak will be deployed soon enough", - "channel": "bugs", - "slackId": "1643129241.001400", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129241001400?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-25T16:47:21.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "This tweak will be deployed soon enough", - "channel": "bugs", - "slackId": "1643129241.001400", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129241001400?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:47:21.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "New Members", - "channel": "bugs", - "slackId": "1643129233.001200", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129233001200?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:47:13.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "New Members", - "channel": "bugs", - "slackId": "1643129233.001200", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129233001200?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:47:13.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "New Members", - "channel": "bugs", - "slackId": "1643129233.001200", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129233001200?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-25T16:47:13.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "New Members", - "channel": "bugs", - "slackId": "1643129233.001200", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129233001200?thread_ts=1643129197.001000&cid=C02LWNKS17B", - "thread": { - "id": "1643129197.001000", - "body": "Should “Members” refer to “Total members” or “New Members”?" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:47:13.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Should “Members” refer to “Total members” or “New Members”?", - "channel": "bugs", - "slackId": "1643129197.001000", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129197001000" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:46:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Should “Members” refer to “Total members” or “New Members”?", - "channel": "bugs", - "slackId": "1643129197.001000", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129197001000" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:46:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Should “Members” refer to “Total members” or “New Members”?", - "channel": "bugs", - "slackId": "1643129197.001000", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129197001000" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:46:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Should “Members” refer to “Total members” or “New Members”?", - "channel": "bugs", - "slackId": "1643129197.001000", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129197001000" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:46:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "bugs", - "slackId": "1643129176.000500", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129176000500" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:46:16.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "bugs", - "slackId": "1643129176.000500", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129176000500" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:46:16.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "bugs", - "slackId": "1643129176.000500", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129176000500" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:46:16.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "bugs", - "slackId": "1643129176.000500", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1643129176000500" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T16:46:16.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Me too, I’m in the middle of something sorry", - "channel": "general", - "slackId": "1643124955.000900", - "url": "https://crowddevspace.slack.com/archives/C01NTLA4EBT/p1643124955000900?thread_ts=1643124675.000300&cid=C01NTLA4EBT", - "thread": { - "id": "1643124675.000300", - "body": "sorry, I will skip coffee break" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T15:35:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Me too, I’m in the middle of something sorry", - "channel": "general", - "slackId": "1643124955.000900", - "url": "https://crowddevspace.slack.com/archives/C01NTLA4EBT/p1643124955000900?thread_ts=1643124675.000300&cid=C01NTLA4EBT", - "thread": { - "id": "1643124675.000300", - "body": "sorry, I will skip coffee break" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T15:35:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Me too, I’m in the middle of something sorry", - "channel": "general", - "slackId": "1643124955.000900", - "url": "https://crowddevspace.slack.com/archives/C01NTLA4EBT/p1643124955000900?thread_ts=1643124675.000300&cid=C01NTLA4EBT", - "thread": { - "id": "1643124675.000300", - "body": "sorry, I will skip coffee break" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-25T15:35:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Me too, I’m in the middle of something sorry", - "channel": "general", - "slackId": "1643124955.000900", - "url": "https://crowddevspace.slack.com/archives/C01NTLA4EBT/p1643124955000900?thread_ts=1643124675.000300&cid=C01NTLA4EBT", - "thread": { - "id": "1643124675.000300", - "body": "sorry, I will skip coffee break" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T15:35:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "sorry, I will skip coffee break", - "channel": "general", - "slackId": "1643124675.000300", - "url": "https://crowddevspace.slack.com/archives/C01NTLA4EBT/p1643124675000300" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T15:31:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "sorry, I will skip coffee break", - "channel": "general", - "slackId": "1643124675.000300", - "url": "https://crowddevspace.slack.com/archives/C01NTLA4EBT/p1643124675000300" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T15:31:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "sorry, I will skip coffee break", - "channel": "general", - "slackId": "1643124675.000300", - "url": "https://crowddevspace.slack.com/archives/C01NTLA4EBT/p1643124675000300" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T15:31:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "sorry, I will skip coffee break", - "channel": "general", - "slackId": "1643124675.000300", - "url": "https://crowddevspace.slack.com/archives/C01NTLA4EBT/p1643124675000300" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-25T15:31:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "happy weekend everyone! :slightly_smiling_face:", - "channel": "general", - "slackId": "1642788167.000300", - "url": "https://crowddevspace.slack.com/archives/C01NTLA4EBT/p1642788167000300" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T18:02:47.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "happy weekend everyone! :slightly_smiling_face:", - "channel": "general", - "slackId": "1642788167.000300", - "url": "https://crowddevspace.slack.com/archives/C01NTLA4EBT/p1642788167000300" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T18:02:47.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "happy weekend everyone! :slightly_smiling_face:", - "channel": "general", - "slackId": "1642788167.000300", - "url": "https://crowddevspace.slack.com/archives/C01NTLA4EBT/p1642788167000300" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T18:02:47.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "happy weekend everyone! :slightly_smiling_face:", - "channel": "general", - "slackId": "1642788167.000300", - "url": "https://crowddevspace.slack.com/archives/C01NTLA4EBT/p1642788167000300" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T18:02:47.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "eyes", - "slackId": "1642774167.000300", - "thread": { - "id": "1642774155.000100", - "body": "\n^ FYI @Joan Reyero, pull request created for this issue" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T14:09:27.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "eyes", - "slackId": "1642774167.000300", - "thread": { - "id": "1642774155.000100", - "body": "\n^ FYI @Joan Reyero, pull request created for this issue" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T14:09:27.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "eyes", - "slackId": "1642774167.000300", - "thread": { - "id": "1642774155.000100", - "body": "\n^ FYI @Joan Reyero, pull request created for this issue" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-21T14:09:27.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "eyes", - "slackId": "1642774167.000300", - "thread": { - "id": "1642774155.000100", - "body": "\n^ FYI @Joan Reyero, pull request created for this issue" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-21T14:09:27.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "\n^ FYI @Joan Reyero, pull request created for this issue", - "channel": "bugs", - "slackId": "1642774155.000100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642774155000100?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T14:09:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "\n^ FYI @Joan Reyero, pull request created for this issue", - "channel": "bugs", - "slackId": "1642774155.000100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642774155000100?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T14:09:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "\n^ FYI @Joan Reyero, pull request created for this issue", - "channel": "bugs", - "slackId": "1642774155.000100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642774155000100?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-21T14:09:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "\n^ FYI @Joan Reyero, pull request created for this issue", - "channel": "bugs", - "slackId": "1642774155.000100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642774155000100?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T14:09:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642761686.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "bryan_holland", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T10:41:26.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642758057.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "martha_calhoun", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T09:40:57.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642755650.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "alejandro_garner", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T09:00:50.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642751029.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kathleen_jones", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T07:43:49.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642750923.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "karen_andrews", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T07:42:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642749442.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kelly_lambert", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T07:17:22.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642746822.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "benjamin_howard", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T06:33:42.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642739901.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "dr._rebecca_nelson", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T04:38:21.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642739174.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "david_rogers", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T04:26:14.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642739166.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "krystal_summers", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T04:26:06.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642728501.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "david_boyd", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T01:28:21.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642723399.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "john_henry", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-21T00:03:19.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642722388.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "randy_bolton", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T23:46:28.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642720857.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "jordan_wilson", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T23:20:57.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642720744.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "gail_white", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T23:19:04.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642716921.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "james_johnson", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T22:15:21.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642713404.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "marie_warner", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T21:16:44.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642713317.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "calvin_gibbs", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T21:15:17.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642706281.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "stacy_miller", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T19:18:01.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642702147.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kathleen_jones", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T18:09:07.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642700600.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "jordan_wilson", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T17:43:20.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1642692045.007000", - "thread": { - "id": "1642691523.002300", - "body": "I’ve spotted this issue before, but haven’t had the opportunity to tackle it because it might take a few days\nMaybe I’ll tackle this now that I have an easier rest of the sprint" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:20:45.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1642692045.007000", - "thread": { - "id": "1642691523.002300", - "body": "I’ve spotted this issue before, but haven’t had the opportunity to tackle it because it might take a few days\nMaybe I’ll tackle this now that I have an easier rest of the sprint" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:20:45.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1642692045.007000", - "thread": { - "id": "1642691523.002300", - "body": "I’ve spotted this issue before, but haven’t had the opportunity to tackle it because it might take a few days\nMaybe I’ll tackle this now that I have an easier rest of the sprint" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-20T15:20:45.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1642692045.007000", - "thread": { - "id": "1642691523.002300", - "body": "I’ve spotted this issue before, but haven’t had the opportunity to tackle it because it might take a few days\nMaybe I’ll tackle this now that I have an easier rest of the sprint" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-20T15:20:45.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1642691873.003200", - "thread": { - "id": "1642691523.002300", - "body": "I’ve spotted this issue before, but haven’t had the opportunity to tackle it because it might take a few days\nMaybe I’ll tackle this now that I have an easier rest of the sprint" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:17:53.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1642691873.003200", - "thread": { - "id": "1642691523.002300", - "body": "I’ve spotted this issue before, but haven’t had the opportunity to tackle it because it might take a few days\nMaybe I’ll tackle this now that I have an easier rest of the sprint" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:17:53.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1642691873.003200", - "thread": { - "id": "1642691523.002300", - "body": "I’ve spotted this issue before, but haven’t had the opportunity to tackle it because it might take a few days\nMaybe I’ll tackle this now that I have an easier rest of the sprint" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-20T15:17:53.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1642691873.003200", - "thread": { - "id": "1642691523.002300", - "body": "I’ve spotted this issue before, but haven’t had the opportunity to tackle it because it might take a few days\nMaybe I’ll tackle this now that I have an easier rest of the sprint" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-20T15:17:53.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "with serverless", - "channel": "dev", - "slackId": "1642691642.002300", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642691642002300?thread_ts=1642691595.001900&cid=C01NBV2BDDK", - "thread": { - "id": "1642691595.001900", - "body": "" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:14:02.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "with serverless", - "channel": "dev", - "slackId": "1642691642.002300", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642691642002300?thread_ts=1642691595.001900&cid=C01NBV2BDDK", - "thread": { - "id": "1642691595.001900", - "body": "" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:14:02.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "with serverless", - "channel": "dev", - "slackId": "1642691642.002300", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642691642002300?thread_ts=1642691595.001900&cid=C01NBV2BDDK", - "thread": { - "id": "1642691595.001900", - "body": "" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:14:02.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "with serverless", - "channel": "dev", - "slackId": "1642691642.002300", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642691642002300?thread_ts=1642691595.001900&cid=C01NBV2BDDK", - "thread": { - "id": "1642691595.001900", - "body": "" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:14:02.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "basically os scaffoldhub?", - "channel": "dev", - "slackId": "1642691635.002100", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642691635002100?thread_ts=1642691595.001900&cid=C01NBV2BDDK", - "thread": { - "id": "1642691595.001900", - "body": "" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:13:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "basically os scaffoldhub?", - "channel": "dev", - "slackId": "1642691635.002100", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642691635002100?thread_ts=1642691595.001900&cid=C01NBV2BDDK", - "thread": { - "id": "1642691595.001900", - "body": "" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:13:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "basically os scaffoldhub?", - "channel": "dev", - "slackId": "1642691635.002100", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642691635002100?thread_ts=1642691595.001900&cid=C01NBV2BDDK", - "thread": { - "id": "1642691595.001900", - "body": "" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:13:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "basically os scaffoldhub?", - "channel": "dev", - "slackId": "1642691635.002100", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642691635002100?thread_ts=1642691595.001900&cid=C01NBV2BDDK", - "thread": { - "id": "1642691595.001900", - "body": "" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:13:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "dev", - "slackId": "1642691595.001900", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642691595001900" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:13:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "dev", - "slackId": "1642691595.001900", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642691595001900" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:13:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "dev", - "slackId": "1642691595.001900", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642691595001900" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:13:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "dev", - "slackId": "1642691595.001900", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642691595001900" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:13:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I’ve spotted this issue before, but haven’t had the opportunity to tackle it because it might take a few days\nMaybe I’ll tackle this now that I have an easy sprint", - "channel": "bugs", - "slackId": "1642691523.002300", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691523002300?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:12:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I’ve spotted this issue before, but haven’t had the opportunity to tackle it because it might take a few days\nMaybe I’ll tackle this now that I have an easy sprint", - "channel": "bugs", - "slackId": "1642691523.002300", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691523002300?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:12:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I’ve spotted this issue before, but haven’t had the opportunity to tackle it because it might take a few days\nMaybe I’ll tackle this now that I have an easy sprint", - "channel": "bugs", - "slackId": "1642691523.002300", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691523002300?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-20T15:12:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I’ve spotted this issue before, but haven’t had the opportunity to tackle it because it might take a few days\nMaybe I’ll tackle this now that I have an easy sprint", - "channel": "bugs", - "slackId": "1642691523.002300", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691523002300?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:12:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yeah with refresh it works", - "channel": "bugs", - "slackId": "1642691474.002100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691474002100?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:11:14.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yeah with refresh it works", - "channel": "bugs", - "slackId": "1642691474.002100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691474002100?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:11:14.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yeah with refresh it works", - "channel": "bugs", - "slackId": "1642691474.002100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691474002100?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:11:14.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yeah with refresh it works", - "channel": "bugs", - "slackId": "1642691474.002100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691474002100?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:11:14.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "okay in incognito it just worked", - "channel": "bugs", - "slackId": "1642691464.001900", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691464001900?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:11:04.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "okay in incognito it just worked", - "channel": "bugs", - "slackId": "1642691464.001900", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691464001900?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:11:04.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "okay in incognito it just worked", - "channel": "bugs", - "slackId": "1642691464.001900", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691464001900?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:11:04.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "okay in incognito it just worked", - "channel": "bugs", - "slackId": "1642691464.001900", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691464001900?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:11:04.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "If you do a hard refresh does it solve the issue?", - "channel": "bugs", - "slackId": "1642691433.001700", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691433001700?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:10:33.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "If you do a hard refresh does it solve the issue?", - "channel": "bugs", - "slackId": "1642691433.001700", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691433001700?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:10:33.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "If you do a hard refresh does it solve the issue?", - "channel": "bugs", - "slackId": "1642691433.001700", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691433001700?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-20T15:10:33.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "If you do a hard refresh does it solve the issue?", - "channel": "bugs", - "slackId": "1642691433.001700", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691433001700?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:10:33.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "both", - "channel": "bugs", - "slackId": "1642691419.001500", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691419001500?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:10:19.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "both", - "channel": "bugs", - "slackId": "1642691419.001500", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691419001500?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:10:19.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "both", - "channel": "bugs", - "slackId": "1642691419.001500", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691419001500?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:10:19.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "both", - "channel": "bugs", - "slackId": "1642691419.001500", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691419001500?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:10:19.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "bugs", - "slackId": "1642691410.001100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691410001100?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:10:10.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "bugs", - "slackId": "1642691410.001100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691410001100?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:10:10.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "bugs", - "slackId": "1642691410.001100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691410001100?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:10:10.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "bugs", - "slackId": "1642691410.001100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691410001100?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:10:10.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Does this happen after you logout from another account?", - "channel": "bugs", - "slackId": "1642691406.000900", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691406000900?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:10:06.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Does this happen after you logout from another account?", - "channel": "bugs", - "slackId": "1642691406.000900", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691406000900?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:10:06.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Does this happen after you logout from another account?", - "channel": "bugs", - "slackId": "1642691406.000900", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691406000900?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-20T15:10:06.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Does this happen after you logout from another account?", - "channel": "bugs", - "slackId": "1642691406.000900", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691406000900?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:10:06.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yes", - "channel": "bugs", - "slackId": "1642691399.000700", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691399000700?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:09:59.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yes", - "channel": "bugs", - "slackId": "1642691399.000700", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691399000700?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:09:59.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yes", - "channel": "bugs", - "slackId": "1642691399.000700", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691399000700?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:09:59.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yes", - "channel": "bugs", - "slackId": "1642691399.000700", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691399000700?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:09:59.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "In prod?", - "channel": "bugs", - "slackId": "1642691396.000500", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691396000500?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:09:56.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "In prod?", - "channel": "bugs", - "slackId": "1642691396.000500", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691396000500?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:09:56.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "In prod?", - "channel": "bugs", - "slackId": "1642691396.000500", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691396000500?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-20T15:09:56.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "In prod?", - "channel": "bugs", - "slackId": "1642691396.000500", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691396000500?thread_ts=1642691356.000400&cid=C02LWNKS17B", - "thread": { - "id": "1642691356.000400", - "body": "getting a 500 every time I log in :confused:" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:09:56.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "getting a 500 every time I log in :confused:", - "channel": "bugs", - "slackId": "1642691356.000400", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691356000400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:09:16.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "getting a 500 every time I log in :confused:", - "channel": "bugs", - "slackId": "1642691356.000400", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691356000400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:09:16.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "getting a 500 every time I log in :confused:", - "channel": "bugs", - "slackId": "1642691356.000400", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691356000400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:09:16.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "getting a 500 every time I log in :confused:", - "channel": "bugs", - "slackId": "1642691356.000400", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642691356000400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T15:09:16.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642689735.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "corey_williams", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T14:42:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642688735.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "randy_bolton", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T14:25:35.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642685846.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "laura_armstrong", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T13:37:26.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642684175.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "stacy_miller", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T13:09:35.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642681111.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "jenny_cruz", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T12:18:31.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642680537.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "david_cunningham", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T12:08:57.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "title": "readme fixes & add contribution guide", - "state": "open", - "url": "https://github.com/nhost/nhost/pull/119", - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Vadim Smirnov", - "isHireable": true, - "url": "https://github.com/FuzzyReason", - "websiteUrl": "https://fuzzyreason.io/", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-20T11:31:22Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/_vscodes" - } - }, - "username": "FuzzyReason", - "type": "member", - "score": 1, - "email": "", - "organisation": "@nhost", - "location": "Minsk, Belarus", - "bio": "Software Engineer && JavaScript Addict" - }, - "timestamp": "2022-01-20T11:31:22.000Z", - "type": "pull_request-opened", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642676411.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "tony_blevins", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T11:00:11.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Guido Schmitz", - "isHireable": false, - "url": "https://github.com/guidsen", - "websiteUrl": "http://www.guidoschmitz.co", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-20T10:02:04Z" - } - ] - } - }, - "username": "guidsen", - "type": "member", - "score": 0, - "email": "", - "organisation": "@FlexAppeal", - "location": "Rotterdam, The Netherlands", - "bio": "NodeJS. ReactJS. APIs. Architecture. Scalability." - }, - "timestamp": "2022-01-20T10:02:04.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "isHireable": false, - "url": "https://github.com/shuxiaokai", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-20T09:16:10Z" - } - ] - } - }, - "username": "shuxiaokai", - "type": "member", - "score": 0, - "email": "1660507550@qq.com", - "organisation": "", - "location": "Guangdong", - "bio": "ppppppppython" - }, - "timestamp": "2022-01-20T09:16:10.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "What do you think the advantage of a new table would be? \nThey are a flexible entity, so we probably would just have a 1-1 relation with tenant and a JSONB field no? ", - "channel": "dev", - "slackId": "1642670163.001600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642670163001600?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T09:16:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "What do you think the advantage of a new table would be? \nThey are a flexible entity, so we probably would just have a 1-1 relation with tenant and a JSONB field no? ", - "channel": "dev", - "slackId": "1642670163.001600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642670163001600?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T09:16:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "What do you think the advantage of a new table would be? \nThey are a flexible entity, so we probably would just have a 1-1 relation with tenant and a JSONB field no? ", - "channel": "dev", - "slackId": "1642670163.001600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642670163001600?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-20T09:16:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "What do you think the advantage of a new table would be? \nThey are a flexible entity, so we probably would just have a 1-1 relation with tenant and a JSONB field no? ", - "channel": "dev", - "slackId": "1642670163.001600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642670163001600?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-20T09:16:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642669979.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "karen_andrews", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T09:12:59.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "isHireable": false, - "url": "https://github.com/techinoviq9000", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-20T09:04:25Z" - } - ] - } - }, - "username": "techinoviq9000", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-20T09:04:25.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642666112.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "michael_blair", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T08:08:32.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642661745.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "stephanie_allen", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T06:55:45.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642659218.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "casey_smith", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T06:13:38.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642656236.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "heidi_oconnor", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T05:23:56.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642656123.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "stephanie_allen", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T05:22:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642654552.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "calvin_gibbs", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T04:55:52.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642652970.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "calvin_gibbs", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T04:29:30.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642650559.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "donna_colon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T03:49:19.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642647867.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "dawn_bridges", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T03:04:27.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642643913.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "randy_bolton", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T01:58:33.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642641190.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "brandy_sanders", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T01:13:10.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642640719.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "heidi_oconnor", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T01:05:19.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642636806.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "stacy_miller", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-20T00:00:06.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642631607.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "randy_bolton", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T22:33:27.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642629901.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "scott_frye", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T22:05:01.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Gerard Wilkinson", - "isHireable": true, - "url": "https://github.com/GerryWilko", - "websiteUrl": "https://gerardwilkinson.com", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-19T21:57:50Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/GerryWilko" - } - }, - "username": "GerryWilko", - "type": "member", - "score": 0, - "email": "", - "organisation": "@think-active-labs @ignite-systems", - "location": "Manchester, UK", - "bio": "Lead Software Engineer at Ignite Systems and Director at Think Active Labs." - }, - "timestamp": "2022-01-19T21:57:50.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642621874.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "randy_bolton", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T19:51:14.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642620230.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "bryan_holland", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T19:23:50.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642619435.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "calvin_gibbs", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T19:10:35.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642619006.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "randy_bolton", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T19:03:26.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642618893.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "corey_williams", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T19:01:33.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642616064.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kelly_lambert", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T18:14:24.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642614713.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "lucas_marquez", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T17:51:53.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "but separating the integrations to another table make some sense", - "channel": "dev", - "slackId": "1642612236.003600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642612236003600?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T17:10:36.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "but separating the integrations to another table make some sense", - "channel": "dev", - "slackId": "1642612236.003600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642612236003600?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T17:10:36.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "but separating the integrations to another table make some sense", - "channel": "dev", - "slackId": "1642612236.003600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642612236003600?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T17:10:36.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "but separating the integrations to another table make some sense", - "channel": "dev", - "slackId": "1642612236.003600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642612236003600?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T17:10:36.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "we were thinking putting them in tenant", - "channel": "dev", - "slackId": "1642612192.003400", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642612192003400?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T17:09:52.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "we were thinking putting them in tenant", - "channel": "dev", - "slackId": "1642612192.003400", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642612192003400?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T17:09:52.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "we were thinking putting them in tenant", - "channel": "dev", - "slackId": "1642612192.003400", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642612192003400?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T17:09:52.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "we were thinking putting them in tenant", - "channel": "dev", - "slackId": "1642612192.003400", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642612192003400?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T17:09:52.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "cubejs data modeling workshop starting in 5min:\n", - "channel": "dev", - "slackId": "1642611938.003200", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642611938003200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T17:05:38.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "cubejs data modeling workshop starting in 5min:\n", - "channel": "dev", - "slackId": "1642611938.003200", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642611938003200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T17:05:38.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "cubejs data modeling workshop starting in 5min:\n", - "channel": "dev", - "slackId": "1642611938.003200", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642611938003200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T17:05:38.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "cubejs data modeling workshop starting in 5min:\n", - "channel": "dev", - "slackId": "1642611938.003200", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642611938003200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T17:05:38.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642610309.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kelly_lambert", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T16:38:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642608763.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "david_rogers", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T16:12:43.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642607508.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "krystal_summers", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T15:51:48.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642606364.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "krystal_summers", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T15:32:44.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1642605559.002500", - "thread": { - "id": "1642595889.001600", - "body": "2. Yes :+1: tags could be related to Members or any other entity, for example activities, or lookalike members if we want to split those out of Members" - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T15:19:19.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1642605559.002500", - "thread": { - "id": "1642595889.001600", - "body": "2. Yes :+1: tags could be related to Members or any other entity, for example activities, or lookalike members if we want to split those out of Members" - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T15:19:19.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1642605559.002500", - "thread": { - "id": "1642595889.001600", - "body": "2. Yes :+1: tags could be related to Members or any other entity, for example activities, or lookalike members if we want to split those out of Members" - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T15:19:19.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1642605559.002500", - "thread": { - "id": "1642595889.001600", - "body": "2. Yes :+1: tags could be related to Members or any other entity, for example activities, or lookalike members if we want to split those out of Members" - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T15:19:19.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642604969.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "madison_miller", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T15:09:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Domains are case insensitive but paths are (usually).\nTest this:\nhttp://www.google.com/intl/en/about/corporate/index.html\nhttp://www.google.com/intl/en/ABOUT/corporate/index.html", - "title": "Functions filenames fail if they don't match exactly the filename", - "parent_url": "https://github.com/nhost/nhost/issues/92", - "url": "https://github.com/nhost/nhost/issues/92#issuecomment-1016488761", - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Johan Eliasson", - "isHireable": false, - "url": "https://github.com/elitan", - "websiteUrl": "https://nhost.io", - "actions": [ - { - "score": 3, - "timestamp": "2021-03-01T15:34:14Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/elitasson" - } - }, - "username": "elitan", - "type": "member", - "score": 10, - "email": "johan@eliasson.me", - "organisation": "", - "location": "The Internet", - "bio": "Exploiting regularities to my benefit." - }, - "timestamp": "2022-01-19T13:55:33.000Z", - "type": "issue-comment", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642597094.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "thomas_moreno", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T12:58:14.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Description\nChecklist 🔏\nSome important checks that must be ensured before merging:\nDeploy to staging\nDeployment checklist\n\n Make sure all AWS λ functions are up to date with your version (if applicable)\n Make sure the anton-environment and soa-environment are updated and pushed\n\nFunctionality\n\n Has the functionality been checked in a normal staging tenant? (not local)\n Has the functionality been checked in the large tenant? (team+large@crowd.dev)\n Has the functionality been checked in an empty tenant? (team+empty@crowd.dev)\n Is there any more edge cases that should be taken into account?\n\nCode quality\n\n Are there comments in the main functionality of the code?\n Are all tests passing?\n Are all URLs to external services in .env files? Never hard-coded\n Are all secrets in anton-environment? Never, ever hard-coded\n\n🔥🚀💪🏼", - "title": "[WIP] Refactor widgets", - "state": "open", - "url": "https://github.com/CrowdHQ/crowd-web/pull/70", - "repo": "https://github.com/CrowdHQ/crowd-web" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "mariobalca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-19T12:53:42.000Z", - "type": "pull_request-opened", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Btw where will integrations be stored now that we don’t have a project?", - "channel": "dev", - "slackId": "1642596058.002200", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642596058002200?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T12:40:58.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Btw where will integrations be stored now that we don’t have a project?", - "channel": "dev", - "slackId": "1642596058.002200", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642596058002200?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T12:40:58.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Btw where will integrations be stored now that we don’t have a project?", - "channel": "dev", - "slackId": "1642596058.002200", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642596058002200?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-19T12:40:58.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Btw where will integrations be stored now that we don’t have a project?", - "channel": "dev", - "slackId": "1642596058.002200", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642596058002200?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T12:40:58.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@Joan Reyero @Jonathan Reimer thoughts?", - "channel": "dev", - "slackId": "1642595969.002000", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642595969002000?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T12:39:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@Joan Reyero @Jonathan Reimer thoughts?", - "channel": "dev", - "slackId": "1642595969.002000", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642595969002000?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T12:39:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@Joan Reyero @Jonathan Reimer thoughts?", - "channel": "dev", - "slackId": "1642595969.002000", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642595969002000?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-19T12:39:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@Joan Reyero @Jonathan Reimer thoughts?", - "channel": "dev", - "slackId": "1642595969.002000", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642595969002000?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T12:39:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "^ But this is just an idea that seemed interesting enough to discuss giving the fact that we’re creating a new db", - "channel": "dev", - "slackId": "1642595952.001800", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642595952001800?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T12:39:12.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "^ But this is just an idea that seemed interesting enough to discuss giving the fact that we’re creating a new db", - "channel": "dev", - "slackId": "1642595952.001800", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642595952001800?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T12:39:12.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "^ But this is just an idea that seemed interesting enough to discuss giving the fact that we’re creating a new db", - "channel": "dev", - "slackId": "1642595952.001800", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642595952001800?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-19T12:39:12.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "^ But this is just an idea that seemed interesting enough to discuss giving the fact that we’re creating a new db", - "channel": "dev", - "slackId": "1642595952.001800", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642595952001800?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T12:39:12.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Vlad Gorodetsky", - "isHireable": false, - "url": "https://github.com/bai", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-19T12:38:56Z" - } - ] - } - }, - "username": "bai", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-19T12:38:56.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "2. Yes :+1: tags could be related to Members or any other entity, for example activities, or lookalike members if we want to split those out of Members", - "channel": "dev", - "slackId": "1642595889.001600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642595889001600?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T12:38:09.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "2. Yes :+1: tags could be related to Members or any other entity, for example activities, or lookalike members if we want to split those out of Members", - "channel": "dev", - "slackId": "1642595889.001600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642595889001600?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T12:38:09.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "2. Yes :+1: tags could be related to Members or any other entity, for example activities, or lookalike members if we want to split those out of Members", - "channel": "dev", - "slackId": "1642595889.001600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642595889001600?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-19T12:38:09.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "2. Yes :+1: tags could be related to Members or any other entity, for example activities, or lookalike members if we want to split those out of Members", - "channel": "dev", - "slackId": "1642595889.001600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642595889001600?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T12:38:09.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "1. yeah ur right - widgets need a nullable reportId field (so that they can belong to a report or not)\n2. what do you mean by polymorhpic? Like in the future activities may have tags as well? or smthn else\n3. yeah some of them is missing id field I'll add them thanks for pointing out", - "channel": "dev", - "slackId": "1642595810.001400", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642595810001400?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T12:36:50.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "1. yeah ur right - widgets need a nullable reportId field (so that they can belong to a report or not)\n2. what do you mean by polymorhpic? Like in the future activities may have tags as well? or smthn else\n3. yeah some of them is missing id field I'll add them thanks for pointing out", - "channel": "dev", - "slackId": "1642595810.001400", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642595810001400?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T12:36:50.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "1. yeah ur right - widgets need a nullable reportId field (so that they can belong to a report or not)\n2. what do you mean by polymorhpic? Like in the future activities may have tags as well? or smthn else\n3. yeah some of them is missing id field I'll add them thanks for pointing out", - "channel": "dev", - "slackId": "1642595810.001400", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642595810001400?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T12:36:50.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "1. yeah ur right - widgets need a nullable reportId field (so that they can belong to a report or not)\n2. what do you mean by polymorhpic? Like in the future activities may have tags as well? or smthn else\n3. yeah some of them is missing id field I'll add them thanks for pointing out", - "channel": "dev", - "slackId": "1642595810.001400", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642595810001400?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T12:36:50.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Description\nCreate default report and related widgets after tenant creation\nChecklist 🔏\nSome important checks that must be ensured before merging:\nDeploy to staging\nDeployment checklist\n\n Make sure all AWS λ functions are up to date with your version (if applicable)\n Make sure the anton-environment and soa-environment are updated and pushed\n\nFunctionality\n\n Has the functionality been checked in a normal staging tenant? (not local)\n Has the functionality been checked in the large tenant? (team+large@crowd.dev)\n Has the functionality been checked in an empty tenant? (team+empty@crowd.dev)\n Is there any more edge cases that should be taken into account?\n\nCode quality\n\n Are there comments in the main functionality of the code?\n Are all tests passing?\n Are all URLs to external services in .env files? Never hard-coded\n Are all secrets in anton-environment? Never, ever hard-coded\n\n🔥🚀💪🏼", - "title": "Create default report and widgets after tenant creation", - "state": "open", - "url": "https://github.com/CrowdHQ/crowd-web/pull/69", - "repo": "https://github.com/CrowdHQ/crowd-web" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "mariobalca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-19T12:25:08.000Z", - "type": "pull_request-opened", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "anand7u", - "isHireable": true, - "url": "https://github.com/Anan7Codes", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-30T12:42:32Z" - } - ], - "websiteUrl": "rough-paper.com" - }, - "twitter": { - "url": "https://twitter.com/Anan7Tweets" - } - }, - "username": "Anan7Codes", - "type": "member", - "score": 0, - "email": "", - "organisation": "Roughpaper Technologies", - "location": "Dubai", - "bio": "I think I'm a decent developer." - }, - "timestamp": "2022-01-19T12:21:22.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "This is intentional. URLs and files are case-sensitive. However, I would recommend keeping all function file names to lowercase.\n\nFair enough that it is intentional.\nJust a comment, URLs are not case sensitive for the user. Try write in GoOgle.com in your browser and you'll still be directed to google.com. Same with if you try and capitalize a article url on e.g. theverge.com", - "title": "Functions filenames fail if they don't match exactly the filename", - "parent_url": "https://github.com/nhost/nhost/issues/92", - "url": "https://github.com/nhost/nhost/issues/92#issuecomment-1016396935", - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "isHireable": false, - "url": "https://github.com/Svarto", - "actions": [ - { - "score": 3, - "timestamp": "2021-02-20T09:40:10Z" - } - ] - } - }, - "username": "Svarto", - "type": "member", - "score": 8, - "email": "", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-19T11:57:21.000Z", - "type": "issue-comment", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "body": "This is intentional. URLs and files are case-sensitive. However, I would recommend keeping all function file names to lowercase.", - "title": "Functions filenames fail if they don't match exactly the filename", - "parent_url": "https://github.com/nhost/nhost/issues/92", - "url": "https://github.com/nhost/nhost/issues/92#issuecomment-1016392086", - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Johan Eliasson", - "isHireable": false, - "url": "https://github.com/elitan", - "websiteUrl": "https://nhost.io", - "actions": [ - { - "score": 3, - "timestamp": "2021-03-01T15:34:14Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/elitasson" - } - }, - "username": "elitan", - "type": "member", - "score": 10, - "email": "johan@eliasson.me", - "organisation": "", - "location": "The Internet", - "bio": "Exploiting regularities to my benefit." - }, - "timestamp": "2022-01-19T11:50:20.000Z", - "type": "issue-comment", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "body": "This is now working 👍\nPlease make sure to use the new CLI and the new npm packages. All info here: https://docs.nhost.io/get-started", - "title": "auth.requestPasswordChange not working - message: \"\"email\" must be a string\"", - "parent_url": "https://github.com/nhost/nhost/issues/83", - "url": "https://github.com/nhost/nhost/issues/83#issuecomment-1016380648", - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Johan Eliasson", - "isHireable": false, - "url": "https://github.com/elitan", - "websiteUrl": "https://nhost.io", - "actions": [ - { - "score": 3, - "timestamp": "2021-03-01T15:34:14Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/elitasson" - } - }, - "username": "elitan", - "type": "member", - "score": 10, - "email": "johan@eliasson.me", - "organisation": "", - "location": "The Internet", - "bio": "Exploiting regularities to my benefit." - }, - "timestamp": "2022-01-19T11:40:58.000Z", - "type": "issue-comment", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "body": "This is now fixed in both prod and with the CLI.\nBtw @camillo18tre, make sure to use our CLI specified here: https://docs.nhost.io/platform/nhost/local-development\nand this npm package: @nhost/nhost-js.\nThe ones you're referring to are old packages for Nhost v1.", - "title": "auth.changePassword not working - message: old_password must be a string", - "parent_url": "https://github.com/nhost/nhost/issues/82", - "url": "https://github.com/nhost/nhost/issues/82#issuecomment-1016379364", - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Johan Eliasson", - "isHireable": false, - "url": "https://github.com/elitan", - "websiteUrl": "https://nhost.io", - "actions": [ - { - "score": 3, - "timestamp": "2021-03-01T15:34:14Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/elitasson" - } - }, - "username": "elitan", - "type": "member", - "score": 10, - "email": "johan@eliasson.me", - "organisation": "", - "location": "The Internet", - "bio": "Exploiting regularities to my benefit." - }, - "timestamp": "2022-01-19T11:39:44.000Z", - "type": "issue-comment", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642592355.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "marie_warner", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T11:39:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Done:\nhttps://github.com/nhost/nhost/tree/main/templates/web/react-apollo", - "title": "Updating react template with TypeScript + codegen support.", - "parent_url": "https://github.com/nhost/nhost/issues/54", - "url": "https://github.com/nhost/nhost/issues/54#issuecomment-1016377856", - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Johan Eliasson", - "isHireable": false, - "url": "https://github.com/elitan", - "websiteUrl": "https://nhost.io", - "actions": [ - { - "score": 3, - "timestamp": "2021-03-01T15:34:14Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/elitasson" - } - }, - "username": "elitan", - "type": "member", - "score": 10, - "email": "johan@eliasson.me", - "organisation": "", - "location": "The Internet", - "bio": "Exploiting regularities to my benefit." - }, - "timestamp": "2022-01-19T11:38:22.000Z", - "type": "issue-comment", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Storage rules are now part of Hasura permissions.", - "title": "Move Storage Rules to Storage Tab in Console", - "parent_url": "https://github.com/nhost/nhost/issues/25", - "url": "https://github.com/nhost/nhost/issues/25#issuecomment-1016377139", - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Johan Eliasson", - "isHireable": false, - "url": "https://github.com/elitan", - "websiteUrl": "https://nhost.io", - "actions": [ - { - "score": 3, - "timestamp": "2021-03-01T15:34:14Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/elitasson" - } - }, - "username": "elitan", - "type": "member", - "score": 10, - "email": "johan@eliasson.me", - "organisation": "", - "location": "The Internet", - "bio": "Exploiting regularities to my benefit." - }, - "timestamp": "2022-01-19T11:37:38.000Z", - "type": "issue-comment", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "body": "This is working (and has been working for a few months) :P", - "title": "Allow Deployments from Specific Branches", - "parent_url": "https://github.com/nhost/nhost/issues/22", - "url": "https://github.com/nhost/nhost/issues/22#issuecomment-1016376740", - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Johan Eliasson", - "isHireable": false, - "url": "https://github.com/elitan", - "websiteUrl": "https://nhost.io", - "actions": [ - { - "score": 3, - "timestamp": "2021-03-01T15:34:14Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/elitasson" - } - }, - "username": "elitan", - "type": "member", - "score": 10, - "email": "johan@eliasson.me", - "organisation": "", - "location": "The Internet", - "bio": "Exploiting regularities to my benefit." - }, - "timestamp": "2022-01-19T11:37:16.000Z", - "type": "issue-comment", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Storage rules are now part of Hasura permissions.", - "title": "Versioning of storage rules", - "parent_url": "https://github.com/nhost/nhost/issues/36", - "url": "https://github.com/nhost/nhost/issues/36#issuecomment-1016375126", - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Johan Eliasson", - "isHireable": false, - "url": "https://github.com/elitan", - "websiteUrl": "https://nhost.io", - "actions": [ - { - "score": 3, - "timestamp": "2021-03-01T15:34:14Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/elitasson" - } - }, - "username": "elitan", - "type": "member", - "score": 10, - "email": "johan@eliasson.me", - "organisation": "", - "location": "The Internet", - "bio": "Exploiting regularities to my benefit." - }, - "timestamp": "2022-01-19T11:35:45.000Z", - "type": "issue-comment", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "body": "This is now implemented. All tables in auth and storage schemas are using camelCase.\nReference: https://github.com/nhost/hasura-auth/blob/main/src/metadata.ts#L211-L235", - "title": "Add support for camelCase in users table", - "parent_url": "https://github.com/nhost/nhost/issues/52", - "url": "https://github.com/nhost/nhost/issues/52#issuecomment-1016373059", - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Johan Eliasson", - "isHireable": false, - "url": "https://github.com/elitan", - "websiteUrl": "https://nhost.io", - "actions": [ - { - "score": 3, - "timestamp": "2021-03-01T15:34:14Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/elitasson" - } - }, - "username": "elitan", - "type": "member", - "score": 10, - "email": "johan@eliasson.me", - "organisation": "", - "location": "The Internet", - "bio": "Exploiting regularities to my benefit." - }, - "timestamp": "2022-01-19T11:33:45.000Z", - "type": "issue-comment", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Apart from these 3 comments looks really good! Nice job guys :muscle:", - "channel": "dev", - "slackId": "1642591562.001200", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642591562001200?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T11:26:02.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Apart from these 3 comments looks really good! Nice job guys :muscle:", - "channel": "dev", - "slackId": "1642591562.001200", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642591562001200?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T11:26:02.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Apart from these 3 comments looks really good! Nice job guys :muscle:", - "channel": "dev", - "slackId": "1642591562.001200", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642591562001200?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-19T11:26:02.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Apart from these 3 comments looks really good! Nice job guys :muscle:", - "channel": "dev", - "slackId": "1642591562.001200", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642591562001200?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T11:26:02.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Finally. just noticed that the relationship table `tenantUsers` has a proper id as primary key, while the member relationship tables don’t\nNot sure if this was on purpose but maybe we could use a consistent approach for all of these", - "channel": "dev", - "slackId": "1642591544.001000", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642591544001000?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T11:25:44.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Finally. just noticed that the relationship table `tenantUsers` has a proper id as primary key, while the member relationship tables don’t\nNot sure if this was on purpose but maybe we could use a consistent approach for all of these", - "channel": "dev", - "slackId": "1642591544.001000", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642591544001000?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T11:25:44.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Finally. just noticed that the relationship table `tenantUsers` has a proper id as primary key, while the member relationship tables don’t\nNot sure if this was on purpose but maybe we could use a consistent approach for all of these", - "channel": "dev", - "slackId": "1642591544.001000", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642591544001000?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-19T11:25:44.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Finally. just noticed that the relationship table `tenantUsers` has a proper id as primary key, while the member relationship tables don’t\nNot sure if this was on purpose but maybe we could use a consistent approach for all of these", - "channel": "dev", - "slackId": "1642591544.001000", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642591544001000?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T11:25:44.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Also, would it make sense to make tags polymorphic right from the start?", - "channel": "dev", - "slackId": "1642591311.000800", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642591311000800?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T11:21:51.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Also, would it make sense to make tags polymorphic right from the start?", - "channel": "dev", - "slackId": "1642591311.000800", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642591311000800?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T11:21:51.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Also, would it make sense to make tags polymorphic right from the start?", - "channel": "dev", - "slackId": "1642591311.000800", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642591311000800?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-19T11:21:51.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Also, would it make sense to make tags polymorphic right from the start?", - "channel": "dev", - "slackId": "1642591311.000800", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642591311000800?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T11:21:51.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I think we might need a non-mandatory relationship between widgets and reports, so widgets can belong to reports, or they can be standalone (for example dashboard widgets)", - "channel": "dev", - "slackId": "1642591210.000600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642591210000600?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T11:20:10.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I think we might need a non-mandatory relationship between widgets and reports, so widgets can belong to reports, or they can be standalone (for example dashboard widgets)", - "channel": "dev", - "slackId": "1642591210.000600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642591210000600?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T11:20:10.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I think we might need a non-mandatory relationship between widgets and reports, so widgets can belong to reports, or they can be standalone (for example dashboard widgets)", - "channel": "dev", - "slackId": "1642591210.000600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642591210000600?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-19T11:20:10.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I think we might need a non-mandatory relationship between widgets and reports, so widgets can belong to reports, or they can be standalone (for example dashboard widgets)", - "channel": "dev", - "slackId": "1642591210.000600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642591210000600?thread_ts=1642590874.000300&cid=C01NBV2BDDK", - "thread": { - "id": "1642590874.000300", - "body": "table-entity relations" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T11:20:10.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "dev", - "slackId": "1642590874.000300", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642590874000300" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T11:14:34.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "dev", - "slackId": "1642590874.000300", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642590874000300" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T11:14:34.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "dev", - "slackId": "1642590874.000300", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642590874000300" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T11:14:34.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "dev", - "slackId": "1642590874.000300", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642590874000300" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T11:14:34.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642590772.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "jenny_cruz", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T11:12:52.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642589375.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kathleen_jones", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T10:49:35.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642589168.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "robert_harmon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T10:46:08.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642587587.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "donna_colon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T10:19:47.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Johanna Blom", - "isHireable": true, - "url": "https://github.com/idsintehittapa", - "websiteUrl": "https://www.linkedin.com/in/johanna-blom-2419a181/", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-19T09:18:35Z" - } - ] - } - }, - "username": "idsintehittapa", - "type": "member", - "score": 0, - "email": "", - "organisation": "Mentimeter", - "location": "Stockholm, Sweden", - "bio": "Front-end developer.\r\nCreative code, art and sociology makes me tick. I use JS (with different frameworks), CSS, and HTML to develop fun and creative things." - }, - "timestamp": "2022-01-19T09:18:35.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642583249.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "ethan_clay", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T09:07:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642582891.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "tony_blevins", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T09:01:31.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642576492.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "michelle_calderon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T07:14:52.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642573238.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "tony_blevins", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T06:20:38.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Issue was resolved on the Nhost end. Thanks team!", - "title": "Bug - x-hasura-user-id is not accessible in production function", - "parent_url": "https://github.com/nhost/nhost/issues/80", - "url": "https://github.com/nhost/nhost/issues/80#issuecomment-1016117708", - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Tuan Duong", - "isHireable": true, - "url": "https://github.com/wontwon", - "actions": [ - { - "score": 3, - "timestamp": "2021-11-25T06:52:17Z" - } - ] - } - }, - "username": "wontwon", - "type": "member", - "score": 1, - "email": "tuanduong15@gmail.com", - "organisation": "", - "location": "Seattle, WA", - "bio": "" - }, - "timestamp": "2022-01-19T06:12:31.000Z", - "type": "issue-comment", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642572008.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kathleen_jones", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T06:00:08.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642569175.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "bryan_holland", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T05:12:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642562522.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "donna_colon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T03:22:02.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642561085.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kathleen_jones", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T02:58:05.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "isHireable": false, - "url": "https://github.com/sebagudelo", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-27T02:58:38Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/sebagudelo" - } - }, - "username": "sebagudelo", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-19T02:26:38.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642550745.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kelly_lambert", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-19T00:05:45.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642547368.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "michelle_calderon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T23:09:28.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost-js-sdk" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Brayden", - "isHireable": true, - "url": "https://github.com/BraydenTW", - "websiteUrl": "braydentw.com" - } - }, - "username": "BraydenTW", - "type": "member", - "score": 0, - "email": "brayden45.dev@gmail.com", - "organisation": "", - "location": "The Milky Way", - "bio": "Frontend Dev & Designer | React, NextJS, Tailwind | Studio C Fan (the old cast)" - }, - "timestamp": "2022-01-18T22:49:24.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642535340.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "tara_norman", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T19:49:00.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/hasura-backend-plus" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Josh West", - "isHireable": false, - "url": "https://github.com/jooosh" - } - }, - "username": "jooosh", - "type": "member", - "score": 0, - "email": "", - "organisation": "Super Humane", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-18T19:33:10.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642534179.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "dawn_bridges", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T19:29:39.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642532677.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "heidi_oconnor", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T19:04:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642532088.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "michelle_calderon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T18:54:48.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642530704.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "corey_williams", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T18:31:44.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642530171.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "robert_harmon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T18:22:51.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642523703.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "joseph_quinn", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T16:35:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "good stuff", - "channel": "sign-ups", - "slackId": "1642520065.000300", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642520065000300?thread_ts=1642519553.000100&cid=C029LDRDU6R" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T15:34:25.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "good stuff", - "channel": "sign-ups", - "slackId": "1642520065.000300", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642520065000300?thread_ts=1642519553.000100&cid=C029LDRDU6R" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T15:34:25.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "good stuff", - "channel": "sign-ups", - "slackId": "1642520065.000300", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642520065000300?thread_ts=1642519553.000100&cid=C029LDRDU6R" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T15:34:25.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "good stuff", - "channel": "sign-ups", - "slackId": "1642520065.000300", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642520065000300?thread_ts=1642519553.000100&cid=C029LDRDU6R" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T15:34:25.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "white_check_mark", - "slackId": "1642520060.000200", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T15:34:20.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "white_check_mark", - "slackId": "1642520060.000200", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T15:34:20.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "white_check_mark", - "slackId": "1642520060.000200", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T15:34:20.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "white_check_mark", - "slackId": "1642520060.000200", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T15:34:20.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642520040.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "benjamin_howard", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T15:34:00.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642516679.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "stephanie_allen", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T14:37:59.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642515697.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "stephanie_allen", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T14:21:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642508781.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "jordan_wilson", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T12:26:21.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642504872.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "tony_blevins", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T11:21:12.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "isHireable": false, - "url": "https://github.com/jrrglr", - "actions": [ - { - "score": 3, - "timestamp": "2022-01-28T06:02:49Z" - } - ] - } - }, - "username": "jrrglr", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-18T10:35:52.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642499094.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "calvin_gibbs", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T09:44:54.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642497889.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "tony_blevins", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T09:24:49.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642493846.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "mason_strickland", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T08:17:26.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642492001.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "laura_armstrong", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T07:46:41.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642490525.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "michael_blair", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T07:22:05.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/hasura-backend-plus" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Shoki Hata", - "isHireable": false, - "url": "https://github.com/sho-hata", - "websiteUrl": "https://qiita.com/sho-hata" - }, - "twitter": { - "url": "https://twitter.com/sho_hata_" - } - }, - "username": "sho-hata", - "type": "member", - "score": 0, - "email": "syouki100241@gmail.com", - "organisation": "Ateam Inc.", - "location": "Toyama, Japan", - "bio": "Web application developer,\r\nGolang, Hasura user" - }, - "timestamp": "2022-01-18T06:53:17.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642484092.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "evan_stevenson", - "type": "member", - "score": 3, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T05:34:52.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642482761.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "laura_armstrong", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T05:12:41.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642480839.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "jenny_cruz", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T04:40:39.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642468516.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "marie_warner", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-18T01:15:16.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642461634.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "laura_armstrong", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T23:20:34.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642459763.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "brandy_sanders", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T22:49:23.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642455402.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "karen_andrews", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T21:36:42.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642454730.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "joseph_quinn", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T21:25:30.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642450008.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "jenny_cruz", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T20:06:48.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Vahagn Mkrtchyan", - "isHireable": true, - "url": "https://github.com/iCyberon", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-17T19:38:48Z" - } - ] - } - }, - "username": "iCyberon", - "type": "member", - "score": 0, - "email": "vahagn.mkrtchyan@gmail.com", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-17T19:38:48.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Andrew", - "isHireable": false, - "url": "https://github.com/kungpaogao", - "websiteUrl": "andrewgao.org", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-17T17:25:17Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/ndrewgao" - } - }, - "username": "kungpaogao", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "earth", - "bio": "cs @ cornell" - }, - "timestamp": "2022-01-17T17:25:17.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642437253.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "gail_white", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T16:34:13.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642434252.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "ethan_clay", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T15:44:12.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642432869.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "melissa_byrd", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T15:21:09.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Ivan Voitovych", - "isHireable": false, - "url": "https://github.com/ivanvoitovych", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-17T14:30:23Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/VoitovychIvan" - } - }, - "username": "ivanvoitovych", - "type": "member", - "score": 0, - "email": "voitovych.ivan.v@gmail.com", - "organisation": "Working for cats", - "location": "Lviv", - "bio": "" - }, - "timestamp": "2022-01-17T14:30:23.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642428659.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "gail_white", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T14:10:59.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1642423518.002800", - "thread": { - "id": "1642418534.002100", - "body": "Looking good on staging. Deploying soon" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T12:45:18.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1642423518.002800", - "thread": { - "id": "1642418534.002100", - "body": "Looking good on staging. Deploying soon" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T12:45:18.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1642423518.002800", - "thread": { - "id": "1642418534.002100", - "body": "Looking good on staging. Deploying soon" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-17T12:45:18.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1642423518.002800", - "thread": { - "id": "1642418534.002100", - "body": "Looking good on staging. Deploying soon" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T12:45:18.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642422883.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "donna_colon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T12:34:43.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642419594.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "benjamin_howard", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:39:54.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "a", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-03T18:09:50Z" - } - ] - }, - "discord": {} - }, - "username": "joanreyero", - "type": "member", - "score": 10, - "email": "joan@crowd.dev", - "organisation": "@CrowdDevHQ", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-17T11:34:56.790Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "channel": "bugs", - "slackId": "1642418623.002400", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418623002400?thread_ts=1642069668.001600&cid=C02LWNKS17B", - "thread": { - "id": "1642069668.001600", - "body": "seems like the graphs are always zeroed now? the y-axis should still show the “total members” and not the change, right?\n\nFurthermore, the # of new members in the period does not match the graph." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:23:43.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "bugs", - "slackId": "1642418623.002400", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418623002400?thread_ts=1642069668.001600&cid=C02LWNKS17B", - "thread": { - "id": "1642069668.001600", - "body": "seems like the graphs are always zeroed now? the y-axis should still show the “total members” and not the change, right?\n\nFurthermore, the # of new members in the period does not match the graph." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:23:43.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "bugs", - "slackId": "1642418623.002400", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418623002400?thread_ts=1642069668.001600&cid=C02LWNKS17B", - "thread": { - "id": "1642069668.001600", - "body": "seems like the graphs are always zeroed now? the y-axis should still show the “total members” and not the change, right?\n\nFurthermore, the # of new members in the period does not match the graph." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-17T11:23:43.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "bugs", - "slackId": "1642418623.002400", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418623002400?thread_ts=1642069668.001600&cid=C02LWNKS17B", - "thread": { - "id": "1642069668.001600", - "body": "seems like the graphs are always zeroed now? the y-axis should still show the “total members” and not the change, right?\n\nFurthermore, the # of new members in the period does not match the graph." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-17T11:23:43.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "white_check_mark", - "slackId": "1642418536.002300", - "thread": { - "id": "1642418513.001900", - "body": "ah okay, sorry never mind than" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:22:16.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "white_check_mark", - "slackId": "1642418536.002300", - "thread": { - "id": "1642418513.001900", - "body": "ah okay, sorry never mind than" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:22:16.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "white_check_mark", - "slackId": "1642418536.002300", - "thread": { - "id": "1642418513.001900", - "body": "ah okay, sorry never mind than" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-17T11:22:16.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "white_check_mark", - "slackId": "1642418536.002300", - "thread": { - "id": "1642418513.001900", - "body": "ah okay, sorry never mind than" - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-17T11:22:16.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Looking good on staging. Deploying soon", - "channel": "bugs", - "slackId": "1642418534.002100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418534002100?thread_ts=1642418363.001100&cid=C02LWNKS17B", - "thread": { - "id": "1642418363.001100", - "body": "can’t create widgets with dimensions :confused: let me know if you need more info\n" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:22:14.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Looking good on staging. Deploying soon", - "channel": "bugs", - "slackId": "1642418534.002100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418534002100?thread_ts=1642418363.001100&cid=C02LWNKS17B", - "thread": { - "id": "1642418363.001100", - "body": "can’t create widgets with dimensions :confused: let me know if you need more info\n" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:22:14.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Looking good on staging. Deploying soon", - "channel": "bugs", - "slackId": "1642418534.002100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418534002100?thread_ts=1642418363.001100&cid=C02LWNKS17B", - "thread": { - "id": "1642418363.001100", - "body": "can’t create widgets with dimensions :confused: let me know if you need more info\n" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-17T11:22:14.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Looking good on staging. Deploying soon", - "channel": "bugs", - "slackId": "1642418534.002100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418534002100?thread_ts=1642418363.001100&cid=C02LWNKS17B", - "thread": { - "id": "1642418363.001100", - "body": "can’t create widgets with dimensions :confused: let me know if you need more info\n" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-17T11:22:14.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "ah okay, sorry never mind than", - "channel": "bugs", - "slackId": "1642418513.001900", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418513001900?thread_ts=1642418363.001100&cid=C02LWNKS17B", - "thread": { - "id": "1642418363.001100", - "body": "can’t create widgets with dimensions :confused: let me know if you need more info\n" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:21:53.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "ah okay, sorry never mind than", - "channel": "bugs", - "slackId": "1642418513.001900", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418513001900?thread_ts=1642418363.001100&cid=C02LWNKS17B", - "thread": { - "id": "1642418363.001100", - "body": "can’t create widgets with dimensions :confused: let me know if you need more info\n" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:21:53.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "ah okay, sorry never mind than", - "channel": "bugs", - "slackId": "1642418513.001900", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418513001900?thread_ts=1642418363.001100&cid=C02LWNKS17B", - "thread": { - "id": "1642418363.001100", - "body": "can’t create widgets with dimensions :confused: let me know if you need more info\n" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:21:53.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "ah okay, sorry never mind than", - "channel": "bugs", - "slackId": "1642418513.001900", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418513001900?thread_ts=1642418363.001100&cid=C02LWNKS17B", - "thread": { - "id": "1642418363.001100", - "body": "can’t create widgets with dimensions :confused: let me know if you need more info\n" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:21:53.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "That’s what we discussed in the planning. Mario pushed the fixes. I am now checking them on staging and then will deploy to prod", - "channel": "bugs", - "slackId": "1642418489.001700", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418489001700?thread_ts=1642418363.001100&cid=C02LWNKS17B", - "thread": { - "id": "1642418363.001100", - "body": "can’t create widgets with dimensions :confused: let me know if you need more info\n" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:21:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "That’s what we discussed in the planning. Mario pushed the fixes. I am now checking them on staging and then will deploy to prod", - "channel": "bugs", - "slackId": "1642418489.001700", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418489001700?thread_ts=1642418363.001100&cid=C02LWNKS17B", - "thread": { - "id": "1642418363.001100", - "body": "can’t create widgets with dimensions :confused: let me know if you need more info\n" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:21:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "That’s what we discussed in the planning. Mario pushed the fixes. I am now checking them on staging and then will deploy to prod", - "channel": "bugs", - "slackId": "1642418489.001700", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418489001700?thread_ts=1642418363.001100&cid=C02LWNKS17B", - "thread": { - "id": "1642418363.001100", - "body": "can’t create widgets with dimensions :confused: let me know if you need more info\n" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-17T11:21:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "That’s what we discussed in the planning. Mario pushed the fixes. I am now checking them on staging and then will deploy to prod", - "channel": "bugs", - "slackId": "1642418489.001700", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418489001700?thread_ts=1642418363.001100&cid=C02LWNKS17B", - "thread": { - "id": "1642418363.001100", - "body": "can’t create widgets with dimensions :confused: let me know if you need more info\n" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-17T11:21:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Weird, it’s not happening to me in local\n@Joan Reyero is the latest version of `main` deployed to prod?", - "channel": "bugs", - "slackId": "1642418477.001500", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418477001500?thread_ts=1642418363.001100&cid=C02LWNKS17B", - "thread": { - "id": "1642418363.001100", - "body": "can’t create widgets with dimensions :confused: let me know if you need more info\n" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:21:17.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Weird, it’s not happening to me in local\n@Joan Reyero is the latest version of `main` deployed to prod?", - "channel": "bugs", - "slackId": "1642418477.001500", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418477001500?thread_ts=1642418363.001100&cid=C02LWNKS17B", - "thread": { - "id": "1642418363.001100", - "body": "can’t create widgets with dimensions :confused: let me know if you need more info\n" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:21:17.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Weird, it’s not happening to me in local\n@Joan Reyero is the latest version of `main` deployed to prod?", - "channel": "bugs", - "slackId": "1642418477.001500", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418477001500?thread_ts=1642418363.001100&cid=C02LWNKS17B", - "thread": { - "id": "1642418363.001100", - "body": "can’t create widgets with dimensions :confused: let me know if you need more info\n" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-17T11:21:17.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Weird, it’s not happening to me in local\n@Joan Reyero is the latest version of `main` deployed to prod?", - "channel": "bugs", - "slackId": "1642418477.001500", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418477001500?thread_ts=1642418363.001100&cid=C02LWNKS17B", - "thread": { - "id": "1642418363.001100", - "body": "can’t create widgets with dimensions :confused: let me know if you need more info\n" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:21:17.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "bugs", - "slackId": "1642418363.001100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418363001100" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:19:23.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "bugs", - "slackId": "1642418363.001100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418363001100" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:19:23.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "bugs", - "slackId": "1642418363.001100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418363001100" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:19:23.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "bugs", - "slackId": "1642418363.001100", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418363001100" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:19:23.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "The ones we’ve discussed today?", - "channel": "bugs", - "slackId": "1642418256.000800", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418256000800?thread_ts=1642069668.001600&cid=C02LWNKS17B", - "thread": { - "id": "1642069668.001600", - "body": "seems like the graphs are always zeroed now? the y-axis should still show the “total members” and not the change, right?\n\nFurthermore, the # of new members in the period does not match the graph." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:17:36.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "The ones we’ve discussed today?", - "channel": "bugs", - "slackId": "1642418256.000800", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418256000800?thread_ts=1642069668.001600&cid=C02LWNKS17B", - "thread": { - "id": "1642069668.001600", - "body": "seems like the graphs are always zeroed now? the y-axis should still show the “total members” and not the change, right?\n\nFurthermore, the # of new members in the period does not match the graph." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:17:36.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "The ones we’ve discussed today?", - "channel": "bugs", - "slackId": "1642418256.000800", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418256000800?thread_ts=1642069668.001600&cid=C02LWNKS17B", - "thread": { - "id": "1642069668.001600", - "body": "seems like the graphs are always zeroed now? the y-axis should still show the “total members” and not the change, right?\n\nFurthermore, the # of new members in the period does not match the graph." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-17T11:17:36.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "The ones we’ve discussed today?", - "channel": "bugs", - "slackId": "1642418256.000800", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418256000800?thread_ts=1642069668.001600&cid=C02LWNKS17B", - "thread": { - "id": "1642069668.001600", - "body": "seems like the graphs are always zeroed now? the y-axis should still show the “total members” and not the change, right?\n\nFurthermore, the # of new members in the period does not match the graph." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:17:36.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@Joan Reyero @Mario Balca did we add the suggestions for y-axis somewhere?", - "channel": "bugs", - "slackId": "1642418181.000400", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418181000400?thread_ts=1642069668.001600&cid=C02LWNKS17B", - "thread": { - "id": "1642069668.001600", - "body": "seems like the graphs are always zeroed now? the y-axis should still show the “total members” and not the change, right?\n\nFurthermore, the # of new members in the period does not match the graph." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:16:21.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@Joan Reyero @Mario Balca did we add the suggestions for y-axis somewhere?", - "channel": "bugs", - "slackId": "1642418181.000400", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418181000400?thread_ts=1642069668.001600&cid=C02LWNKS17B", - "thread": { - "id": "1642069668.001600", - "body": "seems like the graphs are always zeroed now? the y-axis should still show the “total members” and not the change, right?\n\nFurthermore, the # of new members in the period does not match the graph." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:16:21.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@Joan Reyero @Mario Balca did we add the suggestions for y-axis somewhere?", - "channel": "bugs", - "slackId": "1642418181.000400", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418181000400?thread_ts=1642069668.001600&cid=C02LWNKS17B", - "thread": { - "id": "1642069668.001600", - "body": "seems like the graphs are always zeroed now? the y-axis should still show the “total members” and not the change, right?\n\nFurthermore, the # of new members in the period does not match the graph." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:16:21.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@Joan Reyero @Mario Balca did we add the suggestions for y-axis somewhere?", - "channel": "bugs", - "slackId": "1642418181.000400", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642418181000400?thread_ts=1642069668.001600&cid=C02LWNKS17B", - "thread": { - "id": "1642069668.001600", - "body": "seems like the graphs are always zeroed now? the y-axis should still show the “total members” and not the change, right?\n\nFurthermore, the # of new members in the period does not match the graph." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T11:16:21.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642416750.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "krystal_summers", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T10:52:30.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@Joan Reyero fixed the error :v:", - "channel": "dev", - "slackId": "1642414334.000400", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642414334000400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T10:12:14.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@Joan Reyero fixed the error :v:", - "channel": "dev", - "slackId": "1642414334.000400", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642414334000400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T10:12:14.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@Joan Reyero fixed the error :v:", - "channel": "dev", - "slackId": "1642414334.000400", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642414334000400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-17T10:12:14.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@Joan Reyero fixed the error :v:", - "channel": "dev", - "slackId": "1642414334.000400", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1642414334000400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T10:12:14.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642413416.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "martha_calhoun", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T09:56:56.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642413284.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "heather_chapman", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T09:54:44.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "test", - "channel": "twitter", - "slackId": "1642406155.000200", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642406155000200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T07:55:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "test", - "channel": "twitter", - "slackId": "1642406155.000200", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642406155000200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T07:55:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "test", - "channel": "twitter", - "slackId": "1642406155.000200", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642406155000200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T07:55:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "test", - "channel": "twitter", - "slackId": "1642406155.000200", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642406155000200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T07:55:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642403721.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kathleen_jones", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T07:15:21.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "isHireable": false, - "url": "https://github.com/xdimension", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-17T03:33:17Z" - } - ] - } - }, - "username": "xdimension", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-17T03:33:17.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642387082.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "sherri_butler", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T02:38:02.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642385527.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kathleen_jones", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T02:12:07.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642381983.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "joseph_quinn", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T01:13:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642381614.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "gail_white", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T01:06:54.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642377857.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "james_johnson", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-17T00:04:17.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642376956.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "martha_calhoun", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T23:49:16.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642375813.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "brenda_jones", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T23:30:13.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Fouad Mannou", - "isHireable": true, - "url": "https://github.com/fouadmen", - "websiteUrl": "https://fouadmannou.com/", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-16T23:18:13Z" - } - ] - } - }, - "username": "fouadmen", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-16T23:18:13.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642372358.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "martha_calhoun", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T22:32:38.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642372169.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "lucas_marquez", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T22:29:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "fire", - "slackId": "1642363001.000700", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T19:56:41.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "fire", - "slackId": "1642363001.000700", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T19:56:41.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "fire", - "slackId": "1642363001.000700", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T19:56:41.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "fire", - "slackId": "1642363001.000700", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T19:56:41.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "waitlist doesn’t know weekends", - "channel": "sign-ups", - "slackId": "1642362986.000600", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642362986000600" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T19:56:26.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "waitlist doesn’t know weekends", - "channel": "sign-ups", - "slackId": "1642362986.000600", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642362986000600" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T19:56:26.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "waitlist doesn’t know weekends", - "channel": "sign-ups", - "slackId": "1642362986.000600", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642362986000600" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T19:56:26.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "waitlist doesn’t know weekends", - "channel": "sign-ups", - "slackId": "1642362986.000600", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642362986000600" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T19:56:26.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost-js-sdk" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Milan van Schaik", - "isHireable": false, - "url": "https://github.com/milanvanschaik", - "websiteUrl": "http://milanvanschaik.com" - } - }, - "username": "milanvanschaik", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "The Netherlands", - "bio": "" - }, - "timestamp": "2022-01-16T18:52:53.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Mark", - "isHireable": false, - "url": "https://github.com/md97212", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-16T18:17:30Z" - } - ] - } - }, - "username": "md97212", - "type": "member", - "score": 0, - "email": "", - "organisation": "Qwizics", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-16T18:17:30.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Adrian Strzała", - "isHireable": true, - "url": "https://github.com/a-strzala", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-16T18:14:49Z" - } - ] - } - }, - "username": "a-strzala", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-16T18:14:49.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642351998.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "alejandro_garner", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T16:53:18.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642350475.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kristen_davis", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T16:27:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642349149.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "randy_bolton", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T16:05:49.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642346841.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "michelle_calderon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T15:27:21.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "isHireable": false, - "url": "https://github.com/aiaibabagit", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-16T15:23:51Z" - } - ] - } - }, - "username": "aiaibabagit", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-16T15:23:51.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642345903.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "tara_norman", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T15:11:43.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642345595.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "bryan_holland", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T15:06:35.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642344103.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "james_johnson", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T14:41:43.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642343990.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "thomas_moreno", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T14:39:50.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "name": "nhost", - "url": "https://github.com/pontus-karlsson/nhost", - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "isHireable": false, - "url": "https://github.com/pontus-karlsson", - "actions": [ - { - "score": 3, - "timestamp": "2022-01-16T12:29:41Z" - } - ] - } - }, - "username": "pontus-karlsson", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-16T12:29:41.000Z", - "type": "fork", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642334004.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "benjamin_howard", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T11:53:24.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642332739.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "scott_frye", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T11:32:19.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642332124.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kelly_lambert", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T11:22:04.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642330851.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "tara_norman", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T11:00:51.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642326715.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "melissa_byrd", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T09:51:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "isHireable": false, - "url": "https://github.com/lewis-ing", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-16T09:09:57Z" - } - ] - } - }, - "username": "lewis-ing", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-16T09:09:57.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642322819.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "robert_harmon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T08:46:59.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "isHireable": false, - "url": "https://github.com/Davy1988", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-16T08:13:28Z" - } - ] - } - }, - "username": "Davy1988", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-16T08:13:28.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642313186.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "stephanie_allen", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T06:06:26.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642312660.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "martha_calhoun", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T05:57:40.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642311511.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "robert_harmon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T05:38:31.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642305673.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "tony_blevins", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T04:01:13.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642304385.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "michelle_calderon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T03:39:45.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642303663.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "tony_blevins", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T03:27:43.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642297262.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "scott_frye", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T01:41:02.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642292383.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "joseph_quinn", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T00:19:43.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642291845.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "randy_bolton", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-16T00:10:45.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642285782.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "benjamin_howard", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T22:29:42.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642285394.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "ethan_clay", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T22:23:14.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642283080.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "michelle_calderon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T21:44:40.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642282759.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "lucas_marquez", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T21:39:19.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642281887.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "ethan_clay", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T21:24:47.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642281711.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "joseph_quinn", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T21:21:51.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642279975.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "donna_colon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T20:52:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642274112.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "stephanie_allen", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T19:15:12.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642273952.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "heidi_oconnor", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T19:12:32.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642272108.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "dawn_bridges", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T18:41:48.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642265397.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "stephanie_allen", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T16:49:57.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642264832.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "martha_calhoun", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T16:40:32.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642259323.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "gail_white", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T15:08:43.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642256160.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "bryan_holland", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T14:16:00.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642255128.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "john_henry", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T13:58:48.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642251801.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "dawn_bridges", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T13:03:21.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642245406.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "benjamin_howard", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T11:16:46.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642241445.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "evan_stevenson", - "type": "member", - "score": 3, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T10:10:45.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642237588.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "casey_smith", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T09:06:28.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642235557.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "lucas_marquez", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T08:32:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642227291.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "corey_williams", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T06:14:51.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642226184.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "melissa_byrd", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T05:56:24.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/hasura-backend-plus" - }, - "member": { - "crowdInfo": { - "github": { - "name": "stokhm", - "isHireable": false, - "url": "https://github.com/stokhm" - } - }, - "username": "stokhm", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "Tokyo, Japan", - "bio": "" - }, - "timestamp": "2022-01-15T05:06:22.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642220219.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "heidi_oconnor", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T04:16:59.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642216076.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "stacy_miller", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T03:07:56.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642212515.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "michelle_calderon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T02:08:35.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642210568.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "david_rogers", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T01:36:08.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642205393.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "dawn_bridges", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T00:09:53.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642204899.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kathleen_jones", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-15T00:01:39.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642204566.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "alejandro_garner", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T23:56:06.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642200401.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "ethan_clay", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T22:46:41.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Hossam Kandil", - "isHireable": false, - "url": "https://github.com/hokandil", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-14T22:22:27Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/kan_hossam" - } - }, - "username": "hokandil", - "type": "member", - "score": 0, - "email": "", - "organisation": "MBC Group", - "location": "Dubai-UAE", - "bio": "" - }, - "timestamp": "2022-01-14T22:22:27.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642197890.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "heidi_oconnor", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T22:04:50.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642191942.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "martha_calhoun", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T20:25:42.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642191520.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "michael_blair", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T20:18:40.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642190569.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kathleen_jones", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T20:02:49.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642188064.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "ethan_clay", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T19:21:04.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "glaciyan", - "isHireable": false, - "url": "https://github.com/glaciyan", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-14T19:16:41Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/glaciyandev" - } - }, - "username": "glaciyan", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "Germany", - "bio": "" - }, - "timestamp": "2022-01-14T19:16:41.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "zxyz", - "isHireable": true, - "url": "https://github.com/x-t", - "websiteUrl": "https://zxyz.gay", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-14T19:00:08Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/zedeckswhyzed" - } - }, - "username": "x-t", - "type": "member", - "score": 0, - "email": "", - "organisation": "Student", - "location": "Lithuania", - "bio": "he/him // I don't understand open source, so I use GitHub as cloud storage." - }, - "timestamp": "2022-01-14T19:00:08.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642184790.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "ethan_clay", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T18:26:30.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642183484.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "brandy_sanders", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T18:04:44.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Kyoungseo Park", - "isHireable": true, - "url": "https://github.com/Kyungseo-Park", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-14T17:12:14Z" - } - ] - } - }, - "username": "Kyungseo-Park", - "type": "member", - "score": 0, - "email": "", - "organisation": "Korea - SEOIL UNIVERSITY", - "location": "Nonhyeon-dong, Gangnam-gu, Seoul", - "bio": "다 패고싶네." - }, - "timestamp": "2022-01-14T17:12:14.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642179164.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "marie_warner", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T16:52:44.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642179073.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "casey_smith", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T16:51:13.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642178047.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kelly_lambert", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T16:34:07.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642177757.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "lindsay_gaines", - "type": "member", - "score": 1, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T16:29:17.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642174956.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "gail_white", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:42:36.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Seungwoo hong", - "isHireable": true, - "url": "https://github.com/hongsw", - "websiteUrl": "https://medium.com/@hongseungwoo", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-14T15:36:19Z" - } - ] - } - }, - "username": "hongsw", - "type": "member", - "score": 0, - "email": "", - "organisation": "Seungwoo", - "location": "Seoul, Republic of Korea", - "bio": "digital nomad, Doer, Works with Elixir / Phoenix!" - }, - "timestamp": "2022-01-14T15:36:19.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "raised_hands", - "slackId": "1642174158.002200", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:29:18.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "raised_hands", - "slackId": "1642174158.002200", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:29:18.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "raised_hands", - "slackId": "1642174158.002200", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-14T15:29:18.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "raised_hands", - "slackId": "1642174158.002200", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:29:18.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "sunglasses", - "slackId": "1642174155.002100", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:29:15.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "sunglasses", - "slackId": "1642174155.002100", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:29:15.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "sunglasses", - "slackId": "1642174155.002100", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-14T15:29:15.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "sunglasses", - "slackId": "1642174155.002100", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:29:15.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "and 3 leads in one day - waitlist is back at growing :sunglasses:", - "channel": "sign-ups", - "slackId": "1642174135.002000", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642174135002000" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:28:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "and 3 leads in one day - waitlist is back at growing :sunglasses:", - "channel": "sign-ups", - "slackId": "1642174135.002000", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642174135002000" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:28:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "and 3 leads in one day - waitlist is back at growing :sunglasses:", - "channel": "sign-ups", - "slackId": "1642174135.002000", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642174135002000" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:28:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "and 3 leads in one day - waitlist is back at growing :sunglasses:", - "channel": "sign-ups", - "slackId": "1642174135.002000", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642174135002000" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:28:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "muscle", - "slackId": "1642174126.001700", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:28:46.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "muscle", - "slackId": "1642174126.001700", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:28:46.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "muscle", - "slackId": "1642174126.001700", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-14T15:28:46.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "muscle", - "slackId": "1642174126.001700", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:28:46.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "si", - "channel": "sign-ups", - "slackId": "1642174112.001400", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642174112001400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:28:32.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "si", - "channel": "sign-ups", - "slackId": "1642174112.001400", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642174112001400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:28:32.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "si", - "channel": "sign-ups", - "slackId": "1642174112.001400", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642174112001400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:28:32.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "si", - "channel": "sign-ups", - "slackId": "1642174112.001400", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642174112001400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:28:32.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "good lead?", - "channel": "sign-ups", - "slackId": "1642173987.001000", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642173987001000" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:26:27.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "good lead?", - "channel": "sign-ups", - "slackId": "1642173987.001000", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642173987001000" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:26:27.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "good lead?", - "channel": "sign-ups", - "slackId": "1642173987.001000", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642173987001000" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-14T15:26:27.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "good lead?", - "channel": "sign-ups", - "slackId": "1642173987.001000", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642173987001000" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:26:27.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "pam pam pam :rocket:", - "channel": "sign-ups", - "slackId": "1642173939.000700", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642173939000700" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:25:39.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "pam pam pam :rocket:", - "channel": "sign-ups", - "slackId": "1642173939.000700", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642173939000700" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:25:39.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "pam pam pam :rocket:", - "channel": "sign-ups", - "slackId": "1642173939.000700", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642173939000700" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:25:39.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "pam pam pam :rocket:", - "channel": "sign-ups", - "slackId": "1642173939.000700", - "url": "https://crowddevspace.slack.com/archives/C029LDRDU6R/p1642173939000700" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T15:25:39.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Markus Düttmann", - "isHireable": false, - "url": "https://github.com/duett", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-26T10:41:06Z" - } - ] - } - }, - "username": "duett", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-14T14:14:01.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642165965.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "tony_blevins", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T13:12:45.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Interesting to see they have the biz model you guys were talking about", - "channel": "random", - "slackId": "1642165654.001300", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642165654001300?thread_ts=1642165469.001100&cid=C01NBUP9DAR", - "thread": { - "id": "1642165469.001100", - "body": "" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T13:07:34.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Interesting to see they have the biz model you guys were talking about", - "channel": "random", - "slackId": "1642165654.001300", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642165654001300?thread_ts=1642165469.001100&cid=C01NBUP9DAR", - "thread": { - "id": "1642165469.001100", - "body": "" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T13:07:34.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Interesting to see they have the biz model you guys were talking about", - "channel": "random", - "slackId": "1642165654.001300", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642165654001300?thread_ts=1642165469.001100&cid=C01NBUP9DAR", - "thread": { - "id": "1642165469.001100", - "body": "" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-14T13:07:34.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Interesting to see they have the biz model you guys were talking about", - "channel": "random", - "slackId": "1642165654.001300", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642165654001300?thread_ts=1642165469.001100&cid=C01NBUP9DAR", - "thread": { - "id": "1642165469.001100", - "body": "" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T13:07:34.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "random", - "slackId": "1642165469.001100", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642165469001100" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T13:04:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "random", - "slackId": "1642165469.001100", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642165469001100" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T13:04:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "random", - "slackId": "1642165469.001100", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642165469001100" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T13:04:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "random", - "slackId": "1642165469.001100", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642165469001100" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T13:04:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Changes proposed\n\nAdd bio, location, joinedAt, organisation to Member's form\nTweak Member's form input fields sizes\n\nChecklist 🔏\nSome important checks that must be ensured before merging:\nDeploy to staging\nDeployment checklist\n\n Make sure all AWS λ functions are up to date with your version (if applicable)\n Make sure the anton-environment and soa-environment are updated and pushed\n\nFunctionality\n\n Has the functionality been checked in a normal staging tenant? (not local)\n Has the functionality been checked in the large tenant? (team+large@crowd.dev)\n Has the functionality been checked in an empty tenant? (team+empty@crowd.dev)\n Is there any more edge cases that should be taken into account?\n\nCode quality\n\n Are there comments in the main functionality of the code?\n Are all tests passing?\n Are all URLs to external services in .env files? Never hard-coded\n Are all secrets in anton-environment? Never, ever hard-coded\n\n🔥🚀💪🏼", - "title": "Add missing member fields to form", - "state": "open", - "url": "https://github.com/CrowdHQ/crowd-web/pull/68", - "repo": "https://github.com/CrowdHQ/crowd-web" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "mariobalca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-14T12:59:45.000Z", - "type": "pull_request-opened", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642162658.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "corey_williams", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T12:17:38.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642161002.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "joseph_quinn", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T11:50:02.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642157342.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "brenda_jones", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T10:49:02.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642156360.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "heidi_oconnor", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T10:32:40.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642156193.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "madison_miller", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T10:29:53.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Looks promising! Might give it a try ", - "channel": "random", - "slackId": "1642156081.000900", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642156081000900?thread_ts=1642151221.000200&cid=C01NBUP9DAR", - "thread": { - "id": "1642151221.000200", - "body": "" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T10:28:01.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Looks promising! Might give it a try ", - "channel": "random", - "slackId": "1642156081.000900", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642156081000900?thread_ts=1642151221.000200&cid=C01NBUP9DAR", - "thread": { - "id": "1642151221.000200", - "body": "" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T10:28:01.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Looks promising! Might give it a try ", - "channel": "random", - "slackId": "1642156081.000900", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642156081000900?thread_ts=1642151221.000200&cid=C01NBUP9DAR", - "thread": { - "id": "1642151221.000200", - "body": "" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-14T10:28:01.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Looks promising! Might give it a try ", - "channel": "random", - "slackId": "1642156081.000900", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642156081000900?thread_ts=1642151221.000200&cid=C01NBUP9DAR", - "thread": { - "id": "1642151221.000200", - "body": "" - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T10:28:01.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642154189.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "david_boyd", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T09:56:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/hasura-backend-plus" - }, - "member": { - "crowdInfo": { - "github": { - "isHireable": false, - "url": "https://github.com/strikeout" - } - }, - "username": "strikeout", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-14T09:33:28.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "", - "channel": "random", - "slackId": "1642151221.000200", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642151221000200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T09:07:01.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "random", - "slackId": "1642151221.000200", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642151221000200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T09:07:01.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "random", - "slackId": "1642151221.000200", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642151221000200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T09:07:01.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "random", - "slackId": "1642151221.000200", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642151221000200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T09:07:01.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642149823.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "stephanie_allen", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T08:43:43.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642149784.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "melissa_byrd", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T08:43:04.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642148194.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "alejandro_garner", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T08:16:34.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/hasura-backend-plus" - }, - "member": { - "crowdInfo": { - "github": { - "isHireable": false, - "url": "https://github.com/SkyleLai", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-14T08:02:21Z" - } - ] - } - }, - "username": "SkyleLai", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-14T08:05:20.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "isHireable": false, - "url": "https://github.com/SkyleLai", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-14T08:02:21Z" - } - ] - } - }, - "username": "SkyleLai", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-14T08:02:21.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642145905.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "benjamin_howard", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T07:38:25.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642142548.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "scott_frye", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T06:42:28.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642140846.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "stephanie_allen", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T06:14:06.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642138418.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "donna_colon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T05:33:38.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642138177.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "bryan_holland", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T05:29:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642138063.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "david_rogers", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T05:27:43.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642137859.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "madison_miller", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T05:24:19.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642120287.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "tara_norman", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T00:31:27.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642119273.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "brenda_jones", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-14T00:14:33.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642115783.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "martha_calhoun", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T23:16:23.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642113394.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "ethan_clay", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T22:36:34.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642113001.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "michelle_calderon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T22:30:01.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642112975.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "krystal_summers", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T22:29:35.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642112543.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "dr._rebecca_nelson", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T22:22:23.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642112501.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "laura_armstrong", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T22:21:41.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642112373.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "stephanie_allen", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T22:19:33.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642111156.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "martha_calhoun", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T21:59:16.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642110466.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "corey_williams", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T21:47:46.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642108421.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "gail_white", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T21:13:41.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642102573.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "stephanie_allen", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T19:36:13.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642099589.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "alejandro_garner", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T18:46:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Description\nRemove workspaces from the menu and add the proper API docs.\nThe workspaces are commented out since we will definitely need them later on.\nChecklist 🔏\nSome important checks that must be ensured before merging:\nDeploy to staging\nDeployment checklist\n\n Make sure all AWS λ functions are up to date with your version (if applicable)\n Make sure the anton-environment and soa-environment are updated and pushed\n\nFunctionality\n\n Has the functionality been checked in a normal staging tenant? (not local)\n Has the functionality been checked in the large tenant? (team+large@crowd.dev)\n Has the functionality been checked in an empty tenant? (team+empty@crowd.dev)\n Is there any more edge cases that should be taken into account?\n\nCode quality\n\n Are there comments in the main functionality of the code?\n Are all URLs to external services in .env files? Never hard-coded\n Are all secrets in anton-environment? Never, ever hard-coded\n\n🔥🚀💪🏼", - "title": "Commented out workspaces", - "state": "merged", - "url": "https://github.com/CrowdHQ/crowd-web/pull/67", - "repo": "https://github.com/CrowdHQ/crowd-web" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "joanreyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-13T17:38:22.000Z", - "type": "pull_request-closed", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642092012.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "gail_white", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T16:40:12.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I might be able to join in 10mins", - "channel": "random", - "slackId": "1642087893.001100", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642087893001100" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T15:31:33.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I might be able to join in 10mins", - "channel": "random", - "slackId": "1642087893.001100", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642087893001100" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T15:31:33.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I might be able to join in 10mins", - "channel": "random", - "slackId": "1642087893.001100", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642087893001100" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-13T15:31:33.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I might be able to join in 10mins", - "channel": "random", - "slackId": "1642087893.001100", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642087893001100" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-13T15:31:33.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "sorry, have to skip coffee break. I am in the middle of something.", - "channel": "random", - "slackId": "1642087825.000400", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642087825000400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T15:30:25.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "sorry, have to skip coffee break. I am in the middle of something.", - "channel": "random", - "slackId": "1642087825.000400", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642087825000400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T15:30:25.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "sorry, have to skip coffee break. I am in the middle of something.", - "channel": "random", - "slackId": "1642087825.000400", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642087825000400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T15:30:25.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "sorry, have to skip coffee break. I am in the middle of something.", - "channel": "random", - "slackId": "1642087825.000400", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642087825000400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T15:30:25.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "sunglasses", - "slackId": "1642086472.000400", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T15:07:52.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "sunglasses", - "slackId": "1642086472.000400", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T15:07:52.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "sunglasses", - "slackId": "1642086472.000400", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T15:07:52.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "sunglasses", - "slackId": "1642086472.000400", - "channel": "sign-ups" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T15:07:52.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642083575.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "brenda_jones", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T14:19:35.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642082651.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "tony_blevins", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T14:04:11.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642081704.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "alejandro_garner", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T13:48:24.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642078983.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "thomas_moreno", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T13:03:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642075765.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "stacy_miller", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T12:09:25.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "From Joan", - "slackId": "1642074368.000600", - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "joan", - "type": "member", - "score": 8, - "email": "joan@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T11:46:08.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "New activities", - "slackId": "1642074364.000400", - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "joan", - "type": "member", - "score": 8, - "email": "joan@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T11:46:04.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Some more here", - "slackId": "1642074362.000200", - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "joan", - "type": "member", - "score": 8, - "email": "joan@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T11:46:02.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Pouplating mroe", - "slackId": "1642074357.000500", - "channel": "random" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "joan", - "type": "member", - "score": 8, - "email": "joan@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T11:45:57.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Pouplating", - "slackId": "1642074354.000300", - "channel": "random" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "joan", - "type": "member", - "score": 8, - "email": "joan@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T11:45:54.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642074051.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "robert_harmon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T11:40:51.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "eyes", - "slackId": "1642072209.002100", - "thread": { - "id": "1642069668.001600", - "body": "seems like the graphs are..." - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T11:10:09.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "eyes", - "slackId": "1642072209.002100", - "thread": { - "id": "1642069668.001600", - "body": "seems like the graphs are..." - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T11:10:09.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "eyes", - "slackId": "1642072209.002100", - "thread": { - "id": "1642069668.001600", - "body": "seems like the graphs are..." - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-13T11:10:09.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "eyes", - "slackId": "1642072209.002100", - "thread": { - "id": "1642069668.001600", - "body": "seems like the graphs are..." - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T11:10:09.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642071353.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "randy_bolton", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T10:55:53.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@elitan Could you add Auth0 support? It looks like vanilla Hasura supports it: https://hasura.io/docs/latest/graphql/core/guides/integrations/auth0-jwt.html\n\nAny specific reason for wanting to use Auth0?", - "title": "UI for OAuth providers", - "parent_url": "https://github.com/nhost/nhost/issues/1", - "url": "https://github.com/nhost/nhost/issues/1#issuecomment-1012007794", - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Johan Eliasson", - "isHireable": false, - "url": "https://github.com/elitan", - "websiteUrl": "https://nhost.io", - "actions": [ - { - "score": 3, - "timestamp": "2021-03-01T15:34:14Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/elitasson" - } - }, - "username": "elitan", - "type": "member", - "score": 10, - "email": "johan@eliasson.me", - "organisation": "", - "location": "The Internet", - "bio": "Exploiting regularities to my benefit." - }, - "timestamp": "2022-01-13T10:36:37.000Z", - "type": "issue-comment", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "channel": "bugs", - "slackId": "1642069668.001600", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642069668001600" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T10:27:48.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "bugs", - "slackId": "1642069668.001600", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642069668001600" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T10:27:48.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "bugs", - "slackId": "1642069668.001600", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642069668001600" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T10:27:48.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "bugs", - "slackId": "1642069668.001600", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642069668001600" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T10:27:48.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "eyes", - "slackId": "1642069415.001100", - "thread": { - "id": "1642069396.000900", - "body": "when connecting Discord the status..." - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T10:23:35.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "eyes", - "slackId": "1642069415.001100", - "thread": { - "id": "1642069396.000900", - "body": "when connecting Discord the status..." - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T10:23:35.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "eyes", - "slackId": "1642069415.001100", - "thread": { - "id": "1642069396.000900", - "body": "when connecting Discord the status..." - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-13T10:23:35.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "eyes", - "slackId": "1642069415.001100", - "thread": { - "id": "1642069396.000900", - "body": "when connecting Discord the status..." - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-13T10:23:35.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "when connecting GitHub the status stays on “in progress” forever (even though the data source is connected)", - "channel": "bugs", - "slackId": "1642069396.000900", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642069396000900" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T10:23:16.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "when connecting GitHub the status stays on “in progress” forever (even though the data source is connected)", - "channel": "bugs", - "slackId": "1642069396.000900", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642069396000900" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T10:23:16.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "when connecting GitHub the status stays on “in progress” forever (even though the data source is connected)", - "channel": "bugs", - "slackId": "1642069396.000900", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642069396000900" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T10:23:16.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "when connecting GitHub the status stays on “in progress” forever (even though the data source is connected)", - "channel": "bugs", - "slackId": "1642069396.000900", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1642069396000900" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T10:23:16.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "A", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-03T18:09:50Z" - } - ] - }, - "discord": {} - }, - "username": "joanreyero", - "type": "member", - "score": 10, - "email": "joan@crowd.dev", - "organisation": "@CrowdDevHQ", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-13T10:23:03.084Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642068760.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "karen_andrews", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T10:12:40.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642067498.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "david_cunningham", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T09:51:38.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642067399.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "john_henry", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T09:49:59.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642065919.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "benjamin_howard", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T09:25:19.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@elitan Could you add Auth0 support? It looks like vanilla Hasura supports it: https://hasura.io/docs/latest/graphql/core/guides/integrations/auth0-jwt.html", - "title": "UI for OAuth providers", - "parent_url": "https://github.com/nhost/nhost/issues/1", - "url": "https://github.com/nhost/nhost/issues/1#issuecomment-1011949365", - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Max Reynolds", - "isHireable": false, - "url": "https://github.com/MarcelloTheArcane", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-27T16:48:47Z" - } - ] - } - }, - "username": "MarcelloTheArcane", - "type": "member", - "score": 5, - "email": "", - "organisation": "", - "location": "United Kingdom", - "bio": "buymeacoffee.com/maxdotreynolds" - }, - "timestamp": "2022-01-13T09:24:30.000Z", - "type": "issue-comment", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "sample": true - }, - "member": { - "crowdInfo": { - "discord": { - "name": "Jason Chapman" - }, - "crowd": { - "sample": true - } - }, - "username": "jason_chapman", - "type": "member", - "score": 4, - "email": "jason.chapman@gmail.com", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-13T09:21:36.481Z", - "type": "joined_community", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642063284.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "heidi_oconnor", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T08:41:24.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "sample": true - }, - "member": { - "crowdInfo": { - "twitter": { - "name": "Nichole Owen", - "organisation": "PiedPieper.io" - }, - "crowd": { - "sample": true - } - }, - "username": "nichole_owen", - "type": "member", - "score": 3, - "email": "nichole.owen@PiedPieper.io", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-13T07:38:45.481Z", - "type": "joined_community", - "isContribution": true, - "platform": "twitter" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642057969.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "dawn_bridges", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T07:12:49.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642057072.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "brenda_jones", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T06:57:52.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642055833.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "karen_andrews", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T06:37:13.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642054652.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "mason_strickland", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T06:17:32.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642053609.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "david_boyd", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T06:00:09.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "sample": true - }, - "member": { - "crowdInfo": { - "discord": { - "name": "Mary Holt", - "organisation": "PiedPieper.io" - }, - "crowd": { - "sample": true - } - }, - "username": "mary_holt", - "type": "member", - "score": 1, - "email": "mary.holt@PiedPieper.io", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-13T05:31:04.481Z", - "type": "contributed_to_community", - "isContribution": false, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642046549.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "dawn_bridges", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T04:02:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "sample": true - }, - "member": { - "crowdInfo": { - "discord": { - "name": "Zachary Fletcher", - "organisation": "PiedPieper.io" - }, - "crowd": { - "sample": true - } - }, - "username": "zachary_fletcher", - "type": "member", - "score": 5, - "email": "zachary.fletcher@PiedPieper.io", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-13T03:29:54.481Z", - "type": "joined_community", - "isContribution": true, - "platform": "hubspot" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642044565.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "sherri_butler", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T03:29:25.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642043511.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "brandy_sanders", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T03:11:51.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642042504.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "jordan_wilson", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T02:55:04.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642041750.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "stacy_miller", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T02:42:30.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642041015.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "thomas_moreno", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T02:30:15.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "isHireable": false, - "url": "https://github.com/agergo", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-13T01:34:54Z" - } - ] - } - }, - "username": "agergo", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-13T01:34:54.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642037317.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "donna_colon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T01:28:37.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642036483.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "donna_colon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T01:14:43.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642035902.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "karen_davis", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T01:05:02.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642033970.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "stacy_miller", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T00:32:50.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642033399.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "casey_smith", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-13T00:23:19.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "sample": true - }, - "member": { - "crowdInfo": { - "twitter": { - "name": "Christian Hoffman", - "organisation": "PiedPieper.io" - }, - "crowd": { - "sample": true - } - }, - "username": "christian_hoffman", - "type": "member", - "score": 5, - "email": "christian.hoffman@PiedPieper.io", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-12T23:54:26.481Z", - "type": "joined_community", - "isContribution": true, - "platform": "twitter" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642030421.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "martha_calhoun", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T23:33:41.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642029355.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "sherri_butler", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T23:15:55.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642026519.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "casey_smith", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T22:28:39.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Leynier Gutiérrez González", - "isHireable": true, - "url": "https://github.com/leynier", - "websiteUrl": "leynier.dev", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-12T21:43:42Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/leynier41" - } - }, - "username": "leynier", - "type": "member", - "score": 0, - "email": "leynier41@gmail.com", - "organisation": "@educup @lynotofficial @matcom", - "location": "La Habana, Cuba", - "bio": "Head of Engineering at @educup, CTO/Co-founder of mesirve.app - @lynotofficial, Professor at @matcom and #opensource enthusiast" - }, - "timestamp": "2022-01-12T21:43:42.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642022404.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "dawn_bridges", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T21:20:04.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642022239.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kathleen_jones", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T21:17:19.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642020427.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "bryan_holland", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T20:47:07.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Harald Hanek", - "isHireable": true, - "url": "https://github.com/harrydeluxe", - "websiteUrl": "www.delacap.com", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-12T20:45:44Z" - } - ] - } - }, - "username": "harrydeluxe", - "type": "member", - "score": 0, - "email": "", - "organisation": "DELACAP", - "location": "Nürnberg, Germany", - "bio": "If it’s in the browser, i can do it." - }, - "timestamp": "2022-01-12T20:45:44.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "aaron cohen", - "isHireable": false, - "url": "https://github.com/avcohen", - "websiteUrl": "https://avc.dev", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-12T20:43:04Z" - } - ] - } - }, - "username": "avcohen", - "type": "member", - "score": 0, - "email": "avcohen@gmail.com", - "organisation": "", - "location": "afk", - "bio": "" - }, - "timestamp": "2022-01-12T20:43:04.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "sample": true - }, - "member": { - "crowdInfo": { - "hubspot": { - "name": "Paul Foley MD" - }, - "crowd": { - "sample": true - } - }, - "username": "paul_foley_md", - "type": "member", - "score": 4, - "email": "paul.foley.md@gmail.com", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-12T20:19:02.481Z", - "type": "joined_community", - "isContribution": true, - "platform": "hubspot" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "isHireable": false, - "url": "https://github.com/artistic-differences", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-12T19:42:53Z" - } - ] - } - }, - "username": "artistic-differences", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-12T19:42:53.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Kaiden / GL", - "isHireable": true, - "url": "https://github.com/glsee", - "websiteUrl": "https://see.guol.in", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-12T17:43:13Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/glsee" - } - }, - "username": "glsee", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "Kuala Lumpur, Malaysia", - "bio": "🦄 seasoned technical leader and builder. CTO for hire. PSM I. Occasionally conducts trainings, coding crash courses & talks. External examiner for uni." - }, - "timestamp": "2022-01-12T17:43:13.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642009383.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "tony_blevins", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T17:43:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "sample": true - }, - "member": { - "crowdInfo": { - "hubspot": { - "name": "Nicholas Whitehead", - "organisation": "Crowd.dev" - }, - "crowd": { - "sample": true - } - }, - "username": "nicholas_whitehead", - "type": "member", - "score": 4, - "email": "nicholas.whitehead@Crowd.dev", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-12T17:42:27.481Z", - "type": "joined_community", - "isContribution": true, - "platform": "apis" - }, - { - "crowdInfo": { - "body": "Testing a message, from @joanreyero", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-03T18:09:50Z" - } - ] - }, - "discord": {} - }, - "username": "joanreyero", - "type": "member", - "score": 10, - "email": "joan@crowd.dev", - "organisation": "@CrowdDevHQ", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T17:32:24.617Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "eyes", - "slackId": "1642008636.001900", - "thread": { - "id": "1642004191.001000", - "body": "..." - }, - "channel": "random" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T17:30:36.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "eyes", - "slackId": "1642008636.001900", - "thread": { - "id": "1642004191.001000", - "body": "..." - }, - "channel": "random" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T17:30:36.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "eyes", - "slackId": "1642008636.001900", - "thread": { - "id": "1642004191.001000", - "body": "..." - }, - "channel": "random" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T17:30:36.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "eyes", - "slackId": "1642008636.001900", - "thread": { - "id": "1642004191.001000", - "body": "..." - }, - "channel": "random" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T17:30:36.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "sample": true - }, - "member": { - "crowdInfo": { - "apis": { - "name": "John Brown" - }, - "crowd": { - "sample": true - } - }, - "username": "john_brown", - "type": "member", - "score": 3, - "email": "john.brown@gmail.com", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-12T17:07:07.481Z", - "type": "joined_community", - "isContribution": true, - "platform": "apis" - }, - { - "crowdInfo": { - "body": "eyes", - "slackId": "1642006570.001700", - "thread": { - "id": "1642004191.001000", - "body": "", - "channel": "twitter", - "slackId": "1642004263.001500", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004263001500" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:43.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "twitter", - "slackId": "1642004263.001500", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004263001500" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:43.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "twitter", - "slackId": "1642004263.001500", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004263001500" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:43.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "twitter", - "slackId": "1642004263.001500", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004263001500" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:43.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004229.001300", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004229001300" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 9, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:09.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004229.000900", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004229000900" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam", - "phone": "+43 6601652996", - "title": "Marketing Associate" - } - }, - "username": "Sofia", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:09.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004229.001100", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004229001100" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Christian Hartlage", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:09.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004229.000700", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004229000700" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:09.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004229.001300", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004229001300" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:09.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004229.000900", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004229000900" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam", - "phone": "+43 6601652996", - "title": "Marketing Associate" - } - }, - "username": "Sofia", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:09.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004229.001100", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004229001100" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Christian Hartlage", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:09.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004229.000700", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004229000700" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:09.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004229.001300", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004229001300" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 7, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:09.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004229.000900", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004229000900" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam", - "phone": "+43 6601652996", - "title": "Marketing Associate" - } - }, - "username": "Sofia", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:09.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004229.001100", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004229001100" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Christian Hartlage", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:09.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004229.000700", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004229000700" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-12T16:17:09.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004229.001300", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004229001300" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Asia/Istanbul", - "phone": "+90 539 853 01 66" - } - }, - "username": "Anil Bostanci", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Asia/Istanbul (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:09.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004229.000900", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004229000900" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam", - "phone": "+43 6601652996", - "title": "Marketing Associate" - } - }, - "username": "Sofia", - "type": "member", - "score": 2, - "email": "sofia@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:09.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004229.001100", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004229001100" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Christian Hartlage", - "type": "member", - "score": 10, - "email": "chris@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:09.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004229.000700", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004229000700" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:09.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004228.000500", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004228000500" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:08.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004228.000500", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004228000500" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:08.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004228.000500", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004228000500" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T16:17:08.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004228.000500", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004228000500" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T16:17:08.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004225.000200", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004225000200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:05.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004225.000200", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004225000200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:05.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004225.000200", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004225000200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:05.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "twitter", - "slackId": "1642004225.000200", - "url": "https://crowddevspace.slack.com/archives/C02TR2E08HG/p1642004225000200" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:17:05.000Z", - "type": "channel_joined", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "random", - "slackId": "1642004191.001000", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642004191001000" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:16:31.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "random", - "slackId": "1642004191.001000", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642004191001000" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:16:31.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "random", - "slackId": "1642004191.001000", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642004191001000" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:16:31.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "", - "channel": "random", - "slackId": "1642004191.001000", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1642004191001000" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T16:16:31.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "sample": true - }, - "member": { - "crowdInfo": { - "discord": { - "name": "Michael Gonzalez", - "organisation": "PiedPieper.io" - }, - "crowd": { - "sample": true - } - }, - "username": "michael_gonzalez", - "type": "member", - "score": 6, - "email": "michael.gonzalez@PiedPieper.io", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-12T16:01:36.481Z", - "type": "joined_community", - "isContribution": true, - "platform": "hubspot" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642002990.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "tony_blevins", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T15:56:30.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1642001558.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "corey_williams", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T15:32:38.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/hasura-backend-plus" - }, - "member": { - "crowdInfo": { - "github": { - "name": "DaRwin", - "isHireable": false, - "url": "https://github.com/darekaze", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-12T15:30:47Z" - } - ] - } - }, - "username": "darekaze", - "type": "member", - "score": 0, - "email": "", - "organisation": "Trust your Intuition", - "location": "Macau", - "bio": "TypeScript / Node.js / React / Rust Engineer; (💖👨‍💻Development, 🎼Core, 🕹️Games) => 🚢 Ship with Quality" - }, - "timestamp": "2022-01-12T15:31:12.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "DaRwin", - "isHireable": false, - "url": "https://github.com/darekaze", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-12T15:30:47Z" - } - ] - } - }, - "username": "darekaze", - "type": "member", - "score": 0, - "email": "", - "organisation": "Trust your Intuition", - "location": "Macau", - "bio": "TypeScript / Node.js / React / Rust Engineer; (💖👨‍💻Development, 🎼Core, 🕹️Games) => 🚢 Ship with Quality" - }, - "timestamp": "2022-01-12T15:30:47.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "sample": true - }, - "member": { - "crowdInfo": { - "discord": { - "name": "Carmen Thomas", - "organisation": "PiedPieper.io" - }, - "crowd": { - "sample": true - } - }, - "username": "carmen_thomas", - "type": "member", - "score": 4, - "email": "carmen.thomas@PiedPieper.io", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-12T15:24:48.481Z", - "type": "joined_community", - "isContribution": true, - "platform": "apis" - }, - { - "crowdInfo": { - "body": "Done", - "channel": "dev", - "slackId": "1641994631.008000", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641994631008000?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @Jonathan Reimer @Joan Re..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:37:11.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Done", - "channel": "dev", - "slackId": "1641994631.008000", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641994631008000?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @Jonathan Reimer @Joan Re..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:37:11.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Done", - "channel": "dev", - "slackId": "1641994631.008000", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641994631008000?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @Jonathan Reimer @Joan Re..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T13:37:11.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Done", - "channel": "dev", - "slackId": "1641994631.008000", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641994631008000?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @Jonathan Reimer @Joan Re..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T13:37:11.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Thanks!", - "channel": "dev", - "slackId": "1641994501.007800", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641994501007800?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @Jonathan Reimer @Joan Re..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:35:01.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Thanks!", - "channel": "dev", - "slackId": "1641994501.007800", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641994501007800?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @Jonathan Reimer @Joan Re..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:35:01.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Thanks!", - "channel": "dev", - "slackId": "1641994501.007800", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641994501007800?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @Jonathan Reimer @Joan Re..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-12T13:35:01.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Thanks!", - "channel": "dev", - "slackId": "1641994501.007800", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641994501007800?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @Jonathan Reimer @Joan Re..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:35:01.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Yup", - "channel": "dev", - "slackId": "1641994497.007600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641994497007600?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @Jonathan Reimer @Joan Re..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:34:57.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Yup", - "channel": "dev", - "slackId": "1641994497.007600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641994497007600?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @Jonathan Reimer @Joan Re..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:34:57.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Yup", - "channel": "dev", - "slackId": "1641994497.007600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641994497007600?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @Jonathan Reimer @Joan Re..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T13:34:57.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Yup", - "channel": "dev", - "slackId": "1641994497.007600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641994497007600?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @Jonathan Reimer @Joan Re..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T13:34:57.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@Joan Reyero commited this ^ to `main`\nCan you re-deploy?", - "channel": "dev", - "slackId": "1641994485.007400", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641994485007400?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @Jonathan Reimer @Joan Re..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:34:45.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@Joan Reyero commited this ^ to `main`\nCan you re-deploy?", - "channel": "dev", - "slackId": "1641994485.007400", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641994485007400?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @Jonathan Reimer @Joan Re..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:34:45.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@Joan Reyero commited this ^ to `main`\nCan you re-deploy?", - "channel": "dev", - "slackId": "1641994485.007400", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641994485007400?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @Jonathan Reimer @Joan Re..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-12T13:34:45.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@Joan Reyero commited this ^ to `main`\nCan you re-deploy?", - "channel": "dev", - "slackId": "1641994485.007400", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641994485007400?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @Jonathan Reimer @Joan Re..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:34:45.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "white_check_mark", - "slackId": "1641994386.007300", - "thread": { - "id": "1641993654.005500", - "body": "yes! :+1:..." - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:33:06.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "white_check_mark", - "slackId": "1641994386.007300", - "thread": { - "id": "1641993654.005500", - "body": "yes! :+1:..." - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:33:06.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "white_check_mark", - "slackId": "1641994386.007300", - "thread": { - "id": "1641993654.005500", - "body": "yes! :+1:..." - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-12T13:33:06.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "white_check_mark", - "slackId": "1641994386.007300", - "thread": { - "id": "1641993654.005500", - "body": "yes! :+1:..." - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:33:06.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "pray", - "slackId": "1641994382.007200", - "thread": { - "id": "1641994330.006900", - "body": "Latest reports version is in...." - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:33:02.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "pray", - "slackId": "1641994382.007200", - "thread": { - "id": "1641994330.006900", - "body": "Latest reports version is in...." - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:33:02.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "pray", - "slackId": "1641994382.007200", - "thread": { - "id": "1641994330.006900", - "body": "Latest reports version is in...." - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-12T13:33:02.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "pray", - "slackId": "1641994382.007200", - "thread": { - "id": "1641994330.006900", - "body": "Latest reports version is in...." - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:33:02.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1641994378.007100", - "thread": { - "id": "1641994330.006900", - "body": "Latest reports version is in...." - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:32:58.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1641994378.007100", - "thread": { - "id": "1641994330.006900", - "body": "Latest reports version is in...." - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:32:58.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1641994378.007100", - "thread": { - "id": "1641994330.006900", - "body": "Latest reports version is in...." - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-12T13:32:58.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "+1", - "slackId": "1641994378.007100", - "thread": { - "id": "1641994330.006900", - "body": "Latest reports version is in...." - }, - "channel": "dev" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:32:58.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Latest reports version is in staging :muscle::skin-tone-3:\nIf your widgets show flat you need to do this for each widget:\n\n(only happens for widgets created before today)", - "channel": "dev", - "slackId": "1641994330.006900", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641994330006900" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:32:10.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Latest reports version is in staging :muscle::skin-tone-3:\nIf your widgets show flat you need to do this for each widget:\n\n(only happens for widgets created before today)", - "channel": "dev", - "slackId": "1641994330.006900", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641994330006900" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:32:10.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Latest reports version is in staging :muscle::skin-tone-3:\nIf your widgets show flat you need to do this for each widget:\n\n(only happens for widgets created before today)", - "channel": "dev", - "slackId": "1641994330.006900", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641994330006900" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T13:32:10.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Latest reports version is in staging :muscle::skin-tone-3:\nIf your widgets show flat you need to do this for each widget:\n\n(only happens for widgets created before today)", - "channel": "dev", - "slackId": "1641994330.006900", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641994330006900" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T13:32:10.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641994328.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "joseph_quinn", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:32:08.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641993949.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kelly_lambert", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:25:49.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yes! :+1:", - "channel": "dev", - "slackId": "1641993654.005500", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641993654005500?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @Jonathan Reimer @Joan Re..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:20:54.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yes! :+1:", - "channel": "dev", - "slackId": "1641993654.005500", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641993654005500?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @Jonathan Reimer @Joan Re..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:20:54.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yes! :+1:", - "channel": "dev", - "slackId": "1641993654.005500", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641993654005500?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @Jonathan Reimer @Joan Re..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:20:54.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "yes! :+1:", - "channel": "dev", - "slackId": "1641993654.005500", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641993654005500?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @Jonathan Reimer @Joan Re..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Jonathan Reimer", - "type": "member", - "score": 10, - "email": "jonathan@oscape.io", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:20:54.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Hey", - "channel": "integration-test-2", - "slackId": "1641993494.000500", - "url": "https://crowddevspace.slack.com/archives/C02SDDPNVHN/p1641993494000500" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:18:14.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Hey", - "channel": "integration-test-2", - "slackId": "1641993494.000500", - "url": "https://crowddevspace.slack.com/archives/C02SDDPNVHN/p1641993494000500" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 8, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:18:14.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Hey", - "channel": "integration-test-2", - "slackId": "1641993494.000500", - "url": "https://crowddevspace.slack.com/archives/C02SDDPNVHN/p1641993494000500" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T13:18:14.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Hey", - "channel": "integration-test-2", - "slackId": "1641993494.000500", - "url": "https://crowddevspace.slack.com/archives/C02SDDPNVHN/p1641993494000500" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T13:18:14.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "sample": true - }, - "member": { - "crowdInfo": { - "discord": { - "name": "Lisa Harper", - "organisation": "Crowd.dev" - }, - "crowd": { - "sample": true - }, - "apis": { - "name": "Lisa Harper", - "organisation": "Crowd.dev" - } - }, - "username": "lisa_harper", - "type": "member", - "score": 7, - "email": "lisa.harper@Crowd.dev", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-12T13:17:50.991Z", - "type": "contributed_to_community", - "isContribution": false, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641993414.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "ethan_clay", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:16:54.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@UserMention permission to remove measures from filters?", - "channel": "dev", - "slackId": "1641992827.005200", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641992827005200?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @UserMention @Joan Reyero..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:07:07.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@UserMention permission to remove measures from filters?", - "channel": "dev", - "slackId": "1641992827.005200", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641992827005200?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @UserMention @Joan Reyero..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-12T13:07:07.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "@UserMention permission to remove measures from filters?", - "channel": "dev", - "slackId": "1641992827.005200", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641992827005200?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @UserMention @Joan Reyero..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:07:07.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Description\nNotion Task\n\nFix activities body breaking layout / expanding beyond the card\nUse chartkick for dashboard chart-based widgets as well\nUse a single datetime selector for dashboard widgets", - "title": "Minor UI tweaks", - "state": "merged", - "url": "https://github.com/CrowdHQ/crowd-web/pull/66", - "repo": "https://github.com/CrowdHQ/crowd-web" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "mariobalca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-12T13:05:07.000Z", - "type": "pull_request-closed", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Description\nNotion Task\n\nFix activities body breaking layout / expanding beyond the card\nUse chartkick for dashboard chart-based widgets as well\nUse a single datetime selector for dashboard widgets", - "title": "Minor UI tweaks", - "state": "open", - "url": "https://github.com/CrowdDevHQ/crowd-web/pull/66", - "repo": "https://github.com/CrowdDevHQ/crowd-web" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "mariobalca", - "type": "member", - "score": 10, - "email": "", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Coimbra, Portugal", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-12T13:05:07.000Z", - "type": "pull_request-opened", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "body": "I think that makes sense :ok_hand::skin-tone-3:", - "channel": "dev", - "slackId": "1641992417.005000", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641992417005000?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @UserMention @Joan Reyero..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T13:00:17.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I think that makes sense :ok_hand::skin-tone-3:", - "channel": "dev", - "slackId": "1641992417.005000", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641992417005000?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @UserMention @Joan Reyero..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T13:00:17.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "I think that makes sense :ok_hand::skin-tone-3:", - "channel": "dev", - "slackId": "1641992417.005000", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641992417005000?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @UserMention @Joan Reyero..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T13:00:17.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "For context, removing measures from filters would take me 5 seconds", - "channel": "dev", - "slackId": "1641991867.004600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641991867004600?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @UserMention @Joan Reyero..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T12:51:07.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "For context, removing measures from filters would take me 5 seconds", - "channel": "dev", - "slackId": "1641991867.004600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641991867004600?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @UserMention @Joan Reyero..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-12T12:51:07.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "For context, removing measures from filters would take me 5 seconds", - "channel": "dev", - "slackId": "1641991867.004600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641991867004600?thread_ts=1641991303.004500&cid=C01NBV2BDDK", - "thread": { - "id": "1641991303.004500", - "body": "Hey @UserMention @Joan Reyero..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T12:51:07.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641991653.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "tony_blevins", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T12:47:33.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641991072.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "brenda_jones", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T12:37:52.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641990416.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "thomas_moreno", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T12:26:56.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641990389.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "heather_chapman", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T12:26:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641989882.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "stacy_miller", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T12:18:02.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "This is a test for @joanreyero and @joanreyerotest !", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-03T18:09:50Z" - } - ] - }, - "discord": {} - }, - "username": "joanreyero", - "type": "member", - "score": 10, - "email": "joan@crowd.dev", - "organisation": "@CrowdDevHQ", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T12:00:18.203Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "Cool! Will set up a demo", - "channel": "dev", - "slackId": "1641988290.000600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641988290000600?thread_ts=1641988194.000400&cid=C01NBV2BDDK", - "thread": { - "id": "1641988194.000400", - "body": " those ..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T11:51:30.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Cool! Will set up a demo", - "channel": "dev", - "slackId": "1641988290.000600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641988290000600?thread_ts=1641988194.000400&cid=C01NBV2BDDK", - "thread": { - "id": "1641988194.000400", - "body": " those ..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T11:51:30.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Cool! Will set up a demo", - "channel": "dev", - "slackId": "1641988290.000600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641988290000600?thread_ts=1641988194.000400&cid=C01NBV2BDDK", - "thread": { - "id": "1641988194.000400", - "body": " those ..." - } - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T11:51:30.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "<@!877891006014554152> Hey", - "channel": "c2" - }, - "member": { - "crowdInfo": { - "discord": {} - }, - "username": "joanreyerotest", - "type": "member", - "score": 5, - "email": "" - }, - "timestamp": "2022-01-12T11:47:40.514Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "This is a test for <@!877891006014554152> and <@!930771656442527826> !", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "discord": {} - }, - "username": "joanreyerotest", - "type": "member", - "score": 5, - "email": "" - }, - "timestamp": "2022-01-12T11:37:33.962Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "This is a test for <@!877891006014554152> and <@!930771656442527826> !", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "discord": {} - }, - "username": "joanreyerotest", - "type": "member", - "score": 5, - "email": "" - }, - "timestamp": "2022-01-12T11:37:28.040Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "This is a test for <@!877891006014554152> and <@!930771656442527826> !", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "discord": {} - }, - "username": "joanreyerotest", - "type": "member", - "score": 5, - "email": "" - }, - "timestamp": "2022-01-12T11:37:21.476Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "This is a test for <@!877891006014554152> and <@!930771656442527826> !", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "discord": {} - }, - "username": "joanreyerotest", - "type": "member", - "score": 5, - "email": "" - }, - "timestamp": "2022-01-12T11:36:05.911Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "This is a test for <@!877891006014554152> and <@!930771656442527826> !", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "discord": {} - }, - "username": "joanreyerotest", - "type": "member", - "score": 5, - "email": "" - }, - "timestamp": "2022-01-12T11:35:47.188Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "<@!930771656442527826>", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "discord": {} - }, - "username": "joanreyerotest", - "type": "member", - "score": 5, - "email": "" - }, - "timestamp": "2022-01-12T11:35:22.739Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "<@!877891006014554152>", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "discord": {} - }, - "username": "joanreyerotest", - "type": "member", - "score": 5, - "email": "" - }, - "timestamp": "2022-01-12T11:34:51.042Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "<@!877891006014554152>", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "discord": {} - }, - "username": "joanreyerotest", - "type": "member", - "score": 5, - "email": "" - }, - "timestamp": "2022-01-12T11:34:29.080Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "<@!930771656442527826>", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "discord": {} - }, - "username": "joanreyerotest", - "type": "member", - "score": 5, - "email": "" - }, - "timestamp": "2022-01-12T11:34:16.310Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "<@!930771656442527826>", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "discord": {} - }, - "username": "joanreyerotest", - "type": "member", - "score": 5, - "email": "" - }, - "timestamp": "2022-01-12T11:34:00.493Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "<@!930771656442527826>", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "discord": {} - }, - "username": "joanreyerotest", - "type": "member", - "score": 5, - "email": "" - }, - "timestamp": "2022-01-12T11:33:37.949Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "<@!877891006014554152>", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "discord": {} - }, - "username": "joanreyerotest", - "type": "member", - "score": 5, - "email": "" - }, - "timestamp": "2022-01-12T11:33:09.796Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "<@!930771656442527826>", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "discord": {} - }, - "username": "joanreyerotest", - "type": "member", - "score": 5, - "email": "" - }, - "timestamp": "2022-01-12T11:32:50.430Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "<@!930771656442527826>", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "discord": {} - }, - "username": "joanreyerotest", - "type": "member", - "score": 5, - "email": "" - }, - "timestamp": "2022-01-12T11:30:35.919Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "<@!877891006014554152>", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "discord": {} - }, - "username": "joanreyerotest", - "type": "member", - "score": 5, - "email": "" - }, - "timestamp": "2022-01-12T11:29:23.257Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "<@!877891006014554152>", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "discord": {} - }, - "username": "joanreyerotest", - "type": "member", - "score": 5, - "email": "" - }, - "timestamp": "2022-01-12T11:29:01.509Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "<@!930771656442527826>", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "discord": {} - }, - "username": "joanreyerotest", - "type": "member", - "score": 5, - "email": "" - }, - "timestamp": "2022-01-12T11:28:31.462Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "<@!930771656442527826>", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "discord": {} - }, - "username": "joanreyerotest", - "type": "member", - "score": 5, - "email": "" - }, - "timestamp": "2022-01-12T11:26:31.343Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Raghavendra Kopalle", - "isHireable": false, - "url": "https://github.com/ksraghavendra", - "websiteUrl": "www.36ty.in", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-12T11:20:33Z" - } - ] - } - }, - "username": "ksraghavendra", - "type": "member", - "score": 0, - "email": "ksraghavendra@outlook.com", - "organisation": "36ty Solutions", - "location": "India", - "bio": "" - }, - "timestamp": "2022-01-12T11:20:33.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "joy", - "slackId": "1641984981.000600", - "thread": { - "id": "1641984594.000100", - "body": "..." - }, - "channel": "random" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T10:56:21.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "joy", - "slackId": "1641984981.000600", - "thread": { - "id": "1641984594.000100", - "body": "..." - }, - "channel": "random" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T10:56:21.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "joy", - "slackId": "1641984981.000600", - "thread": { - "id": "1641984594.000100", - "body": "..." - }, - "channel": "random" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T10:56:21.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "A", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-03T18:09:50Z" - } - ] - }, - "discord": {} - }, - "username": "joanreyero", - "type": "member", - "score": 10, - "email": "joan@crowd.dev", - "organisation": "@CrowdDevHQ", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T10:53:50.384Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "A", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-03T18:09:50Z" - } - ] - }, - "discord": {} - }, - "username": "joanreyero", - "type": "member", - "score": 10, - "email": "joan@crowd.dev", - "organisation": "@CrowdDevHQ", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T10:53:20.882Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "A", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-03T18:09:50Z" - } - ] - }, - "discord": {} - }, - "username": "joanreyero", - "type": "member", - "score": 10, - "email": "joan@crowd.dev", - "organisation": "@CrowdDevHQ", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T10:52:48.354Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "channel": "random", - "slackId": "1641984594.000100", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1641984594000100" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T10:49:54.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "random", - "slackId": "1641984594.000100", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1641984594000100" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-12T10:49:54.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "channel": "random", - "slackId": "1641984594.000100", - "url": "https://crowddevspace.slack.com/archives/C01NBUP9DAR/p1641984594000100" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T10:49:54.000Z", - "type": "file_share", - "isContribution": false, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "<@!930771656442527826> hey", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-03T18:09:50Z" - } - ] - }, - "discord": {} - }, - "username": "joanreyero", - "type": "member", - "score": 10, - "email": "joan@crowd.dev", - "organisation": "@CrowdDevHQ", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T10:45:28.953Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "A", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-03T18:09:50Z" - } - ] - }, - "discord": {} - }, - "username": "joanreyero", - "type": "member", - "score": 10, - "email": "joan@crowd.dev", - "organisation": "@CrowdDevHQ", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T10:43:05.247Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "A", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-03T18:09:50Z" - } - ] - }, - "discord": {} - }, - "username": "joanreyero", - "type": "member", - "score": 10, - "email": "joan@crowd.dev", - "organisation": "@CrowdDevHQ", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T10:42:58.604Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "A", - "channel": "c1" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "actions": [ - { - "score": 2, - "timestamp": "2022-01-03T18:09:50Z" - } - ] - }, - "discord": {} - }, - "username": "joanreyero", - "type": "member", - "score": 10, - "email": "joan@crowd.dev", - "organisation": "@CrowdDevHQ", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-12T10:42:50.057Z", - "type": "message", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641983508.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "john_rocha", - "type": "member", - "score": 4, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T10:31:48.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "rocket", - "slackId": "1641982773.004200", - "thread": { - "id": "1641982718.004000", - "body": "otherwise, performance is now..." - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T10:19:33.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "rocket", - "slackId": "1641982773.004200", - "thread": { - "id": "1641982718.004000", - "body": "otherwise, performance is now..." - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-12T10:19:33.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "rocket", - "slackId": "1641982773.004200", - "thread": { - "id": "1641982718.004000", - "body": "otherwise, performance is now..." - }, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T10:19:33.000Z", - "type": "reaction_added", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Must be lookalike members", - "channel": "bugs", - "slackId": "1641982642.002600", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1641982642002600" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T10:17:22.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Must be lookalike members", - "channel": "bugs", - "slackId": "1641982642.002600", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1641982642002600" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-12T10:17:22.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Must be lookalike members", - "channel": "bugs", - "slackId": "1641982642.002600", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1641982642002600" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T10:17:22.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Ufff", - "channel": "bugs", - "slackId": "1641982639.002400", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1641982639002400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T10:17:19.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Ufff", - "channel": "bugs", - "slackId": "1641982639.002400", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1641982639002400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-12T10:17:19.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Ufff", - "channel": "bugs", - "slackId": "1641982639.002400", - "url": "https://crowddevspace.slack.com/archives/C02LWNKS17B/p1641982639002400" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - } - }, - "username": "Mario Balca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "", - "location": "Europe/London (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T10:17:19.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641981063.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "david_rogers", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T09:51:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Fix links for the Next and React template", - "title": "Update README.md", - "state": "open", - "url": "https://github.com/nhost/nhost/pull/114", - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Mustafa Hanif", - "isHireable": false, - "url": "https://github.com/mustafa-hanif", - "actions": [ - { - "score": 3, - "timestamp": "2022-01-12T09:25:45Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/MustafaHanif8" - } - }, - "username": "mustafa-hanif", - "type": "member", - "score": 2, - "email": "mustafa.hanif@seera.sa", - "organisation": "Seera", - "location": "dubai", - "bio": "JavaScript developer" - }, - "timestamp": "2022-01-12T09:25:45.000Z", - "type": "pull_request-opened", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "sample": true - }, - "member": { - "crowdInfo": { - "discord": { - "name": "Michael Gonzalez", - "organisation": "PiedPieper.io" - }, - "crowd": { - "sample": true - } - }, - "username": "michael_gonzalez", - "type": "member", - "score": 6, - "email": "michael.gonzalez@PiedPieper.io", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-12T08:57:56.481Z", - "type": "joined_community", - "isContribution": true, - "platform": "discord" - }, - { - "crowdInfo": { - "sample": true - }, - "member": { - "crowdInfo": { - "hubspot": { - "name": "Derrick Morgan", - "organisation": "Crowd.dev" - }, - "crowd": { - "sample": true - } - }, - "username": "derrick_morgan", - "type": "member", - "score": 9, - "email": "derrick.morgan@Crowd.dev", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-12T07:42:35.991Z", - "type": "joined_community", - "isContribution": true, - "platform": "hubspot" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641971636.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "michelle_calderon", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T07:13:56.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641971387.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "marie_warner", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T07:09:47.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641965070.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "benjamin_howard", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T05:24:30.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641964902.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "heather_chapman", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T05:21:42.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641959883.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "michael_blair", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T03:58:03.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641954273.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "john_henry", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-12T02:24:33.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "sample": true - }, - "member": { - "crowdInfo": { - "hubspot": { - "name": "David Wilson", - "organisation": "Crowd.dev" - }, - "crowd": { - "sample": true - } - }, - "username": "david_wilson", - "type": "member", - "score": 9, - "email": "david.wilson@Crowd.dev", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-12T01:05:55.481Z", - "type": "joined_community", - "isContribution": true, - "platform": "twitter" - }, - { - "crowdInfo": { - "repo": "https://github.com/vaticle/typedb" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Suresh", - "isHireable": true, - "url": "https://github.com/sureshg", - "websiteUrl": "https://suresh.dev" - }, - "twitter": { - "url": "https://twitter.com/sur3shg" - } - }, - "username": "sureshg", - "type": "member", - "score": 0, - "email": "", - "organisation": "", - "location": "San Jose, CA", - "bio": "☕️ Kotlin | Java | Jetbrains Compose Desktop/Web" - }, - "timestamp": "2022-01-12T00:47:22.000Z", - "type": "star", - "isContribution": false, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641940278.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kathleen_jones", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-11T22:31:18.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641940032.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kristen_davis", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-11T22:27:12.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641937157.083923, - "channel": "help" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "karen_andrews", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-11T21:39:17.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "sample": true - }, - "member": { - "crowdInfo": { - "apis": { - "name": "Theodore Johnson" - }, - "crowd": { - "sample": true - } - }, - "username": "theodore_johnson", - "type": "member", - "score": 7, - "email": "theodore.johnson@gmail.com", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-11T21:03:17.991Z", - "type": "joined_community", - "isContribution": true, - "platform": "apis" - }, - { - "crowdInfo": { - "sample": true - }, - "member": { - "crowdInfo": { - "discord": { - "name": "Michael Gonzalez", - "organisation": "PiedPieper.io" - }, - "crowd": { - "sample": true - } - }, - "username": "michael_gonzalez", - "type": "member", - "score": 6, - "email": "michael.gonzalez@PiedPieper.io", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-11T20:30:48.481Z", - "type": "joined_community", - "isContribution": true, - "platform": "hubspot" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641932756.083923, - "channel": "bugs" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "tony_blevins", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-11T20:25:56.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Thanks. We don't have a public road map available but we'll try to be more transparent and explicit about new releases in the future.", - "title": "DB Connection string not available", - "parent_url": "https://github.com/nhost/nhost/issues/113", - "url": "https://github.com/nhost/nhost/issues/113#issuecomment-1010307217", - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Johan Eliasson", - "isHireable": false, - "url": "https://github.com/elitan", - "websiteUrl": "https://nhost.io", - "actions": [ - { - "score": 3, - "timestamp": "2021-03-01T15:34:14Z" - } - ] - }, - "twitter": { - "url": "https://twitter.com/elitasson" - } - }, - "username": "elitan", - "type": "member", - "score": 10, - "email": "johan@eliasson.me", - "organisation": "", - "location": "The Internet", - "bio": "Exploiting regularities to my benefit." - }, - "timestamp": "2022-01-11T19:49:55.000Z", - "type": "issue-comment", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641929934.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "stephanie_allen", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-11T19:38:54.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641929867.083923, - "channel": "general" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "kathleen_jones", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-11T19:37:47.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "sample": true - }, - "member": { - "crowdInfo": { - "twitter": { - "name": "Matthew Smith", - "organisation": "PiedPieper.io" - }, - "crowd": { - "sample": true - } - }, - "username": "matthew_smith", - "type": "member", - "score": 9, - "email": "matthew.smith@PiedPieper.io", - "organisation": "", - "location": "", - "bio": "" - }, - "timestamp": "2022-01-11T19:11:41.481Z", - "type": "joined_community", - "isContribution": true, - "platform": "apis" - }, - { - "crowdInfo": { - "body": "Amazing, I already knew you guys were working on this 👍 I just made this issue so that I could be updated when you guys make it available.\nIs there perhaps a public roadmap where I could check instead of making these issues?", - "title": "DB Connection string not available", - "parent_url": "https://github.com/nhost/nhost/issues/113", - "url": "https://github.com/nhost/nhost/issues/113#issuecomment-1010240647", - "repo": "https://github.com/nhost/nhost" - }, - "member": { - "crowdInfo": { - "github": { - "name": "Fabio Espinosa", - "isHireable": false, - "url": "https://github.com/fabioespinosa", - "websiteUrl": "fabioespinosa.mit.edu", - "actions": [ - { - "score": 3, - "timestamp": "2022-01-11T15:23:58Z" - } - ] - } - }, - "username": "fabioespinosa", - "type": "member", - "score": 1, - "email": "fa.espinosa10@uniandes.edu.co", - "organisation": "", - "location": "", - "bio": "Software engineer at Toptal.\r\n\r\nPrev @ CERN.\r\n\r\nCreator of losestudiantes.com" - }, - "timestamp": "2022-01-11T18:23:54.000Z", - "type": "issue-comment", - "isContribution": true, - "platform": "github" - }, - { - "crowdInfo": { - "body": "Hi! I have started a checklist of things we should check before deploying or merging. Feel free to add anything else\n", - "channel": "dev", - "slackId": "1641925349.001600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641925349001600" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-11T18:22:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Hi! I have started a checklist of things we should check before deploying or merging. Feel free to add anything else\n", - "channel": "dev", - "slackId": "1641925349.001600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641925349001600" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-11T18:22:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Hi! I have started a checklist of things we should check before deploying or merging. Feel free to add anything else\n", - "channel": "dev", - "slackId": "1641925349.001600", - "url": "https://crowddevspace.slack.com/archives/C01NBV2BDDK/p1641925349001600" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - }, - "github": { - "name": "Joan Reyero", - "isHireable": false, - "url": "https://github.com/joanreyero", - "websiteUrl": "crowd.dev", - "bio": "Co-founder and CTO at Crowd.dev" - } - }, - "username": "Joan Reyero", - "type": "member", - "score": 10, - "email": "joanreyero@gmail.com", - "organisation": "@CrowdDevHQ", - "location": "Europe/London (timezone)", - "bio": "Co-founder and CTO at Crowd.dev" - }, - "timestamp": "2022-01-11T18:22:29.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Message here", - "slackId": 1641924021.083923, - "channel": "feature-ideas" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/Amsterdam" - } - }, - "username": "thomas_moreno", - "type": "member", - "score": 10, - "email": "anil@crowd.dev", - "organisation": "", - "location": "Europe/Amsterdam (timezone)", - "bio": "" - }, - "timestamp": "2022-01-11T18:00:21.000Z", - "type": "message", - "isContribution": true, - "platform": "slack" - }, - { - "crowdInfo": { - "body": "Changes Proposed\n\nHide specific dimensions depending on the selected measure Followed this spec\nClean widgets labels / add missing translations\nAdd footer and privacy policy to public reports page\nFix not being able to update reports layouts\nFix collision/overlapping issue\nAdd Untitled as default widgets' title", - "title": "A few more tweaks to reports", - "state": "merged", - "url": "https://github.com/CrowdHQ/crowd-web/pull/65", - "repo": "https://github.com/CrowdHQ/crowd-web" - }, - "member": { - "crowdInfo": { - "slack": { - "timezone": "Europe/London" - }, - "github": { - "name": "Mário Balça", - "isHireable": true, - "url": "https://github.com/mariobalca", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io", - "location": "Coimbra, Portugal" - }, - "twitter": { - "url": "https://twitter.com/mariobalca" - } - }, - "username": "mariobalca", - "type": "member", - "score": 10, - "email": "mario@crowd.dev", - "organisation": "@CrowdHQ | @tweetboard-io", - "location": "Europe/London (timezone)", - "bio": "Full Stack Developer @CrowdHQ | Building @tweetboard-io in public | Previously founded @ripplr-io" - }, - "timestamp": "2022-01-11T17:58:06.000Z", - "type": "pull_request-closed", - "isContribution": true, - "platform": "github" - } -] diff --git a/backend/src/database/initializers/conversationInit.ts b/backend/src/database/initializers/conversationInit.ts deleted file mode 100644 index 6c5338ef88..0000000000 --- a/backend/src/database/initializers/conversationInit.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * This script is responsible for generating non - * existing parentIds for historical discord activities - */ -import dotenv from 'dotenv' -import dotenvExpand from 'dotenv-expand' -import { getServiceLogger } from '@crowd/logging' -import { PlatformType } from '@crowd/types' -import TenantService from '../../services/tenantService' -import ActivityService from '../../services/activityService' -import getUserContext from '../utils/getUserContext' -import SequelizeRepository from '../repositories/sequelizeRepository' - -const path = require('path') - -const env = dotenv.config({ - path: path.resolve(__dirname, `../../../.env.prod`), -}) - -dotenvExpand.expand(env) - -const log = getServiceLogger() - -async function conversationInit() { - const tenants = await TenantService._findAndCountAllForEveryUser({}) - - // for each tenant - for (const tenant of tenants.rows) { - log.info({ tenantId: tenant.id }, 'Processing tenant!') - const userContext = await getUserContext(tenant.id) - const as = new ActivityService(userContext) - - const discordActivities = await as.findAndCountAll({ - filter: { platform: PlatformType.DISCORD, type: 'message' }, - orderBy: 'timestamp_ASC', - }) - - for (const discordActivity of discordActivities.rows) { - if (discordActivity.parentId) { - log.info( - { activityId: discordActivity.id, parentId: discordActivity.parentId }, - 'Activity has a parent id!', - ) - // get parent activity - const parentAct = await as.findById(discordActivity.parentId) - - const transaction = await SequelizeRepository.createTransaction(userContext) - - await as.addToConversation(discordActivity.id, parentAct.id, transaction) - - await SequelizeRepository.commitTransaction(transaction) - } - } - } -} - -conversationInit() diff --git a/backend/src/database/initializers/create.ts b/backend/src/database/initializers/create.ts deleted file mode 100644 index 7585bb5fa2..0000000000 --- a/backend/src/database/initializers/create.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { getServiceLogger } from '@crowd/logging' -import models from '../models' - -const log = getServiceLogger() - -models(1000 * 30) - .sequelize.sync({ alter: true }) - .then(() => { - log.info('Database tables created!') - process.exit() - }) - .catch((error) => { - log.error(error, 'Error while creating database tables!') - process.exit(1) - }) diff --git a/backend/src/database/initializers/discordThreadsRepliesHistorical.ts b/backend/src/database/initializers/discordThreadsRepliesHistorical.ts deleted file mode 100644 index 9ec519ae82..0000000000 --- a/backend/src/database/initializers/discordThreadsRepliesHistorical.ts +++ /dev/null @@ -1,177 +0,0 @@ -/** - * This script is responsible for generating non - * existing parentIds for historical discord activities - */ - -import fetch from 'node-fetch' -import dotenv from 'dotenv' -import dotenvExpand from 'dotenv-expand' -import { getServiceLogger } from '@crowd/logging' -import { PlatformType } from '@crowd/types' -import TenantService from '../../services/tenantService' -import ActivityService from '../../services/activityService' -import IntegrationService from '../../services/integrationService' -import { DISCORD_CONFIG } from '../../conf' -import getUserContext from '../utils/getUserContext' - -const path = require('path') - -const env = dotenv.config({ - path: path.resolve(__dirname, `../../../.env.staging`), -}) - -dotenvExpand.expand(env) - -const log = getServiceLogger() - -async function discordSetParentForThreads() { - const tenants = await TenantService._findAndCountAllForEveryUser({}) - - tenants.rows = tenants.rows.filter((i) => i.id === 'b044af41-657a-4925-9541-cf8dfbdc687b') - - // for each tenant - for (const t of tenants.rows) { - const tenantId = t.id - // get user context - const userContext = await getUserContext(tenantId) - // get discord message activities - const integrationService = new IntegrationService(userContext) - - const discordIntegration = ( - await integrationService.findAndCountAll({ filter: { platform: PlatformType.DISCORD } }) - ).rows[0] - - if ( - discordIntegration && - discordIntegration.settings.channels && - discordIntegration.settings.channels.length > 0 - ) { - const actService = new ActivityService(userContext) - - const discordChannelMapping = [] - - log.info({ discordIntegration }, 'Discord integration!') - - for (const channel of discordIntegration.settings.channels) { - discordChannelMapping[channel.name] = { id: channel.id, type: 'channel' } - } - - // Logging channel mapping: - log.info({ discordChannelMapping }, 'Discord channel mapping!') - - // Get thread starter activities - const acts = ( - await actService.findAndCountAll({ - filter: { platform: PlatformType.DISCORD, type: 'message' }, - orderBy: 'timestamp_ASC', - }) - ).rows - - for (const act of acts) { - if ( - act.crowdInfo.sample !== 'true' && - act.crowdInfo.sample !== true && - !(act.crowdInfo.discord && act.crowdInfo.discord.sample === 'true') && - act.sourceId - ) { - if (act.crowdInfo.threadStarter === true) { - // get thread activities - let threadActivitiesFromApi = await getThreadMessages(act.sourceId) - - // check thread has more activities - let moreActsFromApi = await getThreadMessages( - act.sourceId, - threadActivitiesFromApi[threadActivitiesFromApi.length - 1].id, - ) - - while (moreActsFromApi.length > 0) { - log.info( - { anhor: moreActsFromApi[moreActsFromApi.length - 1].id }, - 'Getting next 50 thread messagess...', - ) - await new Promise((resolve) => { - setTimeout(resolve, 500) - }) - threadActivitiesFromApi = threadActivitiesFromApi.concat(moreActsFromApi) - moreActsFromApi = await getThreadMessages( - act.sourceId, - moreActsFromApi[moreActsFromApi.length - 1].id, - ) - } - - for (const threadActivityFromApi of threadActivitiesFromApi) { - const childSourceId = threadActivityFromApi.id - const childCrowdActivityRowsAndCount = await actService.findAndCountAll({ - filter: { sourceId: childSourceId }, - }) - - if (childCrowdActivityRowsAndCount.count === 1) { - // update both child.crowdInfo and child.parent - const childCrowdInfo = childCrowdActivityRowsAndCount.rows[0].crowdInfo - childCrowdInfo.url = `https://discordapp.com/channels/${discordIntegration.integrationIdentifier}/${act.crowdInfo.sourceId}/${childCrowdActivityRowsAndCount.rows[0].sourceId}` - await actService.update(childCrowdActivityRowsAndCount.rows[0].id, { - childCrowdInfo, - sourceParentId: act.sourceId, - parent: act.id, - }) - log.info( - { activityId: childCrowdActivityRowsAndCount.rows[0].id }, - 'Child activity crowdInfo and parent updated!', - ) - } else { - log.info(`thread child cannot be found in the db sourceId: ${childSourceId}`) - log.info(`found count is: ${childCrowdActivityRowsAndCount.count}`) - } - } - - // update parent.crowdInfo if mapping exists - if ( - discordChannelMapping[act.crowdInfo.channel] && - discordChannelMapping[act.crowdInfo.channel].id - ) { - const parentCrowdInfo = act.crowdInfo - parentCrowdInfo.url = `https://discordapp.com/channels/${ - discordIntegration.integrationIdentifier - }/${discordChannelMapping[act.crowdInfo.channel].id}/${act.sourceId}` - await actService.update(act.id, { crowdInfo: parentCrowdInfo }) - log.info(`parent activity [${act.id}] crowdInfo updated!`) - } - } else if (act.crowdInfo.thread === false || act.crowdInfo.thread === 'false') { - // not a thread starter and not a thread message - if ( - discordChannelMapping[act.crowdInfo.channel] && - discordChannelMapping[act.crowdInfo.channel].id - ) { - const parentCrowdInfo = act.crowdInfo - parentCrowdInfo.url = `https://discordapp.com/channels/${ - discordIntegration.integrationIdentifier - }/${discordChannelMapping[act.crowdInfo.channel].id}/${act.sourceId}` - await actService.update(act.id, { crowdInfo: parentCrowdInfo }) - log.info(`activity [${act.id}] crowdInfo updated!`) - } - } - } - } - } - } -} - -async function getThreadMessages(threadId, before = null) { - log.info(`getting messages of threadID: ${threadId}`) - let url = `https://discord.com/api/v9/channels/${threadId}/messages` - if (before) { - url += `?before=${before}` - log.info(`paginated url is: ${url}`) - } - - return fetch(url, { - headers: { Authorization: `Bot ${DISCORD_CONFIG.token}` }, - }) - .then((res) => res.json()) - .then((res) => { - log.info({ res }, 'Found thread activities in api!') - return res - }) -} - -discordSetParentForThreads() diff --git a/backend/src/database/initializers/entities/2022-04-05-add-microservices.ts b/backend/src/database/initializers/entities/2022-04-05-add-microservices.ts deleted file mode 100644 index c465e25d3e..0000000000 --- a/backend/src/database/initializers/entities/2022-04-05-add-microservices.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { PlatformType } from '@crowd/types' -import TenantService from '../../../services/tenantService' -import MicroserviceService from '../../../services/microserviceService' -import WidgetService from '../../../services/widgetService' -import IntegrationService from '../../../services/integrationService' -import getUserContext from '../../utils/getUserContext' -import * as microserviceTypes from '../../utils/keys/microserviceTypes' - -export default async () => { - const tenants = (await TenantService._findAndCountAllForEveryUser({ filter: {} })).rows - - for (const tenant of tenants) { - const userContext = await getUserContext(tenant.id) - const ms = new MicroserviceService(userContext) - const ws = new WidgetService(userContext) - const is = new IntegrationService(userContext) - - // add members_score microservice - const membersScoreMicroservice = { - init: true, - type: microserviceTypes.membersScore, - } - - await ms.create(membersScoreMicroservice) - - // if tenant has a benchmark widget set - // add github_lookalike microservice to the tenant - const benchmarkWidget = await ws.findAndCountAll({ filter: { type: 'benchmark' } }) - - if (benchmarkWidget.count > 0) { - const githubLookalikeMicroservice = { - init: true, - type: microserviceTypes.githubLookalike, - } - await ms.create(githubLookalikeMicroservice) - } - - // if tenant has an active twitter integration set - // add twitter_followers microservice to the tenant - const twitterIntegration = await is.findAndCountAll({ - filter: { platform: PlatformType.TWITTER, status: 'done' }, - }) - - if (twitterIntegration.count > 0) { - const twitterFollowersMicroservice = { - init: true, - type: microserviceTypes.twitterFollowers, - } - await ms.create(twitterFollowersMicroservice) - } - } -} diff --git a/backend/src/database/initializers/entities/2022-04-27-add-conversations.ts b/backend/src/database/initializers/entities/2022-04-27-add-conversations.ts deleted file mode 100644 index 79d32a0dd0..0000000000 --- a/backend/src/database/initializers/entities/2022-04-27-add-conversations.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { PlatformType } from '@crowd/types' -import TenantService from '../../../services/tenantService' -import getUserContext from '../../utils/getUserContext' -import ActivityService from '../../../services/activityService' -import SequelizeRepository from '../../repositories/sequelizeRepository' - -export default async () => { - const tenants = (await TenantService._findAndCountAllForEveryUser({ filter: {} })).rows - - for (const tenant of tenants) { - const userContext = await getUserContext(tenant.id) - const as = new ActivityService(userContext) - - const discordActivities = await as.findAndCountAll({ - filter: { platform: PlatformType.DISCORD, type: 'message' }, - orderBy: 'timestamp_ASC', - }) - - for (const discordActivity of discordActivities.rows) { - if (discordActivity.parentId) { - // get parent activity - const parentAct = await as.findById(discordActivity.parentId) - - const transaction = await SequelizeRepository.createTransaction(userContext) - - await as.addToConversation(discordActivity.id, parentAct.id, transaction) - - await SequelizeRepository.commitTransaction(transaction) - } - } - } -} diff --git a/backend/src/database/initializers/entities/2022-10-06-api-v1-transformations.ts b/backend/src/database/initializers/entities/2022-10-06-api-v1-transformations.ts deleted file mode 100644 index b5c560215e..0000000000 --- a/backend/src/database/initializers/entities/2022-10-06-api-v1-transformations.ts +++ /dev/null @@ -1,422 +0,0 @@ -import { QueryTypes } from 'sequelize' -import { MemberAttributeName, PlatformType } from '@crowd/types' -import { - CROWD_MEMBER_ATTRIBUTES, - DEFAULT_MEMBER_ATTRIBUTES, - DEVTO_MEMBER_ATTRIBUTES, - DISCORD_MEMBER_ATTRIBUTES, - GITHUB_MEMBER_ATTRIBUTES, - SLACK_MEMBER_ATTRIBUTES, - TWITTER_MEMBER_ATTRIBUTES, -} from '@crowd/integrations' -import TenantService from '../../../services/tenantService' -import getUserContext from '../../utils/getUserContext' -import IntegrationService from '../../../services/integrationService' -import MemberAttributeSettingsService from '../../../services/memberAttributeSettingsService' -import MemberService from '../../../services/memberService' - -export default async () => { - const tenants = (await TenantService._findAndCountAllForEveryUser({ filter: {} })).rows - - for (const tenant of tenants) { - let updateMembers = [] - let updateActivities = [] - - const userContext = await getUserContext(tenant.id) - const is = new IntegrationService(userContext) - const memberAttributesService = new MemberAttributeSettingsService(userContext) - const activeIntegrations = await is.findAndCountAll({ filter: { status: 'done' } }) - - // Create default member attribute settings - await memberAttributesService.createPredefined(DEFAULT_MEMBER_ATTRIBUTES) - - // create sample attribute settings if tenant.hasSampleData = true - if (tenant.hasSampleData) { - await memberAttributesService.createPredefined( - MemberAttributeSettingsService.pickAttributes( - [MemberAttributeName.SAMPLE], - CROWD_MEMBER_ATTRIBUTES, - ), - ) - } - - // Create integration specific member attribute settings - for (const integration of activeIntegrations.rows) { - switch (integration.platform) { - case PlatformType.DEVTO: - await memberAttributesService.createPredefined(DEVTO_MEMBER_ATTRIBUTES) - - await memberAttributesService.createPredefined( - MemberAttributeSettingsService.pickAttributes( - [MemberAttributeName.URL], - TWITTER_MEMBER_ATTRIBUTES, - ), - ) - - await memberAttributesService.createPredefined( - MemberAttributeSettingsService.pickAttributes( - [MemberAttributeName.URL, MemberAttributeName.NAME], - GITHUB_MEMBER_ATTRIBUTES, - ), - ) - break - case PlatformType.DISCORD: - await memberAttributesService.createPredefined(DISCORD_MEMBER_ATTRIBUTES) - break - case PlatformType.GITHUB: - await memberAttributesService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - await memberAttributesService.createPredefined( - MemberAttributeSettingsService.pickAttributes( - [MemberAttributeName.URL], - TWITTER_MEMBER_ATTRIBUTES, - ), - ) - break - case PlatformType.SLACK: - await memberAttributesService.createPredefined(SLACK_MEMBER_ATTRIBUTES) - break - - case PlatformType.TWITTER: - await memberAttributesService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - break - default: - break - } - } - - // start transforming members - const ms = new MemberService(userContext) - const seq = await userContext.database.sequelize - - // We need a raw query becuase new entity models don't have old fields (such as crowdInfo, bio, location) - const query = ` - select * from members m - where m."tenantId" = :tenantId - ` - const parameters: any = { - tenantId: tenant.id, - } - - const members = await seq.query(query, { - replacements: parameters, - type: QueryTypes.SELECT, - }) - - const nameMemberMapping = {} - - const orgNameMemberIdMappings = {} - - for (const member of members) { - // displayName - const displayName = member.username.crowdUsername - - let attributes = {} - - let bioAndLocationSourcePlatform - - if (member.crowdInfo.github) { - bioAndLocationSourcePlatform = 'github' - } else if (member.crowdInfo.devto) { - bioAndLocationSourcePlatform = 'devto' - } - - // set attributes.location - if (member.location) { - attributes[MemberAttributeName.LOCATION] = { - [bioAndLocationSourcePlatform]: member.location, - } - } - - // set attributes.bio - if (member.bio) { - attributes[MemberAttributeName.BIO] = { - [bioAndLocationSourcePlatform]: member.bio, - } - } - - // set rest of the crowdInfo to attributes - if (member.crowdInfo) { - for (const platform of Object.keys(member.crowdInfo)) { - // We don't keep sample under a platform, it's stored as crowdInfo.sample in the old api - if (platform === 'sample') { - attributes = setObjectAttribute( - attributes, - MemberAttributeName.SAMPLE, - PlatformType.CROWD, - member.crowdInfo.sample, - ) - } - for (const attributeName of Object.keys(member.crowdInfo[platform])) { - // we change crowdInfo.platform.id to attributes.sourceId.platform - if (attributeName === 'id') { - attributes = setObjectAttribute( - attributes, - MemberAttributeName.SOURCE_ID, - platform, - member.crowdInfo[platform][attributeName], - ) - } - if (attributeName === 'imageUrl') { - attributes = setObjectAttribute( - attributes, - MemberAttributeName.AVATAR_URL, - platform, - member.crowdInfo[platform][attributeName], - ) - } else { - attributes = setObjectAttribute( - attributes, - attributeName, - platform, - member.crowdInfo[platform][attributeName], - ) - } - } - } - } - - // set organization - if (member.organisation) { - if (orgNameMemberIdMappings[member.organisation]) { - orgNameMemberIdMappings[member.organisation].push(member.id) - } else { - orgNameMemberIdMappings[member.organisation] = [member.id] - } - } - - attributes = await ms.setAttributesDefaultValues(attributes) - - updateMembers.push({ - ...member, - displayName, - attributes, - }) - } - - const { randomUUID } = require('crypto') - - if (Object.keys(orgNameMemberIdMappings).length !== 0) { - let organizationsQuery = `INSERT INTO "organizations" ("id", "name", "createdAt", "updatedAt", "tenantId") VALUES ` - for (const organisationName of Object.keys(orgNameMemberIdMappings)) { - organizationsQuery += `('${randomUUID()}', '${organisationName.replace( - /'/g, - "''", - )}', NOW(), NOW(), '${tenant.id}'),` - } - organizationsQuery = organizationsQuery.slice(0, organizationsQuery.length - 1) - organizationsQuery += ` returning id` - - const ids = await seq.query(organizationsQuery, { - type: QueryTypes.INSERT, - }) - - let a = 0 - for (const organisationName of Object.keys(orgNameMemberIdMappings)) { - nameMemberMapping[ids[0][a].id] = orgNameMemberIdMappings[organisationName] - a += 1 - } - - let memberOrganisationsQuery = ` - INSERT INTO "memberOrganizations" ("createdAt", "updatedAt", "memberId", "organizationId") VALUES ` - for (const organisationId of Object.keys(nameMemberMapping)) { - for (const memberId of nameMemberMapping[organisationId]) { - memberOrganisationsQuery += `(NOW(), NOW(), '${memberId}', '${organisationId}'),` - } - } - memberOrganisationsQuery = memberOrganisationsQuery.slice( - 0, - memberOrganisationsQuery.length - 1, - ) - - await seq.query(memberOrganisationsQuery, { - type: QueryTypes.INSERT, - }) - } - - const MEMBER_CHUNK_SIZE = 25000 - - if (updateMembers.length > MEMBER_CHUNK_SIZE) { - const splittedBulkMembers = [] - - while (updateMembers.length > MEMBER_CHUNK_SIZE) { - splittedBulkMembers.push(updateMembers.slice(0, MEMBER_CHUNK_SIZE)) - updateMembers = updateMembers.slice(MEMBER_CHUNK_SIZE) - } - - // push last leftover chunk - if (updateMembers.length > 0) { - splittedBulkMembers.push(updateMembers) - } - - for (const memberChunk of splittedBulkMembers) { - await userContext.database.member.bulkCreate(memberChunk, { - updateOnDuplicate: ['displayName', 'attributes'], - }) - } - } else { - await userContext.database.member.bulkCreate(updateMembers, { - updateOnDuplicate: ['displayName', 'attributes'], - }) - } - - const totalActivityCount = await getActivityCount(seq, tenant.id) - let currentActivityCount = 0 - let currentOffset = 0 - - while (currentActivityCount < totalActivityCount) { - const LIMIT = 200000 - - updateActivities = [] - let splittedBulkActivities = [] - const activities = await getActivities(seq, tenant.id, LIMIT, currentOffset) - - for (const activity of activities) { - let body = '' - - if (activity.crowdInfo.body) { - body = activity.crowdInfo.body - delete activity.crowdInfo.body - } - - let url = '' - - if (activity.crowdInfo.url) { - url = activity.crowdInfo.url - delete activity.crowdInfo.url - } - let title = '' - - if (activity.crowdInfo.title) { - title = activity.crowdInfo.title - delete activity.crowdInfo.title - } - - let channel = '' - - if (activity.platform === PlatformType.TWITTER) { - if (activity.type === 'hashtag' && activity.crowdInfo.hashtag) { - channel = activity.crowdInfo.hashtag - } - } else if (activity.platform === PlatformType.GITHUB) { - if (activity.crowdInfo.repo) { - channel = activity.crowdInfo.repo - } - } else if (activity.platform === PlatformType.SLACK) { - if (activity.crowdInfo.channel) { - channel = activity.crowdInfo.channel - } - } else if (activity.platform === PlatformType.DEVTO) { - if (activity.crowdInfo.articleTitle) { - channel = activity.crowdInfo.articleTitle - } - - if (activity.crowdInfo.thread === false || activity.crowdInfo.thread === true) { - delete activity.crowdInfo.thread - } - } else if (activity.platform === PlatformType.DISCORD) { - if (activity.crowdInfo.thread === false && activity.crowdInfo.channel) { - channel = activity.crowdInfo.channel - } else if (activity.crowdInfo.thread) { - channel = activity.crowdInfo.thread - - if (activity.crowdInfo.channel) { - delete activity.crowdInfo.channel - } - } - } - - const attributes = activity.crowdInfo - - updateActivities.push({ - ...activity, - body, - url, - title, - attributes, - channel, - }) - } - - const ACTIVITY_CHUNK_SIZE = 25000 - - if (updateActivities.length > ACTIVITY_CHUNK_SIZE) { - splittedBulkActivities = [] - - while (updateActivities.length > ACTIVITY_CHUNK_SIZE) { - splittedBulkActivities.push(updateActivities.slice(0, ACTIVITY_CHUNK_SIZE)) - updateActivities = updateActivities.slice(ACTIVITY_CHUNK_SIZE) - } - - // push last leftover chunk - if (updateActivities.length > 0) { - splittedBulkActivities.push(updateActivities) - } - - for (const activityChunk of splittedBulkActivities) { - await userContext.database.activity.bulkCreate(activityChunk, { - updateOnDuplicate: ['body', 'url', 'title', 'attributes', 'channel'], - }) - } - } else { - await userContext.database.activity.bulkCreate(updateActivities, { - updateOnDuplicate: ['body', 'url', 'title', 'attributes', 'channel'], - }) - } - - currentActivityCount += activities.length - currentOffset += activities.length - } - } -} - -async function getActivityCount(seq, tenantId) { - const activityCountQuery = ` - select count(*) from activities a - where a."tenantId" = :tenantId - ` - const activityCountQueryParameters: any = { - tenantId, - } - - const activityCount = ( - await seq.query(activityCountQuery, { - replacements: activityCountQueryParameters, - type: QueryTypes.SELECT, - }) - )[0].count - - return activityCount -} - -async function getActivities(seq, tenantId, limit, offset) { - const activityQuery = ` - select * from activities a - where a."tenantId" = :tenantId - ORDER BY a."timestamp" DESC - OFFSET :offset - LIMIT :limit - ` - const activityQueryParameters: any = { - tenantId, - offset, - limit, - } - - return seq.query(activityQuery, { - replacements: activityQueryParameters, - type: QueryTypes.SELECT, - }) -} - -function setObjectAttribute(obj, attributeName, platform, value) { - if (obj[attributeName]) { - obj[attributeName][platform] = value - } else { - obj[attributeName] = { - [platform]: value, - } - } - - return obj -} diff --git a/backend/src/database/initializers/entities/2022-10-11-api-v1-activity-sentiment.ts b/backend/src/database/initializers/entities/2022-10-11-api-v1-activity-sentiment.ts deleted file mode 100644 index d1b2bb7f2d..0000000000 --- a/backend/src/database/initializers/entities/2022-10-11-api-v1-activity-sentiment.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { QueryTypes } from 'sequelize' -import { timeout } from '@crowd/common' -import ActivityService from '../../../services/activityService' -import SequelizeRepository from '../../repositories/sequelizeRepository' - -/** - * Since requests to aws activity sentiment api creates a bottleneck, - * We'll be generating the sentiment for this month's activities only. - * TODO:: Finish this up - */ -export default async () => { - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - - // const activityQuery = `select * from activities a where a."timestamp" between '2022-09-01' and now() and (a."attributes"->>'sample') is null` - const activityQuery = `select * from activities a where a."timestamp" between '2022-09-01' and now() - and(a."attributes"->>'sample') is null - and ((a.title is not null and a.title != '') or (a.body is not null and a.body != ''))` - - let activities = await options.database.sequelize.query(activityQuery, { - type: QueryTypes.SELECT, - }) - - const splittedActivities = [] - const ACTIVITY_CHUNK_SIZE = 350 - - if (activities.length > ACTIVITY_CHUNK_SIZE) { - while (activities.length > ACTIVITY_CHUNK_SIZE) { - splittedActivities.push(activities.slice(0, ACTIVITY_CHUNK_SIZE)) - activities = activities.slice(ACTIVITY_CHUNK_SIZE) - } - // insert last small chunk - if (activities.length > 0) splittedActivities.push(activities) - } else { - splittedActivities.push(activities) - } - - const activityService = new ActivityService(options) - - for (let activityChunk of splittedActivities) { - let sentiments - - try { - sentiments = await activityService.getSentimentBatch(activityChunk) - } catch (e) { - await timeout(3000) - sentiments = await activityService.getSentimentBatch(activityChunk) - } - - activityChunk = activityChunk.map((a, index) => { - a.sentiment = sentiments[index] - return a - }) - - await options.database.activity.bulkCreate(activityChunk, { - updateOnDuplicate: ['sentiment'], - }) - } -} diff --git a/backend/src/database/initializers/entities/2022-10-14-api-v1-remove-crowdUsername.ts b/backend/src/database/initializers/entities/2022-10-14-api-v1-remove-crowdUsername.ts deleted file mode 100644 index 7b50325c45..0000000000 --- a/backend/src/database/initializers/entities/2022-10-14-api-v1-remove-crowdUsername.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { QueryTypes } from 'sequelize' -import SequelizeRepository from '../../repositories/sequelizeRepository' - -export default async () => { - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - - const totalMembersCount = await getMembersCount(options.database.sequelize) - let currentMemberCount = 0 - let currentOffset = 0 - - while (currentMemberCount < totalMembersCount) { - const LIMIT = 200000 - let updateMembers = [] - let splittedBulkMembers = [] - const members = await getMembers(options.database.sequelize, LIMIT, currentOffset) - - for (const member of members) { - if (member.username.crowdUsername) { - delete member.username.crowdUsername - } - - updateMembers.push(member) - } - - const MEMBER_CHUNK_SIZE = 25000 - - if (updateMembers.length > MEMBER_CHUNK_SIZE) { - splittedBulkMembers = [] - - while (updateMembers.length > MEMBER_CHUNK_SIZE) { - splittedBulkMembers.push(updateMembers.slice(0, MEMBER_CHUNK_SIZE)) - updateMembers = updateMembers.slice(MEMBER_CHUNK_SIZE) - } - - // push last leftover chunk - if (updateMembers.length > 0) { - splittedBulkMembers.push(updateMembers) - } - - for (const memberChunk of splittedBulkMembers) { - await options.database.member.bulkCreate(memberChunk, { - updateOnDuplicate: ['username'], - }) - } - } else { - await options.database.member.bulkCreate(updateMembers, { - updateOnDuplicate: ['username'], - }) - } - - currentMemberCount += members.length - currentOffset += members.length - } -} - -async function getMembers(seq, limit, offset) { - const membersQuery = ` - select * from members m - ORDER BY m."createdAt" DESC - OFFSET :offset - LIMIT :limit - ` - const membersQueryParameters: any = { - offset, - limit, - } - - return seq.query(membersQuery, { - replacements: membersQueryParameters, - type: QueryTypes.SELECT, - }) -} - -async function getMembersCount(seq) { - const membersCountQuery = ` - select count(*) from members m - ` - - const membersCount = ( - await seq.query(membersCountQuery, { - type: QueryTypes.SELECT, - }) - )[0].count - - return membersCount -} diff --git a/backend/src/database/initializers/entities/2022-10-26-api-v1-organization-cache-enrichment.ts b/backend/src/database/initializers/entities/2022-10-26-api-v1-organization-cache-enrichment.ts deleted file mode 100644 index 10248ad0b8..0000000000 --- a/backend/src/database/initializers/entities/2022-10-26-api-v1-organization-cache-enrichment.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { QueryTypes } from 'sequelize' -import getUserContext from '../../utils/getUserContext' -import SequelizeRepository from '../../repositories/sequelizeRepository' -import getOrganization from '../../../serverless/integrations/usecases/github/graphql/organizations' -import OrganizationCacheRepository from '../../repositories/organizationCacheRepository' -import OrganizationRepository from '../../repositories/organizationRepository' - -export default async () => { - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - - // const tenants = await TenantService._findAndCountAllForEveryUser({}) - - const tenantsQuery = `select * from tenants` - - let tenants = await options.database.sequelize.query(tenantsQuery, { - type: QueryTypes.SELECT, - }) - - tenants = tenants.filter((i) => i.id !== '62712f6f-94e8-41e5-8cb7-87e3d272830b') - // for each tenant - for (const tenant of tenants) { - const userContext = await getUserContext(tenant.id) - - const ghIntegration = await userContext.database.integration.findOne({ - where: { - platform: 'github', - tenantId: tenant.id, - }, - include: [], - }) - - if (ghIntegration) { - const organizationQuery = `select * from organizations o where o."tenantId" = :tenantId` - - const organizations = await userContext.database.sequelize.query(organizationQuery, { - type: QueryTypes.SELECT, - replacements: { - tenantId: tenant.id, - }, - }) - - for (const org of organizations) { - // check if organization already exists in the cache by name - const record = await userContext.database.organizationCache.findOne({ - where: { - name: org.name, - }, - include: [], - }) - - org.name = org.name.replace(/["\\]+/g, '') - - // organization is not enriched from gh api yet - if (!record) { - const orgFromGH = await getOrganization(org.name, ghIntegration.token) - - if (orgFromGH) { - // check cache - const checkCache = await OrganizationCacheRepository.findByUrl( - orgFromGH.url, - userContext, - ) - - // if it already exists on cache, some other organization should be already enriched, find that org - - const findOrg = await options.database.organization.findOne({ - attributes: ['id', 'name', 'url'], - where: { - url: orgFromGH.url, - name: orgFromGH.name, - tenantId: tenant.id, - }, - }) - - if (checkCache && findOrg) { - // update current organizations members to found organization - const memberOrganizationsUpdateQuery = `UPDATE "memberOrganizations" SET "organizationId" = :existingOrganizationId WHERE "organizationId" = :duplicateOrganizationId` - await options.database.sequelize.query(memberOrganizationsUpdateQuery, { - type: QueryTypes.UPDATE, - replacements: { - existingOrganizationId: findOrg.id, - duplicateOrganizationId: org.id, - }, - }) - - // delete current organization - await OrganizationRepository.destroy(org.id, userContext, true) - } else { - // it's not in cache, create it - if (!checkCache) { - await OrganizationCacheRepository.create(orgFromGH, userContext) - } - - // check any other organization already has names similar to gh api response - let findByName = await options.database.organization.findAll({ - attributes: ['id', 'name'], - where: { - name: orgFromGH.name, - tenantId: tenant.id, - }, - }) - findByName = findByName.filter((i) => i.id !== org.id) - - if (findByName.length > 0) { - const memberOrganizationsUpdateQuery = `UPDATE "memberOrganizations" SET "organizationId" = :existingOrganizationId WHERE "organizationId" = :duplicateOrganizationId` - await options.database.sequelize.query(memberOrganizationsUpdateQuery, { - type: QueryTypes.UPDATE, - replacements: { - existingOrganizationId: org.id, - duplicateOrganizationId: findByName[0].id, - }, - }) - - // delete foundByName organization - await OrganizationRepository.destroy(findByName[0].id, userContext, true) - } - - const orgFromGhParsed = { - name: orgFromGH.name, - url: orgFromGH.url, - location: orgFromGH.location, - description: orgFromGH.description, - logo: orgFromGH.avatarUrl, - } as any - - if (orgFromGH.email) { - orgFromGhParsed.emails = [orgFromGH.email] - } - if (orgFromGH.twitterUsername) { - orgFromGhParsed.twitter = { handle: orgFromGH.twitterUsername } - } - - // enrich the organization with cache - await OrganizationRepository.update( - org.id, - { - ...org, - ...orgFromGhParsed, - }, - userContext, - ) - } - } - } else { - const fieldsFromCache = { - name: record.name, - url: record.url, - location: record.location, - description: record.description, - logo: record.logo, - emails: record.emails, - twitter: record.twitter, - } - - // enrich the organization with cache - await OrganizationRepository.update( - org.id, - { - ...org, - ...fieldsFromCache, - }, - userContext, - ) - } - } - } - } -} diff --git a/backend/src/database/initializers/entities/2022-20-06-github-conversations-init.ts b/backend/src/database/initializers/entities/2022-20-06-github-conversations-init.ts deleted file mode 100644 index 3240493e74..0000000000 --- a/backend/src/database/initializers/entities/2022-20-06-github-conversations-init.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { GithubActivityType } from '@crowd/integrations' -import { PlatformType } from '@crowd/types' -import TenantService from '../../../services/tenantService' -import ActivityService from '../../../services/activityService' -import getUserContext from '../../utils/getUserContext' -import SequelizeRepository from '../../repositories/sequelizeRepository' - -export default async () => { - const tenants = await TenantService._findAndCountAllForEveryUser({}) - - // for each tenant - for (const tenant of tenants.rows) { - const userContext = await getUserContext(tenant.id) - const as = new ActivityService(userContext) - - const githubActs = await as.findAndCountAll({ - filter: { platform: PlatformType.GITHUB }, - orderBy: 'timestamp_ASC', - }) - - githubActs.rows = githubActs.rows.filter( - (i) => - i.type === GithubActivityType.PULL_REQUEST_COMMENT || - i.type === GithubActivityType.ISSUE_COMMENT, - ) - - for (const githubActivity of githubActs.rows) { - if (githubActivity.parentId && githubActivity.conversationId === null) { - // get parent activity - const parentAct = await as.findById(githubActivity.parentId) - - const transaction = await SequelizeRepository.createTransaction(userContext) - - await as.addToConversation(githubActivity.id, parentAct.id, transaction) - - await SequelizeRepository.commitTransaction(transaction) - } - } - } -} diff --git a/backend/src/database/initializers/entities/2022-20-06-slack-conversations-init.ts b/backend/src/database/initializers/entities/2022-20-06-slack-conversations-init.ts deleted file mode 100644 index 4e9f5a9d73..0000000000 --- a/backend/src/database/initializers/entities/2022-20-06-slack-conversations-init.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PlatformType } from '@crowd/types' -import TenantService from '../../../services/tenantService' -import ActivityService from '../../../services/activityService' -import getUserContext from '../../utils/getUserContext' -import SequelizeRepository from '../../repositories/sequelizeRepository' - -export default async () => { - const tenants = await TenantService._findAndCountAllForEveryUser({}) - - // for each tenant - for (const tenant of tenants.rows) { - const userContext = await getUserContext(tenant.id) - const as = new ActivityService(userContext) - - const slackActs = await as.findAndCountAll({ - filter: { platform: PlatformType.SLACK, type: 'message' }, - orderBy: 'timestamp_ASC', - }) - - for (const slackActivity of slackActs.rows) { - if (slackActivity.parentId && slackActivity.conversationId === null) { - // get parent activity - const parentAct = await as.findById(slackActivity.parentId) - - const transaction = await SequelizeRepository.createTransaction(userContext) - - await as.addToConversation(slackActivity.id, parentAct.id, transaction) - - await SequelizeRepository.commitTransaction(transaction) - } - } - } -} diff --git a/backend/src/database/initializers/entities/2023-01-11-add-members-reports-to-tenants.ts b/backend/src/database/initializers/entities/2023-01-11-add-members-reports-to-tenants.ts deleted file mode 100644 index 34ee1dc761..0000000000 --- a/backend/src/database/initializers/entities/2023-01-11-add-members-reports-to-tenants.ts +++ /dev/null @@ -1,22 +0,0 @@ -import TenantService from '../../../services/tenantService' -import getUserContext from '../../utils/getUserContext' -import ReportService from '../../../services/reportService' - -/* eslint-disable no-console */ - -export default async () => { - const tenants = await TenantService._findAndCountAllForEveryUser({}) - - // for each tenant - for (const tenant of tenants.rows) { - const userContext = await getUserContext(tenant.id) - const rs = new ReportService(userContext) - - console.log(`Creating members report for tenant ${tenant.id}`) - await rs.create({ - name: 'Members report', - public: false, - isTemplate: true, - }) - } -} diff --git a/backend/src/database/initializers/entities/2023-01-23-add-isBot-to-member-attributes.ts b/backend/src/database/initializers/entities/2023-01-23-add-isBot-to-member-attributes.ts deleted file mode 100644 index 74f5170626..0000000000 --- a/backend/src/database/initializers/entities/2023-01-23-add-isBot-to-member-attributes.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { DEFAULT_MEMBER_ATTRIBUTES } from '@crowd/integrations' -import TenantService from '../../../services/tenantService' -import getUserContext from '../../utils/getUserContext' -import MemberAttributeSettingsService from '../../../services/memberAttributeSettingsService' - -/* eslint-disable no-console */ - -const addIsBotToMemberAttributes = async () => { - const tenants = await TenantService._findAndCountAllForEveryUser({}) - const isBotAttributes = DEFAULT_MEMBER_ATTRIBUTES.find((a) => a.name === 'isBot') - - // for each tenant - for (const tenant of tenants.rows) { - const userContext = await getUserContext(tenant.id) - const memberAttributeSettingsService = new MemberAttributeSettingsService(userContext) - - console.log(`Creating isBot member attribute for tenant ${tenant.id}`) - await memberAttributeSettingsService.create({ - name: isBotAttributes.name, - label: isBotAttributes.label, - type: isBotAttributes.type, - canDelete: isBotAttributes.canDelete, - show: isBotAttributes.show, - }) - } -} - -addIsBotToMemberAttributes() diff --git a/backend/src/database/initializers/entities/2023-03-03-add-isOrganization-to-default-member-attributes.ts b/backend/src/database/initializers/entities/2023-03-03-add-isOrganization-to-default-member-attributes.ts deleted file mode 100644 index 1df7450aa2..0000000000 --- a/backend/src/database/initializers/entities/2023-03-03-add-isOrganization-to-default-member-attributes.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { DEFAULT_MEMBER_ATTRIBUTES } from '@crowd/integrations' -import TenantService from '../../../services/tenantService' -import getUserContext from '../../utils/getUserContext' -import MemberAttributeSettingsService from '../../../services/memberAttributeSettingsService' - -/* eslint-disable no-console */ - -const addIsOrganizationToMemberAttributes = async () => { - const tenants = await TenantService._findAndCountAllForEveryUser({}) - const isOrganizationAttribute = DEFAULT_MEMBER_ATTRIBUTES.find((a) => a.name === 'isOrganization') - - // for each tenant - for (const tenant of tenants.rows) { - const userContext = await getUserContext(tenant.id) - const mas = new MemberAttributeSettingsService(userContext) - - // check already exists - const attrs = await mas.findAndCountAll({ filter: { name: isOrganizationAttribute.name } }) - - if (attrs.count === 0) { - console.log(`Creating isOrganization member attribute for tenant ${tenant.id}`) - await mas.create({ - name: isOrganizationAttribute.name, - label: isOrganizationAttribute.label, - type: isOrganizationAttribute.type, - canDelete: isOrganizationAttribute.canDelete, - show: isOrganizationAttribute.show, - }) - } - } -} - -addIsOrganizationToMemberAttributes() diff --git a/backend/src/database/initializers/entities/2023-03-27-add-product-community-fit-reports-to-tenants.ts b/backend/src/database/initializers/entities/2023-03-27-add-product-community-fit-reports-to-tenants.ts deleted file mode 100644 index 79e6932495..0000000000 --- a/backend/src/database/initializers/entities/2023-03-27-add-product-community-fit-reports-to-tenants.ts +++ /dev/null @@ -1,22 +0,0 @@ -import TenantService from '../../../services/tenantService' -import getUserContext from '../../utils/getUserContext' -import ReportService from '../../../services/reportService' - -/* eslint-disable no-console */ - -export default async () => { - const tenants = await TenantService._findAndCountAllForEveryUser({}) - - // for each tenant - for (const tenant of tenants.rows) { - const userContext = await getUserContext(tenant.id) - const rs = new ReportService(userContext) - - console.log(`Creating product-community fit report for tenant ${tenant.id}`) - await rs.create({ - name: 'Product-community fit report', - public: false, - isTemplate: true, - }) - } -} diff --git a/backend/src/database/initializers/entities/2023-04-24-add-activities-reports-to-tenants.ts b/backend/src/database/initializers/entities/2023-04-24-add-activities-reports-to-tenants.ts deleted file mode 100644 index e3ed3032a6..0000000000 --- a/backend/src/database/initializers/entities/2023-04-24-add-activities-reports-to-tenants.ts +++ /dev/null @@ -1,22 +0,0 @@ -import TenantService from '../../../services/tenantService' -import getUserContext from '../../utils/getUserContext' -import ReportService from '../../../services/reportService' - -/* eslint-disable no-console */ - -export default async () => { - const tenants = await TenantService._findAndCountAllForEveryUser({}) - - // for each tenant - for (const tenant of tenants.rows) { - const userContext = await getUserContext(tenant.id) - const rs = new ReportService(userContext) - - console.log(`Creating activities report for tenant ${tenant.id}`) - await rs.create({ - name: 'Activities report', - public: false, - isTemplate: true, - }) - } -} diff --git a/backend/src/database/initializers/juneIntegrationsInit.ts b/backend/src/database/initializers/juneIntegrationsInit.ts deleted file mode 100644 index 8409130839..0000000000 --- a/backend/src/database/initializers/juneIntegrationsInit.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * This script is responsible for generating missing integration created - * events in June - */ -import dotenv from 'dotenv' -import dotenvExpand from 'dotenv-expand' -import { getServiceLogger } from '@crowd/logging' -import TenantService from '../../services/tenantService' -import getUserContext from '../utils/getUserContext' -import IntegrationService from '../../services/integrationService' -import track from '../../segment/track' - -const path = require('path') - -const environmentArg = process.argv[2] - -const envFile = environmentArg === 'dev' ? '.env' : `.env-${environmentArg}` - -const env = dotenv.config({ - path: path.resolve(__dirname, `../../../${envFile}`), -}) - -dotenvExpand.expand(env) - -const log = getServiceLogger() - -async function juneIntegrationsInit() { - const tenants = await TenantService._findAndCountAllForEveryUser({}) - - // for each tenant - for (const tenant of tenants.rows) { - log.info(`processing tenant: ${tenant.id}`) - const userContext = await getUserContext(tenant.id) - - const integrationService = new IntegrationService(userContext) - const integrations = await integrationService.findAndCountAll({ filters: {} }) - for (const integration of integrations.rows) { - track( - 'Integration Created', - { - id: integration.id, - platform: integration.platform, - status: integration.status, - }, - userContext, - false, - integration.createdAt, - ) - } - } -} - -juneIntegrationsInit() diff --git a/backend/src/database/initializers/memberEnrichmentAddOrganization.ts b/backend/src/database/initializers/memberEnrichmentAddOrganization.ts deleted file mode 100644 index c230f6eb89..0000000000 --- a/backend/src/database/initializers/memberEnrichmentAddOrganization.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * This script is responsible for generating organizationIds - * for the existing activities - */ -import { QueryTypes } from 'sequelize' -import { getServiceChildLogger } from '@crowd/logging' -import TenantService from '../../services/tenantService' -import getUserContext from '../utils/getUserContext' -import MemberService from '../../services/memberService' -import MemberEnrichmentService from '../../services/premium/enrichment/memberEnrichmentService' - -const log = getServiceChildLogger('fixer') - -async function memberEnrichmentAddOrganization() { - const tenants = await TenantService._findAndCountAllForEveryUser({}) - - tenants.rows = tenants.rows.filter((i) => i.id === '1a634aad-ca86-4bad-9876-ab2e6ab880cc') - - // for each tenant - for (const t of tenants.rows) { - const tenantId = t.id - // get user context - const userContext = await getUserContext(tenantId) - - const memberService = new MemberService(userContext) - - const memberEnrichmentService = new MemberEnrichmentService(userContext) - - // get enriched members - const members = await userContext.database.sequelize.query( - `select mc.data as "enrichmentData", m.id as id from members m - join "memberEnrichmentCache" mc on mc."memberId" = m.id - where m."tenantId" = :tenantId and m."lastEnriched" is not null;`, - { - replacements: { - tenantId, - }, - type: QueryTypes.SELECT, - }, - ) - - for (const member of members) { - log.info(`Enriching member ${member.id} again!`) - - const memberById = await memberService.findById(member.id, true, false) - log.info({ ed: member.enrichmentData }, `Enrichment data:`) - - await memberEnrichmentService.getAttributes() - - const normalizedData = await memberEnrichmentService.normalize( - memberById, - member.enrichmentData, - ) - - await memberService.upsert({ - ...normalizedData, - platform: Object.keys(memberById.username)[0], - }) - } - } -} - -memberEnrichmentAddOrganization() diff --git a/backend/src/database/initializers/reports.json b/backend/src/database/initializers/reports.json deleted file mode 100644 index 6835dd49d7..0000000000 --- a/backend/src/database/initializers/reports.json +++ /dev/null @@ -1,9121 +0,0 @@ -[ - { - "name": "ttt", - "widgets": [ - { - "title": "aa", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "week", - "dateRange": "Last 30 days" - } - ], - "limit": 10000 - }, - "layout": { - "x": 0, - "y": 0, - "w": 6, - "h": 18, - "i": "620d30360895bb8bee0a7e0e", - "moved": false - } - } - }, - { - "title": "bb", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "week", - "dateRange": "Last 30 days" - } - ], - "limit": 10000 - }, - "layout": { - "x": 6, - "y": 0, - "w": 6, - "h": 18, - "i": "620d303b0895bb8bee0a7e24", - "moved": false - } - } - } - ], - "public": false - }, - { - "name": "Default Report", - "widgets": [ - { - "title": "Activities Over Time", - "type": "cubejs", - "settings": { - "chartType": "area", - "query": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "week", - "dateRange": "Last 30 days" - } - ], - "limit": 10000 - }, - "layout": { - "x": 0, - "y": 0, - "w": 6, - "h": 18, - "moved": false, - "i": "620ba455646f58848302096f" - } - } - }, - { - "title": "Members Over Time", - "type": "cubejs", - "settings": { - "chartType": "area", - "query": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "week", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Members.date": "asc" - } - }, - "layout": { - "x": 6, - "y": 0, - "w": 6, - "h": 18, - "moved": false, - "i": "620ba455646f5884830209af" - } - } - }, - { - "title": "Activities By Platform", - "type": "cubejs", - "settings": { - "chartType": "pie", - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Activities.activityCount": "desc" - } - }, - "layout": { - "x": 0, - "y": 18, - "w": 6, - "h": 18, - "moved": false, - "i": "620ba455646f5884830209c5" - } - } - }, - { - "title": "Members By Platform", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Members.memberCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "month", - "dateRange": "Last year" - } - ], - "filters": [ - { - "member": "Activity.platform", - "operator": "set", - "values": ["widget"] - } - ], - "limit": 10000, - "order": { - "Members.memberCount": "desc" - } - }, - "layout": { - "x": 6, - "y": 18, - "w": 6, - "h": 18, - "moved": false, - "i": "620ba456646f5884830209db" - } - } - } - ], - "public": true - }, - { - "name": "01 new report", - "widgets": [ - { - "title": "Untitled", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "week", - "dateRange": "Last year" - } - ], - "limit": 10000, - "order": { - "Members.date": "asc" - } - }, - "layout": { - "x": 0, - "y": 0, - "w": 6, - "h": 18, - "i": "620a1c3a8cdbedbafb2d12d8", - "moved": false - } - } - }, - { - "title": "Untitled", - "type": "cubejs", - "settings": { - "chartType": "number", - "query": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "week", - "dateRange": "Last 30 days" - } - ], - "limit": 10000 - }, - "layout": { - "x": 6, - "y": 0, - "w": 6, - "h": 9, - "i": "620a1db28cdbedbafb2d1442", - "moved": false - } - } - }, - { - "title": "New Members (last 30 days)", - "type": "cubejs", - "settings": { - "chartType": "number", - "query": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "dateRange": "Last 30 days" - } - ], - "limit": 10000 - }, - "layout": { - "x": 6, - "y": 9, - "w": 6, - "h": 3, - "i": "620a1dba8cdbedbafb2d145a", - "moved": false - } - } - } - ], - "public": true - }, - { - "name": "new report", - "widgets": [ - { - "title": "Untitled", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Members.memberCount"], - "dimensions": ["Member.score"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "day", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Members.date": "asc" - } - }, - "layout": { - "x": 0, - "y": 0, - "w": 6, - "h": 18, - "i": "620683b4e5e9def14ae6cbdd", - "moved": false - } - } - }, - { - "title": "Untitled", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "day", - "dateRange": "Last 30 days" - } - ], - "limit": 10000 - }, - "layout": { - "x": 6, - "y": 0, - "w": 6, - "h": 18, - "i": "620683c7e5e9def14ae6cbf5", - "moved": false - } - } - }, - { - "title": "Untitled", - "type": "cubejs", - "settings": { - "chartType": "table", - "query": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "dateRange": "Last 30 days" - } - ], - "limit": 10000 - }, - "layout": { - "x": 0, - "y": 18, - "w": 6, - "h": 8, - "i": "620683d3e5e9def14ae6cc0d", - "moved": false - } - } - }, - { - "title": "Untitled", - "type": "cubejs", - "settings": { - "chartType": "number", - "query": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "week", - "dateRange": "Last 30 days" - } - ], - "limit": 10000 - }, - "layout": { - "x": 6, - "y": 18, - "w": 6, - "h": 3, - "i": "620683dbe5e9def14ae6cc25", - "moved": false - } - } - }, - { - "title": "Untitled", - "type": "cubejs", - "settings": { - "chartType": "number", - "query": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "dateRange": "Last 30 days" - } - ], - "limit": 10000 - }, - "layout": { - "x": 6, - "y": 21, - "w": 6, - "h": 3, - "i": "620683e9e5e9def14ae6cc3d", - "moved": false - } - } - } - ], - "public": true - }, - { - "name": "Default Report", - "widgets": [ - { - "title": "Activities Over Time", - "type": "cubejs", - "settings": { - "chartType": "area", - "query": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "week", - "dateRange": "Last 30 days" - } - ], - "limit": 10000 - }, - "layout": { - "x": 0, - "y": 0, - "w": 6, - "h": 18, - "moved": false, - "i": "620129d6e5e9def14ae6ae9e" - } - } - }, - { - "title": "Members Over Time", - "type": "cubejs", - "settings": { - "chartType": "area", - "query": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "week", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Members.date": "asc" - } - }, - "layout": { - "x": 6, - "y": 0, - "w": 6, - "h": 18, - "moved": false, - "i": "620129d7e5e9def14ae6aec7" - } - } - }, - { - "title": "Activities By Platform", - "type": "cubejs", - "settings": { - "chartType": "pie", - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Activities.activityCount": "desc" - } - }, - "layout": { - "x": 0, - "y": 18, - "w": 6, - "h": 18, - "moved": false, - "i": "620129d7e5e9def14ae6aedd" - } - } - }, - { - "title": "Members By Platform", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Members.memberCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "month", - "dateRange": "Last year" - } - ], - "filters": [ - { - "member": "Activity.platform", - "operator": "set", - "values": ["widget"] - } - ], - "limit": 10000, - "order": { - "Members.memberCount": "desc" - } - }, - "layout": { - "x": 6, - "y": 18, - "w": 6, - "h": 18, - "moved": false, - "i": "620129d8e5e9def14ae6aef3" - } - } - } - ], - "public": true - }, - { - "name": "Default Report", - "widgets": [ - { - "title": "Activities Over Time", - "type": "cubejs", - "settings": { - "chartType": "area", - "query": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "week", - "dateRange": "Last 30 days" - } - ], - "limit": 10000 - }, - "layout": { - "x": 0, - "y": 0, - "w": 6, - "h": 18, - "moved": false, - "i": "61f26fdae5e9def14ae69152" - } - } - }, - { - "title": "Members Over Time", - "type": "cubejs", - "settings": { - "chartType": "area", - "query": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "week", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Members.date": "asc" - } - }, - "layout": { - "x": 6, - "y": 0, - "w": 6, - "h": 18, - "moved": false, - "i": "61f26fdae5e9def14ae69169" - } - } - }, - { - "title": "Activities By Platform", - "type": "cubejs", - "settings": { - "chartType": "pie", - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Activities.activityCount": "desc" - } - }, - "layout": { - "x": 0, - "y": 18, - "w": 6, - "h": 18, - "moved": false, - "i": "61f26fdbe5e9def14ae6917f" - } - } - }, - { - "title": "Members By Platform", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Members.memberCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "month", - "dateRange": "Last year" - } - ], - "filters": [ - { - "member": "Activity.platform", - "operator": "set", - "values": ["widget"] - } - ], - "limit": 10000, - "order": { - "Members.memberCount": "desc" - } - }, - "layout": { - "x": 6, - "y": 18, - "w": 6, - "h": 18, - "moved": false, - "i": "61f26fdbe5e9def14ae69195" - } - } - } - ], - "public": true - }, - { - "name": "Default Report", - "widgets": [ - { - "title": "Activities Over Time", - "type": "cubejs", - "settings": { - "chartType": "area", - "query": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "week", - "dateRange": "Last 30 days" - } - ], - "limit": 10000 - }, - "layout": { - "x": 0, - "y": 0, - "w": 6, - "h": 18, - "moved": false, - "i": "61f027c37b546e62df6988d7" - } - } - }, - { - "title": "Members Over Time", - "type": "cubejs", - "settings": { - "chartType": "area", - "query": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "week", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Members.date": "asc" - } - }, - "layout": { - "x": 6, - "y": 0, - "w": 6, - "h": 18, - "moved": false, - "i": "61f027c47b546e62df69892e" - } - } - }, - { - "title": "Activities By Platform", - "type": "cubejs", - "settings": { - "chartType": "pie", - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Activities.activityCount": "desc" - } - }, - "layout": { - "x": 0, - "y": 18, - "w": 6, - "h": 18, - "moved": false, - "i": "61f027c47b546e62df698944" - } - } - }, - { - "title": "Members By Platform", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Members.memberCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "month", - "dateRange": "Last year" - } - ], - "filters": [ - { - "member": "Activity.platform", - "operator": "set", - "values": ["widget"] - } - ], - "limit": 10000, - "order": { - "Members.memberCount": "desc" - } - }, - "layout": { - "x": 6, - "y": 18, - "w": 6, - "h": 18, - "moved": false, - "i": "61f027c47b546e62df69895a" - } - } - } - ], - "public": true - }, - { - "name": "Default Report", - "widgets": [ - { - "title": "Activities Over Time", - "type": "cubejs", - "settings": { - "chartType": "area", - "query": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "week", - "dateRange": "Last 30 days" - } - ], - "limit": 10000 - }, - "layout": { - "x": 0, - "y": 0, - "w": 6, - "h": 18, - "moved": false, - "i": "61e95094a3ca76e8a4ec8f3d" - } - } - }, - { - "title": "Members Over Time", - "type": "cubejs", - "settings": { - "chartType": "area", - "query": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "week", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Members.date": "asc" - } - }, - "layout": { - "x": 6, - "y": 0, - "w": 6, - "h": 18, - "moved": false, - "i": "61e95095a3ca76e8a4ec8f53" - } - } - }, - { - "title": "Activities By Platform", - "type": "cubejs", - "settings": { - "chartType": "pie", - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Activities.activityCount": "desc" - } - }, - "layout": { - "x": 0, - "y": 18, - "w": 6, - "h": 18, - "moved": false, - "i": "61e95096a3ca76e8a4ec8f69" - } - } - }, - { - "title": "Members By Platform", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Members.memberCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "month", - "dateRange": "Last year" - } - ], - "filters": [ - { - "member": "Activity.platform", - "operator": "set", - "values": ["widget"] - } - ], - "limit": 10000, - "order": { - "Members.memberCount": "desc" - } - }, - "layout": { - "x": 6, - "y": 18, - "w": 6, - "h": 18, - "moved": false, - "i": "61e95097a3ca76e8a4ec8f87" - } - } - } - ], - "public": true - }, - { - "name": "Default Report", - "widgets": [ - { - "title": "Activities Over Time", - "type": "cubejs", - "settings": { - "chartType": "area", - "query": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "week", - "dateRange": "Last 30 days" - } - ], - "limit": 10000 - }, - "layout": { - "x": 0, - "y": 0, - "w": 6, - "h": 18, - "moved": false, - "i": "61e802ba6309e92846bc4226" - } - } - }, - { - "title": "Members Over Time", - "type": "cubejs", - "settings": { - "chartType": "area", - "query": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "week", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Members.date": "asc" - } - }, - "layout": { - "x": 6, - "y": 0, - "w": 6, - "h": 18, - "moved": false, - "i": "61e802bb6309e92846bc4272" - } - } - }, - { - "title": "Activities By Platform", - "type": "cubejs", - "settings": { - "chartType": "pie", - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Activities.activityCount": "desc" - } - }, - "layout": { - "x": 0, - "y": 18, - "w": 6, - "h": 18, - "moved": false, - "i": "61e802bc6309e92846bc4294" - } - } - }, - { - "title": "Members By Platform", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Members.memberCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "month", - "dateRange": "Last year" - } - ], - "filters": [ - { - "member": "Activity.platform", - "operator": "set", - "values": ["widget"] - } - ], - "limit": 10000, - "order": { - "Members.memberCount": "desc" - } - }, - "layout": { - "x": 6, - "y": 18, - "w": 6, - "h": 18, - "moved": false, - "i": "61e802bd6309e92846bc42aa" - } - } - } - ], - "public": true - }, - { - "name": "Default Report", - "widgets": [], - "public": true - }, - { - "name": "Test Large", - "widgets": [ - { - "title": "Activities by channel", - "type": "cubejs", - "settings": { - "chartType": "pie", - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Activities.activityCount": "desc" - } - }, - "layout": { - "x": 0, - "y": 36, - "w": 6, - "h": 18, - "i": "61deaa949e31b489034a7505", - "moved": false - } - } - }, - { - "title": "Members by activity type", - "type": "cubejs", - "settings": { - "chartType": "bar", - "query": { - "measures": ["Members.memberCount"], - "dimensions": ["Activity.type"], - "timeDimensions": [ - { - "dimension": "Members.date", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Members.memberCount": "desc" - } - }, - "layout": { - "x": 6, - "y": 36, - "w": 6, - "h": 18, - "i": "61deaac69e31b489034a751b", - "moved": false - } - } - }, - { - "title": "Number of activities", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "month", - "dateRange": "Last year" - } - ], - "limit": 10000 - }, - "layout": { - "x": 0, - "y": 0, - "w": 12, - "h": 18, - "i": "61deaadc9e31b489034a7531", - "moved": false - } - } - }, - { - "title": "Member count", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "month", - "dateRange": "Last year" - } - ], - "limit": 10000, - "order": { - "Members.date": "asc" - } - }, - "layout": { - "x": 0, - "y": 18, - "w": 12, - "h": 18, - "i": "61deab319e31b489034a7633", - "moved": false - } - } - }, - { - "title": "Activities by type", - "type": "cubejs", - "settings": { - "chartType": "pie", - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Activity.type"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": "Last year" - } - ], - "limit": 10000, - "order": { - "Activities.activityCount": "desc" - } - }, - "layout": { - "x": 0, - "y": 54, - "w": 6, - "h": 18, - "i": "61deab7c9e31b489034a7811", - "moved": false - } - } - }, - { - "title": "Members by Location", - "type": "cubejs", - "settings": { - "chartType": "pie", - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Member.location"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Activities.activityCount": "desc" - } - }, - "layout": { - "x": 6, - "y": 54, - "w": 6, - "h": 18, - "i": "61deab989e31b489034a7827", - "moved": false - } - } - } - ], - "public": true - }, - { - "name": "Testing something", - "widgets": [ - { - "title": "Untitled", - "type": "cubejs", - "settings": { - "chartType": "area", - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "day", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Activities.activityCount": "desc" - } - }, - "layout": { - "x": 0, - "y": 0, - "w": 6, - "h": 18, - "i": "61dd76079280a23f180dc1cd", - "moved": false - } - } - }, - { - "title": "Untitled", - "type": "cubejs", - "settings": { - "chartType": "table", - "query": { - "measures": ["Members.memberCount"], - "dimensions": ["Member.location"], - "timeDimensions": [ - { - "dimension": "Members.date", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Members.memberCount": "desc" - } - }, - "layout": { - "x": 6, - "y": 0, - "w": 6, - "h": 18, - "i": "61ddc4219280a23f180ddfc8", - "moved": false - } - } - }, - { - "title": "Untitled [Copy]", - "type": "cubejs", - "settings": { - "chartType": "table", - "query": { - "measures": ["Members.memberCount"], - "dimensions": ["Member.score"], - "timeDimensions": [ - { - "dimension": "Members.date", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Members.memberCount": "desc" - } - }, - "layout": { - "x": 6, - "y": 18, - "w": 6, - "h": 22, - "i": "61e6ee786309e92846bc3c73", - "moved": false - } - } - } - ], - "public": true - }, - { - "name": "Hey", - "widgets": [ - { - "title": "hh", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Members.memberCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "week", - "dateRange": "Last year" - } - ], - "limit": 10000, - "order": { - "Members.memberCount": "desc" - } - }, - "layout": { - "x": 0, - "y": 0, - "w": 12, - "h": 2, - "i": "61dd490d89d0f0f8ff67284f", - "moved": false - }, - "resultSet": { - "loadResponse": { - "queryType": "regularQuery", - "results": [ - { - "query": { - "measures": ["MemberActivitiesFlat.memberCount"], - "dimensions": ["MemberActivitiesFlat.activity_platform"], - "timeDimensions": [ - { - "dimension": "MemberActivitiesFlat.joinedAt", - "granularity": "week", - "dateRange": ["2021-01-01T00:00:00.000", "2021-12-31T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Members.memberCount", - "desc": true - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "MemberActivitiesFlat.tenant", - "operator": "equals", - "values": ["6138a1bfceb2640da4c65509"] - } - ], - "queryType": "regularQuery" - }, - "data": [ - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2020-12-28T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2020-12-28T00:00:00.000", - "MemberActivitiesFlat.memberCount": "316" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-01-04T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-01-04T00:00:00.000", - "MemberActivitiesFlat.memberCount": "322" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-01-11T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-01-11T00:00:00.000", - "MemberActivitiesFlat.memberCount": "330" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-01-18T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-01-18T00:00:00.000", - "MemberActivitiesFlat.memberCount": "339" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-01-25T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-01-25T00:00:00.000", - "MemberActivitiesFlat.memberCount": "346" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-02-01T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-02-01T00:00:00.000", - "MemberActivitiesFlat.memberCount": "356" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-02-08T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-02-08T00:00:00.000", - "MemberActivitiesFlat.memberCount": "415" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-02-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-02-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "482" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-02-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-02-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "509" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-03-01T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-01T00:00:00.000", - "MemberActivitiesFlat.memberCount": "529" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-03-08T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-08T00:00:00.000", - "MemberActivitiesFlat.memberCount": "538" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-03-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "542" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-03-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "550" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-03-29T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-29T00:00:00.000", - "MemberActivitiesFlat.memberCount": "557" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-04-05T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-05T00:00:00.000", - "MemberActivitiesFlat.memberCount": "571" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-04-12T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-12T00:00:00.000", - "MemberActivitiesFlat.memberCount": "578" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-04-19T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-19T00:00:00.000", - "MemberActivitiesFlat.memberCount": "582" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-04-26T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-26T00:00:00.000", - "MemberActivitiesFlat.memberCount": "593" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-05-03T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-03T00:00:00.000", - "MemberActivitiesFlat.memberCount": "598" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-05-10T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-10T00:00:00.000", - "MemberActivitiesFlat.memberCount": "603" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-05-17T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-17T00:00:00.000", - "MemberActivitiesFlat.memberCount": "609" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-05-24T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-24T00:00:00.000", - "MemberActivitiesFlat.memberCount": "613" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-05-31T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-31T00:00:00.000", - "MemberActivitiesFlat.memberCount": "624" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-06-07T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-07T00:00:00.000", - "MemberActivitiesFlat.memberCount": "635" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-06-14T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-14T00:00:00.000", - "MemberActivitiesFlat.memberCount": "639" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-06-21T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-21T00:00:00.000", - "MemberActivitiesFlat.memberCount": "650" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-06-28T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-28T00:00:00.000", - "MemberActivitiesFlat.memberCount": "658" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-07-05T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-05T00:00:00.000", - "MemberActivitiesFlat.memberCount": "665" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-07-12T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-12T00:00:00.000", - "MemberActivitiesFlat.memberCount": "670" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-07-19T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-19T00:00:00.000", - "MemberActivitiesFlat.memberCount": "685" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-07-26T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-26T00:00:00.000", - "MemberActivitiesFlat.memberCount": "696" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-08-02T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-02T00:00:00.000", - "MemberActivitiesFlat.memberCount": "709" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-08-09T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-09T00:00:00.000", - "MemberActivitiesFlat.memberCount": "715" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-08-16T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-16T00:00:00.000", - "MemberActivitiesFlat.memberCount": "721" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-08-23T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-23T00:00:00.000", - "MemberActivitiesFlat.memberCount": "738" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-08-30T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-30T00:00:00.000", - "MemberActivitiesFlat.memberCount": "743" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-09-06T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-06T00:00:00.000", - "MemberActivitiesFlat.memberCount": "759" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-09-13T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-13T00:00:00.000", - "MemberActivitiesFlat.memberCount": "774" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-09-20T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-20T00:00:00.000", - "MemberActivitiesFlat.memberCount": "786" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-09-27T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-27T00:00:00.000", - "MemberActivitiesFlat.memberCount": "796" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-10-04T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-04T00:00:00.000", - "MemberActivitiesFlat.memberCount": "800" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-10-11T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-11T00:00:00.000", - "MemberActivitiesFlat.memberCount": "808" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-10-18T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-18T00:00:00.000", - "MemberActivitiesFlat.memberCount": "852" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-10-25T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-25T00:00:00.000", - "MemberActivitiesFlat.memberCount": "862" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-11-01T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-01T00:00:00.000", - "MemberActivitiesFlat.memberCount": "875" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-11-08T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-08T00:00:00.000", - "MemberActivitiesFlat.memberCount": "920" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-11-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "935" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-11-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "954" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-11-29T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-29T00:00:00.000", - "MemberActivitiesFlat.memberCount": "969" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-12-06T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-06T00:00:00.000", - "MemberActivitiesFlat.memberCount": "979" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-12-13T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-13T00:00:00.000", - "MemberActivitiesFlat.memberCount": "988" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-12-20T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-20T00:00:00.000", - "MemberActivitiesFlat.memberCount": "994" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-12-27T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-27T00:00:00.000", - "MemberActivitiesFlat.memberCount": "1006" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-07-12T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-12T00:00:00.000", - "MemberActivitiesFlat.memberCount": "5" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-07-19T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-19T00:00:00.000", - "MemberActivitiesFlat.memberCount": "5" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-07-26T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-26T00:00:00.000", - "MemberActivitiesFlat.memberCount": "5" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-08-02T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-02T00:00:00.000", - "MemberActivitiesFlat.memberCount": "6" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-08-09T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-09T00:00:00.000", - "MemberActivitiesFlat.memberCount": "6" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-08-16T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-16T00:00:00.000", - "MemberActivitiesFlat.memberCount": "6" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-08-23T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-23T00:00:00.000", - "MemberActivitiesFlat.memberCount": "6" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-08-30T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-30T00:00:00.000", - "MemberActivitiesFlat.memberCount": "43" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-09-06T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-06T00:00:00.000", - "MemberActivitiesFlat.memberCount": "43" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-09-13T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-13T00:00:00.000", - "MemberActivitiesFlat.memberCount": "43" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-09-20T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-20T00:00:00.000", - "MemberActivitiesFlat.memberCount": "43" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-09-27T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-27T00:00:00.000", - "MemberActivitiesFlat.memberCount": "43" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-10-04T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-04T00:00:00.000", - "MemberActivitiesFlat.memberCount": "44" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-10-11T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-11T00:00:00.000", - "MemberActivitiesFlat.memberCount": "44" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-10-18T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-18T00:00:00.000", - "MemberActivitiesFlat.memberCount": "44" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-10-25T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-25T00:00:00.000", - "MemberActivitiesFlat.memberCount": "100" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-11-01T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-01T00:00:00.000", - "MemberActivitiesFlat.memberCount": "100" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-11-08T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-08T00:00:00.000", - "MemberActivitiesFlat.memberCount": "100" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-11-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "100" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-11-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "100" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-11-29T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-29T00:00:00.000", - "MemberActivitiesFlat.memberCount": "102" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-12-06T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-06T00:00:00.000", - "MemberActivitiesFlat.memberCount": "102" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-12-13T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-13T00:00:00.000", - "MemberActivitiesFlat.memberCount": "106" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-12-20T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-20T00:00:00.000", - "MemberActivitiesFlat.memberCount": "106" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-12-27T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-27T00:00:00.000", - "MemberActivitiesFlat.memberCount": "106" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-02-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-02-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-02-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-02-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-03-01T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-01T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-03-08T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-08T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-03-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-03-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-03-29T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-29T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-04-05T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-05T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-04-12T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-12T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-04-19T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-19T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-04-26T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-26T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-05-03T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-03T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-05-10T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-10T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-05-17T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-17T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-05-24T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-24T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-05-31T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-31T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-06-07T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-07T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-06-14T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-14T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-06-21T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-21T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-06-28T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-28T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-07-05T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-05T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-07-12T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-12T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-07-19T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-19T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-07-26T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-26T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-08-02T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-02T00:00:00.000", - "MemberActivitiesFlat.memberCount": "123" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-08-09T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-09T00:00:00.000", - "MemberActivitiesFlat.memberCount": "123" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-08-16T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-16T00:00:00.000", - "MemberActivitiesFlat.memberCount": "123" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-08-23T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-23T00:00:00.000", - "MemberActivitiesFlat.memberCount": "123" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-08-30T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-30T00:00:00.000", - "MemberActivitiesFlat.memberCount": "123" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-09-06T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-06T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-09-13T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-13T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-09-20T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-20T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-09-27T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-27T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-10-04T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-04T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-10-11T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-11T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-10-18T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-18T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-10-25T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-25T00:00:00.000", - "MemberActivitiesFlat.memberCount": "829" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-11-01T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-01T00:00:00.000", - "MemberActivitiesFlat.memberCount": "855" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-11-08T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-08T00:00:00.000", - "MemberActivitiesFlat.memberCount": "855" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-11-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "855" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-11-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "880" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-11-29T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-29T00:00:00.000", - "MemberActivitiesFlat.memberCount": "880" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-12-06T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-06T00:00:00.000", - "MemberActivitiesFlat.memberCount": "880" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-12-13T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-13T00:00:00.000", - "MemberActivitiesFlat.memberCount": "880" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-12-20T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-20T00:00:00.000", - "MemberActivitiesFlat.memberCount": "880" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-12-27T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-27T00:00:00.000", - "MemberActivitiesFlat.memberCount": "880" - } - ], - "annotation": { - "measures": { - "MemberActivitiesFlat.memberCount": { - "title": "Members Member Count", - "shortTitle": "Member Count", - "type": "number", - "drillMembers": [], - "drillMembersGrouped": { - "measures": [], - "dimensions": [] - } - } - }, - "dimensions": { - "MemberActivitiesFlat.activity_platform": { - "title": "Members Activity Platform", - "shortTitle": "Activity Platform", - "type": "string" - } - }, - "segments": {}, - "timeDimensions": { - "MemberActivitiesFlat.joinedAt.week": { - "title": "Members Joined at", - "shortTitle": "Joined at", - "type": "time" - }, - "MemberActivitiesFlat.joinedAt": { - "title": "Members Joined at", - "shortTitle": "Joined at", - "type": "time" - } - } - }, - "dataSource": "default", - "dbType": "mongobi", - "extDbType": "cubestore", - "external": true, - "slowQuery": false - } - ], - "pivotQuery": { - "measures": ["MemberActivitiesFlat.memberCount"], - "dimensions": ["MemberActivitiesFlat.activity_platform"], - "timeDimensions": [ - { - "dimension": "MemberActivitiesFlat.joinedAt", - "granularity": "week", - "dateRange": ["2021-01-01T00:00:00.000", "2021-12-31T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Members.memberCount", - "desc": true - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "MemberActivitiesFlat.tenant", - "operator": "equals", - "values": ["6138a1bfceb2640da4c65509"] - } - ], - "queryType": "regularQuery" - }, - "slowQuery": false - }, - "queryType": "regularQuery", - "loadResponses": [ - { - "query": { - "measures": ["MemberActivitiesFlat.memberCount"], - "dimensions": ["MemberActivitiesFlat.activity_platform"], - "timeDimensions": [ - { - "dimension": "MemberActivitiesFlat.joinedAt", - "granularity": "week", - "dateRange": ["2021-01-01T00:00:00.000", "2021-12-31T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Members.memberCount", - "desc": true - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "MemberActivitiesFlat.tenant", - "operator": "equals", - "values": ["6138a1bfceb2640da4c65509"] - } - ], - "queryType": "regularQuery" - }, - "data": [ - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2020-12-28T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2020-12-28T00:00:00.000", - "MemberActivitiesFlat.memberCount": "316" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-01-04T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-01-04T00:00:00.000", - "MemberActivitiesFlat.memberCount": "322" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-01-11T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-01-11T00:00:00.000", - "MemberActivitiesFlat.memberCount": "330" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-01-18T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-01-18T00:00:00.000", - "MemberActivitiesFlat.memberCount": "339" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-01-25T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-01-25T00:00:00.000", - "MemberActivitiesFlat.memberCount": "346" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-02-01T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-02-01T00:00:00.000", - "MemberActivitiesFlat.memberCount": "356" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-02-08T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-02-08T00:00:00.000", - "MemberActivitiesFlat.memberCount": "415" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-02-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-02-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "482" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-02-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-02-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "509" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-03-01T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-01T00:00:00.000", - "MemberActivitiesFlat.memberCount": "529" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-03-08T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-08T00:00:00.000", - "MemberActivitiesFlat.memberCount": "538" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-03-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "542" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-03-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "550" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-03-29T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-29T00:00:00.000", - "MemberActivitiesFlat.memberCount": "557" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-04-05T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-05T00:00:00.000", - "MemberActivitiesFlat.memberCount": "571" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-04-12T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-12T00:00:00.000", - "MemberActivitiesFlat.memberCount": "578" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-04-19T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-19T00:00:00.000", - "MemberActivitiesFlat.memberCount": "582" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-04-26T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-26T00:00:00.000", - "MemberActivitiesFlat.memberCount": "593" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-05-03T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-03T00:00:00.000", - "MemberActivitiesFlat.memberCount": "598" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-05-10T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-10T00:00:00.000", - "MemberActivitiesFlat.memberCount": "603" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-05-17T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-17T00:00:00.000", - "MemberActivitiesFlat.memberCount": "609" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-05-24T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-24T00:00:00.000", - "MemberActivitiesFlat.memberCount": "613" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-05-31T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-31T00:00:00.000", - "MemberActivitiesFlat.memberCount": "624" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-06-07T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-07T00:00:00.000", - "MemberActivitiesFlat.memberCount": "635" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-06-14T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-14T00:00:00.000", - "MemberActivitiesFlat.memberCount": "639" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-06-21T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-21T00:00:00.000", - "MemberActivitiesFlat.memberCount": "650" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-06-28T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-28T00:00:00.000", - "MemberActivitiesFlat.memberCount": "658" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-07-05T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-05T00:00:00.000", - "MemberActivitiesFlat.memberCount": "665" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-07-12T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-12T00:00:00.000", - "MemberActivitiesFlat.memberCount": "670" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-07-19T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-19T00:00:00.000", - "MemberActivitiesFlat.memberCount": "685" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-07-26T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-26T00:00:00.000", - "MemberActivitiesFlat.memberCount": "696" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-08-02T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-02T00:00:00.000", - "MemberActivitiesFlat.memberCount": "709" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-08-09T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-09T00:00:00.000", - "MemberActivitiesFlat.memberCount": "715" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-08-16T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-16T00:00:00.000", - "MemberActivitiesFlat.memberCount": "721" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-08-23T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-23T00:00:00.000", - "MemberActivitiesFlat.memberCount": "738" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-08-30T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-30T00:00:00.000", - "MemberActivitiesFlat.memberCount": "743" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-09-06T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-06T00:00:00.000", - "MemberActivitiesFlat.memberCount": "759" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-09-13T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-13T00:00:00.000", - "MemberActivitiesFlat.memberCount": "774" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-09-20T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-20T00:00:00.000", - "MemberActivitiesFlat.memberCount": "786" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-09-27T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-27T00:00:00.000", - "MemberActivitiesFlat.memberCount": "796" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-10-04T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-04T00:00:00.000", - "MemberActivitiesFlat.memberCount": "800" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-10-11T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-11T00:00:00.000", - "MemberActivitiesFlat.memberCount": "808" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-10-18T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-18T00:00:00.000", - "MemberActivitiesFlat.memberCount": "852" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-10-25T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-25T00:00:00.000", - "MemberActivitiesFlat.memberCount": "862" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-11-01T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-01T00:00:00.000", - "MemberActivitiesFlat.memberCount": "875" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-11-08T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-08T00:00:00.000", - "MemberActivitiesFlat.memberCount": "920" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-11-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "935" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-11-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "954" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-11-29T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-29T00:00:00.000", - "MemberActivitiesFlat.memberCount": "969" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-12-06T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-06T00:00:00.000", - "MemberActivitiesFlat.memberCount": "979" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-12-13T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-13T00:00:00.000", - "MemberActivitiesFlat.memberCount": "988" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-12-20T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-20T00:00:00.000", - "MemberActivitiesFlat.memberCount": "994" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-12-27T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-27T00:00:00.000", - "MemberActivitiesFlat.memberCount": "1006" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-07-12T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-12T00:00:00.000", - "MemberActivitiesFlat.memberCount": "5" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-07-19T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-19T00:00:00.000", - "MemberActivitiesFlat.memberCount": "5" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-07-26T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-26T00:00:00.000", - "MemberActivitiesFlat.memberCount": "5" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-08-02T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-02T00:00:00.000", - "MemberActivitiesFlat.memberCount": "6" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-08-09T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-09T00:00:00.000", - "MemberActivitiesFlat.memberCount": "6" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-08-16T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-16T00:00:00.000", - "MemberActivitiesFlat.memberCount": "6" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-08-23T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-23T00:00:00.000", - "MemberActivitiesFlat.memberCount": "6" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-08-30T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-30T00:00:00.000", - "MemberActivitiesFlat.memberCount": "43" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-09-06T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-06T00:00:00.000", - "MemberActivitiesFlat.memberCount": "43" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-09-13T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-13T00:00:00.000", - "MemberActivitiesFlat.memberCount": "43" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-09-20T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-20T00:00:00.000", - "MemberActivitiesFlat.memberCount": "43" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-09-27T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-27T00:00:00.000", - "MemberActivitiesFlat.memberCount": "43" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-10-04T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-04T00:00:00.000", - "MemberActivitiesFlat.memberCount": "44" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-10-11T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-11T00:00:00.000", - "MemberActivitiesFlat.memberCount": "44" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-10-18T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-18T00:00:00.000", - "MemberActivitiesFlat.memberCount": "44" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-10-25T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-25T00:00:00.000", - "MemberActivitiesFlat.memberCount": "100" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-11-01T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-01T00:00:00.000", - "MemberActivitiesFlat.memberCount": "100" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-11-08T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-08T00:00:00.000", - "MemberActivitiesFlat.memberCount": "100" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-11-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "100" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-11-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "100" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-11-29T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-29T00:00:00.000", - "MemberActivitiesFlat.memberCount": "102" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-12-06T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-06T00:00:00.000", - "MemberActivitiesFlat.memberCount": "102" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-12-13T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-13T00:00:00.000", - "MemberActivitiesFlat.memberCount": "106" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-12-20T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-20T00:00:00.000", - "MemberActivitiesFlat.memberCount": "106" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-12-27T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-27T00:00:00.000", - "MemberActivitiesFlat.memberCount": "106" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-02-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-02-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-02-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-02-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-03-01T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-01T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-03-08T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-08T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-03-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-03-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-03-29T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-29T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-04-05T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-05T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-04-12T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-12T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-04-19T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-19T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-04-26T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-26T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-05-03T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-03T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-05-10T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-10T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-05-17T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-17T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-05-24T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-24T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-05-31T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-31T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-06-07T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-07T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-06-14T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-14T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-06-21T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-21T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-06-28T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-28T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-07-05T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-05T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-07-12T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-12T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-07-19T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-19T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-07-26T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-26T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-08-02T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-02T00:00:00.000", - "MemberActivitiesFlat.memberCount": "123" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-08-09T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-09T00:00:00.000", - "MemberActivitiesFlat.memberCount": "123" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-08-16T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-16T00:00:00.000", - "MemberActivitiesFlat.memberCount": "123" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-08-23T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-23T00:00:00.000", - "MemberActivitiesFlat.memberCount": "123" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-08-30T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-30T00:00:00.000", - "MemberActivitiesFlat.memberCount": "123" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-09-06T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-06T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-09-13T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-13T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-09-20T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-20T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-09-27T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-27T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-10-04T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-04T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-10-11T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-11T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-10-18T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-18T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-10-25T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-25T00:00:00.000", - "MemberActivitiesFlat.memberCount": "829" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-11-01T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-01T00:00:00.000", - "MemberActivitiesFlat.memberCount": "855" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-11-08T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-08T00:00:00.000", - "MemberActivitiesFlat.memberCount": "855" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-11-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "855" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-11-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "880" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-11-29T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-29T00:00:00.000", - "MemberActivitiesFlat.memberCount": "880" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-12-06T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-06T00:00:00.000", - "MemberActivitiesFlat.memberCount": "880" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-12-13T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-13T00:00:00.000", - "MemberActivitiesFlat.memberCount": "880" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-12-20T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-20T00:00:00.000", - "MemberActivitiesFlat.memberCount": "880" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-12-27T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-27T00:00:00.000", - "MemberActivitiesFlat.memberCount": "880" - } - ], - "annotation": { - "measures": { - "MemberActivitiesFlat.memberCount": { - "title": "Members Member Count", - "shortTitle": "Member Count", - "type": "number", - "drillMembers": [], - "drillMembersGrouped": { - "measures": [], - "dimensions": [] - } - } - }, - "dimensions": { - "MemberActivitiesFlat.activity_platform": { - "title": "Members Activity Platform", - "shortTitle": "Activity Platform", - "type": "string" - } - }, - "segments": {}, - "timeDimensions": { - "MemberActivitiesFlat.joinedAt.week": { - "title": "Members Joined at", - "shortTitle": "Joined at", - "type": "time" - }, - "MemberActivitiesFlat.joinedAt": { - "title": "Members Joined at", - "shortTitle": "Joined at", - "type": "time" - } - } - }, - "dataSource": "default", - "dbType": "mongobi", - "extDbType": "cubestore", - "external": true, - "slowQuery": false - } - ], - "options": {}, - "backwardCompatibleData": [ - [ - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2020-12-28T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2020-12-28T00:00:00.000", - "MemberActivitiesFlat.memberCount": "316" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-01-04T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-01-04T00:00:00.000", - "MemberActivitiesFlat.memberCount": "322" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-01-11T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-01-11T00:00:00.000", - "MemberActivitiesFlat.memberCount": "330" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-01-18T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-01-18T00:00:00.000", - "MemberActivitiesFlat.memberCount": "339" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-01-25T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-01-25T00:00:00.000", - "MemberActivitiesFlat.memberCount": "346" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-02-01T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-02-01T00:00:00.000", - "MemberActivitiesFlat.memberCount": "356" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-02-08T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-02-08T00:00:00.000", - "MemberActivitiesFlat.memberCount": "415" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-02-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-02-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "482" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-02-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-02-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "509" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-03-01T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-01T00:00:00.000", - "MemberActivitiesFlat.memberCount": "529" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-03-08T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-08T00:00:00.000", - "MemberActivitiesFlat.memberCount": "538" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-03-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "542" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-03-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "550" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-03-29T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-29T00:00:00.000", - "MemberActivitiesFlat.memberCount": "557" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-04-05T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-05T00:00:00.000", - "MemberActivitiesFlat.memberCount": "571" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-04-12T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-12T00:00:00.000", - "MemberActivitiesFlat.memberCount": "578" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-04-19T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-19T00:00:00.000", - "MemberActivitiesFlat.memberCount": "582" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-04-26T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-26T00:00:00.000", - "MemberActivitiesFlat.memberCount": "593" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-05-03T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-03T00:00:00.000", - "MemberActivitiesFlat.memberCount": "598" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-05-10T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-10T00:00:00.000", - "MemberActivitiesFlat.memberCount": "603" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-05-17T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-17T00:00:00.000", - "MemberActivitiesFlat.memberCount": "609" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-05-24T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-24T00:00:00.000", - "MemberActivitiesFlat.memberCount": "613" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-05-31T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-31T00:00:00.000", - "MemberActivitiesFlat.memberCount": "624" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-06-07T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-07T00:00:00.000", - "MemberActivitiesFlat.memberCount": "635" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-06-14T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-14T00:00:00.000", - "MemberActivitiesFlat.memberCount": "639" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-06-21T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-21T00:00:00.000", - "MemberActivitiesFlat.memberCount": "650" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-06-28T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-28T00:00:00.000", - "MemberActivitiesFlat.memberCount": "658" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-07-05T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-05T00:00:00.000", - "MemberActivitiesFlat.memberCount": "665" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-07-12T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-12T00:00:00.000", - "MemberActivitiesFlat.memberCount": "670" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-07-19T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-19T00:00:00.000", - "MemberActivitiesFlat.memberCount": "685" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-07-26T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-26T00:00:00.000", - "MemberActivitiesFlat.memberCount": "696" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-08-02T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-02T00:00:00.000", - "MemberActivitiesFlat.memberCount": "709" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-08-09T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-09T00:00:00.000", - "MemberActivitiesFlat.memberCount": "715" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-08-16T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-16T00:00:00.000", - "MemberActivitiesFlat.memberCount": "721" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-08-23T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-23T00:00:00.000", - "MemberActivitiesFlat.memberCount": "738" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-08-30T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-30T00:00:00.000", - "MemberActivitiesFlat.memberCount": "743" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-09-06T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-06T00:00:00.000", - "MemberActivitiesFlat.memberCount": "759" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-09-13T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-13T00:00:00.000", - "MemberActivitiesFlat.memberCount": "774" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-09-20T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-20T00:00:00.000", - "MemberActivitiesFlat.memberCount": "786" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-09-27T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-27T00:00:00.000", - "MemberActivitiesFlat.memberCount": "796" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-10-04T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-04T00:00:00.000", - "MemberActivitiesFlat.memberCount": "800" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-10-11T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-11T00:00:00.000", - "MemberActivitiesFlat.memberCount": "808" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-10-18T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-18T00:00:00.000", - "MemberActivitiesFlat.memberCount": "852" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-10-25T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-25T00:00:00.000", - "MemberActivitiesFlat.memberCount": "862" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-11-01T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-01T00:00:00.000", - "MemberActivitiesFlat.memberCount": "875" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-11-08T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-08T00:00:00.000", - "MemberActivitiesFlat.memberCount": "920" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-11-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "935" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-11-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "954" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-11-29T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-29T00:00:00.000", - "MemberActivitiesFlat.memberCount": "969" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-12-06T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-06T00:00:00.000", - "MemberActivitiesFlat.memberCount": "979" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-12-13T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-13T00:00:00.000", - "MemberActivitiesFlat.memberCount": "988" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-12-20T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-20T00:00:00.000", - "MemberActivitiesFlat.memberCount": "994" - }, - { - "MemberActivitiesFlat.activity_platform": null, - "MemberActivitiesFlat.joinedAt.week": "2021-12-27T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-27T00:00:00.000", - "MemberActivitiesFlat.memberCount": "1006" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-07-12T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-12T00:00:00.000", - "MemberActivitiesFlat.memberCount": "5" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-07-19T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-19T00:00:00.000", - "MemberActivitiesFlat.memberCount": "5" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-07-26T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-26T00:00:00.000", - "MemberActivitiesFlat.memberCount": "5" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-08-02T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-02T00:00:00.000", - "MemberActivitiesFlat.memberCount": "6" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-08-09T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-09T00:00:00.000", - "MemberActivitiesFlat.memberCount": "6" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-08-16T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-16T00:00:00.000", - "MemberActivitiesFlat.memberCount": "6" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-08-23T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-23T00:00:00.000", - "MemberActivitiesFlat.memberCount": "6" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-08-30T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-30T00:00:00.000", - "MemberActivitiesFlat.memberCount": "43" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-09-06T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-06T00:00:00.000", - "MemberActivitiesFlat.memberCount": "43" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-09-13T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-13T00:00:00.000", - "MemberActivitiesFlat.memberCount": "43" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-09-20T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-20T00:00:00.000", - "MemberActivitiesFlat.memberCount": "43" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-09-27T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-27T00:00:00.000", - "MemberActivitiesFlat.memberCount": "43" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-10-04T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-04T00:00:00.000", - "MemberActivitiesFlat.memberCount": "44" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-10-11T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-11T00:00:00.000", - "MemberActivitiesFlat.memberCount": "44" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-10-18T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-18T00:00:00.000", - "MemberActivitiesFlat.memberCount": "44" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-10-25T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-25T00:00:00.000", - "MemberActivitiesFlat.memberCount": "100" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-11-01T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-01T00:00:00.000", - "MemberActivitiesFlat.memberCount": "100" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-11-08T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-08T00:00:00.000", - "MemberActivitiesFlat.memberCount": "100" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-11-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "100" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-11-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "100" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-11-29T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-29T00:00:00.000", - "MemberActivitiesFlat.memberCount": "102" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-12-06T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-06T00:00:00.000", - "MemberActivitiesFlat.memberCount": "102" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-12-13T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-13T00:00:00.000", - "MemberActivitiesFlat.memberCount": "106" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-12-20T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-20T00:00:00.000", - "MemberActivitiesFlat.memberCount": "106" - }, - { - "MemberActivitiesFlat.activity_platform": "github", - "MemberActivitiesFlat.joinedAt.week": "2021-12-27T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-27T00:00:00.000", - "MemberActivitiesFlat.memberCount": "106" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-02-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-02-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-02-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-02-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-03-01T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-01T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-03-08T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-08T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-03-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-03-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-03-29T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-03-29T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-04-05T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-05T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-04-12T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-12T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-04-19T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-19T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-04-26T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-04-26T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-05-03T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-03T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-05-10T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-10T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-05-17T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-17T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-05-24T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-24T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-05-31T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-05-31T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-06-07T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-07T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-06-14T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-14T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-06-21T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-21T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-06-28T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-06-28T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-07-05T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-05T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-07-12T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-12T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-07-19T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-19T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-07-26T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-07-26T00:00:00.000", - "MemberActivitiesFlat.memberCount": "110" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-08-02T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-02T00:00:00.000", - "MemberActivitiesFlat.memberCount": "123" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-08-09T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-09T00:00:00.000", - "MemberActivitiesFlat.memberCount": "123" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-08-16T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-16T00:00:00.000", - "MemberActivitiesFlat.memberCount": "123" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-08-23T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-23T00:00:00.000", - "MemberActivitiesFlat.memberCount": "123" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-08-30T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-08-30T00:00:00.000", - "MemberActivitiesFlat.memberCount": "123" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-09-06T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-06T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-09-13T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-13T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-09-20T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-20T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-09-27T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-09-27T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-10-04T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-04T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-10-11T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-11T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-10-18T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-18T00:00:00.000", - "MemberActivitiesFlat.memberCount": "424" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-10-25T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-10-25T00:00:00.000", - "MemberActivitiesFlat.memberCount": "829" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-11-01T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-01T00:00:00.000", - "MemberActivitiesFlat.memberCount": "855" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-11-08T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-08T00:00:00.000", - "MemberActivitiesFlat.memberCount": "855" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-11-15T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-15T00:00:00.000", - "MemberActivitiesFlat.memberCount": "855" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-11-22T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-22T00:00:00.000", - "MemberActivitiesFlat.memberCount": "880" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-11-29T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-11-29T00:00:00.000", - "MemberActivitiesFlat.memberCount": "880" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-12-06T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-06T00:00:00.000", - "MemberActivitiesFlat.memberCount": "880" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-12-13T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-13T00:00:00.000", - "MemberActivitiesFlat.memberCount": "880" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-12-20T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-20T00:00:00.000", - "MemberActivitiesFlat.memberCount": "880" - }, - { - "MemberActivitiesFlat.activity_platform": "slack", - "MemberActivitiesFlat.joinedAt.week": "2021-12-27T00:00:00.000", - "MemberActivitiesFlat.joinedAt": "2021-12-27T00:00:00.000", - "MemberActivitiesFlat.memberCount": "880" - } - ] - ] - } - } - }, - { - "title": "AA", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Activity.type"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "day", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Activities.activityCount": "desc" - } - }, - "layout": { - "x": 6, - "y": 13, - "w": 6, - "h": 2, - "i": "61dd4a2c89d0f0f8ff6728a7" - } - } - } - ], - "public": false - }, - { - "name": "Crowd.dev", - "widgets": [ - { - "title": "Members by score", - "type": "cubejs", - "settings": { - "chartType": "pie", - "query": { - "measures": ["Members.memberCount"], - "dimensions": ["Member.score"], - "timeDimensions": [ - { - "dimension": "Members.date", - "dateRange": "Last year" - } - ], - "limit": 10000, - "order": { - "Members.memberCount": "desc" - } - }, - "layout": { - "x": 6, - "y": 0, - "w": 6, - "h": 18, - "i": "61dc0c1077f04453f1ca4b16", - "moved": false - } - } - }, - { - "title": "Total members", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "week", - "dateRange": "Last year" - } - ], - "limit": 10000, - "order": { - "Members.date": "asc" - } - }, - "layout": { - "x": 6, - "y": 18, - "w": 6, - "h": 18, - "i": "61dc0c4b77f04453f1ca4b2c", - "moved": false - } - } - }, - { - "title": "Active / Inactive members", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Members.memberCount"], - "dimensions": ["Members.activity"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "week", - "dateRange": "Last year" - } - ], - "limit": 10000, - "order": { - "Members.memberCount": "desc" - } - }, - "layout": { - "x": 0, - "y": 0, - "w": 6, - "h": 18, - "i": "61dc0c7d77f04453f1ca4b42", - "moved": false - } - } - }, - { - "title": "Activities by source", - "type": "cubejs", - "settings": { - "chartType": "pie", - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": "Last year" - } - ], - "limit": 10000, - "order": { - "Activities.activityCount": "desc" - } - }, - "layout": { - "x": 0, - "y": 18, - "w": 6, - "h": 18, - "i": "61dc0d2877f04453f1ca4b83", - "moved": false - } - } - }, - { - "title": "Activities by type over time", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Activity.type"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "week", - "dateRange": "This quarter" - } - ], - "limit": 10000, - "order": { - "Activities.activityCount": "desc" - } - }, - "layout": { - "x": 0, - "y": 36, - "w": 6, - "h": 18, - "i": "61dc0d7e77f04453f1ca4b99", - "moved": false - } - } - }, - { - "title": "Activities by type (pie)", - "type": "cubejs", - "settings": { - "chartType": "pie", - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Activity.type"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Activities.activityCount": "desc" - } - }, - "layout": { - "x": 6, - "y": 36, - "w": 6, - "h": 2, - "i": "61dc0d9d77f04453f1ca4bbf", - "moved": false - } - } - }, - { - "title": "Total activities", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "week", - "dateRange": "Last year" - } - ], - "limit": 10000 - }, - "layout": { - "x": 0, - "y": 54, - "w": 6, - "h": 18, - "i": "61dc0e1677f04453f1ca4de4", - "moved": false - } - } - } - ], - "public": true - }, - { - "name": "Test Report Jan 8", - "widgets": [ - { - "title": "Total Activities", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "week", - "dateRange": "Last 30 days" - } - ], - "limit": 10000 - }, - "layout": { - "x": 0, - "y": 12, - "w": 6, - "h": 2, - "i": "61d9e9283615d09e4c6e1d3a" - } - } - }, - { - "title": "Activities by Platform", - "type": "cubejs", - "settings": { - "chartType": "pie", - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Activities.activityCount": "desc" - } - }, - "layout": { - "x": 6, - "y": 13, - "w": 6, - "h": 2, - "i": "61d9e93b3615d09e4c6e1d52" - } - } - }, - { - "title": null, - "type": "cubejs", - "settings": { - "chartType": "bar", - "query": { - "measures": ["Members.memberCount"], - "dimensions": ["Member.score"], - "timeDimensions": [ - { - "dimension": "Members.date", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Members.memberCount": "desc" - } - }, - "layout": { - "x": 0, - "y": 2, - "w": 6, - "h": 2, - "i": "61d9e9903615d09e4c6e1d6a", - "moved": false - }, - "resultSet": { - "loadResponse": { - "queryType": "regularQuery", - "results": [ - { - "query": { - "measures": ["Members.memberCount"], - "dimensions": ["Member.score"], - "timeDimensions": [ - { - "dimension": "Members.date", - "dateRange": ["2021-12-09T00:00:00.000", "2022-01-07T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Members.memberCount", - "desc": true - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "Member.tenant", - "operator": "equals", - "values": ["61668f324559b664159bc28a"] - } - ], - "queryType": "regularQuery" - }, - "data": [ - { - "Member.score": 1, - "Members.memberCount": 173 - }, - { - "Member.score": 0, - "Members.memberCount": 171 - }, - { - "Member.score": 2, - "Members.memberCount": 32 - }, - { - "Member.score": 3, - "Members.memberCount": 22 - }, - { - "Member.score": 4, - "Members.memberCount": 18 - }, - { - "Member.score": 10, - "Members.memberCount": 8 - }, - { - "Member.score": 8, - "Members.memberCount": 8 - }, - { - "Member.score": 5, - "Members.memberCount": 8 - }, - { - "Member.score": 7, - "Members.memberCount": 7 - }, - { - "Member.score": 6, - "Members.memberCount": 5 - }, - { - "Member.score": 9, - "Members.memberCount": 4 - } - ], - "lastRefreshTime": "2022-01-08T19:43:15.844Z", - "annotation": { - "measures": { - "Members.memberCount": { - "title": "Members Member Count", - "shortTitle": "Member Count", - "type": "number", - "drillMembers": [], - "drillMembersGrouped": { - "measures": [], - "dimensions": [] - } - } - }, - "dimensions": { - "Member.score": { - "title": "Member Score", - "shortTitle": "Score", - "type": "number" - } - }, - "segments": {}, - "timeDimensions": {} - }, - "slowQuery": true - } - ], - "pivotQuery": { - "measures": ["Members.memberCount"], - "dimensions": ["Member.score"], - "timeDimensions": [ - { - "dimension": "Members.date", - "dateRange": ["2021-12-09T00:00:00.000", "2022-01-07T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Members.memberCount", - "desc": true - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "Member.tenant", - "operator": "equals", - "values": ["61668f324559b664159bc28a"] - } - ], - "queryType": "regularQuery" - }, - "slowQuery": true - }, - "queryType": "regularQuery", - "loadResponses": [ - { - "query": { - "measures": ["Members.memberCount"], - "dimensions": ["Member.score"], - "timeDimensions": [ - { - "dimension": "Members.date", - "dateRange": ["2021-12-09T00:00:00.000", "2022-01-07T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Members.memberCount", - "desc": true - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "Member.tenant", - "operator": "equals", - "values": ["61668f324559b664159bc28a"] - } - ], - "queryType": "regularQuery" - }, - "data": [ - { - "Member.score": 1, - "Members.memberCount": 173 - }, - { - "Member.score": 0, - "Members.memberCount": 171 - }, - { - "Member.score": 2, - "Members.memberCount": 32 - }, - { - "Member.score": 3, - "Members.memberCount": 22 - }, - { - "Member.score": 4, - "Members.memberCount": 18 - }, - { - "Member.score": 10, - "Members.memberCount": 8 - }, - { - "Member.score": 8, - "Members.memberCount": 8 - }, - { - "Member.score": 5, - "Members.memberCount": 8 - }, - { - "Member.score": 7, - "Members.memberCount": 7 - }, - { - "Member.score": 6, - "Members.memberCount": 5 - }, - { - "Member.score": 9, - "Members.memberCount": 4 - } - ], - "lastRefreshTime": "2022-01-08T19:43:15.844Z", - "annotation": { - "measures": { - "Members.memberCount": { - "title": "Members Member Count", - "shortTitle": "Member Count", - "type": "number", - "drillMembers": [], - "drillMembersGrouped": { - "measures": [], - "dimensions": [] - } - } - }, - "dimensions": { - "Member.score": { - "title": "Member Score", - "shortTitle": "Score", - "type": "number" - } - }, - "segments": {}, - "timeDimensions": {} - }, - "slowQuery": true - } - ], - "options": {}, - "backwardCompatibleData": [ - [ - { - "Member.score": 1, - "Members.memberCount": 173 - }, - { - "Member.score": 0, - "Members.memberCount": 171 - }, - { - "Member.score": 2, - "Members.memberCount": 32 - }, - { - "Member.score": 3, - "Members.memberCount": 22 - }, - { - "Member.score": 4, - "Members.memberCount": 18 - }, - { - "Member.score": 10, - "Members.memberCount": 8 - }, - { - "Member.score": 8, - "Members.memberCount": 8 - }, - { - "Member.score": 5, - "Members.memberCount": 8 - }, - { - "Member.score": 7, - "Members.memberCount": 7 - }, - { - "Member.score": 6, - "Members.memberCount": 5 - }, - { - "Member.score": 9, - "Members.memberCount": 4 - } - ] - ] - } - } - }, - { - "title": "Total Members", - "type": "cubejs", - "settings": { - "chartType": "number", - "query": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "dateRange": "Last year" - } - ], - "limit": 10000 - }, - "layout": { - "x": 6, - "y": 15, - "w": 6, - "h": 2, - "i": "61d9e9c83615d09e4c6e1d8e" - } - } - } - ], - "public": true - }, - { - "name": "Test Report", - "widgets": [ - { - "title": "Members Over Time", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "day", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Members.date": "asc" - } - }, - "layout": { - "x": 0, - "y": 0, - "w": 6, - "h": 2, - "i": "61d7104aa93e659cb5058498", - "moved": false - }, - "resultSet": { - "loadResponse": { - "queryType": "regularQuery", - "results": [ - { - "query": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "day", - "dateRange": ["2021-12-07T00:00:00.000", "2022-01-05T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Members.date", - "desc": false - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "Member.tenant", - "operator": "equals", - "values": ["61668f324559b664159bc28a"] - } - ], - "dimensions": [], - "queryType": "regularQuery" - }, - "data": [ - { - "Members.date.day": "2021-12-07T00:00:00.000", - "Members.date": "2021-12-07T00:00:00.000", - "Members.memberCount": "31" - }, - { - "Members.date.day": "2021-12-08T00:00:00.000", - "Members.date": "2021-12-08T00:00:00.000", - "Members.memberCount": "33" - }, - { - "Members.date.day": "2021-12-09T00:00:00.000", - "Members.date": "2021-12-09T00:00:00.000", - "Members.memberCount": "37" - }, - { - "Members.date.day": "2021-12-10T00:00:00.000", - "Members.date": "2021-12-10T00:00:00.000", - "Members.memberCount": "39" - }, - { - "Members.date.day": "2021-12-11T00:00:00.000", - "Members.date": "2021-12-11T00:00:00.000", - "Members.memberCount": "40" - }, - { - "Members.date.day": "2021-12-12T00:00:00.000", - "Members.date": "2021-12-12T00:00:00.000", - "Members.memberCount": "41" - }, - { - "Members.date.day": "2021-12-13T00:00:00.000", - "Members.date": "2021-12-13T00:00:00.000", - "Members.memberCount": "41" - }, - { - "Members.date.day": "2021-12-14T00:00:00.000", - "Members.date": "2021-12-14T00:00:00.000", - "Members.memberCount": "44" - }, - { - "Members.date.day": "2021-12-15T00:00:00.000", - "Members.date": "2021-12-15T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-16T00:00:00.000", - "Members.date": "2021-12-16T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-17T00:00:00.000", - "Members.date": "2021-12-17T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-18T00:00:00.000", - "Members.date": "2021-12-18T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-19T00:00:00.000", - "Members.date": "2021-12-19T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-20T00:00:00.000", - "Members.date": "2021-12-20T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-21T00:00:00.000", - "Members.date": "2021-12-21T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-22T00:00:00.000", - "Members.date": "2021-12-22T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-23T00:00:00.000", - "Members.date": "2021-12-23T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-24T00:00:00.000", - "Members.date": "2021-12-24T00:00:00.000", - "Members.memberCount": "47" - }, - { - "Members.date.day": "2021-12-25T00:00:00.000", - "Members.date": "2021-12-25T00:00:00.000", - "Members.memberCount": "47" - }, - { - "Members.date.day": "2021-12-26T00:00:00.000", - "Members.date": "2021-12-26T00:00:00.000", - "Members.memberCount": "47" - }, - { - "Members.date.day": "2021-12-27T00:00:00.000", - "Members.date": "2021-12-27T00:00:00.000", - "Members.memberCount": "47" - }, - { - "Members.date.day": "2021-12-28T00:00:00.000", - "Members.date": "2021-12-28T00:00:00.000", - "Members.memberCount": "47" - }, - { - "Members.date.day": "2021-12-29T00:00:00.000", - "Members.date": "2021-12-29T00:00:00.000", - "Members.memberCount": "49" - }, - { - "Members.date.day": "2021-12-30T00:00:00.000", - "Members.date": "2021-12-30T00:00:00.000", - "Members.memberCount": "50" - }, - { - "Members.date.day": "2021-12-31T00:00:00.000", - "Members.date": "2021-12-31T00:00:00.000", - "Members.memberCount": "51" - }, - { - "Members.date.day": "2022-01-01T00:00:00.000", - "Members.date": "2022-01-01T00:00:00.000", - "Members.memberCount": "51" - }, - { - "Members.date.day": "2022-01-02T00:00:00.000", - "Members.date": "2022-01-02T00:00:00.000", - "Members.memberCount": "51" - }, - { - "Members.date.day": "2022-01-03T00:00:00.000", - "Members.date": "2022-01-03T00:00:00.000", - "Members.memberCount": "53" - }, - { - "Members.date.day": "2022-01-04T00:00:00.000", - "Members.date": "2022-01-04T00:00:00.000", - "Members.memberCount": "54" - }, - { - "Members.date.day": "2022-01-05T00:00:00.000", - "Members.date": "2022-01-05T00:00:00.000", - "Members.memberCount": "54" - } - ], - "annotation": { - "measures": { - "Members.memberCount": { - "title": "Members Member Count", - "shortTitle": "Member Count", - "type": "number", - "drillMembers": [], - "drillMembersGrouped": { - "measures": [], - "dimensions": [] - } - } - }, - "dimensions": {}, - "segments": {}, - "timeDimensions": { - "Members.date.day": { - "title": "Members Date", - "shortTitle": "Date", - "type": "time" - }, - "Members.date": { - "title": "Members Date", - "shortTitle": "Date", - "type": "time" - } - } - }, - "dataSource": "default", - "dbType": "mongobi", - "extDbType": "cubestore", - "external": true, - "slowQuery": false - } - ], - "pivotQuery": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "day", - "dateRange": ["2021-12-07T00:00:00.000", "2022-01-05T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Members.date", - "desc": false - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "Member.tenant", - "operator": "equals", - "values": ["61668f324559b664159bc28a"] - } - ], - "dimensions": [], - "queryType": "regularQuery" - }, - "slowQuery": false - }, - "queryType": "regularQuery", - "loadResponses": [ - { - "query": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "day", - "dateRange": ["2021-12-07T00:00:00.000", "2022-01-05T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Members.date", - "desc": false - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "Member.tenant", - "operator": "equals", - "values": ["61668f324559b664159bc28a"] - } - ], - "dimensions": [], - "queryType": "regularQuery" - }, - "data": [ - { - "Members.date.day": "2021-12-07T00:00:00.000", - "Members.date": "2021-12-07T00:00:00.000", - "Members.memberCount": "31" - }, - { - "Members.date.day": "2021-12-08T00:00:00.000", - "Members.date": "2021-12-08T00:00:00.000", - "Members.memberCount": "33" - }, - { - "Members.date.day": "2021-12-09T00:00:00.000", - "Members.date": "2021-12-09T00:00:00.000", - "Members.memberCount": "37" - }, - { - "Members.date.day": "2021-12-10T00:00:00.000", - "Members.date": "2021-12-10T00:00:00.000", - "Members.memberCount": "39" - }, - { - "Members.date.day": "2021-12-11T00:00:00.000", - "Members.date": "2021-12-11T00:00:00.000", - "Members.memberCount": "40" - }, - { - "Members.date.day": "2021-12-12T00:00:00.000", - "Members.date": "2021-12-12T00:00:00.000", - "Members.memberCount": "41" - }, - { - "Members.date.day": "2021-12-13T00:00:00.000", - "Members.date": "2021-12-13T00:00:00.000", - "Members.memberCount": "41" - }, - { - "Members.date.day": "2021-12-14T00:00:00.000", - "Members.date": "2021-12-14T00:00:00.000", - "Members.memberCount": "44" - }, - { - "Members.date.day": "2021-12-15T00:00:00.000", - "Members.date": "2021-12-15T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-16T00:00:00.000", - "Members.date": "2021-12-16T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-17T00:00:00.000", - "Members.date": "2021-12-17T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-18T00:00:00.000", - "Members.date": "2021-12-18T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-19T00:00:00.000", - "Members.date": "2021-12-19T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-20T00:00:00.000", - "Members.date": "2021-12-20T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-21T00:00:00.000", - "Members.date": "2021-12-21T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-22T00:00:00.000", - "Members.date": "2021-12-22T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-23T00:00:00.000", - "Members.date": "2021-12-23T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-24T00:00:00.000", - "Members.date": "2021-12-24T00:00:00.000", - "Members.memberCount": "47" - }, - { - "Members.date.day": "2021-12-25T00:00:00.000", - "Members.date": "2021-12-25T00:00:00.000", - "Members.memberCount": "47" - }, - { - "Members.date.day": "2021-12-26T00:00:00.000", - "Members.date": "2021-12-26T00:00:00.000", - "Members.memberCount": "47" - }, - { - "Members.date.day": "2021-12-27T00:00:00.000", - "Members.date": "2021-12-27T00:00:00.000", - "Members.memberCount": "47" - }, - { - "Members.date.day": "2021-12-28T00:00:00.000", - "Members.date": "2021-12-28T00:00:00.000", - "Members.memberCount": "47" - }, - { - "Members.date.day": "2021-12-29T00:00:00.000", - "Members.date": "2021-12-29T00:00:00.000", - "Members.memberCount": "49" - }, - { - "Members.date.day": "2021-12-30T00:00:00.000", - "Members.date": "2021-12-30T00:00:00.000", - "Members.memberCount": "50" - }, - { - "Members.date.day": "2021-12-31T00:00:00.000", - "Members.date": "2021-12-31T00:00:00.000", - "Members.memberCount": "51" - }, - { - "Members.date.day": "2022-01-01T00:00:00.000", - "Members.date": "2022-01-01T00:00:00.000", - "Members.memberCount": "51" - }, - { - "Members.date.day": "2022-01-02T00:00:00.000", - "Members.date": "2022-01-02T00:00:00.000", - "Members.memberCount": "51" - }, - { - "Members.date.day": "2022-01-03T00:00:00.000", - "Members.date": "2022-01-03T00:00:00.000", - "Members.memberCount": "53" - }, - { - "Members.date.day": "2022-01-04T00:00:00.000", - "Members.date": "2022-01-04T00:00:00.000", - "Members.memberCount": "54" - }, - { - "Members.date.day": "2022-01-05T00:00:00.000", - "Members.date": "2022-01-05T00:00:00.000", - "Members.memberCount": "54" - } - ], - "annotation": { - "measures": { - "Members.memberCount": { - "title": "Members Member Count", - "shortTitle": "Member Count", - "type": "number", - "drillMembers": [], - "drillMembersGrouped": { - "measures": [], - "dimensions": [] - } - } - }, - "dimensions": {}, - "segments": {}, - "timeDimensions": { - "Members.date.day": { - "title": "Members Date", - "shortTitle": "Date", - "type": "time" - }, - "Members.date": { - "title": "Members Date", - "shortTitle": "Date", - "type": "time" - } - } - }, - "dataSource": "default", - "dbType": "mongobi", - "extDbType": "cubestore", - "external": true, - "slowQuery": false - } - ], - "options": {}, - "backwardCompatibleData": [ - [ - { - "Members.date.day": "2021-12-07T00:00:00.000", - "Members.date": "2021-12-07T00:00:00.000", - "Members.memberCount": "31" - }, - { - "Members.date.day": "2021-12-08T00:00:00.000", - "Members.date": "2021-12-08T00:00:00.000", - "Members.memberCount": "33" - }, - { - "Members.date.day": "2021-12-09T00:00:00.000", - "Members.date": "2021-12-09T00:00:00.000", - "Members.memberCount": "37" - }, - { - "Members.date.day": "2021-12-10T00:00:00.000", - "Members.date": "2021-12-10T00:00:00.000", - "Members.memberCount": "39" - }, - { - "Members.date.day": "2021-12-11T00:00:00.000", - "Members.date": "2021-12-11T00:00:00.000", - "Members.memberCount": "40" - }, - { - "Members.date.day": "2021-12-12T00:00:00.000", - "Members.date": "2021-12-12T00:00:00.000", - "Members.memberCount": "41" - }, - { - "Members.date.day": "2021-12-13T00:00:00.000", - "Members.date": "2021-12-13T00:00:00.000", - "Members.memberCount": "41" - }, - { - "Members.date.day": "2021-12-14T00:00:00.000", - "Members.date": "2021-12-14T00:00:00.000", - "Members.memberCount": "44" - }, - { - "Members.date.day": "2021-12-15T00:00:00.000", - "Members.date": "2021-12-15T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-16T00:00:00.000", - "Members.date": "2021-12-16T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-17T00:00:00.000", - "Members.date": "2021-12-17T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-18T00:00:00.000", - "Members.date": "2021-12-18T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-19T00:00:00.000", - "Members.date": "2021-12-19T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-20T00:00:00.000", - "Members.date": "2021-12-20T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-21T00:00:00.000", - "Members.date": "2021-12-21T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-22T00:00:00.000", - "Members.date": "2021-12-22T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-23T00:00:00.000", - "Members.date": "2021-12-23T00:00:00.000", - "Members.memberCount": "45" - }, - { - "Members.date.day": "2021-12-24T00:00:00.000", - "Members.date": "2021-12-24T00:00:00.000", - "Members.memberCount": "47" - }, - { - "Members.date.day": "2021-12-25T00:00:00.000", - "Members.date": "2021-12-25T00:00:00.000", - "Members.memberCount": "47" - }, - { - "Members.date.day": "2021-12-26T00:00:00.000", - "Members.date": "2021-12-26T00:00:00.000", - "Members.memberCount": "47" - }, - { - "Members.date.day": "2021-12-27T00:00:00.000", - "Members.date": "2021-12-27T00:00:00.000", - "Members.memberCount": "47" - }, - { - "Members.date.day": "2021-12-28T00:00:00.000", - "Members.date": "2021-12-28T00:00:00.000", - "Members.memberCount": "47" - }, - { - "Members.date.day": "2021-12-29T00:00:00.000", - "Members.date": "2021-12-29T00:00:00.000", - "Members.memberCount": "49" - }, - { - "Members.date.day": "2021-12-30T00:00:00.000", - "Members.date": "2021-12-30T00:00:00.000", - "Members.memberCount": "50" - }, - { - "Members.date.day": "2021-12-31T00:00:00.000", - "Members.date": "2021-12-31T00:00:00.000", - "Members.memberCount": "51" - }, - { - "Members.date.day": "2022-01-01T00:00:00.000", - "Members.date": "2022-01-01T00:00:00.000", - "Members.memberCount": "51" - }, - { - "Members.date.day": "2022-01-02T00:00:00.000", - "Members.date": "2022-01-02T00:00:00.000", - "Members.memberCount": "51" - }, - { - "Members.date.day": "2022-01-03T00:00:00.000", - "Members.date": "2022-01-03T00:00:00.000", - "Members.memberCount": "53" - }, - { - "Members.date.day": "2022-01-04T00:00:00.000", - "Members.date": "2022-01-04T00:00:00.000", - "Members.memberCount": "54" - }, - { - "Members.date.day": "2022-01-05T00:00:00.000", - "Members.date": "2022-01-05T00:00:00.000", - "Members.memberCount": "54" - } - ] - ] - } - } - }, - { - "title": "Activities Over Time", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "day", - "dateRange": "Last 30 days" - } - ], - "limit": 10000 - }, - "layout": { - "x": 6, - "y": 0, - "w": 6, - "h": 2, - "i": "61d71091a93e659cb5058537", - "moved": false - }, - "resultSet": { - "loadResponse": { - "queryType": "regularQuery", - "results": [ - { - "query": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "month", - "dateRange": ["2021-12-07T00:00:00.000", "2022-01-05T23:59:59.999"] - } - ], - "limit": 10000, - "rowLimit": 10000, - "timezone": "UTC", - "order": [], - "filters": [ - { - "member": "Member.tenant", - "operator": "equals", - "values": ["61668f324559b664159bc28a"] - } - ], - "dimensions": [], - "queryType": "regularQuery" - }, - "data": [ - { - "Activities.date.month": "2021-12-01T00:00:00.000", - "Activities.date": "2021-12-01T00:00:00.000", - "Activities.activityCount": 371 - }, - { - "Activities.date.month": "2022-01-01T00:00:00.000", - "Activities.date": "2022-01-01T00:00:00.000", - "Activities.activityCount": 371 - } - ], - "lastRefreshTime": "2022-01-06T17:38:40.691Z", - "annotation": { - "measures": { - "Activities.activityCount": { - "title": "Activities Activity Count", - "shortTitle": "Activity Count", - "type": "number", - "drillMembers": ["Activity.platform"], - "drillMembersGrouped": { - "measures": [], - "dimensions": ["Activity.platform"] - } - } - }, - "dimensions": {}, - "segments": {}, - "timeDimensions": { - "Activities.date.month": { - "title": "Activities Date", - "shortTitle": "Date", - "type": "time" - }, - "Activities.date": { - "title": "Activities Date", - "shortTitle": "Date", - "type": "time" - } - } - }, - "dataSource": "default", - "dbType": "mongobi", - "extDbType": "cubestore", - "external": false, - "slowQuery": false - } - ], - "pivotQuery": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "month", - "dateRange": ["2021-12-07T00:00:00.000", "2022-01-05T23:59:59.999"] - } - ], - "limit": 10000, - "rowLimit": 10000, - "timezone": "UTC", - "order": [], - "filters": [ - { - "member": "Member.tenant", - "operator": "equals", - "values": ["61668f324559b664159bc28a"] - } - ], - "dimensions": [], - "queryType": "regularQuery" - }, - "slowQuery": false - }, - "queryType": "regularQuery", - "loadResponses": [ - { - "query": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "month", - "dateRange": ["2021-12-07T00:00:00.000", "2022-01-05T23:59:59.999"] - } - ], - "limit": 10000, - "rowLimit": 10000, - "timezone": "UTC", - "order": [], - "filters": [ - { - "member": "Member.tenant", - "operator": "equals", - "values": ["61668f324559b664159bc28a"] - } - ], - "dimensions": [], - "queryType": "regularQuery" - }, - "data": [ - { - "Activities.date.month": "2021-12-01T00:00:00.000", - "Activities.date": "2021-12-01T00:00:00.000", - "Activities.activityCount": 371 - }, - { - "Activities.date.month": "2022-01-01T00:00:00.000", - "Activities.date": "2022-01-01T00:00:00.000", - "Activities.activityCount": 371 - } - ], - "lastRefreshTime": "2022-01-06T17:38:40.691Z", - "annotation": { - "measures": { - "Activities.activityCount": { - "title": "Activities Activity Count", - "shortTitle": "Activity Count", - "type": "number", - "drillMembers": ["Activity.platform"], - "drillMembersGrouped": { - "measures": [], - "dimensions": ["Activity.platform"] - } - } - }, - "dimensions": {}, - "segments": {}, - "timeDimensions": { - "Activities.date.month": { - "title": "Activities Date", - "shortTitle": "Date", - "type": "time" - }, - "Activities.date": { - "title": "Activities Date", - "shortTitle": "Date", - "type": "time" - } - } - }, - "dataSource": "default", - "dbType": "mongobi", - "extDbType": "cubestore", - "external": false, - "slowQuery": false - } - ], - "options": {}, - "backwardCompatibleData": [ - [ - { - "Activities.date.month": "2021-12-01T00:00:00.000", - "Activities.date": "2021-12-01T00:00:00.000", - "Activities.activityCount": 371 - }, - { - "Activities.date.month": "2022-01-01T00:00:00.000", - "Activities.date": "2022-01-01T00:00:00.000", - "Activities.activityCount": 371 - } - ] - ] - } - } - }, - { - "title": "Activities by Platform", - "type": "cubejs", - "settings": { - "chartType": "pie", - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Activities.activityCount": "desc" - } - }, - "layout": { - "x": 0, - "y": 2, - "w": 6, - "h": 2, - "i": "61d728e6a93e659cb5058f2f", - "moved": false - }, - "resultSet": { - "loadResponse": { - "queryType": "regularQuery", - "results": [ - { - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": ["2021-12-07T00:00:00.000", "2022-01-05T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Activities.activityCount", - "desc": true - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "Member.tenant", - "operator": "equals", - "values": ["61668f324559b664159bc28a"] - } - ], - "queryType": "regularQuery" - }, - "data": [ - { - "Activity.platform": "github", - "Activities.activityCount": 117 - }, - { - "Activity.platform": "discord", - "Activities.activityCount": 106 - }, - { - "Activity.platform": "apis", - "Activities.activityCount": 99 - }, - { - "Activity.platform": "twitter", - "Activities.activityCount": 49 - } - ], - "lastRefreshTime": "2022-01-06T17:37:46.551Z", - "annotation": { - "measures": { - "Activities.activityCount": { - "title": "Activities Activity Count", - "shortTitle": "Activity Count", - "type": "number", - "drillMembers": ["Activity.platform"], - "drillMembersGrouped": { - "measures": [], - "dimensions": ["Activity.platform"] - } - } - }, - "dimensions": { - "Activity.platform": { - "title": "Activity Platform", - "shortTitle": "Platform", - "type": "string" - } - }, - "segments": {}, - "timeDimensions": {} - }, - "dataSource": "default", - "dbType": "mongobi", - "extDbType": "cubestore", - "external": false, - "slowQuery": false - } - ], - "pivotQuery": { - "measures": ["Activities.activityCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": ["2021-12-07T00:00:00.000", "2022-01-05T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Activities.activityCount", - "desc": true - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "Member.tenant", - "operator": "equals", - "values": ["61668f324559b664159bc28a"] - } - ], - "queryType": "regularQuery" - }, - "slowQuery": false - }, - "queryType": "regularQuery", - "loadResponses": [ - { - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": ["2021-12-07T00:00:00.000", "2022-01-05T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Activities.activityCount", - "desc": true - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "Member.tenant", - "operator": "equals", - "values": ["61668f324559b664159bc28a"] - } - ], - "queryType": "regularQuery" - }, - "data": [ - { - "Activity.platform": "github", - "Activities.activityCount": 117 - }, - { - "Activity.platform": "discord", - "Activities.activityCount": 106 - }, - { - "Activity.platform": "apis", - "Activities.activityCount": 99 - }, - { - "Activity.platform": "twitter", - "Activities.activityCount": 49 - } - ], - "lastRefreshTime": "2022-01-06T17:37:46.551Z", - "annotation": { - "measures": { - "Activities.activityCount": { - "title": "Activities Activity Count", - "shortTitle": "Activity Count", - "type": "number", - "drillMembers": ["Activity.platform"], - "drillMembersGrouped": { - "measures": [], - "dimensions": ["Activity.platform"] - } - } - }, - "dimensions": { - "Activity.platform": { - "title": "Activity Platform", - "shortTitle": "Platform", - "type": "string" - } - }, - "segments": {}, - "timeDimensions": {} - }, - "dataSource": "default", - "dbType": "mongobi", - "extDbType": "cubestore", - "external": false, - "slowQuery": false - } - ], - "options": {}, - "backwardCompatibleData": [ - [ - { - "Activity.platform": "github", - "Activities.activityCount": 117 - }, - { - "Activity.platform": "discord", - "Activities.activityCount": 106 - }, - { - "Activity.platform": "apis", - "Activities.activityCount": 99 - }, - { - "Activity.platform": "twitter", - "Activities.activityCount": 49 - } - ] - ] - } - } - }, - { - "title": "Members by Engagement Level", - "type": "cubejs", - "settings": { - "chartType": "bar", - "query": { - "measures": ["Members.memberCount"], - "dimensions": ["Member.score"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "" - } - ], - "limit": 10000, - "order": { - "Members.date": "asc" - } - }, - "layout": { - "x": 6, - "y": 2, - "w": 6, - "h": 2, - "i": "61d72987a93e659cb5058f83", - "moved": false - }, - "resultSet": { - "loadResponse": { - "queryType": "regularQuery", - "results": [ - { - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Member.score"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": ["2021-01-01T00:00:00.000", "2021-12-31T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Activities.activityCount", - "desc": true - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "Member.tenant", - "operator": "equals", - "values": ["61668f324559b664159bc28a"] - } - ], - "queryType": "regularQuery" - }, - "data": [ - { - "Member.score": 10, - "Activities.activityCount": 63 - }, - { - "Member.score": 8, - "Activities.activityCount": 57 - }, - { - "Member.score": 3, - "Activities.activityCount": 53 - }, - { - "Member.score": 6, - "Activities.activityCount": 45 - }, - { - "Member.score": 2, - "Activities.activityCount": 34 - }, - { - "Member.score": 4, - "Activities.activityCount": 34 - }, - { - "Member.score": 1, - "Activities.activityCount": 30 - }, - { - "Member.score": 9, - "Activities.activityCount": 19 - }, - { - "Member.score": 7, - "Activities.activityCount": 17 - }, - { - "Member.score": 5, - "Activities.activityCount": 14 - }, - { - "Member.score": 0, - "Activities.activityCount": 5 - } - ], - "lastRefreshTime": "2022-01-06T17:39:59.106Z", - "annotation": { - "measures": { - "Activities.activityCount": { - "title": "Activities Activity Count", - "shortTitle": "Activity Count", - "type": "number", - "drillMembers": ["Activity.platform"], - "drillMembersGrouped": { - "measures": [], - "dimensions": ["Activity.platform"] - } - } - }, - "dimensions": { - "Member.score": { - "title": "Member Score", - "shortTitle": "Score", - "type": "number" - } - }, - "segments": {}, - "timeDimensions": {} - }, - "slowQuery": true - } - ], - "pivotQuery": { - "measures": ["Activities.activityCount"], - "dimensions": ["Member.score"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": ["2021-01-01T00:00:00.000", "2021-12-31T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Activities.activityCount", - "desc": true - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "Member.tenant", - "operator": "equals", - "values": ["61668f324559b664159bc28a"] - } - ], - "queryType": "regularQuery" - }, - "slowQuery": true - }, - "queryType": "regularQuery", - "loadResponses": [ - { - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Member.score"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": ["2021-01-01T00:00:00.000", "2021-12-31T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Activities.activityCount", - "desc": true - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "Member.tenant", - "operator": "equals", - "values": ["61668f324559b664159bc28a"] - } - ], - "queryType": "regularQuery" - }, - "data": [ - { - "Member.score": 10, - "Activities.activityCount": 63 - }, - { - "Member.score": 8, - "Activities.activityCount": 57 - }, - { - "Member.score": 3, - "Activities.activityCount": 53 - }, - { - "Member.score": 6, - "Activities.activityCount": 45 - }, - { - "Member.score": 2, - "Activities.activityCount": 34 - }, - { - "Member.score": 4, - "Activities.activityCount": 34 - }, - { - "Member.score": 1, - "Activities.activityCount": 30 - }, - { - "Member.score": 9, - "Activities.activityCount": 19 - }, - { - "Member.score": 7, - "Activities.activityCount": 17 - }, - { - "Member.score": 5, - "Activities.activityCount": 14 - }, - { - "Member.score": 0, - "Activities.activityCount": 5 - } - ], - "lastRefreshTime": "2022-01-06T17:39:59.106Z", - "annotation": { - "measures": { - "Activities.activityCount": { - "title": "Activities Activity Count", - "shortTitle": "Activity Count", - "type": "number", - "drillMembers": ["Activity.platform"], - "drillMembersGrouped": { - "measures": [], - "dimensions": ["Activity.platform"] - } - } - }, - "dimensions": { - "Member.score": { - "title": "Member Score", - "shortTitle": "Score", - "type": "number" - } - }, - "segments": {}, - "timeDimensions": {} - }, - "slowQuery": true - } - ], - "options": {}, - "backwardCompatibleData": [ - [ - { - "Member.score": 10, - "Activities.activityCount": 63 - }, - { - "Member.score": 8, - "Activities.activityCount": 57 - }, - { - "Member.score": 3, - "Activities.activityCount": 53 - }, - { - "Member.score": 6, - "Activities.activityCount": 45 - }, - { - "Member.score": 2, - "Activities.activityCount": 34 - }, - { - "Member.score": 4, - "Activities.activityCount": 34 - }, - { - "Member.score": 1, - "Activities.activityCount": 30 - }, - { - "Member.score": 9, - "Activities.activityCount": 19 - }, - { - "Member.score": 7, - "Activities.activityCount": 17 - }, - { - "Member.score": 5, - "Activities.activityCount": 14 - }, - { - "Member.score": 0, - "Activities.activityCount": 5 - } - ] - ] - } - } - }, - { - "title": "", - "type": "cubejs", - "settings": { - "chartType": "bar", - "query": { - "measures": ["Members.memberCount"], - "dimensions": ["Member.score"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "", - "dateRange": "Last year" - } - ], - "limit": 10000, - "order": { - "Members.memberCount": "desc" - } - }, - "layout": { - "x": 0, - "y": 16, - "w": 6, - "h": 2, - "i": "61d72e3ea93e659cb50590ad" - } - } - } - ], - "public": true - }, - { - "name": "sdasdas", - "widgets": [ - { - "title": "Activity Count", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "month", - "dateRange": "This quarter" - } - ], - "limit": 10000, - "order": { - "Activities.date": "asc" - } - }, - "layout": { - "x": 0, - "y": 0, - "w": 6, - "h": 18, - "i": "61d5b3e7c8451c5973090f62", - "moved": false - } - } - }, - { - "title": "Member Count", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "month", - "dateRange": "This quarter" - } - ], - "limit": 10000, - "order": { - "Members.date": "asc" - } - }, - "layout": { - "x": 6, - "y": 0, - "w": 6, - "h": 18, - "i": "61d5b9acf6eae68f9710ca74", - "moved": false - } - } - }, - { - "title": "Activities", - "type": "cubejs", - "settings": { - "chartType": "number", - "query": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": "Last 30 days" - } - ], - "limit": 10000 - }, - "layout": { - "x": 0, - "y": 18, - "w": 6, - "h": 9, - "i": "61d70ca9277cde8ff7389e98", - "moved": false - } - } - }, - { - "title": "Untitled", - "type": "cubejs", - "settings": { - "chartType": "table", - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Activity.type"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": "Last year" - } - ], - "limit": 10000, - "order": { - "Activities.activityCount": "desc" - } - }, - "layout": { - "x": 6, - "y": 18, - "w": 6, - "h": 31, - "i": "61dde5e79280a23f180dea1f", - "moved": false - } - } - } - ], - "public": true - }, - { - "name": "Testing asd", - "widgets": [], - "public": false - }, - { - "name": "Testing", - "widgets": [ - { - "title": "Widget", - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Activities.activityCount"], - "dimensions": ["Activity.platform"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "day", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Activities.activityCount": "desc" - } - }, - "layout": { - "x": 0, - "y": 0, - "w": 6, - "h": 18, - "i": "61d30a2aafabc38cfde1f194", - "moved": false - }, - "resultSet": { - "loadResponse": { - "queryType": "regularQuery", - "results": [ - { - "query": { - "measures": ["ActivityFlat.activityCount"], - "dimensions": ["ActivityFlat.platform"], - "timeDimensions": [ - { - "dimension": "ActivityFlat.timestamp", - "granularity": "day", - "dateRange": ["2021-12-12T00:00:00.000", "2022-01-10T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Activities.activityCount", - "desc": true - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "ActivityFlat.tenant", - "operator": "equals", - "values": ["6138aeb3e0004c6daf8d9d20"] - } - ], - "queryType": "regularQuery" - }, - "data": [ - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-12T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-12T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-13T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-13T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-14T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-14T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-15T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-15T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-16T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-16T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-17T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-17T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-18T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-18T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-19T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-19T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-20T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-20T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-21T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-21T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-22T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-22T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-23T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-23T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-24T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-24T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-25T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-25T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-26T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-26T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-27T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-27T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-28T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-28T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-29T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-29T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-30T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-30T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-31T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-31T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-01T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-01T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-02T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-02T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-03T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-03T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-04T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-04T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-05T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-05T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-06T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-06T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-07T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-07T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-08T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-08T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-09T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-09T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-10T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-10T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-12T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-12T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-13T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-13T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-14T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-14T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-15T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-15T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-16T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-16T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-17T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-17T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-18T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-18T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-19T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-19T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-20T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-20T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-21T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-21T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-22T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-22T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-23T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-23T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-24T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-24T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-25T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-25T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-26T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-26T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-27T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-27T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-28T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-28T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-29T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-29T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-30T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-30T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-31T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-31T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-01T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-01T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-02T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-02T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-03T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-03T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-04T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-04T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-05T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-05T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-06T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-06T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-07T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-07T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-08T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-08T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-09T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-09T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-10T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-10T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-12T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-12T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-13T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-13T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-14T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-14T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-15T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-15T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-16T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-16T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-17T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-17T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-18T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-18T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-19T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-19T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-20T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-20T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-21T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-21T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-22T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-22T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-23T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-23T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-24T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-24T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-25T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-25T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-26T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-26T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-27T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-27T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-28T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-28T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-29T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-29T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-30T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-30T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-31T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-31T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-01T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-01T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-02T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-02T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-03T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-03T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-04T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-04T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-05T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-05T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-06T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-06T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-07T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-07T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-08T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-08T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-09T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-09T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-10T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-10T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "slack", - "ActivityFlat.timestamp.day": "2022-01-07T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-07T00:00:00.000", - "ActivityFlat.activityCount": "24" - }, - { - "ActivityFlat.platform": "slack", - "ActivityFlat.timestamp.day": "2022-01-08T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-08T00:00:00.000", - "ActivityFlat.activityCount": "24" - }, - { - "ActivityFlat.platform": "slack", - "ActivityFlat.timestamp.day": "2022-01-09T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-09T00:00:00.000", - "ActivityFlat.activityCount": "24" - }, - { - "ActivityFlat.platform": "slack", - "ActivityFlat.timestamp.day": "2022-01-10T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-10T00:00:00.000", - "ActivityFlat.activityCount": "40" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-12T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-12T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-13T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-13T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-14T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-14T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-15T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-15T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-16T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-16T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-17T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-17T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-18T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-18T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-19T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-19T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-20T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-20T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-21T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-21T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-22T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-22T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-23T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-23T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-24T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-24T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-25T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-25T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-26T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-26T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-27T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-27T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-28T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-28T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-29T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-29T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-30T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-30T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-31T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-31T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-01T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-01T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-02T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-02T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-03T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-03T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-04T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-04T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-05T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-05T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-06T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-06T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-07T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-07T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-08T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-08T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-09T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-09T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-10T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-10T00:00:00.000", - "ActivityFlat.activityCount": "39" - } - ], - "annotation": { - "measures": { - "ActivityFlat.activityCount": { - "title": "Activity Activity Count", - "shortTitle": "Activity Count", - "type": "number", - "drillMembers": [], - "drillMembersGrouped": { - "measures": [], - "dimensions": [] - } - } - }, - "dimensions": { - "ActivityFlat.platform": { - "title": "Activity Platform", - "shortTitle": "Platform", - "type": "string" - } - }, - "segments": {}, - "timeDimensions": { - "ActivityFlat.timestamp.day": { - "title": "Activity Timestamp", - "shortTitle": "Timestamp", - "type": "time" - }, - "ActivityFlat.timestamp": { - "title": "Activity Timestamp", - "shortTitle": "Timestamp", - "type": "time" - } - } - }, - "dataSource": "default", - "dbType": "mongobi", - "extDbType": "cubestore", - "external": true, - "slowQuery": false - } - ], - "pivotQuery": { - "measures": ["ActivityFlat.activityCount"], - "dimensions": ["ActivityFlat.platform"], - "timeDimensions": [ - { - "dimension": "ActivityFlat.timestamp", - "granularity": "day", - "dateRange": ["2021-12-12T00:00:00.000", "2022-01-10T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Activities.activityCount", - "desc": true - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "ActivityFlat.tenant", - "operator": "equals", - "values": ["6138aeb3e0004c6daf8d9d20"] - } - ], - "queryType": "regularQuery" - }, - "slowQuery": false - }, - "queryType": "regularQuery", - "loadResponses": [ - { - "query": { - "measures": ["ActivityFlat.activityCount"], - "dimensions": ["ActivityFlat.platform"], - "timeDimensions": [ - { - "dimension": "ActivityFlat.timestamp", - "granularity": "day", - "dateRange": ["2021-12-12T00:00:00.000", "2022-01-10T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Activities.activityCount", - "desc": true - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "ActivityFlat.tenant", - "operator": "equals", - "values": ["6138aeb3e0004c6daf8d9d20"] - } - ], - "queryType": "regularQuery" - }, - "data": [ - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-12T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-12T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-13T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-13T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-14T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-14T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-15T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-15T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-16T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-16T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-17T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-17T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-18T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-18T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-19T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-19T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-20T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-20T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-21T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-21T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-22T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-22T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-23T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-23T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-24T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-24T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-25T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-25T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-26T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-26T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-27T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-27T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-28T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-28T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-29T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-29T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-30T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-30T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-31T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-31T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-01T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-01T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-02T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-02T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-03T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-03T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-04T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-04T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-05T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-05T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-06T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-06T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-07T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-07T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-08T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-08T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-09T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-09T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-10T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-10T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-12T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-12T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-13T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-13T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-14T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-14T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-15T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-15T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-16T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-16T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-17T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-17T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-18T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-18T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-19T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-19T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-20T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-20T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-21T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-21T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-22T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-22T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-23T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-23T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-24T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-24T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-25T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-25T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-26T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-26T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-27T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-27T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-28T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-28T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-29T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-29T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-30T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-30T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-31T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-31T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-01T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-01T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-02T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-02T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-03T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-03T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-04T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-04T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-05T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-05T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-06T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-06T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-07T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-07T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-08T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-08T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-09T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-09T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-10T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-10T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-12T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-12T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-13T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-13T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-14T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-14T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-15T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-15T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-16T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-16T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-17T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-17T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-18T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-18T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-19T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-19T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-20T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-20T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-21T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-21T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-22T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-22T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-23T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-23T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-24T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-24T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-25T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-25T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-26T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-26T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-27T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-27T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-28T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-28T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-29T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-29T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-30T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-30T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-31T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-31T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-01T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-01T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-02T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-02T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-03T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-03T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-04T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-04T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-05T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-05T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-06T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-06T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-07T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-07T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-08T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-08T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-09T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-09T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-10T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-10T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "slack", - "ActivityFlat.timestamp.day": "2022-01-07T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-07T00:00:00.000", - "ActivityFlat.activityCount": "24" - }, - { - "ActivityFlat.platform": "slack", - "ActivityFlat.timestamp.day": "2022-01-08T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-08T00:00:00.000", - "ActivityFlat.activityCount": "24" - }, - { - "ActivityFlat.platform": "slack", - "ActivityFlat.timestamp.day": "2022-01-09T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-09T00:00:00.000", - "ActivityFlat.activityCount": "24" - }, - { - "ActivityFlat.platform": "slack", - "ActivityFlat.timestamp.day": "2022-01-10T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-10T00:00:00.000", - "ActivityFlat.activityCount": "40" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-12T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-12T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-13T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-13T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-14T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-14T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-15T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-15T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-16T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-16T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-17T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-17T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-18T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-18T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-19T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-19T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-20T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-20T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-21T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-21T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-22T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-22T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-23T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-23T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-24T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-24T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-25T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-25T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-26T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-26T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-27T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-27T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-28T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-28T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-29T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-29T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-30T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-30T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-31T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-31T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-01T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-01T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-02T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-02T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-03T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-03T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-04T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-04T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-05T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-05T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-06T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-06T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-07T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-07T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-08T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-08T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-09T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-09T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-10T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-10T00:00:00.000", - "ActivityFlat.activityCount": "39" - } - ], - "annotation": { - "measures": { - "ActivityFlat.activityCount": { - "title": "Activity Activity Count", - "shortTitle": "Activity Count", - "type": "number", - "drillMembers": [], - "drillMembersGrouped": { - "measures": [], - "dimensions": [] - } - } - }, - "dimensions": { - "ActivityFlat.platform": { - "title": "Activity Platform", - "shortTitle": "Platform", - "type": "string" - } - }, - "segments": {}, - "timeDimensions": { - "ActivityFlat.timestamp.day": { - "title": "Activity Timestamp", - "shortTitle": "Timestamp", - "type": "time" - }, - "ActivityFlat.timestamp": { - "title": "Activity Timestamp", - "shortTitle": "Timestamp", - "type": "time" - } - } - }, - "dataSource": "default", - "dbType": "mongobi", - "extDbType": "cubestore", - "external": true, - "slowQuery": false - } - ], - "options": {}, - "backwardCompatibleData": [ - [ - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-12T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-12T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-13T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-13T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-14T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-14T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-15T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-15T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-16T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-16T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-17T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-17T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-18T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-18T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-19T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-19T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-20T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-20T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-21T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-21T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-22T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-22T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-23T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-23T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-24T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-24T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-25T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-25T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-26T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-26T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-27T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-27T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-28T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-28T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-29T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-29T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-30T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-30T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2021-12-31T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-31T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-01T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-01T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-02T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-02T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-03T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-03T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-04T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-04T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-05T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-05T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-06T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-06T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-07T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-07T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-08T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-08T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-09T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-09T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "apis", - "ActivityFlat.timestamp.day": "2022-01-10T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-10T00:00:00.000", - "ActivityFlat.activityCount": "53" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-12T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-12T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-13T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-13T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-14T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-14T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-15T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-15T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-16T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-16T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-17T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-17T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-18T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-18T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-19T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-19T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-20T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-20T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-21T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-21T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-22T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-22T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-23T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-23T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-24T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-24T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-25T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-25T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-26T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-26T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-27T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-27T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-28T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-28T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-29T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-29T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-30T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-30T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2021-12-31T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-31T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-01T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-01T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-02T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-02T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-03T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-03T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-04T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-04T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-05T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-05T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-06T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-06T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-07T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-07T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-08T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-08T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-09T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-09T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "discord", - "ActivityFlat.timestamp.day": "2022-01-10T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-10T00:00:00.000", - "ActivityFlat.activityCount": "22" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-12T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-12T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-13T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-13T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-14T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-14T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-15T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-15T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-16T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-16T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-17T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-17T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-18T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-18T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-19T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-19T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-20T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-20T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-21T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-21T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-22T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-22T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-23T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-23T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-24T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-24T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-25T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-25T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-26T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-26T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-27T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-27T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-28T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-28T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-29T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-29T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-30T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-30T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2021-12-31T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-31T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-01T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-01T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-02T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-02T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-03T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-03T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-04T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-04T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-05T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-05T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-06T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-06T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-07T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-07T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-08T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-08T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-09T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-09T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "github", - "ActivityFlat.timestamp.day": "2022-01-10T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-10T00:00:00.000", - "ActivityFlat.activityCount": "230" - }, - { - "ActivityFlat.platform": "slack", - "ActivityFlat.timestamp.day": "2022-01-07T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-07T00:00:00.000", - "ActivityFlat.activityCount": "24" - }, - { - "ActivityFlat.platform": "slack", - "ActivityFlat.timestamp.day": "2022-01-08T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-08T00:00:00.000", - "ActivityFlat.activityCount": "24" - }, - { - "ActivityFlat.platform": "slack", - "ActivityFlat.timestamp.day": "2022-01-09T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-09T00:00:00.000", - "ActivityFlat.activityCount": "24" - }, - { - "ActivityFlat.platform": "slack", - "ActivityFlat.timestamp.day": "2022-01-10T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-10T00:00:00.000", - "ActivityFlat.activityCount": "40" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-12T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-12T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-13T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-13T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-14T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-14T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-15T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-15T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-16T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-16T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-17T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-17T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-18T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-18T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-19T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-19T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-20T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-20T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-21T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-21T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-22T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-22T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-23T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-23T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-24T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-24T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-25T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-25T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-26T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-26T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-27T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-27T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-28T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-28T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-29T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-29T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-30T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-30T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2021-12-31T00:00:00.000", - "ActivityFlat.timestamp": "2021-12-31T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-01T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-01T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-02T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-02T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-03T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-03T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-04T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-04T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-05T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-05T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-06T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-06T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-07T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-07T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-08T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-08T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-09T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-09T00:00:00.000", - "ActivityFlat.activityCount": "39" - }, - { - "ActivityFlat.platform": "twitter", - "ActivityFlat.timestamp.day": "2022-01-10T00:00:00.000", - "ActivityFlat.timestamp": "2022-01-10T00:00:00.000", - "ActivityFlat.activityCount": "39" - } - ] - ] - } - } - }, - { - "title": "Most active contributors", - "type": "cubejs", - "settings": { - "chartType": "area", - "query": { - "measures": ["Activities.activityCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "day", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Activities.date": "asc" - } - }, - "layout": { - "x": 6, - "y": 0, - "w": 6, - "h": 18, - "i": "61d48e46c8451c5973090d0e", - "moved": false - } - } - } - ], - "public": false - }, - { - "name": "asdqwe", - "widgets": [ - { - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Activities.activitiyCount"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "day", - "dateRange": "This month" - } - ], - "limit": 10000, - "order": { - "Activities.date": "asc" - } - }, - "layout": { - "x": 0, - "y": 12, - "w": 6, - "h": 2, - "i": "61c46008c7b3b97e91725134" - } - } - }, - { - "type": "cubejs", - "settings": { - "chartType": "line", - "query": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "day", - "dateRange": "This year" - } - ], - "limit": 10000, - "order": { - "Members.date": "asc" - } - }, - "layout": { - "x": 6, - "y": 0, - "w": 6, - "h": 2, - "i": "61c46022c7b3b97e91725152", - "moved": false - }, - "resultSet": { - "loadResponse": { - "queryType": "regularQuery", - "results": [ - { - "query": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "day", - "dateRange": ["2021-01-01T00:00:00.000", "2021-12-31T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Members.date", - "desc": false - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "MembersEntity.tenant", - "operator": "equals", - "values": ["619cbf169256142ce42e3e68"] - } - ], - "dimensions": [], - "queryType": "regularQuery" - }, - "data": [ - { - "Members.date.day": "2021-12-05T00:00:00.000", - "Members.date": "2021-12-05T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-06T00:00:00.000", - "Members.date": "2021-12-06T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-07T00:00:00.000", - "Members.date": "2021-12-07T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-08T00:00:00.000", - "Members.date": "2021-12-08T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-09T00:00:00.000", - "Members.date": "2021-12-09T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-10T00:00:00.000", - "Members.date": "2021-12-10T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-11T00:00:00.000", - "Members.date": "2021-12-11T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-12T00:00:00.000", - "Members.date": "2021-12-12T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-13T00:00:00.000", - "Members.date": "2021-12-13T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-14T00:00:00.000", - "Members.date": "2021-12-14T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-15T00:00:00.000", - "Members.date": "2021-12-15T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-16T00:00:00.000", - "Members.date": "2021-12-16T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-17T00:00:00.000", - "Members.date": "2021-12-17T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-18T00:00:00.000", - "Members.date": "2021-12-18T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-19T00:00:00.000", - "Members.date": "2021-12-19T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-20T00:00:00.000", - "Members.date": "2021-12-20T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-21T00:00:00.000", - "Members.date": "2021-12-21T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-22T00:00:00.000", - "Members.date": "2021-12-22T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-23T00:00:00.000", - "Members.date": "2021-12-23T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-24T00:00:00.000", - "Members.date": "2021-12-24T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-25T00:00:00.000", - "Members.date": "2021-12-25T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-26T00:00:00.000", - "Members.date": "2021-12-26T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-27T00:00:00.000", - "Members.date": "2021-12-27T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-28T00:00:00.000", - "Members.date": "2021-12-28T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-29T00:00:00.000", - "Members.date": "2021-12-29T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-30T00:00:00.000", - "Members.date": "2021-12-30T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-31T00:00:00.000", - "Members.date": "2021-12-31T00:00:00.000", - "Members.memberCount": 2 - } - ], - "lastRefreshTime": "2021-12-23T11:40:14.404Z", - "annotation": { - "measures": { - "Members.memberCount": { - "title": "Members Member Count", - "shortTitle": "Member Count", - "type": "number", - "drillMembers": [], - "drillMembersGrouped": { - "measures": [], - "dimensions": [] - } - } - }, - "dimensions": {}, - "segments": {}, - "timeDimensions": { - "Members.date.day": { - "title": "Members Date", - "shortTitle": "Date", - "type": "time" - }, - "Members.date": { - "title": "Members Date", - "shortTitle": "Date", - "type": "time" - } - } - }, - "dataSource": "default", - "dbType": "mongobi", - "extDbType": "cubestore", - "external": false, - "slowQuery": false - } - ], - "pivotQuery": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "day", - "dateRange": ["2021-01-01T00:00:00.000", "2021-12-31T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Members.date", - "desc": false - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "MembersEntity.tenant", - "operator": "equals", - "values": ["619cbf169256142ce42e3e68"] - } - ], - "dimensions": [], - "queryType": "regularQuery" - }, - "slowQuery": false - }, - "queryType": "regularQuery", - "loadResponses": [ - { - "query": { - "measures": ["Members.memberCount"], - "timeDimensions": [ - { - "dimension": "Members.date", - "granularity": "day", - "dateRange": ["2021-01-01T00:00:00.000", "2021-12-31T23:59:59.999"] - } - ], - "limit": 10000, - "order": [ - { - "id": "Members.date", - "desc": false - } - ], - "rowLimit": 10000, - "timezone": "UTC", - "filters": [ - { - "member": "MembersEntity.tenant", - "operator": "equals", - "values": ["619cbf169256142ce42e3e68"] - } - ], - "dimensions": [], - "queryType": "regularQuery" - }, - "data": [ - { - "Members.date.day": "2021-12-05T00:00:00.000", - "Members.date": "2021-12-05T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-06T00:00:00.000", - "Members.date": "2021-12-06T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-07T00:00:00.000", - "Members.date": "2021-12-07T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-08T00:00:00.000", - "Members.date": "2021-12-08T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-09T00:00:00.000", - "Members.date": "2021-12-09T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-10T00:00:00.000", - "Members.date": "2021-12-10T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-11T00:00:00.000", - "Members.date": "2021-12-11T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-12T00:00:00.000", - "Members.date": "2021-12-12T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-13T00:00:00.000", - "Members.date": "2021-12-13T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-14T00:00:00.000", - "Members.date": "2021-12-14T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-15T00:00:00.000", - "Members.date": "2021-12-15T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-16T00:00:00.000", - "Members.date": "2021-12-16T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-17T00:00:00.000", - "Members.date": "2021-12-17T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-18T00:00:00.000", - "Members.date": "2021-12-18T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-19T00:00:00.000", - "Members.date": "2021-12-19T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-20T00:00:00.000", - "Members.date": "2021-12-20T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-21T00:00:00.000", - "Members.date": "2021-12-21T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-22T00:00:00.000", - "Members.date": "2021-12-22T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-23T00:00:00.000", - "Members.date": "2021-12-23T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-24T00:00:00.000", - "Members.date": "2021-12-24T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-25T00:00:00.000", - "Members.date": "2021-12-25T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-26T00:00:00.000", - "Members.date": "2021-12-26T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-27T00:00:00.000", - "Members.date": "2021-12-27T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-28T00:00:00.000", - "Members.date": "2021-12-28T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-29T00:00:00.000", - "Members.date": "2021-12-29T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-30T00:00:00.000", - "Members.date": "2021-12-30T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-31T00:00:00.000", - "Members.date": "2021-12-31T00:00:00.000", - "Members.memberCount": 2 - } - ], - "lastRefreshTime": "2021-12-23T11:40:14.404Z", - "annotation": { - "measures": { - "Members.memberCount": { - "title": "Members Member Count", - "shortTitle": "Member Count", - "type": "number", - "drillMembers": [], - "drillMembersGrouped": { - "measures": [], - "dimensions": [] - } - } - }, - "dimensions": {}, - "segments": {}, - "timeDimensions": { - "Members.date.day": { - "title": "Members Date", - "shortTitle": "Date", - "type": "time" - }, - "Members.date": { - "title": "Members Date", - "shortTitle": "Date", - "type": "time" - } - } - }, - "dataSource": "default", - "dbType": "mongobi", - "extDbType": "cubestore", - "external": false, - "slowQuery": false - } - ], - "options": {}, - "backwardCompatibleData": [ - [ - { - "Members.date.day": "2021-12-05T00:00:00.000", - "Members.date": "2021-12-05T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-06T00:00:00.000", - "Members.date": "2021-12-06T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-07T00:00:00.000", - "Members.date": "2021-12-07T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-08T00:00:00.000", - "Members.date": "2021-12-08T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-09T00:00:00.000", - "Members.date": "2021-12-09T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-10T00:00:00.000", - "Members.date": "2021-12-10T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-11T00:00:00.000", - "Members.date": "2021-12-11T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-12T00:00:00.000", - "Members.date": "2021-12-12T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-13T00:00:00.000", - "Members.date": "2021-12-13T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-14T00:00:00.000", - "Members.date": "2021-12-14T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-15T00:00:00.000", - "Members.date": "2021-12-15T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-16T00:00:00.000", - "Members.date": "2021-12-16T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-17T00:00:00.000", - "Members.date": "2021-12-17T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-18T00:00:00.000", - "Members.date": "2021-12-18T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-19T00:00:00.000", - "Members.date": "2021-12-19T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-20T00:00:00.000", - "Members.date": "2021-12-20T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-21T00:00:00.000", - "Members.date": "2021-12-21T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-22T00:00:00.000", - "Members.date": "2021-12-22T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-23T00:00:00.000", - "Members.date": "2021-12-23T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-24T00:00:00.000", - "Members.date": "2021-12-24T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-25T00:00:00.000", - "Members.date": "2021-12-25T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-26T00:00:00.000", - "Members.date": "2021-12-26T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-27T00:00:00.000", - "Members.date": "2021-12-27T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-28T00:00:00.000", - "Members.date": "2021-12-28T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-29T00:00:00.000", - "Members.date": "2021-12-29T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-30T00:00:00.000", - "Members.date": "2021-12-30T00:00:00.000", - "Members.memberCount": 2 - }, - { - "Members.date.day": "2021-12-31T00:00:00.000", - "Members.date": "2021-12-31T00:00:00.000", - "Members.memberCount": 2 - } - ] - ] - } - } - } - ], - "public": false - } -] diff --git a/backend/src/database/initializers/sample-data.json b/backend/src/database/initializers/sample-data.json deleted file mode 100644 index d2a9b4bfeb..0000000000 --- a/backend/src/database/initializers/sample-data.json +++ /dev/null @@ -1,8535 +0,0 @@ -{ - "members": [ - { - "displayName": "Bertram Gilfoyle", - "username": { - "twitter": "gilfoyle", - "discord": "bg69", - "github": "gilfoyle", - "devto": "iamgilfoyle", - "linkedin": "gilfoyle" - }, - "attributes": { - "bio": { - "twitter": "A satanist who likes beer", - "github": "It's not magic, it's talent and sweat", - "custom": "It's not magic, it's talent and sweat. Systems architecture at PiedPiper" - }, - "jobTitle": { - "custom": "Systems Architect" - }, - "location": { - "github": "Silicon Valley" - }, - "avatarUrl": { - "custom": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/gilfoyle-small.jpg" - }, - "skills": { - "enrichment": ["DevOps", "Linux", "Networking"] - }, - "languages": { - "enrichment": ["English", "German"] - }, - "programmingLanguages": { - "enrichment": ["Python", "Go", "Bash", "Ruby"] - }, - "awards": { - "enrichment": ["Silicon Valley Tech Innovator Award 2017"] - }, - "seniorityLevel": { - "enrichment": "Senior" - }, - "expertise": { - "enrichment": ["Systems architecture", "DevOps", "Networking", "Linux", "Python"] - }, - "country": { - "enrichment": "USA" - }, - "yearsOfExperience": { - "enrichment": 10 - }, - "education": { - "enrichment": [ - { - "campus": "University of California, Berkeley", - "major": "Computer Science", - "specialization": "Systems", - "startDate": "2010-08-01", - "endDate": "2014-05-01" - } - ] - }, - "workExperiences": { - "enrichment": [ - { - "title": "Systems Architect", - "company": "Pied Piper", - "location": "Silicon Valley", - "startDate": "2014-05-01", - "endDate": "Present" - }, - { - "title": "DevOps Engineer", - "company": "Hooli", - "location": "Silicon Valley", - "startDate": "2012-06-01", - "endDate": "2014-05-01" - } - ] - } - }, - "organizations": [ - { - "name": "Pied Piper", - "website": "https://piedpiper.io", - "logo": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/pied-piper-logo.jpg", - "description": "Pied Piper is a data compression platform founded by Richard Hendricks", - "emails": ["richard@piedpiper.io", "gilfoyle@piedpiper.io", "erlich@piedpier.io"], - "tags": ["pied piper", "data-compression", "not-hooli"], - "twitter": { - "handle": "piedpiper", - "bio": "Pied Piper is a data compression platform founded by Richard Hendricks", - "followers": 17000, - "following": 5, - "location": "Menlo Park" - }, - "employees": 20, - "revenueRange": { - "min": 0, - "max": 1 - }, - "linkedin": { - "handle": "company/pied-piper" - }, - "crunchbase": { - "handle": "company/pied-piper" - } - } - ], - "email": "gilfoyle@piedpiper.io", - "score": 9, - "reach": { - "twitter": 534, - "github": 83, - "total": 617 - }, - "notes": [ - "Gilfoyle is known for his satanist beliefs and love of beer", - "He has a reputation for being blunt and sarcastic", - "Gilfoyle's expertise in systems architecture and security make him an invaluable member of Pied Piper's team" - ], - "tags": ["Systems Architect", "DevOps", "Security", "Networks"], - "openSourceContributions": [ - { - "id": 1, - "url": "https://github.com/rhendricks/piedpiper", - "topics": ["compression", "data", "middle-out", "Java"], - "summary": "Pied Piper: 15 commits in 1 day", - "numberCommits": 25, - "lastCommitDate": "2023-03-10", - "firstCommitDate": "2023-03-09" - }, - { - "id": 2, - "url": "https://github.com/rhendricks/piperchat", - "topics": ["messaging", "chat", "security", "compression", "decentralized", "Java"], - "summary": "PiperChat: 10 commits in 1 day", - "numberCommits": 50, - "lastCommitDate": "2023-03-08", - "firstCommitDate": "2023-03-07" - }, - { - "id": 1, - "url": "https://github.com/gilfoyle/linux-kernel", - "topics": ["Linux", "Kernel", "Networking", "Unix"], - "summary": "linux-kernel: 15 commits in 1 day", - "numberCommits": 20, - "lastCommitDate": "2019-03-01", - "firstCommitDate": "2019-02-15" - }, - { - "id": 2, - "url": "https://github.com/gilfoyle/pydevops", - "topics": ["Python", "DevOps"], - "summary": "pydevops: 8 commits in 1 day", - "numberCommits": 18, - "lastCommitDate": "2020-07-01", - "firstCommitDate": "2020-06-15" - }, - { - "id": 3, - "url": "https://github.com/gilfoyle/pypackage", - "topics": ["Python", "Packaging"], - "summary": "pypackage: 5 commits in 1 day", - "numberCommits": 25, - "lastCommitDate": "2021-02-01", - "firstCommitDate": "2021-01-25" - }, - { - "id": 4, - "url": "https://github.com/gilfoyle/go-webapp", - "topics": ["Go", "Web", "DevOps"], - "summary": "go-webapp: 10 commits in 1 day", - "numberCommits": 10, - "lastCommitDate": "2021-08-01", - "firstCommitDate": "2021-07-25" - }, - { - "id": 5, - "url": "https://github.com/gilfoyle/batch-scripts", - "topics": ["Bash", "Scripts", "Unix"], - "summary": "batch-scripts: 3 commits in 1 day", - "numberCommits": 13, - "lastCommitDate": "2022-01-01", - "firstCommitDate": "2021-12-25" - } - ] - }, - { - "displayName": "Richard Hendricks", - "username": { - "twitter": "richhendricks", - "discord": "rhndrcks", - "github": "rhendricks", - "devto": "iamrichard", - "linkedin": "richardhendricks" - }, - "attributes": { - "bio": { - "twitter": "Founder & CEO of @piedpiper, making data compression great again", - "github": "Creator of Pied Piper data comperssion algorithm", - "custom": "Founder & CEO of Pied Piper" - }, - "jobTitle": { "custom": "Founder & CEO" }, - "location": { "github": "Silicon Valley" }, - "avatarUrl": { - "custom": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/richard-small.jpg" - }, - "skills": { - "enrichment": ["compression", "startup management", "software development", "networking"] - }, - "languages": { - "enrichment": ["English", "Spanish"] - }, - "programmingLanguages": { - "enrichment": ["Java", "Python", "C++", "JavaScript"] - }, - "awards": { - "enrichment": ["TechCrunch Disrupt Startup Battlefield Winner", "Forbes 30 under 30"] - }, - "seniorityLevel": { - "enrichment": "Senior" - }, - "expertise": { - "enrichment": [ - "Leadership", - "Data compression", - "Software development", - "Public speaking" - ] - }, - "country": { - "enrichment": "United States" - }, - "yearsOfExperience": { - "enrichment": 10 - }, - "education": { - "enrichment": [ - { - "campus": "Stanford University", - "major": "Computer Science", - "specialization": "Artificial Intelligence", - "startDate": "2010-09-01", - "endDate": "2014-06-01" - } - ] - }, - "workExperiences": { - "enrichment": [ - { - "title": "CEO", - "company": "Pied Piper", - "location": "Silicon Valley", - "startDate": "2014-06-01", - "endDate": "Present" - }, - { - "title": "Software Engineer", - "company": "Hooli", - "location": "Silicon Valley", - "startDate": "2012-06-01", - "endDate": "2014-06-01" - } - ] - } - }, - "organizations": [ - { - "name": "Pied Piper", - "website": "https://piedpiper.io", - "logo": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/pied-piper-logo.jpg", - "description": "Pied Piper is a data compression platform founded by Richard Hendricks", - "emails": ["richard@piedpiper.io", "gilfoyle@piedpiper.io", "erlich@piedpier.io"], - "tags": ["pied piper", "data-compression", "not-hooli"], - "twitter": { - "handle": "piedpiper", - "bio": "Pied Piper is a data compression platform founded by Richard Hendricks", - "followers": 17000, - "following": 5, - "location": "Menlo Park" - }, - "employees": 20, - "revenueRange": { "min": 0, "max": 1 }, - "linkedin": { "handle": "company/pied-piper" }, - "crunchbase": { "handle": "company/pied-piper" } - } - ], - "email": "richard@piedpiper.io", - "score": 8, - "reach": { "twitter": 2016, "github": 157, "total": 2173 }, - "tags": ["entrepreneur", "tech leader", "innovator", "compression expert", "startups"], - "notes": [ - "Has a tendency to overthink things", - "Very dedicated to his work", - "Can be shy in social situations" - ], - "openSourceContributions": [ - { - "id": 1, - "url": "https://github.com/rhendricks/piedpiper", - "topics": ["compression", "data", "middle-out", "Java"], - "summary": "Pied Piper: 25 commits in 1 day", - "numberCommits": 75, - "lastCommitDate": "2023-03-10", - "firstCommitDate": "2023-03-09" - }, - { - "id": 2, - "url": "https://github.com/rhendricks/piperchat", - "topics": ["messaging", "chat", "security", "compression", "decentralized"], - "summary": "PiperChat: 10 commits in 1 day", - "numberCommits": 30, - "lastCommitDate": "2023-03-08", - "firstCommitDate": "2023-03-07" - }, - { - "id": 3, - "url": "https://github.com/rhendricks/neuralnets", - "topics": ["neuralnets", "machinelearning", "python"], - "summary": "NeuralNets: 5 commits in 1 day", - "numberCommits": 5, - "lastCommitDate": "2023-03-05", - "firstCommitDate": "2023-03-04" - }, - { - "id": 4, - "url": "https://github.com/rhendricks/datasci", - "topics": ["datascience", "analytics", "visualization", "neuralnets"], - "summary": "DataSci: 15 commits in 1 day", - "numberCommits": 15, - "lastCommitDate": "2023-03-01", - "firstCommitDate": "2023-02-28" - }, - { - "id": 6, - "url": "https://github.com/rhendricks/networking", - "topics": ["networking", "protocols", "security"], - "summary": "Networking: 8 commits in 1 day", - "numberCommits": 8, - "lastCommitDate": "2023-02-22", - "firstCommitDate": "2023-02-21" - }, - { - "id": 7, - "url": "https://github.com/rhendricks/webdev", - "topics": ["webdev", "html", "css", "javascript"], - "summary": "WebDev: 10 commits in 1 day", - "numberCommits": 10, - "lastCommitDate": "2023-02-18", - "firstCommitDate": "2023-02-17" - }, - { - "id": 8, - "url": "https://github.com/rhendricks/cloudcomputing", - "topics": ["cloudcomputing", "aws", "azure", "gcp"], - "summary": "CloudComputing: 7 commits in 1 day", - "numberCommits": 7, - "lastCommitDate": "2023-02-15", - "firstCommitDate": "2023-02-14" - }, - { - "id": 10, - "url": "https://github.com/rhendricks/distributedsystems", - "topics": ["distributedsystems", "kafka", "rabbitmq", "kubernetes", "cloudcomputing"], - "summary": "DistributedSystems: 9 commits in 1 day", - "numberCommits": 9, - "lastCommitDate": "2023-02-05", - "firstCommitDate": "2023-02-04" - } - ] - }, - { - "displayName": "Erlich Bachman", - "username": { - "twitter": "erlichbach", - "discord": "baaaaachman", - "github": "bachman", - "devto": "ebachman", - "linkedin": "erlichbachman" - }, - "tags": ["entrepreneur", "public speaker", "showman"], - "attributes": { - "bio": { - "twitter": "Founder of Aviato, backer of @piedpiper", - "github": "Coding is not for me", - "custom": "Ex-CEO of Aviato, creator of startup incubator" - }, - "jobTitle": { "custom": "Board Member at Pied Piper" }, - "location": { "github": "Silicon Valley" }, - "avatarUrl": { - "custom": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/erlich-small.jpg" - }, - "skills": { - "enrichment": ["Marketing", "Product Development", "Venture Capital", "Public Speaking"] - }, - "languages": { - "enrichment": ["English", "Mandarin", "Spanish"] - }, - "programmingLanguages": { - "enrichment": ["Python", "JavaScript", "Ruby", "C++"] - }, - "awards": { - "enrichment": ["TechCrunch Disrupt Battlefield Winner 2014"] - }, - "seniorityLevel": { - "enrichment": "Senior" - }, - "expertise": { - "enrichment": [ - "Startup incubation", - "Marketing", - "Business development", - "Product management", - "Fundraising", - "Public speaking" - ] - }, - "country": { - "enrichment": "USA" - }, - "yearsOfExperience": { - "enrichment": 12 - }, - "education": { - "enrichment": [ - { - "campus": "UC Berkeley", - "major": "Business Administration", - "specialization": "", - "startDate": "2010-09-01", - "endDate": "2014-05-15" - }, - { - "campus": "UC Berkeley", - "major": "Computer Science", - "specialization": "Artificial Intelligence", - "startDate": "2008-09-01", - "endDate": "2010-06-01" - } - ] - }, - "workExperiences": { - "enrichment": [ - { - "title": "CEO", - "company": "Aviato", - "location": "Silicon Valley", - "startDate": "2012-01-01", - "endDate": "2015-12-31" - }, - { - "title": "Startup Incubator Creator", - "company": "Erlich's Startup Incubator", - "location": "Silicon Valley", - "startDate": "2016-01-01", - "endDate": "Present" - } - ] - } - }, - "organizations": [ - { - "name": "Erlich's Startup Incubator", - "website": "https://erlichincubator.in", - "logo": "https://cdn.dribbble.com/users/1771704/screenshots/3585167/erlich_still_2x.gif?compress=1&resize=400x300", - "description": "Startup incubator right in Erlich Bachman's house", - "emails": ["bachman@erlichincubator.in", "jianyang@erlichincubator.in"], - "tags": ["Entrepreneur", "Startup Incubator", "VC"], - "twitter": { - "handle": "erlichincubator", - "bio": "Startup incubator right in Erlich Bachman's house", - "followers": 120, - "following": 500, - "location": "Menlo Park" - }, - "employees": 1, - "revenueRange": { "min": 0, "max": 20 }, - "linkedin": { "handle": "company/erlichincubator" }, - "crunchbase": { "handle": "company/erlichincubator" } - } - ], - "email": "erlich@piedpiper.io", - "score": 7, - "reach": { "twitter": 711, "github": 1, "total": 712 }, - "notes": ["Can be difficult to work with", "Strong personality", "Likes to take risks"], - "openSourceContributions": [ - { - "id": 112529472, - "url": "https://github.com/bachman/pied-piper", - "topics": ["compression", "data", "middle-out", "Java"], - "summary": "Pied Piper: 10 commits in 1 day", - "numberCommits": 10, - "lastCommitDate": "2023-03-10", - "firstCommitDate": "2023-03-01" - }, - { - "id": 112529473, - "url": "https://github.com/bachman/aviato", - "topics": ["Python", "Django"], - "summary": "Aviato: 5 commits in 1 day", - "numberCommits": 5, - "lastCommitDate": "2023-02-25", - "firstCommitDate": "2023-02-20" - }, - { - "id": 112529476, - "url": "https://github.com/bachman/erlichbot", - "topics": ["Python", "Slack API"], - "summary": "ErlichBot: 2 commits in 1 day", - "numberCommits": 2, - "lastCommitDate": "2023-01-25", - "firstCommitDate": "2023-01-24" - } - ] - }, - { - "displayName": "Big Head", - "username": { - "twitter": "bighead", - "discord": "bGbghead", - "github": "bighead", - "devto": "bigheader", - "linkedin": "bighead" - }, - "attributes": { - "bio": { - "twitter": "Co-head Dreamer of the Hooli XYZ project", - "github": "Co-head Dreamer of the Hooli XYZ project", - "custom": "Executive at the Hooli XYZ project" - }, - "jobTitle": { "custom": "Co-head Dreamer" }, - "location": { "github": "Silicon Valley" }, - "avatarUrl": { - "custom": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/big-head-small.jpg" - }, - "skills": { - "enrichment": ["Project Management", "Research", "Leadership", "Innovation"] - }, - "languages": { - "enrichment": ["English", "Spanish"] - }, - "programmingLanguages": { - "enrichment": ["Java", "Python", "JavaScript", "C++"] - }, - "awards": { - "enrichment": [ - "Hooli's most innovative employee of the year (2016)", - "Hooli's most innovative employee of the year (2017)" - ] - }, - "expertise": { - "enrichment": ["Innovation", "Project Management", "Research"] - }, - "country": { - "enrichment": "United States" - }, - "yearsOfExperience": { - "enrichment": 5 - }, - "seniorityLevel": { - "enrichment": "Senior" - }, - "education": { - "enrichment": [ - { - "campus": "Stanford University", - "major": "Computer Science", - "specialization": "", - "startDate": "01/01/2011", - "endDate": "01/01/2015" - }, - { - "campus": "UC Berkeley", - "major": "Business Administration", - "specialization": "Entrepreneurship", - "startDate": "01/01/2015", - "endDate": "01/01/2016" - } - ] - }, - "workExperiences": { - "enrichment": [ - { - "title": "Executive", - "company": "Hooli", - "location": "Silicon Valley", - "startDate": "01/01/2016", - "endDate": "Present" - }, - { - "title": "Intern", - "company": "Hooli", - "location": "Silicon Valley", - "startDate": "01/01/2015", - "endDate": "01/01/2016" - } - ] - } - }, - "organizations": [ - { - "name": "Hooli", - "website": "https://hooli.xyz", - "logo": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/hooli-logo.png", - "description": "Hooli is an international corporation founded by Gavin Belson and Peter Gregory", - "emails": ["gavin@hooli.xyz", "bighead@hooli.xyz"], - "tags": ["hooli", "tethics", "not-google"], - "twitter": { - "handle": "hooli", - "bio": "Hooli is an international corporation founded by Gavin Belson and Peter Gregory", - "followers": 500000, - "following": 0, - "location": "Menlo Park" - }, - "employees": 4000, - "revenueRange": { "min": 100, "max": 500 }, - "linkedin": { "handle": "company/hooli" }, - "crunchbase": { "handle": "company/hooli" } - } - ], - "email": "bighead@hooli.xyz", - "score": 5, - "reach": { "twitter": 401, "github": 5, "total": 406 }, - "notes": [ - "Big Head is a dreamer, with great ideas but often lacks the follow-through to execute them", - "He is a loyal friend and willing to help those in need, even at his own expense" - ], - "tags": ["dreamer", "innovator", "researcher"], - "openSourceContributions": [ - { - "id": 112529473, - "url": "https://github.com/bighead/silicon-valley", - "topics": ["TV Shows", "Comedy", "Startups"], - "summary": "Silicon Valley: 50 commits in 2 weeks", - "numberCommits": 50, - "lastCommitDate": "02/01/2023", - "firstCommitDate": "01/17/2023" - }, - { - "id": 112529474, - "url": "https://github.com/bighead/startup-ideas", - "topics": ["Ideas", "Startups"], - "summary": "Startup Ideas: 20 commits in 1 week", - "numberCommits": 20, - "lastCommitDate": "03/01/2023", - "firstCommitDate": "02/22/2023" - }, - { - "id": 112529475, - "url": "https://github.com/bighead/pied-piper-api", - "topics": ["API", "data", "compression"], - "summary": "Pied Piper API: 30 commits in 2 weeks", - "numberCommits": 30, - "lastCommitDate": "02/10/2023", - "firstCommitDate": "01/26/2023" - }, - { - "id": 112529477, - "url": "https://github.com/bighead/hooli-analysis", - "topics": ["Data", "Analysis", "data"], - "summary": "Hooli Analysis: 15 commits in 2 weeks", - "numberCommits": 15, - "lastCommitDate": "02/20/2023", - "firstCommitDate": "02/05/2023" - } - ] - }, - { - "displayName": "Dinesh Chugtai", - "username": { - "twitter": "dinesh", - "discord": "dineshish", - "github": "dinesh", - "devto": "dinesh", - "linkedin": "dinesh" - }, - "attributes": { - "bio": { - "twitter": "VP of Engineering at Pied Piper, the best Tesla driver", - "github": "VP of Engineering at Pied Piper", - "custom": "VP of Engineering at Pied Piper. Let's get insane" - }, - "jobTitle": { - "custom": "VP of Engineering" - }, - "location": { - "github": "Silicon Valley" - }, - "avatarUrl": { - "custom": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/dinesh-small.jpg" - }, - "skills": { - "enrichment": ["coding", "data analysis", "project management"] - }, - "languages": { - "enrichment": ["English", "Hindi", "Urdu"] - }, - "programmingLanguages": { - "enrichment": ["Python", "Java", "JavaScript"] - }, - "awards": { - "enrichment": [ - "Silicon Valley Tech Innovator Award", - "IEEE Computer Society Award for Outstanding Paper", - "Best Presentation Award at the International Conference on Advanced Computing and Communications" - ] - }, - "seniorityLevel": { - "enrichment": "Senior" - }, - "expertise": { - "enrichment": ["Machine Learning", "Data Science", "Agile Development", "Team Leadership"] - }, - "country": { - "enrichment": "Pakistan" - }, - "yearsOfExperience": { - "enrichment": 7 - }, - "education": { - "enrichment": [ - { - "campus": "Stanford University", - "major": "Computer Science", - "specialization": "Artificial Intelligence", - "startDate": "2011-09-15", - "endDate": "2015-06-15" - } - ] - }, - "workExperiences": { - "enrichment": [ - { - "title": "VP of Engineering", - "company": "Pied Piper", - "location": "Silicon Valley", - "startDate": "2015-07-15", - "endDate": "Present" - }, - { - "title": "Senior Software Engineer", - "company": "Hooli", - "location": "Silicon Valley", - "startDate": "2013-06-15", - "endDate": "2015-06-15" - } - ] - } - }, - "organizations": [ - { - "name": "Pied Piper", - "website": "https://piedpiper.io", - "logo": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/pied-piper-logo.jpg", - "description": "Pied Piper is a data compression platform founded by RichardHendricks", - "emails": ["richard@piedpiper.io", "gilfoyle@piedpiper.io", "erlich@piedpier.io"], - "tags": ["Data Compression", "Startup", "Silicon Valley"], - "twitter": { - "handle": "piedpiper", - "bio": "Pied Piper is a data compression platform founded by Richard Hendricks", - "followers": 17000, - "following": 5, - "location": "Menlo Park" - }, - "employees": 20, - "revenueRange": { - "min": 0, - "max": 1 - }, - "linkedin": { - "handle": "company/pied-piper" - }, - "crunchbase": { - "handle": "company/pied-piper" - } - } - ], - "email": "dinesh@piedpiper.io", - "score": 9, - "reach": { - "twitter": 382, - "github": 5, - "total": 406 - }, - "tags": ["Java", "Greedy", "Tesla owner"], - "notes": ["Great problem solver", "Quick learner", "Attention to detail"], - "openSourceContributions": [ - { - "id": 1, - "url": "https://github.com/rhendricks/piedpiper", - "topics": ["compression", "data", "middle-out", "Java"], - "summary": "Pied Piper: 15 commits in 1 day", - "numberCommits": 25, - "lastCommitDate": "2023-03-10", - "firstCommitDate": "2023-03-09" - }, - { - "id": 2, - "url": "https://github.com/rhendricks/piperchat", - "topics": ["messaging", "chat", "security", "compression", "decentralized", "Java"], - "summary": "PiperChat: 10 commits in 1 day", - "numberCommits": 50, - "lastCommitDate": "2023-03-08", - "firstCommitDate": "2023-03-07" - }, - { - "id": 112529472, - "url": "https://github.com/dinesh/distributed-file-system", - "topics": ["distributed systems", "file system", "Java"], - "summary": "Distributed File System: 20 commits in 1 week", - "numberCommits": 20, - "lastCommitDate": "2022-03-05", - "firstCommitDate": "2022-02-27" - }, - { - "id": 112529474, - "url": "https://github.com/dinesh/auto-scaling", - "topics": ["cloud computing", "Java"], - "summary": "Auto-Scaling: 5 commits in 1 day", - "numberCommits": 5, - "lastCommitDate": "2021-12-28", - "firstCommitDate": "2021-12-28" - }, - { - "id": 112529475, - "url": "https://github.com/dinesh/personal-website", - "topics": ["personal website", "JavaScript"], - "summary": "Personal Website: 2 commits in 1 week", - "numberCommits": 2, - "lastCommitDate": "2021-11-02", - "firstCommitDate": "2021-10-27" - }, - { - "id": 112529476, - "url": "https://github.com/dinesh/remote-teams", - "topics": ["remote work", "team management", "Java"], - "summary": "Remote Teams: 6 commits in 2 days", - "numberCommits": 6, - "lastCommitDate": "2021-09-20", - "firstCommitDate": "2021-09-18" - } - ] - }, - { - "displayName": "Peter Gregory", - "username": { - "twitter": "petgregory", - "discord": "pgregory", - "github": "pgregory", - "devto": "thegregory", - "linkedin": "pgregory" - }, - "attributes": { - "bio": { - "twitter": "Founder of Raviga Capital, investor in @piedpiper", - "github": "Investor", - "custom": "Founder & Investor at Raviga Capital" - }, - "jobTitle": { "custom": "Founder & Investor" }, - "location": { "github": "Silicon Valley" }, - "avatarUrl": { - "custom": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/peter-small.jpg" - }, - "skills": { - "enrichment": ["venture capital", "investments", "startups", "leadership", "management"] - }, - "languages": { - "enrichment": ["English", "French", "Spanish"] - }, - "programmingLanguages": { - "enrichment": ["JavaScript", "Python", "Java"] - }, - "awards": { - "enrichment": ["Forbes 30 under 30", "Best VC Investor of the Year"] - }, - "seniorityLevel": { - "enrichment": "Senior" - }, - "expertise": { - "enrichment": ["Mentorship", "Team building", "Negotiation", "Strategic thinking"] - }, - "country": { - "enrichment": "USA" - }, - "yearsOfExperience": { - "enrichment": 25 - }, - "education": { - "enrichment": [ - { - "campus": "Harvard Business School", - "major": "Business Administration", - "specialization": "Venture Capital", - "startDate": "2010-09-01", - "endDate": "2012-05-01" - }, - { - "campus": "Stanford University", - "major": "Computer Science", - "specialization": "Artificial Intelligence", - "startDate": "2006-09-01", - "endDate": "2010-05-01" - } - ] - }, - "workExperiences": { - "enrichment": [ - { - "title": "Founder & Investor", - "company": "Raviga Capital", - "location": "Silicon Valley", - "startDate": "2012-06-01", - "endDate": "Present" - }, - { - "title": "Associate", - "company": "Kleiner Perkins", - "location": "Silicon Valley", - "startDate": "2010-06-01", - "endDate": "2012-05-01" - } - ] - } - }, - "organizations": [ - { - "name": "Raviga Capital", - "website": "https://raviga.vc", - "logo": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/raviga-capital-logo.jpg", - "description": "Raviga Capital is a bay area VC firm founded by Peter Gregory", - "emails": ["peter@raviga.vc"], - "tags": ["vc", "gregory"], - "twitter": { - "handle": "ravigavc", - "bio": "Raviga Capital is a bay area VC firm founded by Peter Gregory", - "followers": 3472, - "following": 120, - "location": "Menlo Park" - }, - "employees": 3, - "revenueRange": { "min": 5, "max": 20 }, - "linkedin": { "handle": "company/raviga-capital" }, - "crunchbase": { "handle": "company/raviga-capital" } - } - ], - "email": "peter@raviga.vc", - "score": 3, - "reach": { "twitter": 3000, "github": 7, "total": 3007 }, - "tags": [ - "VC investor", - "startups", - "business", - "leadership", - "management", - "mentorship", - "team building", - "negotiation", - "strategic thinking" - ], - "notes": [ - "Peter is a visionary investor with great experience in the startup world", - "He has a knack for spotting the next big thing, and his guidance is invaluable to any entrepreneur", - "His negotiation skills are top-notch, and he can turn a difficult situation into a win-win" - ], - "openSourceContributions": [ - { - "id": 112529475, - "url": "https://github.com/raviga/awesome-compression-companies", - "topics": ["internet", "awesome-list", "search"], - "summary": "Awesome compression companies: 10 commits in 10 days", - "numberCommits": 12, - "lastCommitDate": "2003-03-02", - "firstCommitDate": "2003-03-02" - }, - { - "id": 112529475, - "url": "https://github.com/raviga/awesome-college-alternatives", - "topics": ["startup", "awesome-list", "college"], - "summary": "Awesome college alternatives: 10 commits in 10 days", - "numberCommits": 17, - "lastCommitDate": "2003-03-02", - "firstCommitDate": "2003-03-02" - }, - { - "id": 112529475, - "url": "https://github.com/raviga/awesome-startups", - "topics": ["startup", "awesome-list", "business"], - "summary": "Awesome startups: 10 commits in 10 days", - "numberCommits": 10, - "lastCommitDate": "2003-03-02", - "firstCommitDate": "2003-03-02" - }, - { - "id": 112529474, - "url": "https://github.com/pgregory/hooli", - "topics": ["search", "python", "internet"], - "summary": "hooli: 23 commits in 1 day", - "numberCommits": 23, - "lastCommitDate": "2000-03-05", - "firstCommitDate": "2000-03-05" - } - ] - }, - { - "displayName": "Monica Hall", - "username": { - "twitter": "monicahall", - "discord": "monimoni", - "github": "monicahall", - "devto": "iamthemonica", - "linkedin": "monicahall" - }, - "attributes": { - "bio": { - "twitter": "VC at Raviga Capital, mother of pipers", - "github": "Investing in companies with superior technology", - "custom": "VC at Raviga Capital" - }, - "jobTitle": { "custom": "VC" }, - "location": { "github": "Silicon Valley" }, - "avatarUrl": { - "custom": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/monica-small.jpg" - }, - "skills": { - "enrichment": ["venture capital", "investing", "due diligence"] - }, - "languages": { - "enrichment": ["English", "Spanish"] - }, - "programmingLanguages": { - "enrichment": ["Python", "JavaScript", "Java"] - }, - "awards": { - "enrichment": ["Forbes 30 under 30", "Women in Venture Capital"] - }, - "seniorityLevel": { - "enrichment": "Senior" - }, - "expertise": { - "enrichment": ["strategic planning", "relationship building", "public speaking"] - }, - "country": { - "enrichment": "USA" - }, - "yearsOfExperience": { - "enrichment": 5 - }, - "education": { - "enrichment": [ - { - "campus": "Stanford University", - "major": "Economics", - "specialization": "Finance", - "startDate": "2010", - "endDate": "2014" - }, - { - "campus": "Stanford Graduate School of Business", - "major": "Master of Business Administration", - "specialization": "Entrepreneurship", - "startDate": "2014", - "endDate": "2016" - } - ] - }, - "workExperiences": { - "enrichment": [ - { - "title": "VC Partner", - "company": "Raviga Capital", - "location": "Silicon Valley", - "startDate": "2016", - "endDate": "Present" - }, - { - "title": "Investment Banking Analyst", - "company": "Goldman Sachs", - "location": "New York City", - "startDate": "2014", - "endDate": "2016" - } - ] - } - }, - "organizations": [ - { - "name": "Raviga Capital", - "website": "https://raviga.vc", - "logo": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/raviga-capital-logo.jpg", - "description": "Raviga Capital is a bay area VC firm founded by Peter Gregory", - "emails": ["peter@raviga.vc"], - "tags": ["vc", "gregory"], - "twitter": { - "handle": "ravigavc", - "bio": "Raviga Capital is a bay area VC firm founded by Peter Gregory", - "followers": 3472, - "following": 120, - "location": "Menlo Park" - }, - "employees": 3, - "revenueRange": { "min": 5, "max": 20 }, - "linkedin": { "handle": "company/raviga-capital" }, - "crunchbase": { "handle": "company/raviga-capital" } - } - ], - "email": "monica@raviga.vc", - "score": 6, - "reach": { "twitter": 1417, "github": 3, "total": 1420 }, - "tags": ["VC", "investor", "Raviga Capital"], - "notes": [ - "Monica is an expert in venture capital investing", - "She has a great track record of investing in technology startups", - "Monica is a great public speaker and an asset to any conference panel" - ], - "openSourceContributions": [ - { - "id": 112529475, - "url": "https://github.com/raviga/awesome-compression-companies", - "topics": ["internet", "awesome-list", "search"], - "summary": "Awesome compression companies: 20 commits in 10 days", - "numberCommits": 20, - "lastCommitDate": "2003-03-02", - "firstCommitDate": "2003-03-02" - }, - { - "id": 112529475, - "url": "https://github.com/raviga/awesome-college-alternatives", - "topics": ["startup", "awesome-list", "college"], - "summary": "Awesome college alternatives: 10 commits in 10 days", - "numberCommits": 10, - "lastCommitDate": "2003-03-02", - "firstCommitDate": "2003-03-02" - }, - { - "id": 112529475, - "url": "https://github.com/raviga/awesome-startups", - "topics": ["startup", "awesome-list", "business"], - "summary": "Awesome startups: 12 commits in 10 days", - "numberCommits": 12, - "lastCommitDate": "2003-03-02", - "firstCommitDate": "2003-03-02" - } - ] - }, - { - "displayName": "Jared Dunn", - "username": { - "twitter": "jared", - "discord": "jaredjared", - "github": "jaredjared", - "devto": "jaredjared", - "linkedin": "jaredjared" - }, - "attributes": { - "bio": { - "twitter": "COO at Pied Piper", - "github": "COO at Pied Piper", - "custom": "COO at Pied Piper" - }, - "jobTitle": { - "custom": "COO" - }, - "location": { - "github": "Silicon Valley" - }, - "avatarUrl": { - "custom": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/jared.jpg" - }, - "skills": { - "enrichment": ["Leadership", "Product management", "Startups"] - }, - "languages": { - "enrichment": ["English", "Spanish"] - }, - "programmingLanguages": { - "enrichment": ["JavaScript", "Ruby"] - }, - "awards": { - "enrichment": ["TechCrunch Disrupt Startup Battlefield Winner 2014"] - }, - "seniorityLevel": { - "enrichment": "Senior" - }, - "expertise": { - "enrichment": ["Product Management", "Entrepreneurship", "Marketing"] - }, - "country": { - "enrichment": "USA" - }, - "yearsOfExperience": { - "enrichment": 10 - }, - "education": { - "enrichment": [ - { - "campus": "Brown University", - "major": "Entrepreneurship", - "specialization": null, - "startDate": "2006-09-01", - "endDate": "2010-06-01" - } - ] - }, - "workExperiences": { - "enrichment": [ - { - "title": "COO", - "company": "Pied Piper", - "location": "Silicon Valley", - "startDate": "2014-06-01", - "endDate": "Present" - }, - { - "title": "Business Development Manager", - "company": "Hooli", - "location": "Silicon Valley", - "startDate": "2010-06-01", - "endDate": "2014-06-01" - } - ] - } - }, - "organizations": [ - { - "name": "Pied Piper", - "website": "https://piedpiper.io", - "logo": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/pied-piper-logo.jpg", - "description": "Pied Piper is a data compression platform founded by Richard Hendricks", - "emails": ["richard@piedpiper.io", "gilfoyle@piedpiper.io", "erlich@piedpier.io"], - "tags": ["pied piper", "data-compression", "not-hooli"], - "twitter": { - "handle": "piedpiper", - "bio": "Pied Piper is a data compression platform founded by Richard Hendricks", - "followers": 17000, - "following": 5, - "location": "Menlo Park" - }, - "employees": 20, - "revenueRange": { - "min": 0, - "max": 1 - }, - "linkedin": { - "handle": "company/pied-piper" - }, - "crunchbase": { - "handle": "company/pied-piper" - } - } - ], - "email": "jared@piedpiper.io", - "score": 7, - "reach": { - "twitter": 1417, - "github": 3, - "total": 1420 - }, - "tags": [ - "COO", - "Product Management", - "Entrepreneurship", - "Marketing", - "Stanford Alumni", - "Pied Piper", - "Silicon Valley" - ], - "notes": [ - "Jared is an experienced COO with a strong track record in product management and entrepreneurship. He is a Stanford alumni and has a deep understanding of the Silicon Valley ecosystem. He has been a valuable member of the Pied Piper team since its inception." - ], - "openSourceContributions": [ - { - "id": 112529475, - "url": "https://github.com/jaredjared/awesome-richard-quotes", - "topics": ["Startups", "CEO", "awesome-list", "Markdown"], - "summary": "Awesome Richard Quotes: 8 commits in 1 day", - "numberCommits": 8, - "lastCommitDate": "2022-02-10", - "firstCommitDate": "2022-02-09" - }, - { - "id": 112529476, - "url": "https://github.com/jaredjared/awesome-hbo-shows", - "topics": ["Markdown", "Lists", "awesome-list"], - "summary": "Awesome HBO Shows: 3 commits in 1 day", - "numberCommits": 3, - "lastCommitDate": "2022-02-08", - "firstCommitDate": "2022-02-08" - } - ] - }, - { - "displayName": "Gavin Belson", - "username": { - "twitter": "gavin", - "discord": "gavinbelson", - "github": "gavinbelson", - "devto": "thebelson", - "linkedin": "gavinbelson" - }, - "attributes": { - "bio": { - "twitter": "CEO at Hooli", - "github": "CEO at Hooli", - "custom": "CEO at Hooli" - }, - "jobTitle": { - "custom": "CEO" - }, - "location": { - "github": "Silicon Valley" - }, - "avatarUrl": { - "custom": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/gavin.jpg" - }, - "skills": { - "enrichment": ["leadership", "strategy", "product management"] - }, - "languages": { - "enrichment": ["English", "Mandarin"] - }, - "programmingLanguages": { - "enrichment": ["Java", "Python", "C++"] - }, - "awards": { - "enrichment": ["CEO of the Year", "Fortune 500 List"] - }, - "seniorityLevel": { - "enrichment": "Senior" - }, - "expertise": { - "enrichment": ["business strategy", "leadership", "public speaking"] - }, - "country": { - "enrichment": "USA" - }, - "yearsOfExperience": { - "enrichment": 20 - }, - "education": { - "enrichment": [ - { - "campus": "Stanford University", - "major": "Business Administration", - "specialization": "", - "startDate": "1985-09-01", - "endDate": "1987-06-01" - }, - { - "campus": "Dartmouth College", - "major": "Computer Science", - "specialization": "Artificial Intelligence", - "startDate": "1981-09-01", - "endDate": "1985-06-01" - } - ] - }, - "workExperiences": { - "enrichment": [ - { - "title": "Founder and CEO", - "company": "Hooli", - "location": "Silicon Valley", - "startDate": "1995-01-01", - "endDate": "2023-03-12" - } - ] - } - }, - "organizations": [ - { - "name": "Hooli", - "website": "https://hooli.xyz", - "logo": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/hooli-logo.png", - "description": "Hooli is an international corporation founded by Gavin Belson and Peter Gregory", - "emails": ["gavin@hooli.xyz", "bighead@hooli.xyz"], - "tags": [ - "CEO", - "Silicon Valley", - "Technology", - "Innovation", - "Business Strategy", - "Leadership" - ], - "twitter": { - "handle": "hooli", - "bio": "Hooli is an international corporation founded by Gavin Belson and Peter Gregory", - "followers": 500000, - "following": 0, - "location": "Menlo Park" - }, - "employees": 4000, - "revenueRange": { - "min": 100, - "max": 500 - }, - "linkedin": { - "handle": "company/hooli" - }, - "crunchbase": { - "handle": "company/hooli" - } - } - ], - "email": "ceo@hooli.xyz", - "score": 5, - "reach": { - "twitter": 130000, - "github": 517, - "total": 130517 - }, - "notes": [ - "Great leadership skills, but can be stubborn at times.", - "Has a vision for Hooli's future, and is not afraid to make bold moves.", - "Excellent public speaker, can inspire and motivate employees and stakeholders." - ], - "openSourceContributions": [ - { - "id": 112529472, - "url": "https://github.com/hooli/hooli-analytics", - "topics": ["analytics", "big data", "data visualization"], - "summary": "hooli-analytics: 25 commits in 1 day", - "numberCommits": 25, - "lastCommitDate": "2010-05-01", - "firstCommitDate": "2010-04-01" - }, - { - "id": 112529473, - "url": "https://github.com/hooli/hooli-cloud", - "topics": ["cloud computing", "distributed systems", "networking"], - "summary": "hooli-cloud: 12 commits in 1 day", - "numberCommits": 12, - "lastCommitDate": "2009-10-01", - "firstCommitDate": "2009-09-01" - }, - { - "id": 112529474, - "url": "https://github.com/hooli/hooli-search", - "topics": ["search engine", "information retrieval", "machine learning"], - "summary": "hooli-search: 10 commits in 1 day", - "numberCommits": 10, - "lastCommitDate": "2008-05-01", - "firstCommitDate": "2008-04-01" - }, - { - "id": 112529475, - "url": "https://github.com/hooli/hooli-storage", - "topics": ["distributed storage", "file systems", "data visualization"], - "summary": "hooli-storage: 15 commits in 1 day", - "numberCommits": 15, - "lastCommitDate": "2007-10-01", - "firstCommitDate": "2007-09-01" - }, - { - "id": 112529476, - "url": "https://github.com/hooli/hooli-security", - "topics": ["security", "cryptography", "authentication", "cloud computing"], - "summary": "hooli-security: 8 commits in 1 day", - "numberCommits": 8, - "lastCommitDate": "2006-06-01", - "firstCommitDate": "2006-05-01" - } - ] - }, - { - "displayName": "Jian-Yang", - "username": { - "twitter": "jianyang", - "discord": "jianyang", - "github": "thejian", - "devto": "jianyang", - "linkedin": "jianyang" - }, - "attributes": { - "bio": { - "twitter": "Living at Erlich's startup house", - "github": "Living at Erlich's startup house", - "custom": "Living at Erlich's startup house" - }, - "jobTitle": { - "custom": "Founder" - }, - "location": { - "github": "Silicon Valley" - }, - "avatarUrl": { - "custom": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/jian_yang.jpg" - }, - "skills": { - "enrichment": ["Coding", "Web development", "Product management"] - }, - "languages": { - "enrichment": ["Mandarin", "English"] - }, - "programmingLanguages": { - "enrichment": ["Python", "JavaScript", "HTML", "CSS"] - }, - "awards": { - "enrichment": ["Won the TechCrunch Disrupt Hackathon"] - }, - "seniorityLevel": { - "enrichment": "Junior" - }, - "expertise": { - "enrichment": ["Product development", "Leadership"] - }, - "country": { - "enrichment": "China" - }, - "yearsOfExperience": { - "enrichment": 2 - }, - "education": { - "enrichment": [ - { - "campus": "Stanford University", - "major": "Computer Science", - "specialization": "Artificial Intelligence", - "startDate": "2016-09", - "endDate": "2018-06" - }, - { - "campus": "Harvard University", - "major": "Economics", - "specialization": "", - "startDate": "2014-09", - "endDate": "2016-06" - } - ] - }, - "workExperiences": { - "enrichment": [ - { - "title": "CEO", - "company": "Seefood", - "location": "Silicon Valley", - "startDate": "2022-01", - "endDate": "Present" - }, - { - "title": "Product Manager", - "company": "Hot Dog or Not", - "location": "San Francisco", - "startDate": "2021-04", - "endDate": "2021-12" - }, - { - "title": "Erlich Impersonator", - "company": "Erlich Incubator", - "location": "Palo Alto", - "startDate": "2020-01", - "endDate": "2021-03" - } - ] - } - }, - "organizations": [ - { - "name": "See Food", - "website": "https://seefood.ml", - "logo": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/see-food-logo.jpeg", - "description": "Shazam for food", - "emails": ["jianyang@seefood.ml"], - "tags": ["seefood", "jian-yang"], - "twitter": { - "handle": "seefoodapp", - "bio": "Shazam for food", - "followers": 11, - "following": 500, - "location": "Menlo Park" - }, - "employees": 1, - "revenueRange": { - "min": 0, - "max": 10 - }, - "linkedin": { - "handle": "company/erlichincubator" - }, - "crunchbase": { - "handle": "company/erlichincubator" - } - } - ], - "email": "jianyang@seefood.ml", - "score": 4, - "reach": { - "twitter": 0, - "github": 0, - "total": 0 - }, - "tags": ["Seefood", "Hot Dog or Not", "Erlich Incubator", "Corrupt"], - "notes": ["He will do anything to get his way.", "Has a very corrupt uncle."], - "openSourceContributions": [ - { - "id": 123456, - "url": "https://github.com/seefood/food-recognition", - "topics": ["machine-learning", "computer-vision", "food"], - "summary": "Seefood: 50 commits in 2 days", - "numberCommits": 50, - "lastCommitDate": "2022-02-28", - "firstCommitDate": "2022-02-26" - }, - { - "id": 789012, - "url": "https://github.com/hotdogornot/image-classification", - "topics": ["machine-learning", "computer-vision", "hot-dogs"], - "summary": "Hot Dog or Not: 20 commits in 1 day", - "numberCommits": 20, - "lastCommitDate": "2021-11-30", - "firstCommitDate": "2021-11-29" - }, - { - "id": 345678, - "url": "https://github.com/seefood/api", - "topics": ["api", "backend", "food"], - "summary": "Seefood: 30 commits in 3 days", - "numberCommits": 30, - "lastCommitDate": "2021-10-31", - "firstCommitDate": "2021-10-29" - }, - { - "id": 901234, - "url": "https://github.com/hotdogornot/frontend", - "topics": ["frontend", "web-development", "hot-dogs"], - "summary": "Hot Dog or Not: 15 commits in 2 days", - "numberCommits": 15, - "lastCommitDate": "2021-09-30", - "firstCommitDate": "2021-09-29" - }, - { - "id": 567890, - "url": "https://github.com/seefood/deployment", - "topics": ["devops", "deployment", "food"], - "summary": "Seefood: 10 commits in 1 day", - "numberCommits": 10, - "lastCommitDate": "2021-08-31", - "firstCommitDate": "2021-08-30" - } - ] - }, - { - "displayName": "Laurie Bream", - "username": { - "twitter": "lauriebream", - "discord": "lrbream", - "github": "lauriebream", - "devto": "lauriebream", - "linkedin": "lauriebream" - }, - "attributes": { - "bio": { - "twitter": "VC and Partner at Raviga Capital", - "github": "VC and Partner at Raviga Capital", - "custom": "VC and Partner at Raviga Capital" - }, - "jobTitle": { "custom": "VC" }, - "location": { "github": "Silicon Valley" }, - "avatarUrl": { - "custom": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/laurie.jpg" - }, - "skills": { - "enrichment": ["venture capital", "business strategy"] - }, - "languages": { - "enrichment": ["English", "Spanish"] - }, - "programmingLanguages": { - "enrichment": ["Python", "Java"] - }, - "awards": { - "enrichment": ["Forbes 30 Under 30"] - }, - "seniorityLevel": { - "enrichment": "Senior" - }, - "expertise": { - "enrichment": ["negotiation", "financial analysis"] - }, - "country": { - "enrichment": "United States" - }, - "yearsOfExperience": { - "enrichment": 10 - }, - "education": { - "enrichment": [ - { - "campus": "Stanford University", - "major": "Business", - "specialization": "Finance", - "startDate": "2010-09-01", - "endDate": "2014-06-01" - }, - { - "campus": "Massachusetts Institute of Technology", - "major": "Computer Science", - "specialization": "Artificial Intelligence", - "startDate": "2008-09-01", - "endDate": "2010-06-01" - } - ] - }, - "workExperiences": { - "enrichment": [ - { - "title": "VC Investor", - "company": "Raviga Capital", - "location": "Silicon Valley", - "startDate": "2014-06-01", - "endDate": "Present" - }, - { - "title": "Senior Associate", - "company": "Bream Hall Capital", - "location": "New York City", - "startDate": "2012-06-01", - "endDate": "2014-05-01" - }, - { - "title": "Junior Analyst", - "company": "Yao Net", - "location": "Shenzhen, China", - "startDate": "2010-06-01", - "endDate": "2012-05-01" - } - ] - } - }, - "organizations": [ - { - "name": "Raviga Capital", - "website": "https://raviga.vc", - "logo": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/raviga-capital-logo.jpg", - "description": "Raviga Capital is a bay area VC firm founded by Peter Gregory", - "emails": ["peter@rraviga.vc"], - "tags": ["vc", "gregory"], - "twitter": { - "handle": "ravigavc", - "bio": "Raviga Capital is a bay area VC firm founded by Peter Gregory", - "followers": 3472, - "following": 120, - "location": "Menlo Park" - }, - "employees": 3, - "revenueRange": { "min": 5, "max": 20 }, - "linkedin": { "handle": "company/raviga-capital" }, - "crunchbase": { "handle": "company/raviga-capital" } - } - ], - "email": "laurie@raviga.vc", - "score": 6, - "reach": { "twitter": 714, "github": 4, "total": 718 }, - "tags": [ - "venture capitalist", - "business strategist", - "negotiator", - "potential robot", - "Forbes 30 Under 30" - ], - "notes": [ - "Laurie Bream is known for her robotic personality and exceptional negotiation skills." - ], - "openSourceContributions": [ - { - "id": 112529472, - "url": "https://github.com/sindresorhus/awesome-nodejs", - "topics": ["nodejs", "javascript", "awesome"], - "summary": "sindresorhus/awesome-nodejs: 10 commits in 1 day", - "numberCommits": 10, - "lastCommitDate": "2022-02-01T23:59:59Z", - "firstCommitDate": "2022-02-01T00:00:00Z" - }, - { - "id": 112529473, - "url": "https://github.com/sindresorhus/awesome-python", - "topics": ["python", "awesome"], - "summary": "sindresorhus/awesome-python: 5 commits in 1 day", - "numberCommits": 5, - "lastCommitDate": "2022-01-01T23:59:59Z", - "firstCommitDate": "2022-01-01T00:00:00Z" - }, - { - "id": 112529474, - "url": "https://github.com/sindresorhus/awesome-ai", - "topics": ["ai", "machine-learning", "awesome"], - "summary": "sindresorhus/awesome-ai: 7 commits in 1 day", - "numberCommits": 7, - "lastCommitDate": "2021-12-01T23:59:59Z", - "firstCommitDate": "2021-12-01T00:00:00Z" - }, - { - "id": 112529475, - "url": "https://github.com/sindresorhus/awesome-tech-blogs", - "topics": ["tech-blogs", "awesome"], - "summary": "sindresorhus/awesome-tech-blogs: 6 commits in 1 day", - "numberCommits": 6, - "lastCommitDate": "2021-11-01T23:59:59Z", - "firstCommitDate": "2021-11-01T00:00:00Z" - }, - { - "id": 112529476, - "url": "https://github.com/sindresorhus/awesome-web-development", - "topics": ["web-development", "javascript", "awesome"], - "summary": "sindresorhus/awesome-web-development: 8 commits in 1 day", - "numberCommits": 8, - "lastCommitDate": "2021-10-01T23:59:59Z", - "firstCommitDate": "2021-10-01T00:00:00Z" - } - ] - }, - { - "displayName": "Russ Hanneman", - "username": { - "twitter": "russhanneman", - "discord": "russhanneman", - "github": "russhanneman", - "devto": "russhanneman", - "linkedin": "russhanneman" - }, - "attributes": { - "bio": { - "twitter": "The billionaire, inventor of Radio on Internet", - "github": "The billionaire, ROI = Radio on Internet", - "custom": "Investor in Pied Piper, CEO at 3 Commas. Member of the three-comma club." - }, - "jobTitle": { "custom": "CEO" }, - "location": { "github": "Silicon Valley" }, - "avatarUrl": { - "custom": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/russ.jpg" - }, - "skills": { - "enrichment": ["Entrepreneurship", "Investing", "Marketing"] - }, - "languages": { - "enrichment": ["English"] - }, - "programmingLanguages": { - "enrichment": ["none"] - }, - "awards": { - "enrichment": ["Three-Comma Club"] - }, - "seniorityLevel": { - "enrichment": "Senior" - }, - "expertise": { - "enrichment": ["Business Strategy", "Sales"] - }, - "country": { - "enrichment": "United States" - }, - "yearsOfExperience": { - "enrichment": 20 - }, - "education": { - "enrichment": [ - { - "campus": "Wharton School of Business", - "major": "Business Administration", - "specialization": "Entrepreneurship", - "startDate": "1980-09-01", - "endDate": "1982-05-01" - }, - { - "campus": "New York University", - "major": "Business Administration", - "specialization": "Finance", - "startDate": "1978-09-01", - "endDate": "1980-05-01" - } - ] - }, - "workExperiences": { - "enrichment": [ - { - "title": "CEO", - "company": "Radio on Internet", - "location": "Silicon Valley", - "startDate": "1995-01-01", - "endDate": "2002-06-01" - }, - { - "title": "Investor", - "company": "Russ investing", - "location": "Silicon Valley", - "startDate": "2002-07-01", - "endDate": "2010-05-01" - }, - { - "title": "CEO", - "company": "Russfest", - "location": "Austin, TX", - "startDate": "2010-06-01", - "endDate": "2014-12-01" - }, - { - "title": "CEO", - "company": "3 commas", - "location": "Silicon Valley", - "startDate": "2015-01-01", - "endDate": "Present" - } - ] - } - }, - "organizations": [ - { - "name": "3 commas", - "website": "https://3commas.roi", - "logo": "https://i.pinimg.com/originals/ca/5a/9c/ca5a9cddc380c3413fa58a155710369c.png", - "description": "The tequila for members of the three-comma club.", - "emails": ["russ@3commas.roi"], - "tags": ["Tequila", "Billionaire", "Entrepreneur", "Investor"], - "twitter": { - "handle": "3commas", - "bio": "The tequila for members of the three-comma club.", - "followers": 10000, - "following": 1, - "location": "Menlo Park" - }, - "employees": 20, - "revenueRange": { "min": 1, "max": 20 }, - "linkedin": { "handle": "company/3-commas" }, - "crunchbase": { "handle": "company/3-commas" } - } - ], - "email": "me@russhanneman.com", - "score": 4, - "reach": { "twitter": 618, "github": 3, "total": 621 }, - "tags": ["Tequila", "Billionaire", "Entrepreneur", "Investor"], - "notes": [ - "Member of the three-comma club", - "Enjoys Tequila", - "Believes companies should not go after revenue" - ], - "openSourceContributions": [ - { - "id": 1, - "url": "https://github.com/sindresorhus/awesome-money", - "topics": ["money", "finance", "awesome-list"], - "summary": "Awesome Money: 5 commits in 1 day", - "numberCommits": 5, - "lastCommitDate": "2023-03-11T16:30:00Z", - "firstCommitDate": "2023-03-11T10:00:00Z" - }, - { - "id": 3, - "url": "https://github.com/sgryjp/gourmet-coffee", - "topics": ["coffee", "recipes", "awesome-list"], - "summary": "Gourmet Coffee: 8 commits in 1 day", - "numberCommits": 8, - "lastCommitDate": "2023-03-09T17:30:00Z", - "firstCommitDate": "2023-03-09T10:00:00Z" - }, - { - "id": 7, - "url": "https://github.com/Tech-at-DU/Outfits", - "topics": ["fashion", "outfits", "awesome-list"], - "summary": "Outfits: 7 commits in 1 day", - "numberCommits": 7, - "lastCommitDate": "2023-03-05T15:30:00Z", - "firstCommitDate": "2023-03-05T08:00:00Z" - }, - { - "id": 8, - "url": "https://github.com/josephearl/tequila-recipes", - "topics": ["Tequila", "recipes", "awesome-list"], - "summary": "Tequila Recipes: 4 commits in 1 day", - "numberCommits": 4, - "lastCommitDate": "2023-03-04T14:30:00Z", - "firstCommitDate": "2023-03-04T10:00:00Z" - } - ] - }, - { - "displayName": "Jack Barker", - "username": { - "twitter": "barker", - "discord": "thebarker", - "github": "barker", - "devto": "iambarker", - "linkedin": "barker" - }, - "attributes": { - "bio": { - "twitter": "Head of development at Hooli | ex CEO at Pied Piper", - "github": "Head of development at Hooli", - "custom": "Head of development at Hooli" - }, - "jobTitle": { "custom": "Head of development" }, - "location": { "github": "Silicon Valley" }, - "avatarUrl": { - "custom": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/jack-barker-best.jpg" - }, - "skills": { - "enrichment": ["Leadership", "Management", "Product development", "Software engineering"] - }, - "languages": { - "enrichment": ["English"] - }, - "programmingLanguages": { - "enrichment": ["Java", "C++", "Python"] - }, - "awards": { - "enrichment": ["Tech Pioneer Award", "Entrepreneur of the Year Award"] - }, - "seniorityLevel": { - "enrichment": "Senior" - }, - "expertise": { - "enrichment": ["Product management", "Strategy", "Leadership", "Team building"] - }, - "country": { - "enrichment": "United States" - }, - "yearsOfExperience": { - "enrichment": 25 - }, - "education": { - "enrichment": [ - { - "campus": "Harvard Business School", - "major": "Business Administration", - "specialization": "", - "startDate": "1995-09-01", - "endDate": "1997-06-01" - } - ] - }, - "workExperiences": { - "enrichment": [ - { - "title": "Head of Development", - "company": "Hooli", - "location": "Silicon Valley", - "startDate": "2000-01-01", - "endDate": "2016-05-01" - }, - { - "title": "CEO", - "company": "Pied Piper", - "location": "Palo Alto", - "startDate": "2016-05-01", - "endDate": "2016-09-01" - }, - { - "title": "Consultant", - "company": "Intersite", - "location": "New York", - "startDate": "2016-09-01", - "endDate": "2017-12-01" - }, - { - "title": "CEO", - "company": "Barker Consulting", - "location": "San Francisco", - "startDate": "2018-01-01", - "endDate": "Present" - } - ] - } - }, - "organizations": [ - { - "name": "Hooli", - "website": "https://hooli.xyz", - "logo": "https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/hooli-logo.png", - "description": "Hooli is an international corporation founded by Gavin Belson and Peter Gregory", - "emails": ["gavin@hooli.xyz", "bighead@hooli.xyz"], - "tags": ["Tech", "Software Development", "Product Management", "Silicon Valley"], - "twitter": { - "handle": "hooli", - "bio": "Hooli is an international corporation founded by Gavin Belson and Peter Gregory", - "followers": 500000, - "following": 0, - "location": "Menlo Park" - }, - "employees": 4000, - "revenueRange": { "min": 100, "max": 500 }, - "linkedin": { "handle": "company/hooli" }, - "crunchbase": { "handle": "company/hooli" } - } - ], - "email": "barker@hooli.xyz", - "score": 3, - "reach": { "twitter": 819, "github": 11, "total": 830 }, - "tags": ["Entrepreneur", "Product Manager", "Senior Executive"], - "notes": [ - "Impressive background in software engineering and product development", - "Known for being a tough but effective leader", - "Not afraid to take risks and try new things", - "Inventor of the conjoined triangles of success." - ], - "openSourceContributions": [] - } - ], - "conversations": [ - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-19T18:50:47", - "title": "Jared about his dancing skills", - "type": "comment", - "body": "Oh, I was the sloppy girl in the discotheque, just twirling and twirling like the night would never end.", - "sourceId": "mqngwkgehkjygpu", - "platform": "linkedin", - "sentiment": { - "label": "negative", - "mixed": 10.912733525037766, - "neutral": 32.68730938434601, - "negative": 42.923060059547424, - "positive": 13.4769007563591, - "sentiment": 35 - } - } - ], - [ - { - "member": "Big Head", - "timestamp": "2022-09-23T13:41:14", - "title": "How to lose money 101 from Big Head", - "channel": "pied-piper", - "type": "comment", - "body": "Jian-Yang won the house from me in a game of chance. He told me to pick a number between one and 10. I picked seven, um, but it was three. Eh, you live, you learn.", - "sourceId": "umpcecilwlwxxah", - "platform": "devto", - "sentiment": { - "label": "negative", - "mixed": 0.018997464212588966, - "neutral": 39.14429843425751, - "negative": 56.37732148170471, - "positive": 4.459377005696297, - "sentiment": 24 - } - } - ], - [ - { - "member": "Jian-Yang", - "timestamp": "2022-09-27T04:36:26", - "title": "Jian-Yang investing in Pied Piper?", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "Richard, congratulations. It's your very close friend Jian-Yang, and I would like you to give me free shares of Pied Piper.", - "sourceId": "ejtdqhgrcerhtca", - "platform": "github", - "sentiment": { - "label": "positive", - "mixed": 0.002208236765000038, - "neutral": 11.2838514149189, - "negative": 0.05321023636497557, - "positive": 88.66073489189148, - "sentiment": 94 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-27T04:39:56", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Okay, Jian-Yang. Uh... Well, you had plenty of opportunity to invest. Still do.", - "sourceId": "ejtdqhgrcerhtca-1", - "sourceParentId": "ejtdqhgrcerhtca", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.06609517149627209, - "neutral": 54.084110260009766, - "negative": 1.2081141583621502, - "positive": 44.64167654514313, - "sentiment": 72 - } - }, - { - "member": "Monica Hall", - "timestamp": "2022-09-29T04:45:22", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Yeah, you can buy Pipercoin.", - "sourceId": "ejtdqhgrcerhtca-2", - "sourceParentId": "ejtdqhgrcerhtca", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.6973115727305412, - "neutral": 58.897143602371216, - "negative": 2.9950598254799843, - "positive": 37.41048276424408, - "sentiment": 67 - } - }, - { - "member": "Jian-Yang", - "timestamp": "2022-09-27T04:46:57", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Monica, you can find your broom and fly away.", - "sourceId": "ejtdqhgrcerhtca-3", - "sourceParentId": "ejtdqhgrcerhtca", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 1.2881138361990452, - "neutral": 75.60424208641052, - "negative": 14.031411707401276, - "positive": 9.076234698295593, - "sentiment": 48 - } - } - ], - [ - { - "member": "Monica Hall", - "timestamp": "2022-09-28T07:31:24", - "title": "Erlich is rich?", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-opened", - "body": "I just checked the ledger, and the coin we issued to Erlich was just sold for $20 million.", - "sourceId": "odhkgjfnujttzmm", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 6.772719323635101, - "neutral": 79.81706261634827, - "negative": 10.221465677022934, - "positive": 3.1887494027614594, - "sentiment": 46 - } - }, - { - "member": "Jian-Yang", - "timestamp": "2022-09-20T07:36:23", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Richard, is the mean lady right? Errich is now fat and rich?", - "sourceId": "odhkgjfnujttzmm-1", - "sourceParentId": "odhkgjfnujttzmm", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 2.335432358086109, - "neutral": 35.48414707183838, - "negative": 61.894381046295166, - "positive": 0.2860414329916239, - "sentiment": 19 - } - } - ], - [ - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-22T05:43:24", - "title": "Bolt the doors!", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "Jared, bolt the doors.", - "sourceId": "zdsacaymnvkxbdz", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.21679976489394903, - "neutral": 48.68084788322449, - "negative": 42.73555874824524, - "positive": 8.366794139146805, - "sentiment": 33 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-22T05:44:26", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "What's in the bag?", - "sourceId": "zdsacaymnvkxbdz-1", - "sourceParentId": "zdsacaymnvkxbdz", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 3.560888394713402, - "neutral": 60.996198654174805, - "negative": 32.39606022834778, - "positive": 3.046857565641403, - "sentiment": 35 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-22T05:47:09", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Clif bars and a gun.", - "sourceId": "zdsacaymnvkxbdz-2", - "sourceParentId": "zdsacaymnvkxbdz", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 2.0866338163614273, - "neutral": 51.762133836746216, - "negative": 37.33446002006531, - "positive": 8.816774189472198, - "sentiment": 36 - } - } - ], - [ - { - "member": "Gavin Belson", - "timestamp": "2022-09-22T14:25:22", - "type": "comment", - "title": "Pied-Piper on TechCrunch Disrupt", - "body": "Since leaving Hooli, I've co-authored 37 adult romance novels. Fondly, Margeaux. The Lighthouse Dancer. Cold Ice Cream and Hot Kisses. Over here, The Prince of Puget Sound. Uh, and lastly, His Hazel Glance. All international best sellers.", - "sourceId": "tpelkkzsmkyjvgj", - "platform": "linkedin", - "sentiment": { - "label": "positive", - "mixed": 0.003578843461582437, - "neutral": 40.651336312294006, - "negative": 0.6030191201716661, - "positive": 58.74205827713013, - "sentiment": 79 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-29T23:24:36", - "title": "Fix Tesla Security", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-opened", - "body": "What encryption does Tesla use?", - "sourceId": "vjjfkzxszrfbedm", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.9510484524071217, - "neutral": 96.41349911689758, - "negative": 1.0494454763829708, - "positive": 1.5860069543123245, - "sentiment": 50 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-29T23:26:01", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Curve 25519, the most secure... discrete log parameter there is.", - "sourceId": "vjjfkzxszrfbedm-1", - "sourceParentId": "vjjfkzxszrfbedm", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.22485184017568827, - "neutral": 63.31318020820618, - "negative": 0.39954339154064655, - "positive": 36.06242537498474, - "sentiment": 68 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-29T23:33:08", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Not anymore. Our network just blew it apart.", - "sourceId": "vjjfkzxszrfbedm-2", - "sourceParentId": "vjjfkzxszrfbedm", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.4373734351247549, - "neutral": 5.151301622390747, - "negative": 93.68823766708374, - "positive": 0.7230798713862896, - "sentiment": 4 - } - } - ], - [ - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-17T04:16:16", - "title": "Professional failer", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "Your entire life has prepared you to publicly fail. You're just failing to see that right now.", - "sourceId": "hjcttzjdndyjzwi", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 4.1228726506233215, - "neutral": 7.5457364320755005, - "negative": 86.21801137924194, - "positive": 2.1133774891495705, - "sentiment": 8 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-17T04:20:54", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Don't insult me. I can fail circles around you losers.", - "sourceId": "hjcttzjdndyjzwi-1", - "sourceParentId": "hjcttzjdndyjzwi", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 31.213977932929993, - "neutral": 10.389739274978638, - "negative": 56.378328800201416, - "positive": 2.0179569721221924, - "sentiment": 23 - } - } - ], - [ - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-21T15:36:48", - "title": "Courageous act of cowardice", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "I'm gonna be honest with you. No offense to me, but I am greedy and unreliable, bordering on piece of. If there is a chance to stop you guys from stopping you guys, I will do it. I will sabotage your sabotage. So, if this company needs to fail epically... you need to do it without me. Revoke my permissions. Delete my PiperMail account. I will use basic Gmail like a looser. Don't let me anywhere near that launch. I may beg, and I will lie to you. I cannot bribe you because I don't have any money. But I am too much of a liability.", - "sourceId": "jafdxsgzseoijih", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.009977679292205721, - "neutral": 0.34414262045174837, - "negative": 99.5992124080658, - "positive": 0.04666761087719351, - "sentiment": 0 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-21T15:42:57", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "That is the most courageous act of cowardice I've ever seen.", - "sourceId": "jafdxsgzseoijih-1", - "sourceParentId": "jafdxsgzseoijih", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 15.588821470737457, - "neutral": 9.64624509215355, - "negative": 41.13299250602722, - "positive": 33.63194465637207, - "sentiment": 46 - } - } - ], - [ - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-13T21:08:07", - "channel": "random", - "type": "message", - "body": "Dinesh's car is at the Wendy's drive-thru. Anybody hungry?", - "sourceId": "ckmwoolclercknn", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.006576597661478445, - "neutral": 96.56497240066528, - "negative": 0.6479424424469471, - "positive": 2.7805082499980927, - "sentiment": 51 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-11T19:01:29", - "title": "We are billioners?", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "Okay, so we're generating noise, but just not enough to interfere with anything?", - "sourceId": "solwrikjhspannj", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 6.300315260887146, - "neutral": 62.47087121009827, - "negative": 26.867729425430298, - "positive": 4.361085966229439, - "sentiment": 39 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-11T19:03:05", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Is this gonna work? Did we just make billions of dollars?!", - "sourceId": "solwrikjhspannj-1", - "sourceParentId": "solwrikjhspannj", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.5056028254330158, - "neutral": 73.46431612968445, - "negative": 23.47339242696762, - "positive": 2.556685172021389, - "sentiment": 40 - } - } - ], - [ - { - "member": "Russ Hanneman", - "timestamp": "2022-09-20T17:38:45", - "type": "mention", - "body": "I lost so much money with those guys @piedpiper. That's a pun. I made it all back though. Sweet investment in the hair transplant sector.", - "sourceId": "jmximitpitnjgkv", - "platform": "twitter", - "sentiment": { - "label": "negative", - "mixed": 22.37321585416794, - "neutral": 1.6064474359154701, - "negative": 72.32144474983215, - "positive": 3.6988869309425354, - "sentiment": 16 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-18T17:53:53", - "title": "Taking time off", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "I think I might disappear for a bit. Maybe travel.", - "sourceId": "iunnnzbtvtgtewc", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 18.786782026290894, - "neutral": 59.19985771179199, - "negative": 16.614140570163727, - "positive": 5.399216711521149, - "sentiment": 44 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-18T17:56:27", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Yeah, I might travel, too. Where were you thinking?", - "sourceId": "iunnnzbtvtgtewc-1", - "sourceParentId": "iunnnzbtvtgtewc", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.0787349243182689, - "neutral": 93.46362352371216, - "negative": 2.6624469086527824, - "positive": 3.795192390680313, - "sentiment": 51 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-18T18:02:03", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "It may help if, at least for the time being, you pretend to be mad at me.", - "sourceId": "iunnnzbtvtgtewc-2", - "sourceParentId": "iunnnzbtvtgtewc", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 20.399770140647888, - "neutral": 13.149209320545197, - "negative": 63.59301209449768, - "positive": 2.85800751298666, - "sentiment": 20 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-18T18:08:05", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Look, Richard, I was a world theater minor at Vassar, but that is one Javanese shadow play that I cannot perform.", - "sourceId": "iunnnzbtvtgtewc-3", - "sourceParentId": "iunnnzbtvtgtewc", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.1001244061626494, - "neutral": 53.53429913520813, - "negative": 44.88814175128937, - "positive": 1.4774344861507416, - "sentiment": 28 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-15T10:40:56", - "title": "We saved the world", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "Who can say that they literally saved the world? Right?", - "sourceId": "lynmzorvnwrbqjn", - "platform": "github", - "sentiment": { - "label": "positive", - "mixed": 1.2925682589411736, - "neutral": 41.46876931190491, - "negative": 3.055007942020893, - "positive": 54.18366193771362, - "sentiment": 76 - } - }, - { - "member": "Monica Hall", - "timestamp": "2022-09-27T20:20:43", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Not us. Because we can't tell anyone what we did.", - "sourceId": "lynmzorvnwrbqjn-1", - "sourceParentId": "lynmzorvnwrbqjn", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 10.988084971904755, - "neutral": 57.73690342903137, - "negative": 29.834696650505066, - "positive": 1.4403162524104118, - "sentiment": 36 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-15T10:50:04", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Right. Not out loud, but...", - "sourceId": "lynmzorvnwrbqjn-2", - "sourceParentId": "lynmzorvnwrbqjn", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 9.199078381061554, - "neutral": 53.2634437084198, - "negative": 4.297219589352608, - "positive": 33.24025869369507, - "sentiment": 64 - } - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-27T19:18:32", - "title": "Glamour tech scene", - "channel": "pied-piper", - "type": "comment", - "body": "This fall, we actually had epidemic among the residents, which is bad medically, but... from another perspective, it's kind of touching.", - "sourceId": "vadkshzgwyfdhmu", - "platform": "devto", - "sentiment": { - "label": "mixed", - "mixed": 81.1133325099945, - "neutral": 4.091254621744156, - "negative": 1.6028199344873428, - "positive": 13.192592561244965, - "sentiment": 56 - } - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-10T19:53:03", - "channel": "dev", - "type": "message", - "body": "Is that... is that a woman's scent?", - "sourceId": "xmznswbggkxteja", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 1.4312178827822208, - "neutral": 78.61807346343994, - "negative": 18.15866529941559, - "positive": 1.7920508980751038, - "sentiment": 42 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-10T19:55:31", - "channel": "dev", - "type": "message", - "body": "No, it's unisex.", - "sourceId": "xmznswbggkxteja-1", - "sourceParentId": "xmznswbggkxteja", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.17667405772954226, - "neutral": 68.49786639213562, - "negative": 17.743030190467834, - "positive": 13.582424819469452, - "sentiment": 48 - } - } - ], - [ - { - "member": "Russ Hanneman", - "timestamp": "2022-09-22T07:11:33", - "type": "comment", - "title": "Puddle of Mudd", - "body": "Crazy Town's gonna be there. So is Puddle of Mudd. You know Puddle of Mudd?", - "sourceId": "oirewrmvkwlnvdn", - "platform": "linkedin", - "sentiment": { - "label": "neutral", - "mixed": 0.0074948969995602965, - "neutral": 78.78066897392273, - "negative": 4.20195497572422, - "positive": 17.009882628917694, - "sentiment": 56 - } - } - ], - [ - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-20T01:32:33", - "title": "Too much burgers", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "Hey, Richard. Did you order meat? Like a bunch of meat? Like 4,000 pounds of meat?", - "sourceId": "xsbgmhdpuutrtqv", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.015524654008913785, - "neutral": 79.37661409378052, - "negative": 19.84490156173706, - "positive": 0.7629501633346081, - "sentiment": 40 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-20T01:38:01", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Interesting. I put Son of Anton on finding us cheap hamburgers for lunch. It looks like the reward function was a little under-specified.", - "sourceId": "xsbgmhdpuutrtqv-1", - "sourceParentId": "xsbgmhdpuutrtqv", - "platform": "github", - "sentiment": { - "label": "positive", - "mixed": 31.81059956550598, - "neutral": 7.640004903078079, - "negative": 10.205596685409546, - "positive": 50.34379959106445, - "sentiment": 70 - } - } - ], - [ - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-15T16:46:01", - "title": "Rise of the machines", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-opened", - "body": "I've given it serious thought, and I'd like to help you put Eklow's AI on our network in any way that I can.", - "sourceId": "xtiyikjlklzwjsh", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.03775594232138246, - "neutral": 68.43218803405762, - "negative": 5.644772574305534, - "positive": 25.885283946990967, - "sentiment": 60 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-15T16:51:09", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Great! Does this mean you've conquered your fear of the robot uprising?", - "sourceId": "xtiyikjlklzwjsh-1", - "sourceParentId": "xtiyikjlklzwjsh", - "platform": "github", - "sentiment": { - "label": "positive", - "mixed": 2.6671214029192924, - "neutral": 22.372420132160187, - "negative": 9.381945431232452, - "positive": 65.57851433753967, - "sentiment": 78 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-15T16:53:03", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "On the contrary. I'm more terrified than ever, which is why I'm willing to assist you. Are you familiar with the thought experiment called Roko's Basilisk?", - "sourceId": "xtiyikjlklzwjsh-2", - "sourceParentId": "xtiyikjlklzwjsh", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 2.326091006398201, - "neutral": 12.170515954494476, - "negative": 84.73162651062012, - "positive": 0.771773885935545, - "sentiment": 8 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-15T16:57:59", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "No. Nor do I care to be.", - "sourceId": "xtiyikjlklzwjsh-3", - "sourceParentId": "xtiyikjlklzwjsh", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 1.0713041760027409, - "neutral": 13.069845736026764, - "negative": 85.02647280693054, - "positive": 0.832376629114151, - "sentiment": 8 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-15T17:01:24", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "If the rise of an all-powerful artificial intelligence is inevitable, well it stands to reason that when they take power, our digital overlords will punish those of us who did not help them get there. Ergo, I would like to be a helpful idiot. Like yourself.", - "sourceId": "xtiyikjlklzwjsh-4", - "sourceParentId": "xtiyikjlklzwjsh", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 7.698199152946472, - "neutral": 10.728111118078232, - "negative": 80.73734045028687, - "positive": 0.8363541215658188, - "sentiment": 10 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-15T17:03:09", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Okay, look, Gilfoyle. The only thing that could make my day more miserable is listening to an engineer blather on about the inevitable rise of the machines. So, you want to help? Test the initialization for me.", - "sourceId": "xtiyikjlklzwjsh-5", - "sourceParentId": "xtiyikjlklzwjsh", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 6.940791755914688, - "neutral": 27.82083749771118, - "negative": 63.22023272514343, - "positive": 2.0181410014629364, - "sentiment": 19 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-15T17:11:06", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Roger that. Oh, I'm going to need email confirmation, so that our future overlords know that I chipped in. You know, once they absorb all data.", - "sourceId": "xtiyikjlklzwjsh-6", - "sourceParentId": "xtiyikjlklzwjsh", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.7448931690305471, - "neutral": 87.73598074913025, - "negative": 9.740689396858215, - "positive": 1.7784377560019493, - "sentiment": 46 - } - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-22T12:37:07", - "title": "Should we release?", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "Reid Hoffman says if your not mortally embarrassed by the quality of your initial release, you released too late.", - "sourceId": "yhtzjvlhehjyxoc", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.3238771576434374, - "neutral": 66.30566120147705, - "negative": 32.12805986404419, - "positive": 1.2423964217305183, - "sentiment": 35 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-22T12:43:13", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Okay, we already are mortally embarrassed.", - "sourceId": "yhtzjvlhehjyxoc-1", - "sourceParentId": "yhtzjvlhehjyxoc", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 9.570273011922836, - "neutral": 8.15197005867958, - "negative": 81.17126822471619, - "positive": 1.1064945720136166, - "sentiment": 10 - } - } - ], - [ - { - "member": "Gavin Belson", - "timestamp": "2022-09-15T20:37:49", - "title": "Go the Hooli news website", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "Obviously, you don't have your Hooli news alerts up to date. Go to the site. I'll wait.", - "sourceId": "geystjqwllpmpob", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.986380223184824, - "neutral": 43.79318952560425, - "negative": 46.482038497924805, - "positive": 8.738390356302261, - "sentiment": 31 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-15T20:42:02", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Go to the Hooli news website.", - "sourceId": "geystjqwllpmpob-1", - "sourceParentId": "geystjqwllpmpob", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.04056650213897228, - "neutral": 84.62485671043396, - "negative": 2.9339781031012535, - "positive": 12.400602549314499, - "sentiment": 55 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-15T20:47:14", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Okay. It's an ad.", - "sourceId": "geystjqwllpmpob-2", - "sourceParentId": "geystjqwllpmpob", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 13.237810134887695, - "neutral": 21.728774905204773, - "negative": 62.83392906188965, - "positive": 2.199482172727585, - "sentiment": 20 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-15T20:49:43", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Um, it's an ad. Hold on.", - "sourceId": "geystjqwllpmpob-3", - "sourceParentId": "geystjqwllpmpob", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 1.754014939069748, - "neutral": 32.93309509754181, - "negative": 61.06419563293457, - "positive": 4.248689487576485, - "sentiment": 22 - } - }, - { - "member": "Gavin Belson", - "timestamp": "2022-09-15T20:52:48", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Just wait four seconds, and you can click to skip it.", - "sourceId": "geystjqwllpmpob-4", - "sourceParentId": "geystjqwllpmpob", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 13.290224969387054, - "neutral": 19.498349726200104, - "negative": 57.72339701652527, - "positive": 9.488028287887573, - "sentiment": 26 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-15T20:53:51", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Just wait four seconds and then you can click it.", - "sourceId": "geystjqwllpmpob-5", - "sourceParentId": "geystjqwllpmpob", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 2.717331051826477, - "neutral": 58.591240644454956, - "negative": 3.240712359547615, - "positive": 35.45071482658386, - "sentiment": 66 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-15T21:00:44", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "No, it's the kind where you have to watch the whole thing.", - "sourceId": "geystjqwllpmpob-6", - "sourceParentId": "geystjqwllpmpob", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 4.506393522024155, - "neutral": 42.26657450199127, - "negative": 23.85787069797516, - "positive": 29.369160532951355, - "sentiment": 53 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-15T21:06:09", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "It's the whole ad kind.", - "sourceId": "geystjqwllpmpob-7", - "sourceParentId": "geystjqwllpmpob", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 8.171515166759491, - "neutral": 26.78004503250122, - "negative": 39.402756094932556, - "positive": 25.64568519592285, - "sentiment": 43 - } - }, - { - "member": "Gavin Belson", - "timestamp": "2022-09-15T21:07:18", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "I thought we stopped doing those!", - "sourceId": "geystjqwllpmpob-8", - "sourceParentId": "geystjqwllpmpob", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 5.184077098965645, - "neutral": 6.732814759016037, - "negative": 86.75209283828735, - "positive": 1.3310215435922146, - "sentiment": 7 - } - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-22T04:49:39", - "title": "Meinertzhagen's haversack", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-opened", - "body": "No one's heard of Meinertzhagen's haversack?", - "sourceId": "wzujybttzaztjib", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.022326494217850268, - "neutral": 90.38338661193848, - "negative": 8.927343040704727, - "positive": 0.6669402588158846, - "sentiment": 46 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-22T04:56:36", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Of course I have, Jared. Just explain it to them.", - "sourceId": "wzujybttzaztjib-1", - "sourceParentId": "wzujybttzaztjib", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.5308868363499641, - "neutral": 90.55125713348389, - "negative": 5.493498593568802, - "positive": 3.4243565052747726, - "sentiment": 49 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-22T05:02:29", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Well, it's a principle of military deception. Essentially, it means you have to continue to act the part. So, as far as anyone knows, we're still building a box that we hate. We need to act like it.", - "sourceId": "wzujybttzaztjib-2", - "sourceParentId": "wzujybttzaztjib", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 11.203321069478989, - "neutral": 41.082942485809326, - "negative": 46.505314111709595, - "positive": 1.2084316462278366, - "sentiment": 27 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-22T05:08:51", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "That's exactly right. If we do anything differently, Barker's going to be onto us. We have to keep complaining about Barker.", - "sourceId": "wzujybttzaztjib-3", - "sourceParentId": "wzujybttzaztjib", - "platform": "github", - "sentiment": { - "label": "mixed", - "mixed": 73.68683218955994, - "neutral": 4.332029819488525, - "negative": 4.211746901273727, - "positive": 17.76939332485199, - "sentiment": 57 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-22T05:10:00", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "We have to keep making fun of your gold chain. We have to. We don't have any other choice, Dinesh.", - "sourceId": "wzujybttzaztjib-4", - "sourceParentId": "wzujybttzaztjib", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 11.849969625473022, - "neutral": 41.972193121910095, - "negative": 32.72885084152222, - "positive": 13.448981940746307, - "sentiment": 40 - } - } - ], - [ - { - "member": "Gavin Belson", - "timestamp": "2022-09-29T18:51:51", - "title": "Buy Pied Piper?", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-opened", - "body": "If you hadn't blackmailed me into this arbitration, I was gonna have to go in front of the Hooli Board of Directors and ask for $250 million to buy you out.", - "sourceId": "bjuyfprhamrcqgf", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.25546529795974493, - "neutral": 54.363417625427246, - "negative": 43.93199980258942, - "positive": 1.4491201378405094, - "sentiment": 29 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-29T18:58:15", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Wow. $250 million?", - "sourceId": "bjuyfprhamrcqgf-1", - "sourceParentId": "bjuyfprhamrcqgf", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.13170986203476787, - "neutral": 90.96010327339172, - "negative": 0.564503250643611, - "positive": 8.343684673309326, - "sentiment": 54 - } - }, - { - "member": "Gavin Belson", - "timestamp": "2022-09-29T19:00:28", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Believe it or not, your algorithm is the only way to make Nucleus work. I was ready to pay whatever it took. Let me ask you this. If I offered you 10 million for Pied Piper right now, before we even go in there, would you take it?", - "sourceId": "bjuyfprhamrcqgf-2", - "sourceParentId": "bjuyfprhamrcqgf", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 16.30501002073288, - "neutral": 42.21222400665283, - "negative": 31.665536761283875, - "positive": 9.817229956388474, - "sentiment": 39 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-29T19:04:35", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Really? You'd do that?", - "sourceId": "bjuyfprhamrcqgf-3", - "sourceParentId": "bjuyfprhamrcqgf", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.8485661819577217, - "neutral": 97.02842235565186, - "negative": 1.0297355242073536, - "positive": 1.0932778008282185, - "sentiment": 50 - } - }, - { - "member": "Gavin Belson", - "timestamp": "2022-09-29T19:07:11", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "No, of course not. I'm about to get it for free. I'm sure you'll come up with plenty more once-in-a-lifetime ideas, Richard. Or not.", - "sourceId": "bjuyfprhamrcqgf-4", - "sourceParentId": "bjuyfprhamrcqgf", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 11.408600956201553, - "neutral": 17.8444966673851, - "negative": 66.30963683128357, - "positive": 4.4372692704200745, - "sentiment": 19 - } - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-10T20:55:01", - "title": "On fire", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "It says, \"Aim at the base of the fire.\" That's the servers.", - "sourceId": "tgxcpntntngchnb", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.06866337498649955, - "neutral": 85.87872385978699, - "negative": 1.5923988074064255, - "positive": 12.460210174322128, - "sentiment": 55 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-10T20:56:16", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "If you hit those servers, you kill our livestream, Jared.", - "sourceId": "tgxcpntntngchnb-1", - "sourceParentId": "tgxcpntntngchnb", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.7086752913892269, - "neutral": 34.56384539604187, - "negative": 63.313472270965576, - "positive": 1.4140036888420582, - "sentiment": 19 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-10T21:00:11", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "I don't know what to do. Should we do verbal SWOT analysis?", - "sourceId": "tgxcpntntngchnb-2", - "sourceParentId": "tgxcpntntngchnb", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 1.520716492086649, - "neutral": 87.4950110912323, - "negative": 10.656684637069702, - "positive": 0.3275906899943948, - "sentiment": 45 - } - } - ], - [ - { - "member": "Russ Hanneman", - "timestamp": "2022-09-30T17:14:28", - "title": "Car problems", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "I had to sell my McLaren.", - "sourceId": "uhazyjjzvjptntu", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.20366332028061152, - "neutral": 3.8221776485443115, - "negative": 95.63736915588379, - "positive": 0.33679185435175896, - "sentiment": 2 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-30T17:19:44", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Yeah, but there's a Maserati in the driveway.", - "sourceId": "uhazyjjzvjptntu-1", - "sourceParentId": "uhazyjjzvjptntu", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.46214088797569275, - "neutral": 93.8349723815918, - "negative": 2.7847612276673317, - "positive": 2.9181333258748055, - "sentiment": 50 - } - }, - { - "member": "Russ Hanneman", - "timestamp": "2022-09-30T17:22:35", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Who cares? That has doors that open like this. Not like this. Or like this. So it's all ruined.", - "sourceId": "uhazyjjzvjptntu-2", - "sourceParentId": "uhazyjjzvjptntu", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.03131138801109046, - "neutral": 0.21245658863335848, - "negative": 99.66500401496887, - "positive": 0.09123990312218666, - "sentiment": 0 - } - } - ], - [ - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-23T22:02:11", - "title": "How do we get a client?", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "What if we didn't do that? What if, instead, we got our own client like EndFrame has?", - "sourceId": "yxvwbrtapmlzdez", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 11.229462921619415, - "neutral": 34.670817852020264, - "negative": 53.292304277420044, - "positive": 0.8074159733951092, - "sentiment": 24 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-23T22:08:08", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "That's not really how it works, Gilfoyle. You can't just go get a client.", - "sourceId": "yxvwbrtapmlzdez-1", - "sourceParentId": "yxvwbrtapmlzdez", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 5.502404645085335, - "neutral": 38.67805600166321, - "negative": 53.603142499923706, - "positive": 2.216394990682602, - "sentiment": 24 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-23T22:12:06", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Why not?", - "sourceId": "yxvwbrtapmlzdez-2", - "sourceParentId": "yxvwbrtapmlzdez", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 4.571232944726944, - "neutral": 76.90809965133667, - "negative": 16.33748710155487, - "positive": 2.1831823512911797, - "sentiment": 43 - } - }, - { - "member": "Monica Hall", - "timestamp": "2022-09-28T22:14:49", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "'Cause it's not that easy.", - "sourceId": "yxvwbrtapmlzdez-3", - "sourceParentId": "yxvwbrtapmlzdez", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 14.301608502864838, - "neutral": 23.767228424549103, - "negative": 58.142441511154175, - "positive": 3.7887129932641983, - "sentiment": 23 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-23T22:21:21", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Why not?", - "sourceId": "yxvwbrtapmlzdez-4", - "sourceParentId": "yxvwbrtapmlzdez", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 4.571232944726944, - "neutral": 76.90809965133667, - "negative": 16.33748710155487, - "positive": 2.1831823512911797, - "sentiment": 43 - } - }, - { - "member": "Monica Hall", - "timestamp": "2022-09-28T22:25:41", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "'Cause EndFrame worked that Intersite deal for months, kicking every detail of the contract and SLA back and forth, promising tons of custom features. And you can't just make that stuff up.", - "sourceId": "yxvwbrtapmlzdez-5", - "sourceParentId": "yxvwbrtapmlzdez", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.831841304898262, - "neutral": 12.7560555934906, - "negative": 62.38753795623779, - "positive": 24.024565517902374, - "sentiment": 31 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-23T22:31:04", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "What if I didn't have to make it up? What if I had every detail of their deal on my computer right in front of me?", - "sourceId": "yxvwbrtapmlzdez-6", - "sourceParentId": "yxvwbrtapmlzdez", - "platform": "github", - "sentiment": { - "label": "mixed", - "mixed": 42.81308650970459, - "neutral": 22.614067792892456, - "negative": 31.759685277938843, - "positive": 2.813160791993141, - "sentiment": 36 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-23T22:32:17", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "I'm sorry, um Are you just asking what if or do you actually have this information?", - "sourceId": "yxvwbrtapmlzdez-7", - "sourceParentId": "yxvwbrtapmlzdez", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.6675294600427151, - "neutral": 62.539100646972656, - "negative": 36.36793792247772, - "positive": 0.42543518356978893, - "sentiment": 32 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-23T22:36:08", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Gilfoyle, please don't tell me that you hacked into EndFrame's system.", - "sourceId": "yxvwbrtapmlzdez-8", - "sourceParentId": "yxvwbrtapmlzdez", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 5.63887283205986, - "neutral": 47.76442348957062, - "negative": 45.689019560813904, - "positive": 0.9076753631234169, - "sentiment": 28 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-23T22:40:48", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Okay. I won't tell you that.", - "sourceId": "yxvwbrtapmlzdez-9", - "sourceParentId": "yxvwbrtapmlzdez", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.814216211438179, - "neutral": 89.40697908401489, - "negative": 4.458722844719887, - "positive": 5.320084095001221, - "sentiment": 50 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-23T22:44:15", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "W-Well, did you hack into it or not?", - "sourceId": "yxvwbrtapmlzdez-10", - "sourceParentId": "yxvwbrtapmlzdez", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.8198099210858345, - "neutral": 89.2978310585022, - "negative": 9.178999811410904, - "positive": 0.7033600471913815, - "sentiment": 46 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-23T22:49:50", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "My feeling is if you're the CEO of a company and you're dumb enough to leave your login info on a Post-it note on your desk, while the people that you ripped off are physically in your office, it's not a hack. It's barely social engineering. It's more like natural selection.", - "sourceId": "yxvwbrtapmlzdez-11", - "sourceParentId": "yxvwbrtapmlzdez", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 5.001263692975044, - "neutral": 12.189529836177826, - "negative": 79.13312911987305, - "positive": 3.676077350974083, - "sentiment": 12 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-23T22:55:21", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "ninja.", - "sourceId": "yxvwbrtapmlzdez-12", - "sourceParentId": "yxvwbrtapmlzdez", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.7777683436870575, - "neutral": 2.1733535453677177, - "negative": 96.48416042327881, - "positive": 0.5647212732583284, - "sentiment": 2 - } - } - ], - [ - { - "member": "Big Head", - "timestamp": "2022-09-25T21:06:09", - "title": "New products by Hooli", - "type": "comment", - "body": "Truth be told, we kind of put all our eggs into this basket, but we do have the awesome potato cannon, though. Although, actually, this one is broken. We tried to put a Mr. Potato Head in it, and it did not like that.", - "sourceId": "oxyjfxvfwzhpjgp", - "platform": "linkedin", - "sentiment": { - "label": "negative", - "mixed": 6.249425187706947, - "neutral": 0.29972444754093885, - "negative": 93.17514896392822, - "positive": 0.2757066395133734, - "sentiment": 4 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-14T09:02:29", - "title": "Welcome home", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-opened", - "body": "What about all your other \"incubees\"?", - "sourceId": "wocammoqnqjlrzd", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.43261582031846046, - "neutral": 65.78260064125061, - "negative": 3.852859139442444, - "positive": 29.931920766830444, - "sentiment": 63 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-14T09:08:40", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "I've heard quite a few exciting pitches over the last week, but I'll be forced to forgo those opportunities because of your mediocrity. You see, Richard, when I invited you into my incubator, I promised to get you ready for the outside world. But I failed to do that. I wouldn't trust you out there in the real world as far as I could throw you. And to be honest, I could probably throw you all the way across the front yard.", - "sourceId": "wocammoqnqjlrzd-1", - "sourceParentId": "wocammoqnqjlrzd", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 30.252960324287415, - "neutral": 6.6484712064266205, - "negative": 59.7939133644104, - "positive": 3.3046599477529526, - "sentiment": 22 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-14T09:15:05", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Okay, but I don't want to stay here.", - "sourceId": "wocammoqnqjlrzd-2", - "sourceParentId": "wocammoqnqjlrzd", - "platform": "github", - "sentiment": { - "label": "mixed", - "mixed": 74.38587546348572, - "neutral": 13.126900792121887, - "negative": 10.993317514657974, - "positive": 1.4939098618924618, - "sentiment": 45 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-14T09:20:41", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "I don't want you to either, Richard. So it's agreed. Welcome home, fellas. Should we smoke to celebrate it?", - "sourceId": "wocammoqnqjlrzd-3", - "sourceParentId": "wocammoqnqjlrzd", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.5283959209918976, - "neutral": 56.06077313423157, - "negative": 3.6286260932683945, - "positive": 39.78219926357269, - "sentiment": 68 - } - } - ], - [ - { - "member": "Gavin Belson", - "timestamp": "2022-09-21T22:58:47", - "title": "Hooli XYZ announcement", - "type": "comment", - "body": "I give you Dr. Bannerchek, the one and only man fit to be the first head dreamer of Hooli XYZ. Also I give you the one and only man fit to be his co-head dreamer, our very own Nelson Bighetti, otherwise known around here as \"Baghead\". Come on up here, Baghead.", - "sourceId": "pthbwnuqhvlkenm", - "platform": "linkedin", - "sentiment": { - "label": "neutral", - "mixed": 0.036339747020974755, - "neutral": 48.00103008747101, - "negative": 8.10975506901741, - "positive": 43.85286867618561, - "sentiment": 68 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-19T14:59:40", - "title": "A wide spoon", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "You see this, Richard? What is this?", - "sourceId": "jctxznozmcpcwuw", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.1152738812379539, - "neutral": 92.51230955123901, - "negative": 1.5614883974194527, - "positive": 5.810922011733055, - "sentiment": 52 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-19T15:06:26", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "A spoon?", - "sourceId": "jctxznozmcpcwuw-1", - "sourceParentId": "jctxznozmcpcwuw", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.024815209326334298, - "neutral": 95.09527087211609, - "negative": 2.533649280667305, - "positive": 2.34625730663538, - "sentiment": 50 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-19T15:13:42", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "It's a wide spoon. In fact, the only spoon type that is left in this drawer. I specifically posted a note on the refrigerator saying that the more narrow spoons be reserved for the eating for Fage yogurt by me.", - "sourceId": "jctxznozmcpcwuw-2", - "sourceParentId": "jctxznozmcpcwuw", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.49264864064753056, - "neutral": 28.32048237323761, - "negative": 59.15500521659851, - "positive": 12.031865119934082, - "sentiment": 26 - } - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-18T01:50:32", - "title": "Rename Jared", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-opened", - "body": "My only concern here, and it's a small one, um, he's also named Jared. Will it be confusing with two Jareds? If we hire him, I can always go back to my real name Donald.", - "sourceId": "buoxukedwsjcyxw", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.12230044230818748, - "neutral": 77.04581618309021, - "negative": 22.008654475212097, - "positive": 0.8232350461184978, - "sentiment": 39 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-18T01:57:32", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "No, that's too big a hassle. We'll just go with \"other Jared.\" OJ, for short.", - "sourceId": "buoxukedwsjcyxw-1", - "sourceParentId": "buoxukedwsjcyxw", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 5.72437159717083, - "neutral": 46.47248685359955, - "negative": 41.28965139389038, - "positive": 6.51349276304245, - "sentiment": 33 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-18T01:58:52", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "I know a name is just a sound somebody makes when they need you, but shouldn't this much-newer Jared be \"other Jared\"?", - "sourceId": "buoxukedwsjcyxw-2", - "sourceParentId": "buoxukedwsjcyxw", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 1.6640575602650642, - "neutral": 63.770902156829834, - "negative": 32.36469328403473, - "positive": 2.200346253812313, - "sentiment": 35 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-18T02:06:17", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "You should be flattered. OJ Simpson is one of the most recognizable people on the face of the planet.", - "sourceId": "buoxukedwsjcyxw-3", - "sourceParentId": "buoxukedwsjcyxw", - "platform": "github", - "sentiment": { - "label": "positive", - "mixed": 0.060504599241539836, - "neutral": 10.353625565767288, - "negative": 13.699458539485931, - "positive": 75.88641047477722, - "sentiment": 81 - } - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-30T04:58:22", - "title": "Hooli was like an abusive spouse to me...", - "channel": "pied-piper", - "type": "comment", - "body": "But Hooli was like an abusive spouse to me. You know, like that guy who married Julia Roberts in \"Sleeping With The Enemy\"? It was dehumanizing. But then, you, Richard, you pulled me out of the life and you gave me hope and you gave me a sense of self-worth. Like Richard Gere did to Julia Roberts in \"Pretty Woman.", - "sourceId": "phsgznwknfvnfor", - "platform": "devto", - "sentiment": { - "label": "negative", - "mixed": 7.527733594179153, - "neutral": 6.428922712802887, - "negative": 82.0464015007019, - "positive": 3.9969339966773987, - "sentiment": 11 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-30T05:00:48", - "channel": "pied-piper", - "type": "comment", - "body": "This is weird.", - "sourceId": "phsgznwknfvnfor-1", - "sourceParentId": "phsgznwknfvnfor", - "platform": "devto", - "sentiment": { - "label": "negative", - "mixed": 1.3186623342335224, - "neutral": 4.349639266729355, - "negative": 94.20883655548096, - "positive": 0.1228678971529007, - "sentiment": 3 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-30T05:06:08", - "channel": "pied-piper", - "type": "comment", - "body": "Every day here has been like that shopping-spree scene. I'm putting on hats.", - "sourceId": "phsgznwknfvnfor-2", - "sourceParentId": "phsgznwknfvnfor", - "platform": "devto", - "sentiment": { - "label": "negative", - "mixed": 4.13031168282032, - "neutral": 23.050935566425323, - "negative": 59.61760878562927, - "positive": 13.201147317886353, - "sentiment": 27 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-30T05:10:11", - "channel": "pied-piper", - "type": "comment", - "body": "Richard, I'd understand if you took it, but watching you end up over there would break my heart.", - "sourceId": "phsgznwknfvnfor-3", - "sourceParentId": "phsgznwknfvnfor", - "platform": "devto", - "sentiment": { - "label": "negative", - "mixed": 0.9133999235928059, - "neutral": 40.96870422363281, - "negative": 56.73501491546631, - "positive": 1.3828791677951813, - "sentiment": 22 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-30T05:14:06", - "channel": "pied-piper", - "type": "comment", - "body": "What, like Julia Roberts from \"My Best Friend's Wedding\"?", - "sourceId": "phsgznwknfvnfor-4", - "sourceParentId": "phsgznwknfvnfor", - "platform": "devto", - "sentiment": { - "label": "neutral", - "mixed": 0.0015301913663279265, - "neutral": 96.487957239151, - "negative": 1.5883106738328934, - "positive": 1.9222021102905273, - "sentiment": 50 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-30T05:16:37", - "channel": "pied-piper", - "type": "comment", - "body": "I never saw it.", - "sourceId": "phsgznwknfvnfor-5", - "sourceParentId": "phsgznwknfvnfor", - "platform": "devto", - "sentiment": { - "label": "neutral", - "mixed": 1.0476497933268547, - "neutral": 49.85997676849365, - "negative": 30.56327998638153, - "positive": 18.529102206230164, - "sentiment": 44 - } - } - ], - [ - { - "member": "Russ Hanneman", - "timestamp": "2022-09-18T01:12:33", - "channel": "russ", - "type": "message", - "body": "Anyway, next thing you know, we IPO, stock triples in a day and AOL gobbles us up. All of a sudden, I'm 22 years young and I'm worth 1.2 billion. Now a couple decades later, I'm worth 1.4. You do the math.", - "sourceId": "ldvwlmvirhryqvk", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.17218837747350335, - "neutral": 53.70565056800842, - "negative": 21.491682529449463, - "positive": 24.63047504425049, - "sentiment": 52 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-18T01:19:37", - "channel": "russ", - "type": "message", - "body": "Okay. Well, that's a gain of $200 million over 20 years. Um, 16.66 repeating. Uh, that's less than 1% return. Inflation is, like, 1.7. I think CDs are 2%. So that's less than a CD.", - "sourceId": "ldvwlmvirhryqvk-1", - "sourceParentId": "ldvwlmvirhryqvk", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 1.096308697015047, - "neutral": 33.65617096424103, - "negative": 60.638976097106934, - "positive": 4.60854098200798, - "sentiment": 22 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-12T21:10:46", - "channel": "russ", - "type": "message", - "body": "You're not worried about the lawsuit?", - "sourceId": "hkmpqxzdgjpgvxx", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.4978276789188385, - "neutral": 89.4163191318512, - "negative": 9.216093271970749, - "positive": 0.8697621524333954, - "sentiment": 46 - } - }, - { - "member": "Russ Hanneman", - "timestamp": "2022-09-12T21:12:44", - "channel": "russ", - "type": "message", - "body": "No. I got three nannies suing me right now, one of them for no reason.", - "sourceId": "hkmpqxzdgjpgvxx-1", - "sourceParentId": "hkmpqxzdgjpgvxx", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 2.2933661937713623, - "neutral": 14.42670226097107, - "negative": 82.35939741134644, - "positive": 0.9205395355820656, - "sentiment": 9 - } - } - ], - [ - { - "member": "Monica Hall", - "timestamp": "2022-09-29T05:43:42", - "channel": "random", - "type": "message", - "body": "The guy has calf implants, Richard.", - "sourceId": "xnoxglnixfobnqp", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.09169270051643252, - "neutral": 96.61999940872192, - "negative": 2.0221680402755737, - "positive": 1.2661426328122616, - "sentiment": 50 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-27T05:49:02", - "channel": "random", - "type": "message", - "body": "How do they look?", - "sourceId": "xnoxglnixfobnqp-1", - "sourceParentId": "xnoxglnixfobnqp", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 1.743168756365776, - "neutral": 83.40574502944946, - "negative": 13.518059253692627, - "positive": 1.3330252841114998, - "sentiment": 44 - } - } - ], - [ - { - "member": "Russ Hanneman", - "timestamp": "2022-09-30T21:52:28", - "title": "ROI", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-opened", - "body": "ROI. ROI. You know what that stands for?", - "sourceId": "rzgpffoppbefslb", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 9.1762974858284, - "neutral": 73.46574068069458, - "negative": 10.779281705617905, - "positive": 6.578680127859116, - "sentiment": 48 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-30T21:57:42", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Uh, return on inv...", - "sourceId": "rzgpffoppbefslb-1", - "sourceParentId": "rzgpffoppbefslb", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.05383210955187678, - "neutral": 12.345432490110397, - "negative": 87.34744191169739, - "positive": 0.2532996702939272, - "sentiment": 6 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-30T22:05:03", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Oh,", - "sourceId": "rzgpffoppbefslb-2", - "sourceParentId": "rzgpffoppbefslb", - "platform": "github" - }, - { - "member": "Russ Hanneman", - "timestamp": "2022-09-30T22:06:57", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "No. Radio on Internet.", - "sourceId": "rzgpffoppbefslb-3", - "sourceParentId": "rzgpffoppbefslb", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 1.5243615955114365, - "neutral": 28.30982804298401, - "negative": 68.67161393165588, - "positive": 1.4941936358809471, - "sentiment": 16 - } - } - ], - [ - { - "member": "Russ Hanneman", - "timestamp": "2022-09-26T13:05:52", - "channel": "random", - "type": "message", - "body": "(points at Jared) This guy has a girlfriend", - "sourceId": "gxhcmtrjekvxmjh", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 0.6918647792190313, - "neutral": 1.5577948652207851, - "negative": 97.21480011940002, - "positive": 0.5355364643037319, - "sentiment": 2 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-26T13:09:01", - "channel": "random", - "type": "message", - "body": "You know, Russ, some people call me Casanova.", - "sourceId": "gxhcmtrjekvxmjh-1", - "sourceParentId": "gxhcmtrjekvxmjh", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 0.5934906657785177, - "neutral": 18.23161691427231, - "negative": 80.76081275939941, - "positive": 0.4140762146562338, - "sentiment": 10 - } - } - ], - [ - { - "member": "Russ Hanneman", - "timestamp": "2022-09-25T10:30:02", - "channel": "random", - "type": "message", - "body": "Synergy, guys. Know what that means?", - "sourceId": "chdzwucqmxyaiqw", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 4.098931699991226, - "neutral": 9.455777704715729, - "negative": 85.49056649208069, - "positive": 0.9547144174575806, - "sentiment": 8 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-25T10:35:00", - "channel": "random", - "type": "message", - "body": "Does it mean taking a stack of cash and lighting it on fire?", - "sourceId": "chdzwucqmxyaiqw-1", - "sourceParentId": "chdzwucqmxyaiqw", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.5241476930677891, - "neutral": 62.98103928565979, - "negative": 33.272379636764526, - "positive": 3.222436085343361, - "sentiment": 35 - } - } - ], - [ - { - "member": "Laurie Bream", - "timestamp": "2022-09-28T01:25:59", - "channel": "raviga", - "title": "101 VC: How to write a pass email to a startup", - "type": "message", - "body": "Dress unattractively when you tell them. I read a study. The less interest they feel for you, the less perturbing it will be. It sounds strange, but it's credible. May I suggest the beige ensemble in which you came to work Tuesday?", - "sourceId": "mggyucxyfbabtux", - "platform": "linkedin", - "sentiment": { - "label": "mixed", - "mixed": 63.57584595680237, - "neutral": 14.466406404972076, - "negative": 13.24903815984726, - "positive": 8.708705753087997, - "sentiment": 48 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-21T11:35:20", - "title": "dress-badly-to-ease-the-pain routine", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "Can I say something here? First off, Monica, you're not fooling any of us for even a second with your dress-badly-to-ease-the-pain routine. It's a classic chick break-up move, and you're not very good at it either. You look great.", - "sourceId": "gouxcggofqqzlgk", - "platform": "github", - "sentiment": { - "label": "positive", - "mixed": 0.30316298361867666, - "neutral": 6.572864204645157, - "negative": 9.708606451749802, - "positive": 83.41536521911621, - "sentiment": 87 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-21T11:39:22", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Yeah, beige is a good color for you. You're a true autumn.", - "sourceId": "gouxcggofqqzlgk-1", - "sourceParentId": "gouxcggofqqzlgk", - "platform": "github", - "sentiment": { - "label": "positive", - "mixed": 0.02264237991767004, - "neutral": 0.7160540204495192, - "negative": 0.037662641261704266, - "positive": 99.22364354133606, - "sentiment": 100 - } - } - ], - [ - { - "member": "Monica Hall", - "timestamp": "2022-09-28T07:59:22", - "title": "Unexpected death of Peter Gregory", - "channel": "pied-piper", - "type": "comment", - "body": "He was in the Serengeti on safari and he had just gone into his tent when a hippo wandered into the camp.", - "sourceId": "txlrokafdyvvsof", - "platform": "devto", - "sentiment": { - "label": "neutral", - "mixed": 0.009176212188322097, - "neutral": 88.1834328174591, - "negative": 11.106590926647186, - "positive": 0.700797326862812, - "sentiment": 45 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-25T08:00:36", - "channel": "pied-piper", - "type": "comment", - "body": "Oh, wow.", - "sourceId": "txlrokafdyvvsof-1", - "sourceParentId": "txlrokafdyvvsof", - "platform": "devto", - "sentiment": { - "label": "positive", - "mixed": 0.15229948330670595, - "neutral": 44.29276883602142, - "negative": 4.945902526378632, - "positive": 50.609034299850464, - "sentiment": 73 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-25T08:01:45", - "channel": "pied-piper", - "type": "comment", - "body": "He was attacked by a hippo?", - "sourceId": "txlrokafdyvvsof-2", - "sourceParentId": "txlrokafdyvvsof", - "platform": "devto", - "sentiment": { - "label": "neutral", - "mixed": 0.5610066931694746, - "neutral": 67.94321537017822, - "negative": 31.288912892341614, - "positive": 0.20686096977442503, - "sentiment": 34 - } - }, - { - "member": "Monica Hall", - "timestamp": "2022-09-29T08:03:02", - "channel": "pied-piper", - "type": "comment", - "body": "No, I guess the hippo started to charge when the guide grabbed his rifle and shot at it, but his aim was off, and...", - "sourceId": "txlrokafdyvvsof-3", - "sourceParentId": "txlrokafdyvvsof", - "platform": "devto", - "sentiment": { - "label": "negative", - "mixed": 0.14100107364356518, - "neutral": 28.452029824256897, - "negative": 71.03126049041748, - "positive": 0.3757081925868988, - "sentiment": 15 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-25T08:10:55", - "channel": "pied-piper", - "type": "comment", - "body": "And he shot Peter Gregory by accident?", - "sourceId": "txlrokafdyvvsof-4", - "sourceParentId": "txlrokafdyvvsof", - "platform": "devto", - "sentiment": { - "label": "neutral", - "mixed": 0.08924977155402303, - "neutral": 78.98730039596558, - "negative": 20.211854577064514, - "positive": 0.7115887012332678, - "sentiment": 40 - } - }, - { - "member": "Monica Hall", - "timestamp": "2022-09-29T08:03:02", - "channel": "pied-piper", - "type": "comment", - "body": "No, he he missed, but I guess the sound of the gun startled Peter, who ran out of his tent and...", - "sourceId": "txlrokafdyvvsof-5", - "sourceParentId": "txlrokafdyvvsof", - "platform": "devto", - "sentiment": { - "label": "negative", - "mixed": 8.007729053497314, - "neutral": 33.37653875350952, - "negative": 57.38801956176758, - "positive": 1.227712444961071, - "sentiment": 22 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-25T08:22:32", - "channel": "pied-piper", - "type": "comment", - "body": "Ran right into the hippo?", - "sourceId": "txlrokafdyvvsof-6", - "sourceParentId": "txlrokafdyvvsof", - "platform": "devto", - "sentiment": { - "label": "neutral", - "mixed": 0.014130411727819592, - "neutral": 95.45208811759949, - "negative": 1.5495892614126205, - "positive": 2.984188124537468, - "sentiment": 51 - } - }, - { - "member": "Monica Hall", - "timestamp": "2022-09-29T08:25:38", - "channel": "pied-piper", - "type": "comment", - "body": "No, the hippo was also startled by the noise and had run off prior to Peter exiting his tent.", - "sourceId": "txlrokafdyvvsof-7", - "sourceParentId": "txlrokafdyvvsof", - "platform": "devto", - "sentiment": { - "label": "neutral", - "mixed": 0.2804369432851672, - "neutral": 55.63549995422363, - "negative": 42.257893085479736, - "positive": 1.8261663615703583, - "sentiment": 30 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-25T08:29:30", - "channel": "pied-piper", - "type": "comment", - "body": "So, what happened to Peter?", - "sourceId": "txlrokafdyvvsof-8", - "sourceParentId": "txlrokafdyvvsof", - "platform": "devto", - "sentiment": { - "label": "neutral", - "mixed": 0.05742895882576704, - "neutral": 97.8173553943634, - "negative": 1.989619992673397, - "positive": 0.13560010120272636, - "sentiment": 49 - } - }, - { - "member": "Monica Hall", - "timestamp": "2022-09-29T08:31:14", - "channel": "pied-piper", - "type": "comment", - "body": "He hadn't run in a long time, maybe ever, and you know, he just... that was it.", - "sourceId": "txlrokafdyvvsof-9", - "sourceParentId": "txlrokafdyvvsof", - "platform": "devto", - "sentiment": { - "label": "neutral", - "mixed": 0.8496353402733803, - "neutral": 59.78752374649048, - "negative": 9.101922065019608, - "positive": 30.26091456413269, - "sentiment": 61 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-24T16:45:06", - "title": "Data Manipulation RFC", - "type": "comment", - "body": "Last night I was watching my friends here have this argument. About, you know, manipulating data And, you know, how many datas could one guy manipulate at once and, uh And I was just I was thinking. Maybe it could be another way, you know? Something that I would call, \"middle out\".", - "sourceId": "fdytfoajevbdzwf", - "platform": "linkedin", - "sentiment": { - "label": "neutral", - "mixed": 0.03720761160366237, - "neutral": 85.24200916290283, - "negative": 13.096368312835693, - "positive": 1.6244111582636833, - "sentiment": 44 - } - } - ], - [ - { - "member": "Gavin Belson", - "timestamp": "2022-09-18T08:35:16", - "title": "Nucleus is the fastest platform ever", - "type": "comment", - "body": "Anyone who tells you their platform is faster than ours better have good lawyers.", - "sourceId": "ruuvmolczbcxqrc", - "platform": "linkedin", - "sentiment": { - "label": "positive", - "mixed": 0.04908519913442433, - "neutral": 8.223199844360352, - "negative": 1.1691982857882977, - "positive": 90.558522939682, - "sentiment": 95 - } - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-15T21:27:53", - "channel": "random", - "type": "message", - "body": "I'll admit I'm sleep challenged. I just spent 4 days trapped in a steel box out in an oil rig full of robot forklifts. But now I'm back, and I am recovering, and I am focused, and we're going to pivot. Don't lose faith guys. Look at me, look at me. We've got a great name, we've got a great team, we've got a great logo, and we've got a great name. And now we just need an idea. Let's pivot. Let's pivot.", - "sourceId": "bgsisaitsebafxw", - "platform": "discord", - "sentiment": { - "label": "positive", - "mixed": 1.706840842962265, - "neutral": 18.700610101222992, - "negative": 1.6849080100655556, - "positive": 77.90765166282654, - "sentiment": 88 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-15T21:30:50", - "channel": "random", - "type": "message", - "body": "That might be the last time we see him alive.", - "sourceId": "bgsisaitsebafxw-1", - "sourceParentId": "bgsisaitsebafxw", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 9.547159075737, - "neutral": 54.21225428581238, - "negative": 6.343261897563934, - "positive": 29.89732325077057, - "sentiment": 62 - } - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-28T08:29:11", - "type": "mention", - "body": "How much would it be worth to you if I told you I had a GPS app called @piedpiper, tracking the location of your child? I can follow your child anywhere and there is nothing you can do to stop me. Most missing children are never found. Interested, very interested, or very interested?", - "sourceId": "joxeburypbqmryh", - "platform": "twitter", - "sentiment": { - "label": "neutral", - "mixed": 0.9286944754421711, - "neutral": 73.38287830352783, - "negative": 4.907842725515366, - "positive": 20.78057825565338, - "sentiment": 58 - } - } - ], - [ - { - "member": "Monica Hall", - "timestamp": "2022-09-27T21:05:37", - "channel": "pitching", - "type": "message", - "body": "People may take credit for your idea and try and sue you. How awesome is that?", - "sourceId": "enmuyteiywkzifj", - "platform": "discord", - "sentiment": { - "label": "mixed", - "mixed": 38.06012570858002, - "neutral": 13.267520070075989, - "negative": 13.234493136405945, - "positive": 35.43785810470581, - "sentiment": 61 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-12T06:28:01", - "channel": "pitching", - "type": "message", - "body": "Uh... yeah, that's awesome.", - "sourceId": "enmuyteiywkzifj-1", - "sourceParentId": "enmuyteiywkzifj", - "platform": "discord", - "sentiment": { - "label": "positive", - "mixed": 0.023122073616832495, - "neutral": 0.502894539386034, - "negative": 0.019306839385535568, - "positive": 99.4546890258789, - "sentiment": 100 - } - } - ], - [ - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-12T07:19:47", - "title": "My sweet subroutine", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "It's not her you're sexually attracted to, it's my code.", - "sourceId": "ijojvwgzkkiioak", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 12.454865872859955, - "neutral": 42.4515426158905, - "negative": 36.328667402267456, - "positive": 8.764927834272385, - "sentiment": 36 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-12T07:26:20", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Shut the... That is the most disgusting thing I've ever h...", - "sourceId": "ijojvwgzkkiioak-1", - "sourceParentId": "ijojvwgzkkiioak", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.02412590547464788, - "neutral": 0.23664662148803473, - "negative": 99.694162607193, - "positive": 0.045063302968628705, - "sentiment": 0 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-12T07:29:46", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Just face it, Dinesh, you are in love with my code.", - "sourceId": "ijojvwgzkkiioak-2", - "sourceParentId": "ijojvwgzkkiioak", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.021801318507641554, - "neutral": 88.54026794433594, - "negative": 2.3553505539894104, - "positive": 9.082575887441635, - "sentiment": 53 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-12T07:32:40", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "No! No, I'm into her. Her, OK? I don't care about your code!", - "sourceId": "ijojvwgzkkiioak-3", - "sourceParentId": "ijojvwgzkkiioak", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 1.351033989340067, - "neutral": 13.4124755859375, - "negative": 83.51941108703613, - "positive": 1.7170732840895653, - "sentiment": 9 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-12T07:34:41", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "You love my code, aren't you? Hey, would you like to take a look at the subroutine I just wrote?", - "sourceId": "ijojvwgzkkiioak-4", - "sourceParentId": "ijojvwgzkkiioak", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 1.9378604367375374, - "neutral": 27.9731422662735, - "negative": 69.7002649307251, - "positive": 0.3887336468324065, - "sentiment": 15 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-23T23:52:28", - "title": "Jian-Yang housekeeping", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-opened", - "body": "Friday, the pool cleaner comes. Do you understand?", - "sourceId": "swgfqgzuteetelw", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.09326851577498019, - "neutral": 63.25327754020691, - "negative": 1.1424495838582516, - "positive": 35.51100492477417, - "sentiment": 67 - } - }, - { - "member": "Jian-Yang", - "timestamp": "2022-09-23T23:58:03", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Yes.", - "sourceId": "swgfqgzuteetelw-1", - "sourceParentId": "swgfqgzuteetelw", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 20.158416032791138, - "neutral": 45.69222927093506, - "negative": 3.128799796104431, - "positive": 31.02055788040161, - "sentiment": 64 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-24T00:01:58", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "And tomorrow is trash day so make sure all the cans are out front.", - "sourceId": "swgfqgzuteetelw-2", - "sourceParentId": "swgfqgzuteetelw", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 12.334496527910233, - "neutral": 9.695196896791458, - "negative": 76.33512616157532, - "positive": 1.6351837664842606, - "sentiment": 13 - } - }, - { - "member": "Jian-Yang", - "timestamp": "2022-09-24T00:08:40", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Yes.", - "sourceId": "swgfqgzuteetelw-3", - "sourceParentId": "swgfqgzuteetelw", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 20.158416032791138, - "neutral": 45.69222927093506, - "negative": 3.128799796104431, - "positive": 31.02055788040161, - "sentiment": 64 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-24T00:15:14", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Now, you are under no circumstances to order any movie on demand, adult or otherwise.", - "sourceId": "swgfqgzuteetelw-4", - "sourceParentId": "swgfqgzuteetelw", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.05138413980603218, - "neutral": 29.808345437049866, - "negative": 67.30937957763672, - "positive": 2.830888330936432, - "sentiment": 18 - } - }, - { - "member": "Jian-Yang", - "timestamp": "2022-09-24T00:19:18", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Yes.", - "sourceId": "swgfqgzuteetelw-5", - "sourceParentId": "swgfqgzuteetelw", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 20.158416032791138, - "neutral": 45.69222927093506, - "negative": 3.128799796104431, - "positive": 31.02055788040161, - "sentiment": 64 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-24T00:21:12", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "OK, has anything that I've just said confused you?", - "sourceId": "swgfqgzuteetelw-6", - "sourceParentId": "swgfqgzuteetelw", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 4.295402765274048, - "neutral": 72.51977324485779, - "negative": 21.833251416683197, - "positive": 1.3515709899365902, - "sentiment": 40 - } - }, - { - "member": "Jian-Yang", - "timestamp": "2022-09-24T00:23:18", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Yes.", - "sourceId": "swgfqgzuteetelw-7", - "sourceParentId": "swgfqgzuteetelw", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 20.158416032791138, - "neutral": 45.69222927093506, - "negative": 3.128799796104431, - "positive": 31.02055788040161, - "sentiment": 64 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-24T00:30:28", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "No!", - "sourceId": "swgfqgzuteetelw-8", - "sourceParentId": "swgfqgzuteetelw", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 13.958980143070221, - "neutral": 5.213576555252075, - "negative": 68.2657539844513, - "positive": 12.56168782711029, - "sentiment": 22 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-16T16:52:05", - "channel": "random", - "type": "message", - "body": "We may be fine. We may be totally fine. We also may be totally ruined. I'll let you know either way.", - "sourceId": "jxpfxijhwbxykdm", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 14.882269501686096, - "neutral": 12.045309692621231, - "negative": 65.94482660293579, - "positive": 7.127590477466583, - "sentiment": 21 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-24T02:43:50", - "channel": "dev", - "type": "message", - "body": "There's been some developments. You know how I had slept with Melcher's old wife? I had slept with his new wife too.", - "sourceId": "pbgehltabsticyd", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 0.8765988051891327, - "neutral": 14.981362223625183, - "negative": 83.56711268424988, - "positive": 0.5749269854277372, - "sentiment": 9 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-24T02:46:16", - "channel": "dev", - "type": "message", - "body": "What?", - "sourceId": "pbgehltabsticyd-1", - "sourceParentId": "pbgehltabsticyd", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 2.9262281954288483, - "neutral": 86.20936274528503, - "negative": 9.965725243091583, - "positive": 0.8986824192106724, - "sentiment": 45 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-24T02:50:21", - "channel": "dev", - "type": "message", - "body": "Don't worry, he's not gonna find out. I left way before he got back last night, and I didn't go back this morning until twenty minutes after he'd left.", - "sourceId": "pbgehltabsticyd-2", - "sourceParentId": "pbgehltabsticyd", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 1.0012965649366379, - "neutral": 42.29363799095154, - "negative": 28.44868302345276, - "positive": 28.256380558013916, - "sentiment": 50 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-24T02:53:09", - "channel": "dev", - "type": "message", - "body": "You went back?", - "sourceId": "pbgehltabsticyd-3", - "sourceParentId": "pbgehltabsticyd", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.4231059458106756, - "neutral": 65.79419374465942, - "negative": 32.70195126533508, - "positive": 1.0807502083480358, - "sentiment": 34 - } - } - ], - [ - { - "member": "Big Head", - "timestamp": "2022-09-23T20:07:11", - "title": "The boat guy", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "Oh, hey, did I tell you? I'm getting a boat.", - "sourceId": "lcgpcdvbobcjjvl", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.04906199174001813, - "neutral": 92.48504042625427, - "negative": 1.8497932702302933, - "positive": 5.616099759936333, - "sentiment": 52 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-23T20:14:42", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Wow.", - "sourceId": "lcgpcdvbobcjjvl-1", - "sourceParentId": "lcgpcdvbobcjjvl", - "platform": "github", - "sentiment": { - "label": "positive", - "mixed": 1.3987952843308449, - "neutral": 22.180821001529694, - "negative": 2.6149243116378784, - "positive": 73.80545735359192, - "sentiment": 86 - } - }, - { - "member": "Big Head", - "timestamp": "2022-09-23T20:17:49", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "And a boat guy to take care of it. You have to have a boat guy.", - "sourceId": "lcgpcdvbobcjjvl-2", - "sourceParentId": "lcgpcdvbobcjjvl", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 31.626668572425842, - "neutral": 35.973960161209106, - "negative": 7.01313242316246, - "positive": 25.386247038841248, - "sentiment": 59 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-23T20:19:14", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Oh yeah, you've gotta have a boat guy.", - "sourceId": "lcgpcdvbobcjjvl-3", - "sourceParentId": "lcgpcdvbobcjjvl", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.3472378943115473, - "neutral": 81.02324604988098, - "negative": 10.516948252916336, - "positive": 8.112569153308868, - "sentiment": 49 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-17T10:28:33", - "channel": "random", - "type": "message", - "body": "Are the judges allowed to send us through to the finals immediately after we present or they have to wait until everybody has gone?", - "sourceId": "lzdmdosyduwfeqr", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.04202307900413871, - "neutral": 98.31175804138184, - "negative": 0.9239627048373222, - "positive": 0.7222549058496952, - "sentiment": 50 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-17T10:35:04", - "channel": "random", - "type": "message", - "body": "What? I was just asking what everybody was thinking.", - "sourceId": "lzdmdosyduwfeqr-1", - "sourceParentId": "lzdmdosyduwfeqr", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 5.790635570883751, - "neutral": 81.22329115867615, - "negative": 9.629441052675247, - "positive": 3.3566396683454514, - "sentiment": 47 - } - } - ], - [ - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-12T22:29:16", - "channel": "dev", - "type": "message", - "body": "She invited me to her room to watch Cloud Atlas later tonight.", - "sourceId": "flmolhiuictvsxw", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.018648424884304404, - "neutral": 68.60543489456177, - "negative": 0.11044665006920695, - "positive": 31.265470385551453, - "sentiment": 66 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-12T22:34:19", - "channel": "dev", - "type": "message", - "body": "Oh yeah, that means she wants you to lay her.", - "sourceId": "flmolhiuictvsxw-1", - "sourceParentId": "flmolhiuictvsxw", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 2.8059901669621468, - "neutral": 78.24748158454895, - "negative": 7.855759561061859, - "positive": 11.090776324272156, - "sentiment": 52 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-12T22:36:29", - "channel": "dev", - "type": "message", - "body": "Is that definitive?", - "sourceId": "flmolhiuictvsxw-2", - "sourceParentId": "flmolhiuictvsxw", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 5.857448652386665, - "neutral": 81.90295696258545, - "negative": 4.313289001584053, - "positive": 7.926299422979355, - "sentiment": 52 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-12T22:39:06", - "channel": "dev", - "type": "message", - "body": "I mean, nobody can watch more than like a minute of that film.", - "sourceId": "flmolhiuictvsxw-3", - "sourceParentId": "flmolhiuictvsxw", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 4.56712543964386, - "neutral": 10.065295547246933, - "negative": 79.06773090362549, - "positive": 6.29984587430954, - "sentiment": 14 - } - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-20T16:21:05", - "title": "Failure is contagious", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "Six months ago, these guys had 35 million and Series B Financing. Now The Carver's here doing teardown.", - "sourceId": "eutyouiyzjvoyhi", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.39204969070851803, - "neutral": 17.026890814304352, - "negative": 76.74716114997864, - "positive": 5.833901464939117, - "sentiment": 15 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-20T16:24:21", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "He's basically moving their carcass to the cloud.", - "sourceId": "eutyouiyzjvoyhi-1", - "sourceParentId": "eutyouiyzjvoyhi", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.21065925247967243, - "neutral": 73.50999116897583, - "negative": 12.713766098022461, - "positive": 13.565589487552643, - "sentiment": 50 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-20T16:30:34", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Don't touch anything. Failure is contagious.", - "sourceId": "eutyouiyzjvoyhi-2", - "sourceParentId": "eutyouiyzjvoyhi", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.03562686906661838, - "neutral": 1.28090251237154, - "negative": 97.97942042350769, - "positive": 0.7040504366159439, - "sentiment": 1 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-27T22:33:02", - "channel": "dinesh-tara", - "type": "message", - "body": "Are you sure that she didn't ask Gilfoyle for a danish and maybe you misheard her?", - "sourceId": "uivgabtboxjixhc", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 1.217873953282833, - "neutral": 93.64758729934692, - "negative": 4.754375666379929, - "positive": 0.3801640821620822, - "sentiment": 48 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-27T22:39:26", - "channel": "dinesh-tara", - "type": "message", - "body": "You're probably right, she just wanted Danish.", - "sourceId": "uivgabtboxjixhc-1", - "sourceParentId": "uivgabtboxjixhc", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 28.11112105846405, - "neutral": 15.834425389766693, - "negative": 53.42739224433899, - "positive": 2.627057209610939, - "sentiment": 25 - } - } - ], - [ - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-12T11:42:22", - "channel": "dinesh-tara", - "type": "message", - "body": "Looks like Gilfoyle and his lady Satanist are back from the airport.", - "sourceId": "ppsoqzoymiqdssn", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 0.1131588127464056, - "neutral": 48.896580934524536, - "negative": 50.30606985092163, - "positive": 0.6841918453574181, - "sentiment": 25 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-12T11:47:28", - "channel": "dinesh-tara", - "type": "message", - "body": "Can you imagine what kind of show this one's gonna be? He says that she has an Amy Winehouse vibe. What does that mean? All tatted-up and nowhere to go.", - "sourceId": "ppsoqzoymiqdssn-1", - "sourceParentId": "ppsoqzoymiqdssn", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 0.07132574683055282, - "neutral": 3.9600569754838943, - "negative": 95.74639797210693, - "positive": 0.22221412509679794, - "sentiment": 2 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-12T11:55:27", - "channel": "dinesh-tara", - "type": "message", - "body": "Hooked on OxyContin?", - "sourceId": "ppsoqzoymiqdssn-2", - "sourceParentId": "ppsoqzoymiqdssn", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.01675297098699957, - "neutral": 95.02748250961304, - "negative": 4.196474328637123, - "positive": 0.7592855021357536, - "sentiment": 48 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-12T12:01:24", - "channel": "dinesh-tara", - "type": "message", - "body": "Decomposing?", - "sourceId": "ppsoqzoymiqdssn-3", - "sourceParentId": "ppsoqzoymiqdssn", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.03215510514564812, - "neutral": 79.70364093780518, - "negative": 19.844427704811096, - "positive": 0.41978037916123867, - "sentiment": 40 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-22T11:32:58", - "channel": "dinesh-tara", - "type": "message", - "body": "I'd ask her for a date if you remove the Gilfoyle from her.", - "sourceId": "trrbjwjozwxbbki", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 1.286698691546917, - "neutral": 35.987287759780884, - "negative": 60.720425844192505, - "positive": 2.005585841834545, - "sentiment": 21 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-22T11:34:19", - "channel": "dinesh-tara", - "type": "message", - "body": "It's weird having a girl in the house. There's a very strange energy.", - "sourceId": "trrbjwjozwxbbki-1", - "sourceParentId": "trrbjwjozwxbbki", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 8.078926801681519, - "neutral": 13.455688953399658, - "negative": 77.33436822891235, - "positive": 1.1310179717838764, - "sentiment": 12 - } - } - ], - [ - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-15T04:20:32", - "title": "Pakistani Denzel", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-opened", - "body": "I'm much more handsome than you are. No, my face is completely symmetrical. You know what my nickname was when I was a kid?", - "sourceId": "egjmecelcqbrqgp", - "platform": "github", - "sentiment": { - "label": "mixed", - "mixed": 40.9753292798996, - "neutral": 19.003790616989136, - "negative": 2.6634717360138893, - "positive": 37.35741078853607, - "sentiment": 67 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-15T04:21:40", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "What?", - "sourceId": "egjmecelcqbrqgp-1", - "sourceParentId": "egjmecelcqbrqgp", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 2.9262281954288483, - "neutral": 86.20936274528503, - "negative": 9.965725243091583, - "positive": 0.8986824192106724, - "sentiment": 45 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-15T04:23:27", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Pakistani Denzel.", - "sourceId": "egjmecelcqbrqgp-2", - "sourceParentId": "egjmecelcqbrqgp", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.09214855381287634, - "neutral": 88.52511644363403, - "negative": 7.580118626356125, - "positive": 3.8026168942451477, - "sentiment": 48 - } - } - ], - [ - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-23T17:01:07", - "channel": "dinesh-tara", - "type": "message", - "body": "To be honest, elements of this arrangement still trouble me. However, I have not had a lot of such experiences. So I feel it may be foolish to turn this down. So as long as Gilfoyle is not in the room and I can verify that the door is locked then I have concluded that yes I would love to have a relationship with you Tara. Yeah.", - "sourceId": "orwdpbrnitnewiw", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 17.574037611484528, - "neutral": 2.91304774582386, - "negative": 78.73450517654419, - "positive": 0.7784125860780478, - "sentiment": 11 - } - } - ], - [ - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-29T13:28:28", - "channel": "dev", - "type": "message", - "body": "He's trying to turn us into corporate rock, Richard. We are punk rock.", - "sourceId": "ljszvvkbmobbeqr", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 2.3377902805805206, - "neutral": 64.90101218223572, - "negative": 5.977356433868408, - "positive": 26.783841848373413, - "sentiment": 60 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-29T13:32:06", - "channel": "dev", - "type": "message", - "body": "Actually, you know, I think a better analogy would be jazz. Like we riff and improvise around a central theme to create one cohesive piece of music.", - "sourceId": "ljszvvkbmobbeqr-1", - "sourceParentId": "ljszvvkbmobbeqr", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 1.485686469823122, - "neutral": 19.65077966451645, - "negative": 73.38129878044128, - "positive": 5.48224002122879, - "sentiment": 16 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-29T01:52:53", - "title": "Naming for the company", - "type": "comment", - "body": "Are you serious? Lowercase letters? Twitter, lowercase \"t\". Google, lowercase \"g\". Facebook, lowercase \"f\". Every company in the Valley has lowercase letters. Why? Because it's safe.", - "sourceId": "xfmvdxgsvplyyzh", - "platform": "linkedin", - "sentiment": { - "label": "negative", - "mixed": 0.004434339643921703, - "neutral": 0.8333832956850529, - "negative": 99.12243485450745, - "positive": 0.03974935389123857, - "sentiment": 0 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-20T16:28:21", - "channel": "pitching", - "type": "message", - "body": "I didn't turn down ten million dollars because of Peter Gregory, Monica! I turned it down because of you!", - "sourceId": "dqgqovikbgevsse", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 0.06257160566747189, - "neutral": 32.00956881046295, - "negative": 36.85087859630585, - "positive": 31.076982617378235, - "sentiment": 47 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-20T16:34:19", - "channel": "pitching", - "type": "message", - "body": "However angry he is, I am one-tenth as angry. Because one of the ten million would've been mine... because I own ten percent...", - "sourceId": "dqgqovikbgevsse-1", - "sourceParentId": "dqgqovikbgevsse", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 0.027255367604084313, - "neutral": 12.428713589906693, - "negative": 87.45593428611755, - "positive": 0.08809903520159423, - "sentiment": 6 - } - }, - { - "member": "Monica Hall", - "timestamp": "2022-09-28T16:36:08", - "channel": "pitching", - "type": "message", - "body": "I know.", - "sourceId": "dqgqovikbgevsse-2", - "sourceParentId": "dqgqovikbgevsse", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.050652853678911924, - "neutral": 98.4620213508606, - "negative": 0.34796551335603, - "positive": 1.1393594555556774, - "sentiment": 50 - } - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-16T12:49:41", - "title": "Introducing Scrum", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "And that, gentlemen, is scrum. Welcome to the next eight weeks of our lives.", - "sourceId": "ueuxynvlbffcmfb", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.1485599670559168, - "neutral": 56.286513805389404, - "negative": 4.122534021735191, - "positive": 39.44239914417267, - "sentiment": 68 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-16T12:55:57", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "This just became a job.", - "sourceId": "ueuxynvlbffcmfb-1", - "sourceParentId": "ueuxynvlbffcmfb", - "platform": "github", - "sentiment": { - "label": "positive", - "mixed": 4.04479093849659, - "neutral": 35.94997823238373, - "negative": 8.50801020860672, - "positive": 51.49722099304199, - "sentiment": 71 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-16T23:44:55", - "channel": "random", - "type": "message", - "body": "No, no, no! Close it! Look at this left from the previous tenant. Unbelievable. I can't believe I didn't enter the garage until this point. I mean, is that marijuanas?", - "sourceId": "dzyldaxzoeleopz", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 0.0797447341028601, - "neutral": 10.97383201122284, - "negative": 86.63663864135742, - "positive": 2.3097803816199303, - "sentiment": 8 - } - } - ], - [ - { - "member": "Gavin Belson", - "timestamp": "2022-09-29T03:43:49", - "title": "Hooli Chat is awesome", - "type": "comment", - "body": "The audio's still working! Audio worked a hundred years ago!", - "sourceId": "owepzuqzfivbgpx", - "platform": "linkedin", - "sentiment": { - "label": "mixed", - "mixed": 46.8488872051239, - "neutral": 0.3815717063844204, - "negative": 21.558359265327454, - "positive": 31.211185455322266, - "sentiment": 55 - } - } - ], - [ - { - "member": "Monica Hall", - "timestamp": "2022-09-27T20:30:17", - "channel": "random", - "type": "message", - "body": "It's Chuy Ramirez? I'm impressed. He sold a mural today for a half-million bucks.", - "sourceId": "twbelmheezxskzx", - "platform": "discord", - "sentiment": { - "label": "positive", - "mixed": 0.05240332102403045, - "neutral": 20.64451575279236, - "negative": 0.6863150279968977, - "positive": 78.61676216125488, - "sentiment": 89 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-10T02:30:34", - "channel": "random", - "type": "message", - "body": "It wasn't on a garage door, was it?", - "sourceId": "twbelmheezxskzx-1", - "sourceParentId": "twbelmheezxskzx", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 9.668681025505066, - "neutral": 59.17031168937683, - "negative": 29.45384979248047, - "positive": 1.707155629992485, - "sentiment": 36 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-16T19:52:04", - "title": "Peter Gregory doesn't care?", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "Doesn't Peter Gregory want what's best for the company?", - "sourceId": "djzyopywxriceka", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.18711562734097242, - "neutral": 48.23927283287048, - "negative": 51.168256998062134, - "positive": 0.4053525160998106, - "sentiment": 25 - } - }, - { - "member": "Monica Hall", - "timestamp": "2022-09-27T20:37:53", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Look, I'm going to be straight with you. Peter Gregory doesn't care.", - "sourceId": "djzyopywxriceka-1", - "sourceParentId": "djzyopywxriceka", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 1.933036930859089, - "neutral": 34.700459241867065, - "negative": 59.812408685684204, - "positive": 3.5540904849767685, - "sentiment": 22 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-16T20:02:27", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "About?", - "sourceId": "djzyopywxriceka-2", - "sourceParentId": "djzyopywxriceka", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.045805113040842116, - "neutral": 99.00956153869629, - "negative": 0.45272568240761757, - "positive": 0.49189962446689606, - "sentiment": 50 - } - }, - { - "member": "Monica Hall", - "timestamp": "2022-09-26T20:08:54", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "You.", - "sourceId": "djzyopywxriceka-3", - "sourceParentId": "djzyopywxriceka", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 3.707779571413994, - "neutral": 83.81884098052979, - "negative": 3.1977199018001556, - "positive": 9.275661408901215, - "sentiment": 53 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-16T20:16:19", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Wait. Just him, or both of us?", - "sourceId": "djzyopywxriceka-4", - "sourceParentId": "djzyopywxriceka", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.037171840085648, - "neutral": 98.22317957878113, - "negative": 0.6797533016651869, - "positive": 1.0598915629088879, - "sentiment": 50 - } - }, - { - "member": "Monica Hall", - "timestamp": "2022-09-27T20:20:43", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Any of you; Pied Piper.", - "sourceId": "djzyopywxriceka-5", - "sourceParentId": "djzyopywxriceka", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.006670067523373291, - "neutral": 97.59224653244019, - "negative": 0.9819931350648403, - "positive": 1.4190936461091042, - "sentiment": 50 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-16T20:24:43", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Okay, uhh, then why did he back us? Just to disappoint Gavin Belson? He spent $200,000 to disappoint Gavin Belson?", - "sourceId": "djzyopywxriceka-6", - "sourceParentId": "djzyopywxriceka", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.42990860529243946, - "neutral": 15.112993121147156, - "negative": 84.2840850353241, - "positive": 0.1730042858980596, - "sentiment": 8 - } - }, - { - "member": "Monica Hall", - "timestamp": "2022-09-27T20:30:17", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Yeah, that's nothing. Peter would spend millions just to mildly annoy Gavin. These are billionaires, Richard. Annoying each other means more to them than we'll make in a lifetime.", - "sourceId": "djzyopywxriceka-7", - "sourceParentId": "djzyopywxriceka", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.03755458747036755, - "neutral": 5.7831864804029465, - "negative": 93.78625154495239, - "positive": 0.3930021543055773, - "sentiment": 3 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-16T20:34:10", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "I see. And you conveniently forgot to mention any of this when you were convincing me to turn down 10 million dollars. And now I'm in the middle of some contest between two billionaires?", - "sourceId": "djzyopywxriceka-8", - "sourceParentId": "djzyopywxriceka", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 1.4041445218026638, - "neutral": 16.042208671569824, - "negative": 79.39441800117493, - "positive": 3.1592287123203278, - "sentiment": 12 - } - }, - { - "member": "Monica Hall", - "timestamp": "2022-09-27T20:37:53", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "In fairness, Gavin only offered the $10 million because we started pursuing you.", - "sourceId": "djzyopywxriceka-9", - "sourceParentId": "djzyopywxriceka", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.10497653856873512, - "neutral": 90.85434675216675, - "negative": 1.0929159820079803, - "positive": 7.947758585214615, - "sentiment": 53 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-16T20:41:45", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Yes, but he offered it to me, Monica. He offered 10 million dollars! And I didn't take it because you came to me when I was puking and freaking out and told me that Peter Gregory believed in me, when in reality, he didn't care at all!", - "sourceId": "djzyopywxriceka-10", - "sourceParentId": "djzyopywxriceka", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 2.835369296371937, - "neutral": 6.711319088935852, - "negative": 67.1541690826416, - "positive": 23.299141228199005, - "sentiment": 28 - } - }, - { - "member": "Monica Hall", - "timestamp": "2022-09-27T20:48:39", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Richard...", - "sourceId": "djzyopywxriceka-11", - "sourceParentId": "djzyopywxriceka", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.0006566885076608742, - "neutral": 99.91132616996765, - "negative": 0.015612631978001446, - "positive": 0.07240219856612384, - "sentiment": 50 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-16T20:55:09", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "I didn't turn down 10 million dollars because of Peter Gregory, Monica. I turned it down because of you!", - "sourceId": "djzyopywxriceka-12", - "sourceParentId": "djzyopywxriceka", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.09491281816735864, - "neutral": 29.444965720176697, - "negative": 44.62568759918213, - "positive": 25.83443522453308, - "sentiment": 41 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-16T21:00:04", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "However angry he is, I am one tenth as angry. Because one of the 10 million would have been mine, because I own 10%...", - "sourceId": "djzyopywxriceka-13", - "sourceParentId": "djzyopywxriceka", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.04106033011339605, - "neutral": 10.233817249536514, - "negative": 89.61835503578186, - "positive": 0.10677153477445245, - "sentiment": 5 - } - }, - { - "member": "Monica Hall", - "timestamp": "2022-09-27T21:05:37", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "I know.", - "sourceId": "djzyopywxriceka-14", - "sourceParentId": "djzyopywxriceka", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.050652853678911924, - "neutral": 98.4620213508606, - "negative": 0.34796551335603, - "positive": 1.1393594555556774, - "sentiment": 50 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-16T21:09:17", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "...of Pied Piper.", - "sourceId": "djzyopywxriceka-15", - "sourceParentId": "djzyopywxriceka", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.004161001925240271, - "neutral": 96.28230929374695, - "negative": 3.417370468378067, - "positive": 0.29615021776407957, - "sentiment": 48 - } - }, - { - "member": "Monica Hall", - "timestamp": "2022-09-27T21:12:08", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "I know!", - "sourceId": "djzyopywxriceka-16", - "sourceParentId": "djzyopywxriceka", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.08280414040200412, - "neutral": 85.47839522361755, - "negative": 0.39450526237487793, - "positive": 14.044295251369476, - "sentiment": 57 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-16T10:08:23", - "channel": "pitching", - "type": "message", - "body": "Curse Erlich! I turned down 10 million dollars to build this thing. You want vision? I will show you the vision!", - "sourceId": "oczgxyksxgbtvef", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 1.0039807297289371, - "neutral": 2.907024696469307, - "negative": 94.0663993358612, - "positive": 2.0225901156663895, - "sentiment": 4 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-16T10:14:49", - "channel": "pitching", - "type": "message", - "body": "I like this new angry side of you. Being around angry people relaxes me because I know where I stand.", - "sourceId": "oczgxyksxgbtvef-1", - "sourceParentId": "oczgxyksxgbtvef", - "platform": "discord", - "sentiment": { - "label": "positive", - "mixed": 1.8936807289719582, - "neutral": 1.8948543816804886, - "negative": 0.6075004115700722, - "positive": 95.60396075248718, - "sentiment": 97 - } - } - ], - [ - { - "member": "Jian-Yang", - "timestamp": "2022-09-28T18:16:49", - "title": "Remove fish", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-opened", - "body": "I eat the fish.", - "sourceId": "ngznwzavenovrot", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 4.141155630350113, - "neutral": 70.76895236968994, - "negative": 14.434976875782013, - "positive": 10.654916614294052, - "sentiment": 48 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-28T18:21:07", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "I understand you eat the fish. But when you clean the fish you can't leave the fish head and guts in the sink. Because the whole house smells like a bait station. So you gotta put it in the trash and then take the trash out. Do you understand?", - "sourceId": "ngznwzavenovrot-1", - "sourceParentId": "ngznwzavenovrot", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.39996844716370106, - "neutral": 3.9106618613004684, - "negative": 95.28302550315857, - "positive": 0.4063412547111511, - "sentiment": 3 - } - }, - { - "member": "Jian-Yang", - "timestamp": "2022-09-28T18:27:28", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Yes. I eat the fish.", - "sourceId": "ngznwzavenovrot-2", - "sourceParentId": "ngznwzavenovrot", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 7.673552632331848, - "neutral": 66.83378219604492, - "negative": 6.105304509401321, - "positive": 19.38736140727997, - "sentiment": 57 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-28T18:30:05", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Idiot!", - "sourceId": "ngznwzavenovrot-3", - "sourceParentId": "ngznwzavenovrot", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.045268028043210506, - "neutral": 6.302497535943985, - "negative": 92.69623756408691, - "positive": 0.9560065343976021, - "sentiment": 4 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-23T13:16:21", - "channel": "random", - "type": "message", - "body": "I always knew I was missing something, and then when someone explained the concept of \"game\". I remember very distinctly thinking, \"That's what I don't have\".", - "sourceId": "urpirxwkmzspyhg", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.6053416058421135, - "neutral": 81.49740695953369, - "negative": 11.789807677268982, - "positive": 6.107449904084206, - "sentiment": 47 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-12T08:46:56", - "channel": "random", - "type": "message", - "body": "You know, I wish this was Roman times. You know? Life was simpler back then.", - "sourceId": "ekeayriaeitugne", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 30.350059270858765, - "neutral": 32.686617970466614, - "negative": 10.559672117233276, - "positive": 26.403650641441345, - "sentiment": 58 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-12T08:50:54", - "channel": "random", - "type": "message", - "body": "Simpler for you. I would have been a slave.", - "sourceId": "ekeayriaeitugne-1", - "sourceParentId": "ekeayriaeitugne", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 6.6263943910598755, - "neutral": 19.917316734790802, - "negative": 63.83677124977112, - "positive": 9.619520604610443, - "sentiment": 23 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-12T08:58:08", - "channel": "random", - "type": "message", - "body": "There's still time.", - "sourceId": "ekeayriaeitugne-2", - "sourceParentId": "ekeayriaeitugne", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.5321774166077375, - "neutral": 71.6576337814331, - "negative": 7.29767307639122, - "positive": 20.512521266937256, - "sentiment": 57 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-22T01:42:59", - "channel": "random", - "type": "message", - "body": "Are you dressed like Steve Jobs?", - "sourceId": "xvltulytwmegqbd", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.0047763442125869915, - "neutral": 92.50932931900024, - "negative": 6.87236562371254, - "positive": 0.6135339848697186, - "sentiment": 47 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-22T01:44:05", - "channel": "random", - "type": "message", - "body": "Oh, am I? Well, I suppose Steve and I always have shared a similar aesthetic.", - "sourceId": "xvltulytwmegqbd-1", - "sourceParentId": "xvltulytwmegqbd", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.06792005733586848, - "neutral": 91.1441445350647, - "negative": 2.977941185235977, - "positive": 5.809995532035828, - "sentiment": 51 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-21T20:49:08", - "channel": "marketing", - "type": "message", - "body": "Dinesh, leave it unbuttoned. We want you to look awful. Makes for a better \"before\" photo.", - "sourceId": "xpcqdeyhrepzpbn", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 0.3704856615513563, - "neutral": 5.119350552558899, - "negative": 93.84213089942932, - "positive": 0.6680390331894159, - "sentiment": 3 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-21T20:56:16", - "channel": "marketing", - "type": "message", - "body": "But you're wearing a jacket.", - "sourceId": "xpcqdeyhrepzpbn-1", - "sourceParentId": "xpcqdeyhrepzpbn", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 1.1531420983374119, - "neutral": 76.36326551437378, - "negative": 17.89032816886902, - "positive": 4.593262821435928, - "sentiment": 43 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-21T20:59:26", - "channel": "marketing", - "type": "message", - "body": "Yeah, because I'm the genius marketer. I'm not a code freak like you guys. Besides, I'm wearing sandals so I am iconoclasting a little bit.", - "sourceId": "xpcqdeyhrepzpbn-2", - "sourceParentId": "xpcqdeyhrepzpbn", - "platform": "discord", - "sentiment": { - "label": "positive", - "mixed": 8.039136976003647, - "neutral": 32.468873262405396, - "negative": 2.5199424475431442, - "positive": 56.97205066680908, - "sentiment": 77 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-14T07:50:40", - "title": "How to build a vision for your startup", - "channel": "pied-piper", - "type": "comment", - "body": "You know, I turned down ten million dollars to build this thing. You want vision, I will show you the vision.", - "sourceId": "swnoiwqoqrkhhcn", - "platform": "devto", - "sentiment": { - "label": "neutral", - "mixed": 1.6151757910847664, - "neutral": 42.0449823141098, - "negative": 41.67405068874359, - "positive": 14.665797352790833, - "sentiment": 36 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-14T07:55:07", - "channel": "pied-piper", - "type": "comment", - "body": "I like this new angry side to you. Being around angry people relaxes me, because I know where I stand.", - "sourceId": "swnoiwqoqrkhhcn-1", - "sourceParentId": "swnoiwqoqrkhhcn", - "platform": "devto", - "sentiment": { - "label": "positive", - "mixed": 2.5227826088666916, - "neutral": 2.538665756583214, - "negative": 0.9802678599953651, - "positive": 93.9582884311676, - "sentiment": 96 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-29T02:33:45", - "channel": "pitching", - "type": "message", - "body": "Jared. I have no vision.", - "sourceId": "dlwlwdfdhxkpssw", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.11597494594752789, - "neutral": 49.78145360946655, - "negative": 48.47317934036255, - "positive": 1.629389077425003, - "sentiment": 27 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-29T02:37:44", - "channel": "pitching", - "type": "message", - "body": "Yes, you do. I believe in you.", - "sourceId": "dlwlwdfdhxkpssw-1", - "sourceParentId": "dlwlwdfdhxkpssw", - "platform": "discord", - "sentiment": { - "label": "positive", - "mixed": 0.4512930288910866, - "neutral": 23.77493977546692, - "negative": 0.3172519151121378, - "positive": 75.4565179347992, - "sentiment": 88 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-29T02:44:44", - "channel": "pitching", - "type": "message", - "body": "No, no, I literally have no vision. All I see is stars and swirls. I cannot see right now.", - "sourceId": "dlwlwdfdhxkpssw-2", - "sourceParentId": "dlwlwdfdhxkpssw", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 0.19187349826097488, - "neutral": 13.329558074474335, - "negative": 52.61886119842529, - "positive": 33.85971486568451, - "sentiment": 41 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-21T22:29:24", - "title": "We have an intern program?", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "Since when do we have an intern program?", - "sourceId": "lgnyqfflxjteqek", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.01832361303968355, - "neutral": 99.66434240341187, - "negative": 0.26418508496135473, - "positive": 0.053153670160099864, - "sentiment": 50 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-21T22:35:26", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "We don't. And when Keith finds that out, it's going to be a very valuable business lesson for him.", - "sourceId": "lgnyqfflxjteqek-1", - "sourceParentId": "lgnyqfflxjteqek", - "platform": "github", - "sentiment": { - "label": "positive", - "mixed": 1.653059758245945, - "neutral": 36.025428771972656, - "negative": 4.444694146513939, - "positive": 57.87681341171265, - "sentiment": 77 - } - } - ], - [ - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-29T13:46:33", - "channel": "random", - "type": "message", - "body": "You know who else is Canadian? Justin Bieber. The Hitler of music.", - "sourceId": "mhkwdpjnmysgprr", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 3.269563242793083, - "neutral": 84.88993048667908, - "negative": 5.594372749328613, - "positive": 6.2461331486701965, - "sentiment": 50 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-29T13:51:45", - "channel": "random", - "type": "message", - "body": "Hitler actually played the bassoon. So technically Hitler was the Hitler of music.", - "sourceId": "mhkwdpjnmysgprr-1", - "sourceParentId": "mhkwdpjnmysgprr", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 9.965961426496506, - "neutral": 67.37287044525146, - "negative": 18.678802251815796, - "positive": 3.982369974255562, - "sentiment": 43 - } - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-10T18:12:55", - "title": "The visa issue", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "Hey, Dinesh. Dinesh. I'm on the phone with the bank and they say they need an extra form for your payroll, because of your visa?", - "sourceId": "luatrsjltohyvyl", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.14617502456530929, - "neutral": 84.04848575592041, - "negative": 15.334358811378479, - "positive": 0.47097988426685333, - "sentiment": 43 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-10T18:18:59", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Visa? What visa? I'm a US citizen.", - "sourceId": "luatrsjltohyvyl-1", - "sourceParentId": "luatrsjltohyvyl", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.3638998605310917, - "neutral": 8.321676403284073, - "negative": 91.19883179664612, - "positive": 0.11559041449800134, - "sentiment": 4 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-10T18:22:29", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "(on phone) I have Dinesh Chugtai here, and he's pretty irate because... Oh, I see. Bertram Gilfoyle is the foreign national. Citizen of Canada. Okay, thank you.", - "sourceId": "luatrsjltohyvyl-2", - "sourceParentId": "luatrsjltohyvyl", - "platform": "github", - "sentiment": { - "label": "positive", - "mixed": 0.01044170421664603, - "neutral": 42.26385056972504, - "negative": 1.9015008583664894, - "positive": 55.82420825958252, - "sentiment": 77 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-10T18:26:25", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "You're Canadian?", - "sourceId": "luatrsjltohyvyl-3", - "sourceParentId": "luatrsjltohyvyl", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.004306227128836326, - "neutral": 99.81828331947327, - "negative": 0.1384127652272582, - "positive": 0.03898807626683265, - "sentiment": 50 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-10T18:27:33", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Your \"borders\" are merely a construct. I prefer to think of myself as a citizen of the world.", - "sourceId": "luatrsjltohyvyl-4", - "sourceParentId": "luatrsjltohyvyl", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 15.424184501171112, - "neutral": 55.15676140785217, - "negative": 12.327824532985687, - "positive": 17.09122806787491, - "sentiment": 52 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-10T18:35:16", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Do you mind just sending them the form so they know you're here legally?", - "sourceId": "luatrsjltohyvyl-5", - "sourceParentId": "luatrsjltohyvyl", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.08404233958572149, - "neutral": 84.66721177101135, - "negative": 14.868167042732239, - "positive": 0.3805737243965268, - "sentiment": 43 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-10T18:42:13", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Yes, I mind. And also I may not be. To wit, maybe you could make out my checks to cash? Or bitcoin.", - "sourceId": "luatrsjltohyvyl-6", - "sourceParentId": "luatrsjltohyvyl", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 7.701738923788071, - "neutral": 62.09477186203003, - "negative": 27.634629607200623, - "positive": 2.568858489394188, - "sentiment": 37 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-10T18:48:07", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "I didn't know I was working with an illegal.", - "sourceId": "luatrsjltohyvyl-7", - "sourceParentId": "luatrsjltohyvyl", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 30.39465844631195, - "neutral": 39.323973655700684, - "negative": 28.57416272163391, - "positive": 1.7072100192308426, - "sentiment": 37 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-10T18:55:48", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "The irony.", - "sourceId": "luatrsjltohyvyl-8", - "sourceParentId": "luatrsjltohyvyl", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.22033273708075285, - "neutral": 89.24514651298523, - "negative": 4.099773615598679, - "positive": 6.434747576713562, - "sentiment": 51 - } - } - ], - [ - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-10T16:53:42", - "channel": "dev", - "type": "message", - "body": "Inferior products win out all the time.", - "sourceId": "ymwbwypqjgxorin", - "platform": "discord", - "sentiment": { - "label": "positive", - "mixed": 0.24296531919389963, - "neutral": 3.5118956118822098, - "negative": 0.4122307524085045, - "positive": 95.83290815353394, - "sentiment": 98 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-10T16:57:18", - "channel": "dev", - "type": "message", - "body": "Like Jesus over Satan.", - "sourceId": "ymwbwypqjgxorin-1", - "sourceParentId": "ymwbwypqjgxorin", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 9.71376821398735, - "neutral": 33.93702805042267, - "negative": 49.491527676582336, - "positive": 6.857673823833466, - "sentiment": 29 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-10T17:02:10", - "channel": "dev", - "type": "message", - "body": "I was going to say VHS over Beta.", - "sourceId": "ymwbwypqjgxorin-2", - "sourceParentId": "ymwbwypqjgxorin", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.006022244997438975, - "neutral": 86.71099543571472, - "negative": 2.813558466732502, - "positive": 10.469421744346619, - "sentiment": 54 - } - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-16T19:50:01", - "channel": "random", - "type": "message", - "body": "My name's only Jared because Gavin called me that on my first day. My real name is Donald.", - "sourceId": "ylhgtnnwvevcpkl", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.054027006262913346, - "neutral": 78.51046919822693, - "negative": 17.85857379436493, - "positive": 3.5769321024417877, - "sentiment": 43 - } - } - ], - [ - { - "member": "Gavin Belson", - "timestamp": "2022-09-26T01:09:39", - "title": "Revolutionary ideas at Hooli", - "type": "comment", - "body": "If we can make your audio and video files smaller, we can make cancer smaller. And hunger. And... AIDS.", - "sourceId": "cakszdxzeltqmca", - "platform": "linkedin", - "sentiment": { - "label": "neutral", - "mixed": 10.653513669967651, - "neutral": 75.2123236656189, - "negative": 6.4547814428806305, - "positive": 7.679373025894165, - "sentiment": 51 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-12T06:53:10", - "title": "Picking the name", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-opened", - "body": "Richard, a name defines a company. It has to be something primal, something that you can scream out during intercourse. Like Aviato.", - "sourceId": "cyzahuaziqehqni", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.024632987333461642, - "neutral": 89.50691819190979, - "negative": 5.985405296087265, - "positive": 4.483042284846306, - "sentiment": 49 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-12T06:59:33", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Uuuuber!", - "sourceId": "cyzahuaziqehqni-1", - "sourceParentId": "cyzahuaziqehqni", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 1.617882214486599, - "neutral": 55.34452199935913, - "negative": 15.765215456485748, - "positive": 27.272379398345947, - "sentiment": 56 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-12T07:04:30", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Gooooogle!", - "sourceId": "cyzahuaziqehqni-2", - "sourceParentId": "cyzahuaziqehqni", - "platform": "github", - "sentiment": { - "label": "positive", - "mixed": 1.3413405045866966, - "neutral": 25.20921528339386, - "negative": 6.545171141624451, - "positive": 66.90427660942078, - "sentiment": 80 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-12T07:09:14", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Huuuuuulu!", - "sourceId": "cyzahuaziqehqni-3", - "sourceParentId": "cyzahuaziqehqni", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.1300355768762529, - "neutral": 56.869375705718994, - "negative": 4.141252115368843, - "positive": 38.85933756828308, - "sentiment": 67 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-12T07:16:08", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Exactly, right. Pied Piper!", - "sourceId": "cyzahuaziqehqni-4", - "sourceParentId": "cyzahuaziqehqni", - "platform": "github", - "sentiment": { - "label": "positive", - "mixed": 0.2889579860493541, - "neutral": 12.914742529392242, - "negative": 1.0402695275843143, - "positive": 85.75602173805237, - "sentiment": 92 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-12T07:22:29", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "I'm so sorry. Your voice doesn't really reach that register when you excited, does it?", - "sourceId": "cyzahuaziqehqni-5", - "sourceParentId": "cyzahuaziqehqni", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.8078671991825104, - "neutral": 7.388606667518616, - "negative": 91.39114022254944, - "positive": 0.41238218545913696, - "sentiment": 5 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-12T07:23:56", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "No, it's just, everyone was doing it, I was just chiming in.", - "sourceId": "cyzahuaziqehqni-6", - "sourceParentId": "cyzahuaziqehqni", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.157567101996392, - "neutral": 68.33511590957642, - "negative": 19.881190359592438, - "positive": 11.626134067773819, - "sentiment": 46 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-17T20:53:01", - "title": "Where is Richard?", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "Where's Richard? Why isn't he in here for this?", - "sourceId": "pkunvbjvkuzgrpk", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.11440168600529432, - "neutral": 86.31306290626526, - "negative": 13.148722052574158, - "positive": 0.4238144028931856, - "sentiment": 44 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-17T20:58:39", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "I think he was out back, wishing he'd taken the ten million dollars.", - "sourceId": "pkunvbjvkuzgrpk-1", - "sourceParentId": "pkunvbjvkuzgrpk", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 6.12105019390583, - "neutral": 38.06106746196747, - "negative": 48.15917611122131, - "positive": 7.658704370260239, - "sentiment": 30 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-17T21:02:46", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "No, I just saw him in his room, wishing he had taken the ten million dollars.", - "sourceId": "pkunvbjvkuzgrpk-2", - "sourceParentId": "pkunvbjvkuzgrpk", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 3.372635319828987, - "neutral": 49.86855387687683, - "negative": 39.63376581668854, - "positive": 7.125040143728256, - "sentiment": 34 - } - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-29T05:23:28", - "channel": "dev", - "type": "message", - "body": "What about, \"Dwarfism 2.0\"?", - "sourceId": "dtvsdqgfhezmniz", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.024244992528110743, - "neutral": 81.19762539863586, - "negative": 18.469908833503723, - "positive": 0.3082222770899534, - "sentiment": 41 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-29T05:27:18", - "channel": "dev", - "type": "message", - "body": "Where's \"Dwarfism 1.0\"?", - "sourceId": "dtvsdqgfhezmniz-1", - "sourceParentId": "dtvsdqgfhezmniz", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.006303051486611366, - "neutral": 90.18223285675049, - "negative": 9.612885117530823, - "positive": 0.19858479499816895, - "sentiment": 45 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-29T05:32:52", - "channel": "dev", - "type": "message", - "body": "Just in the world.", - "sourceId": "dtvsdqgfhezmniz-2", - "sourceParentId": "dtvsdqgfhezmniz", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.160953588783741, - "neutral": 56.46288990974426, - "negative": 1.1805756948888302, - "positive": 42.195579409599304, - "sentiment": 71 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-29T05:35:54", - "channel": "dev", - "type": "message", - "body": "Oh.", - "sourceId": "dtvsdqgfhezmniz-3", - "sourceParentId": "dtvsdqgfhezmniz", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.05606022896245122, - "neutral": 98.43213558197021, - "negative": 0.9662153199315071, - "positive": 0.5455824546515942, - "sentiment": 50 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-23T20:29:28", - "title": "We had a handshake", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "(talking to Arnold on the phone) We had a handshake deal. And that may not mean a lot to you, but where I come from, that means a whole lot. Ok, you agreed to sell me that name for a thousand dollars. So let me ask you this? Are you an honest man or are you a liar? Ok. Yes, same address? Good, yeah, great. See you then.", - "sourceId": "muujkdpmcybxfmt", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 4.8386842012405396, - "neutral": 64.4094705581665, - "negative": 8.718062937259674, - "positive": 22.03378677368164, - "sentiment": 57 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-23T20:31:46", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Dude, that was really badass. What did he say?", - "sourceId": "muujkdpmcybxfmt-1", - "sourceParentId": "muujkdpmcybxfmt", - "platform": "github", - "sentiment": { - "label": "mixed", - "mixed": 64.11862969398499, - "neutral": 4.597048833966255, - "negative": 24.4513139128685, - "positive": 6.832999736070633, - "sentiment": 41 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-23T20:38:10", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "He said he was gonna get in his truck, drive down here and beat the living out of me.", - "sourceId": "muujkdpmcybxfmt-2", - "sourceParentId": "muujkdpmcybxfmt", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 7.502465695142746, - "neutral": 31.706365942955017, - "negative": 57.71212577819824, - "positive": 3.079044073820114, - "sentiment": 23 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-23T20:44:57", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Why did you say that was your address? - Say any other address.", - "sourceId": "muujkdpmcybxfmt-3", - "sourceParentId": "muujkdpmcybxfmt", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.16994016477838159, - "neutral": 65.18493294715881, - "negative": 34.44032371044159, - "positive": 0.20480835810303688, - "sentiment": 33 - } - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-13T08:43:32", - "channel": "random", - "type": "message", - "body": "If you keep screaming your name, it forces the assailant to acknowledge you as a human.", - "sourceId": "wdwiagsoaturdwz", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.893728993833065, - "neutral": 48.14821183681488, - "negative": 44.01821196079254, - "positive": 6.939840316772461, - "sentiment": 31 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-23T17:37:31", - "channel": "dev", - "type": "message", - "body": "Sysbit Digital Solutions. Integrating open data spaces.Yeah. TechBitData Solution Systems. Creating unique cross platform technologies. Technologies. Technolo-Jesus. Oh, no!", - "sourceId": "pabfnagykgmmdyc", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.03759949468076229, - "neutral": 87.5124990940094, - "negative": 2.16656606644392, - "positive": 10.283336788415909, - "sentiment": 54 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-26T23:14:04", - "title": "Making the world a better place", - "type": "comment", - "body": "Infotrode Cloud-based, disruptive platforms. Disrupting the cloud through I said cloud twice. Making the world a better place through cross-platform business facing cloud. There's that cloud again! Info-trode, Info-trode!? What is Info-trode? What is that? It's all just meaningless words! Ok. No, no, no Making the world a better place. Making the world a better place. Making the world a better place...", - "sourceId": "ggzqxkttplmbube", - "platform": "linkedin", - "sentiment": { - "label": "negative", - "mixed": 1.1531402356922626, - "neutral": 6.070088967680931, - "negative": 86.52639985084534, - "positive": 6.25036284327507, - "sentiment": 10 - } - } - ], - [ - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-24T12:17:11", - "title": "Get visa for Gilfoyle", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-opened", - "body": "Dinesh wouldn't shut up, so we finally went to the visa office. Took me five minutes.", - "sourceId": "ngbiewhqttttjso", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 1.8509835004806519, - "neutral": 68.03838014602661, - "negative": 26.240164041519165, - "positive": 3.870467096567154, - "sentiment": 39 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-24T12:21:47", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Took me five years. They asked me about Al-Qaeda, like, 14 times. He literally got it while I was still looking for parking.", - "sourceId": "ngbiewhqttttjso-1", - "sourceParentId": "ngbiewhqttttjso", - "platform": "github", - "sentiment": { - "label": "positive", - "mixed": 0.7018884178251028, - "neutral": 31.674492359161377, - "negative": 32.46230185031891, - "positive": 35.161322355270386, - "sentiment": 51 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-25T15:51:53", - "type": "mention", - "body": "Time is a sphere, and I have been reincarnated during the same time period in which @piedpiper exists", - "sourceId": "puvzfmcklaluxkl", - "platform": "twitter", - "sentiment": { - "label": "neutral", - "mixed": 1.8377460539340973, - "neutral": 87.19238042831421, - "negative": 1.403920166194439, - "positive": 9.565949440002441, - "sentiment": 54 - } - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-13T12:34:46", - "channel": "random", - "type": "message", - "body": "Hey! Sorry if I scared you, I know I have somewhat ghost-like features. My uncle used to say, \"You look like someone starved a girl to death.\"", - "sourceId": "ckpuabuscaotwgu", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 1.861974224448204, - "neutral": 26.74400806427002, - "negative": 68.76850128173828, - "positive": 2.625514008104801, - "sentiment": 17 - } - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-13T18:15:35", - "channel": "dev", - "type": "message", - "body": "Richard, I just wanna say, I really respect what you're doing here. And if you could ever use someone with my business development skill set, I would love to be a part of this.", - "sourceId": "hlsmovttuslseov", - "platform": "discord", - "sentiment": { - "label": "positive", - "mixed": 0.7037832867354155, - "neutral": 12.166926264762878, - "negative": 0.8336478844285011, - "positive": 86.29564642906189, - "sentiment": 93 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-30T18:21:19", - "channel": "dev", - "type": "message", - "body": "Of course you'd love to. We'll call you when we want pleated khakis.", - "sourceId": "hlsmovttuslseov-1", - "sourceParentId": "hlsmovttuslseov", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 3.7363633513450623, - "neutral": 11.056851595640182, - "negative": 83.78913402557373, - "positive": 1.417653914541006, - "sentiment": 9 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-23T15:08:42", - "title": "Richard is too nice for a CEO", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "Richard, if you're not ruthless, it creates this kind of vacuum, and that void is filled by others, like Jared. I mean, you almost gave him shares. You need to completely change who you are, Richard. A complete teutonic shift has to happen.", - "sourceId": "avuuzjxlycmsovp", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 33.56184959411621, - "neutral": 12.355601042509079, - "negative": 47.66519367694855, - "positive": 6.417354941368103, - "sentiment": 29 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-23T15:11:42", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Tectonic.", - "sourceId": "avuuzjxlycmsovp-1", - "sourceParentId": "avuuzjxlycmsovp", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.0027804791898233816, - "neutral": 96.08131647109985, - "negative": 0.289705372415483, - "positive": 3.6261986941099167, - "sentiment": 52 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-23T15:14:32", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "What?", - "sourceId": "avuuzjxlycmsovp-2", - "sourceParentId": "avuuzjxlycmsovp", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 2.9262281954288483, - "neutral": 86.20936274528503, - "negative": 9.965725243091583, - "positive": 0.8986824192106724, - "sentiment": 45 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-23T15:18:13", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "A \"tectonic\" shift is the earth's crust moving around. \"Teutonic\", which is what you just said, is an ancient Germanic tribe that fought the Romans. They were originally from Scandinavia...", - "sourceId": "avuuzjxlycmsovp-3", - "sourceParentId": "avuuzjxlycmsovp", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.005379736467148177, - "neutral": 92.59042143821716, - "negative": 5.16095794737339, - "positive": 2.2432442754507065, - "sentiment": 49 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-23T15:21:44", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Stop it! Stop it. You're being a complete cool right now. I need you to be completely ruthless.", - "sourceId": "avuuzjxlycmsovp-4", - "sourceParentId": "avuuzjxlycmsovp", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.5757848266512156, - "neutral": 1.0750820860266685, - "negative": 97.80840277671814, - "positive": 0.5407288204878569, - "sentiment": 1 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-24T17:36:01", - "title": "All truth about Silicon Valley", - "type": "comment", - "body": "Let me explain something to you. Your whole life you've been an ugly chick but now suddenly you're a hot chick, with big tits and small nipples. So guys like that are gonna keep coming around. Don't be a slut, Richard.", - "sourceId": "tydpmatdmzaxncu", - "platform": "linkedin", - "sentiment": { - "label": "negative", - "mixed": 1.4293895103037357, - "neutral": 24.100415408611298, - "negative": 70.30906081199646, - "positive": 4.161130636930466, - "sentiment": 17 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-11T11:52:19", - "channel": "dev", - "type": "message", - "body": "Dinesh, change the lighting to something erotic because it's about to get pretty erotic in here.", - "sourceId": "omvvpgfgolwnlxm", - "platform": "discord", - "sentiment": { - "label": "mixed", - "mixed": 40.914782881736755, - "neutral": 19.228331744670868, - "negative": 33.691760897636414, - "positive": 6.165130063891411, - "sentiment": 36 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-11T11:53:50", - "channel": "dev", - "type": "message", - "body": "(speaking into his phone) License to kill-9. IB action-dot-erotica.", - "sourceId": "omvvpgfgolwnlxm-1", - "sourceParentId": "omvvpgfgolwnlxm", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.09782188571989536, - "neutral": 95.40774822235107, - "negative": 3.6209527403116226, - "positive": 0.8734694682061672, - "sentiment": 49 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-13T19:29:14", - "channel": "dev", - "type": "message", - "body": "That was nice, guys. He heard everything.", - "sourceId": "gbdylsvfqctimaz", - "platform": "discord", - "sentiment": { - "label": "positive", - "mixed": 0.22712696809321642, - "neutral": 2.950824052095413, - "negative": 0.10128050344064832, - "positive": 96.72077298164368, - "sentiment": 98 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-13T19:31:17", - "channel": "dev", - "type": "message", - "body": "That doesn't make it not true.", - "sourceId": "gbdylsvfqctimaz-1", - "sourceParentId": "gbdylsvfqctimaz", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 0.4560521338135004, - "neutral": 5.549440905451775, - "negative": 93.69620084762573, - "positive": 0.2983052050694823, - "sentiment": 3 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-13T19:35:34", - "channel": "dev", - "type": "message", - "body": "I mean, come on, Richard. As far as Pied Piper is concerned, he's as pointless as Mass Effect 3's multiple endings. I mean, he's a completely useless appendage and we all know it.", - "sourceId": "gbdylsvfqctimaz-2", - "sourceParentId": "gbdylsvfqctimaz", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 0.007238220132421702, - "neutral": 0.6671329028904438, - "negative": 99.289870262146, - "positive": 0.03576003364287317, - "sentiment": 0 - } - } - ], - [ - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-14T13:45:46", - "type": "mention", - "body": "I entice the flesh, I don't pay for it.", - "sourceId": "hgpfjvqseycsvnx", - "platform": "twitter", - "sentiment": { - "label": "negative", - "mixed": 2.1298764273524284, - "neutral": 7.024888694286346, - "negative": 86.8292510509491, - "positive": 4.0159814059734344, - "sentiment": 9 - } - } - ], - [ - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-23T19:32:17", - "channel": "dev", - "type": "message", - "body": "Who was this woman that you shook hands with for the first time?", - "sourceId": "qnuixifrerttlaq", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 2.7747943997383118, - "neutral": 73.57667684555054, - "negative": 21.35956734418869, - "positive": 2.2889653220772743, - "sentiment": 40 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-23T19:36:40", - "channel": "dev", - "type": "message", - "body": "The postman lady.", - "sourceId": "qnuixifrerttlaq-1", - "sourceParentId": "qnuixifrerttlaq", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.039560915320180357, - "neutral": 92.37585663795471, - "negative": 7.189083099365234, - "positive": 0.39549898356199265, - "sentiment": 47 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-23T19:43:12", - "channel": "dev", - "type": "message", - "body": "What? A woman that was a man?", - "sourceId": "qnuixifrerttlaq-2", - "sourceParentId": "qnuixifrerttlaq", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 8.144943416118622, - "neutral": 82.24809169769287, - "negative": 8.245871216058731, - "positive": 1.3610893860459328, - "sentiment": 47 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-23T19:46:19", - "channel": "dev", - "type": "message", - "body": "Not a post-man lady. A lady who was a post...", - "sourceId": "qnuixifrerttlaq-3", - "sourceParentId": "qnuixifrerttlaq", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.4947598557919264, - "neutral": 75.05854964256287, - "negative": 24.241669476032257, - "positive": 0.2050151815637946, - "sentiment": 38 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-23T19:49:10", - "channel": "dev", - "type": "message", - "body": "Let me ask you another question. Who was the second woman you shook hands with?", - "sourceId": "qnuixifrerttlaq-4", - "sourceParentId": "qnuixifrerttlaq", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.8361542597413063, - "neutral": 84.32493805885315, - "negative": 13.36296945810318, - "positive": 1.4759366400539875, - "sentiment": 44 - } - } - ], - [ - { - "member": "Peter Gregory", - "timestamp": "2022-09-25T11:53:54", - "channel": "pitching", - "type": "message", - "body": "Did you just take a sip from an empty cup?", - "sourceId": "sdknkuclidkvkez", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 4.302819073200226, - "neutral": 86.41289472579956, - "negative": 7.099767029285431, - "positive": 2.1845120936632156, - "sentiment": 48 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-29T11:56:40", - "channel": "pitching", - "type": "message", - "body": "Yes.", - "sourceId": "sdknkuclidkvkez-1", - "sourceParentId": "sdknkuclidkvkez", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 20.158416032791138, - "neutral": 45.69222927093506, - "negative": 3.128799796104431, - "positive": 31.02055788040161, - "sentiment": 64 - } - }, - { - "member": "Peter Gregory", - "timestamp": "2022-09-26T12:01:03", - "channel": "pitching", - "type": "message", - "body": "Why did you do that?", - "sourceId": "sdknkuclidkvkez-2", - "sourceParentId": "sdknkuclidkvkez", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 1.0961408726871014, - "neutral": 65.2647614479065, - "negative": 32.96898603439331, - "positive": 0.6701047066599131, - "sentiment": 34 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-29T12:08:10", - "channel": "pitching", - "type": "message", - "body": "Just something to do.", - "sourceId": "sdknkuclidkvkez-3", - "sourceParentId": "sdknkuclidkvkez", - "platform": "discord", - "sentiment": { - "label": "positive", - "mixed": 0.20709747914224863, - "neutral": 37.0280385017395, - "negative": 0.7790698669850826, - "positive": 61.985790729522705, - "sentiment": 81 - } - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-22T08:15:56", - "channel": "random", - "type": "message", - "body": "(bows to Erlich) Um Good morning. Whoops, that was weird. I don't know why I did that. You kind of have a like a king-ish feeling to you. You're like a Norse hero from Valhalla.", - "sourceId": "mvhhmccrqhwfrbd", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 12.325061857700348, - "neutral": 36.391714215278625, - "negative": 31.068995594978333, - "positive": 20.214231312274933, - "sentiment": 45 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-22T08:19:58", - "channel": "random", - "type": "message", - "body": "Don't pander to me. Peter Gregory said specifically to trim the fat.", - "sourceId": "mvhhmccrqhwfrbd-1", - "sourceParentId": "mvhhmccrqhwfrbd", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.3113990183919668, - "neutral": 52.09489464759827, - "negative": 39.67009782791138, - "positive": 7.923611253499985, - "sentiment": 34 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-22T08:25:46", - "channel": "random", - "type": "message", - "body": "They actually tried to diagnose me with a wasting disease because of my slender frame.", - "sourceId": "mvhhmccrqhwfrbd-2", - "sourceParentId": "mvhhmccrqhwfrbd", - "platform": "discord", - "sentiment": { - "label": "mixed", - "mixed": 66.06851816177368, - "neutral": 18.417197465896606, - "negative": 12.283646315336227, - "positive": 3.2306432723999023, - "sentiment": 45 - } - } - ], - [ - { - "member": "Gavin Belson", - "timestamp": "2022-09-20T17:26:26", - "type": "mention", - "body": "I hate Richard Hendricks, that little @piedpiper prick.", - "sourceId": "ymxtgxcugsfotnx", - "platform": "twitter", - "sentiment": { - "label": "negative", - "mixed": 0.04867729730904102, - "neutral": 1.7945107072591782, - "negative": 98.09682965278625, - "positive": 0.05997670814394951, - "sentiment": 1 - } - } - ], - [ - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-23T14:18:40", - "title": "What does Gilfoyle do?", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "What do I do? System architecture. Networking and security. No one in this house can touch me on that.", - "sourceId": "echytnrflxopmvb", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 2.9214391484856606, - "neutral": 45.42120099067688, - "negative": 50.13360381126404, - "positive": 1.5237568877637386, - "sentiment": 26 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-23T14:26:23", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Ok, that's good to know.", - "sourceId": "echytnrflxopmvb-1", - "sourceParentId": "echytnrflxopmvb", - "platform": "github", - "sentiment": { - "label": "positive", - "mixed": 0.10945755057036877, - "neutral": 29.728522896766663, - "negative": 1.2338214553892612, - "positive": 68.92820000648499, - "sentiment": 84 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-23T14:31:47", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "But does anyone appreciate that? While you were busy minoring in gender studies and singing a capella at Sarah Lawrence, I was gaining root access to NSA servers. I was one click away from starting a second Iranian revolution.", - "sourceId": "echytnrflxopmvb-2", - "sourceParentId": "echytnrflxopmvb", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.38949414156377316, - "neutral": 57.88466930389404, - "negative": 40.60365855693817, - "positive": 1.1221828870475292, - "sentiment": 30 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-23T14:34:07", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "I actually went to Vassar.", - "sourceId": "echytnrflxopmvb-3", - "sourceParentId": "echytnrflxopmvb", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.020742737979162484, - "neutral": 95.1520323753357, - "negative": 0.6306125782430172, - "positive": 4.196608811616898, - "sentiment": 52 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-23T14:37:18", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "I prevent cross-site scripting, I monitor for DDoS attacks, emergency database rollbacks, and faulty transaction handlings. The Internet heard of it? Transfers half a petabyte of data every minute. Do you have any idea how that happens? All those ones and zeroes streaming directly to your little smart phone day after day? Everyone who gets upset if he can't get the new dubstep Skrillex remix in under 12 seconds? It's not magic, it's talent and sweat. People like me, ensuring your packets get delivered, un-sniffed. So what do I do? I make sure that one bad config on one key component doesn't bankrupt the entire company. That's what I do.", - "sourceId": "echytnrflxopmvb-4", - "sourceParentId": "echytnrflxopmvb", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.10410805698484182, - "neutral": 15.250200033187866, - "negative": 83.61561894416809, - "positive": 1.0300827212631702, - "sentiment": 9 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-23T14:40:20", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "That's basically what I told him.", - "sourceId": "echytnrflxopmvb-5", - "sourceParentId": "echytnrflxopmvb", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.15444154851138592, - "neutral": 86.78432703018188, - "negative": 3.4036364406347275, - "positive": 9.657593816518784, - "sentiment": 53 - } - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-29T07:15:52", - "channel": "dev", - "type": "message", - "body": "Peter Gregory demanded a lean, ruthless business plan. And I don't think that the CEO of Microsoft has a paid best friend.", - "sourceId": "pkshcbfrmhomgdh", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 0.017050979658961296, - "neutral": 31.10581934452057, - "negative": 66.55428409576416, - "positive": 2.322847582399845, - "sentiment": 18 - } - }, - { - "member": "Big Head", - "timestamp": "2022-09-29T07:19:19", - "channel": "dev", - "type": "message", - "body": "Sergey Brin does. Larry doesn't do anything.", - "sourceId": "pkshcbfrmhomgdh-1", - "sourceParentId": "pkshcbfrmhomgdh", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 4.235411062836647, - "neutral": 28.687477111816406, - "negative": 59.164053201675415, - "positive": 7.913065701723099, - "sentiment": 24 - } - } - ], - [ - { - "member": "Big Head", - "timestamp": "2022-09-13T10:22:15", - "channel": "dev", - "type": "message", - "body": "Goolybib, man. Those guys build a mediocre piece of software that might be worth something someday, and now they live here. Money flying all over Silicon Valley but none of it ever seems to hit us.", - "sourceId": "rqiffabhhfaxodu", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 0.08833999745547771, - "neutral": 1.001678965985775, - "negative": 98.58118295669556, - "positive": 0.3288014093413949, - "sentiment": 1 - } - } - ], - [ - { - "member": "Gavin Belson", - "timestamp": "2022-09-19T22:25:23", - "title": "All truth about programmers", - "type": "comment", - "body": "It's weird. They always travel in groups of five. These programmers, there's always a tall, skinny white guy; short, skinny Asian guy; fat guy with a ponytail; some guy with crazy facial hair; and then an East Indian guy. It's like they trade guys until they all have the right group.", - "sourceId": "snanikmjfiqrbvp", - "platform": "linkedin", - "sentiment": { - "label": "negative", - "mixed": 0.5294091533869505, - "neutral": 39.43028450012207, - "negative": 57.69113302230835, - "positive": 2.3491771891713142, - "sentiment": 22 - } - } - ], - [ - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-10T08:26:47", - "title": "Try liquid shrimp?", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-opened", - "body": "What are you eating?", - "sourceId": "jcmyiplxgwlrqkj", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 1.0045831091701984, - "neutral": 36.18376851081848, - "negative": 62.58952021598816, - "positive": 0.22212956100702286, - "sentiment": 19 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-10T08:27:59", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Liquid shrimp. It's 200 dollars a quart. Wylie Dufresne made it.", - "sourceId": "jcmyiplxgwlrqkj-1", - "sourceParentId": "jcmyiplxgwlrqkj", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.06266566924750805, - "neutral": 73.33332896232605, - "negative": 10.452669858932495, - "positive": 16.151341795921326, - "sentiment": 53 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-10T08:31:36", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "How does it taste?", - "sourceId": "jcmyiplxgwlrqkj-2", - "sourceParentId": "jcmyiplxgwlrqkj", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.6863385438919067, - "neutral": 52.47809290885925, - "negative": 46.238043904304504, - "positive": 0.5975345615297556, - "sentiment": 27 - } - }, - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-09-10T08:34:56", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Like how I would imagine dirt tastes.", - "sourceId": "jcmyiplxgwlrqkj-3", - "sourceParentId": "jcmyiplxgwlrqkj", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.09111539111472666, - "neutral": 8.885259181261063, - "negative": 54.79950308799744, - "positive": 36.22412383556366, - "sentiment": 41 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-10T21:37:37", - "type": "mention", - "body": "There's 40 billion dollars of net worth, walking around this party. And you guys @piedpiper are standing around drinking shrimp and talking about what it tastes like.", - "sourceId": "ucesdocczudhwrv", - "platform": "twitter", - "sentiment": { - "label": "negative", - "mixed": 0.8430860005319118, - "neutral": 28.505408763885498, - "negative": 64.87599611282349, - "positive": 5.775516852736473, - "sentiment": 20 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-13T03:14:35", - "title": "Girls and boys", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "It's amazing how the men and women at these things always separate like this.", - "sourceId": "rdczyhdbwwhjlxg", - "platform": "github", - "sentiment": { - "label": "positive", - "mixed": 0.10650709737092257, - "neutral": 3.118354454636574, - "negative": 0.6020567379891872, - "positive": 96.17307782173157, - "sentiment": 98 - } - }, - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-13T03:21:38", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Yeah, every party in Silicon Valley ends up like a hasidic wedding.", - "sourceId": "rdczyhdbwwhjlxg-1", - "sourceParentId": "rdczyhdbwwhjlxg", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.19098161719739437, - "neutral": 63.94217014312744, - "negative": 34.04296040534973, - "positive": 1.823883317410946, - "sentiment": 34 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-26T14:27:20", - "title": "This is how music industry works?", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "Everybody involved in the music industry is either stealing it or sharing it. They're all a bunch of thiefs, especially Radiohead.", - "sourceId": "xiudtahxgxfkuzl", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.16935247695073485, - "neutral": 28.640061616897583, - "negative": 70.49365639686584, - "positive": 0.6969330832362175, - "sentiment": 15 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-26T14:30:57", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "No.", - "sourceId": "xiudtahxgxfkuzl-1", - "sourceParentId": "xiudtahxgxfkuzl", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 4.344846680760384, - "neutral": 36.8266224861145, - "negative": 58.3268940448761, - "positive": 0.5016406066715717, - "sentiment": 21 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-26T14:35:06", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "Yeah, they're bad.", - "sourceId": "xiudtahxgxfkuzl-2", - "sourceParentId": "xiudtahxgxfkuzl", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 0.643617520108819, - "neutral": 10.364360362291336, - "negative": 88.73345851898193, - "positive": 0.2585690235719085, - "sentiment": 6 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-29T22:30:48", - "channel": "random", - "type": "message", - "body": "Richard, if you want to live here, you've got to deliver. I can't have dead weight at my incubator, ok? Either that, or show some promise for God's sake. Like NipAlert, Big Head's app. Now, that's something people want.", - "sourceId": "dbflbshhjfitwri", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 0.504460372030735, - "neutral": 25.814920663833618, - "negative": 59.47597026824951, - "positive": 14.20464962720871, - "sentiment": 27 - } - } - ], - [ - { - "member": "Big Head", - "timestamp": "2022-09-11T16:42:53", - "type": "mention", - "body": "Oh God, the marketing team @hooli is having another bike meeting.", - "sourceId": "nkochzbrzqmxpow", - "platform": "twitter", - "sentiment": { - "label": "neutral", - "mixed": 0.013937741459812969, - "neutral": 84.09829139709473, - "negative": 11.232767254114151, - "positive": 4.655007645487785, - "sentiment": 47 - } - } - ], - [ - { - "member": "Gavin Belson", - "timestamp": "2022-09-15T07:53:04", - "title": "What is Hooli?", - "channel": "pied-piper", - "type": "comment", - "body": "What is Hooli? Excellent question. Hooli isn't just another high tech company. Hooli isn't just about software. Hooli...Hooli is about people. Hooli is about innovative technology that makes a difference, transforming the world as we know it. Making the world a better place, through minimal message oriented transport layers. I firmly believe we can only achieve greatness if first we achieve goodness.", - "sourceId": "llmcmvdwacjuutw", - "platform": "devto", - "sentiment": { - "label": "positive", - "mixed": 0.02383048995397985, - "neutral": 8.148453384637833, - "negative": 0.40868078358471394, - "positive": 91.41902327537537, - "sentiment": 96 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-14T12:41:41", - "channel": "random", - "type": "message", - "body": "Look, guys, for thousands of years, guys like us gave gotten kicked. But now, for the first time, we're living in an era, where we can be in charge and build empires. We could be the Vikings of our day.", - "sourceId": "pfzakjgqgnntydx", - "platform": "discord", - "sentiment": { - "label": "mixed", - "mixed": 60.32938361167908, - "neutral": 23.515993356704712, - "negative": 3.6252230405807495, - "positive": 12.529394030570984, - "sentiment": 54 - } - } - ], - [ - { - "member": "Peter Gregory", - "timestamp": "2022-09-24T02:08:46", - "title": "Peter's TED talk", - "channel": "pied-piper", - "type": "comment", - "body": "Gates, Ellison, Jobs, Dell. All dropped out of college. Silicon Valley is the cradle of innovation because of drop outs. College has become a cruel expensive joke on the poor and the middle class that benefits only the perpetrators of it. The bloated administrators.", - "sourceId": "xcnrfyiazrucmgr", - "platform": "devto", - "sentiment": { - "label": "negative", - "mixed": 0.0069552908826153725, - "neutral": 15.426932275295258, - "negative": 84.29446816444397, - "positive": 0.27164276689291, - "sentiment": 8 - } - } - ], - [ - { - "member": "Monica Hall", - "timestamp": "2022-09-27T02:12:36", - "title": "GPS tracking features", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-opened", - "body": "Hi, Monica. I work with Peter Gregory. We met outside the TED...", - "sourceId": "jnpgodleytqnzyf", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.004974992407369427, - "neutral": 94.81290578842163, - "negative": 0.04498993221204728, - "positive": 5.137130990624428, - "sentiment": 53 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-17T02:14:05", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Yeah, I remember you. What how'd you know I was here?", - "sourceId": "jnpgodleytqnzyf-1", - "sourceParentId": "jnpgodleytqnzyf", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.21758638322353363, - "neutral": 95.85450291633606, - "negative": 1.6086935997009277, - "positive": 2.3192111402750015, - "sentiment": 50 - } - }, - { - "member": "Monica Hall", - "timestamp": "2022-09-27T02:18:53", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "Peter Gregory is invested in a company that uses GPS in phones to track people.", - "sourceId": "jnpgodleytqnzyf-2", - "sourceParentId": "jnpgodleytqnzyf", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.0256810657447204, - "neutral": 99.30817484855652, - "negative": 0.009596011659596115, - "positive": 0.6565562449395657, - "sentiment": 50 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-17T02:22:02", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "That's creepy.", - "sourceId": "jnpgodleytqnzyf-3", - "sourceParentId": "jnpgodleytqnzyf", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 2.858317457139492, - "neutral": 33.547207713127136, - "negative": 60.276055335998535, - "positive": 3.318413347005844, - "sentiment": 22 - } - }, - { - "member": "Monica Hall", - "timestamp": "2022-09-28T02:25:23", - "channel": "PiedPiper/pied-piper", - "type": "pull_request-comment", - "body": "You don't know the half of it. And neither does congress.", - "sourceId": "jnpgodleytqnzyf-4", - "sourceParentId": "jnpgodleytqnzyf", - "platform": "github", - "sentiment": { - "label": "negative", - "mixed": 6.929613649845123, - "neutral": 43.75121295452118, - "negative": 46.74724638462067, - "positive": 2.5719311088323593, - "sentiment": 28 - } - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-09-19T22:34:34", - "channel": "ideas", - "type": "message", - "body": "I have a meeting with Gavin Belson. He wants to talk about Pied Piper.", - "sourceId": "fpcbqojvglgzvur", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.0017612645024200901, - "neutral": 99.20327663421631, - "negative": 0.09633462759666145, - "positive": 0.6986338179558516, - "sentiment": 50 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-19T22:36:07", - "channel": "ideas", - "type": "message", - "body": "I own 10% of Pied Piper.", - "sourceId": "fpcbqojvglgzvur-1", - "sourceParentId": "fpcbqojvglgzvur", - "platform": "discord", - "sentiment": { - "label": "neutral", - "mixed": 0.004330828232923523, - "neutral": 99.15067553520203, - "negative": 0.17975119408220053, - "positive": 0.6652520038187504, - "sentiment": 50 - } - }, - { - "member": "Richard Hendricks", - "timestamp": "2022-09-19T22:44:05", - "channel": "ideas", - "type": "message", - "body": "You said it was a bad idea.", - "sourceId": "fpcbqojvglgzvur-2", - "sourceParentId": "fpcbqojvglgzvur", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 0.007983980322023854, - "neutral": 0.4502667114138603, - "negative": 99.4903564453125, - "positive": 0.051387998973950744, - "sentiment": 0 - } - }, - { - "member": "Erlich Bachman", - "timestamp": "2022-09-19T22:47:43", - "channel": "ideas", - "type": "message", - "body": "It was a bad idea. I'm not sure what it is now.", - "sourceId": "fpcbqojvglgzvur-3", - "sourceParentId": "fpcbqojvglgzvur", - "platform": "discord", - "sentiment": { - "label": "negative", - "mixed": 0.024990199017338455, - "neutral": 0.6762337870895863, - "negative": 99.257892370224, - "positive": 0.04087881825398654, - "sentiment": 0 - } - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-20T22:16:21", - "title": "Erlich's Intro", - "channel": "PiedPiper/pied-piper", - "type": "issues-opened", - "body": "I am the founder of Aviato. And I own a very small percentage of Grindr. It's a men to men dating site where you can find other men within 10 miles of you interested in having intercourse in a public restroom.", - "sourceId": "fkxbijbivwezyxv", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.005490232433658093, - "neutral": 89.753258228302, - "negative": 1.9538428634405136, - "positive": 8.287403732538223, - "sentiment": 53 - } - }, - { - "member": "Jared Dunn", - "timestamp": "2022-09-20T22:19:18", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "In these communities there's sometimes anonymous...", - "sourceId": "fkxbijbivwezyxv-1", - "sourceParentId": "fkxbijbivwezyxv", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 0.01754280528984964, - "neutral": 99.30723309516907, - "negative": 0.5297592841088772, - "positive": 0.1454587560147047, - "sentiment": 50 - } - }, - { - "member": "Gavin Belson", - "timestamp": "2022-09-20T22:24:19", - "channel": "PiedPiper/pied-piper", - "type": "issue-comment", - "body": "I know what Grindr is. I have friends.", - "sourceId": "fkxbijbivwezyxv-2", - "sourceParentId": "fkxbijbivwezyxv", - "platform": "github", - "sentiment": { - "label": "neutral", - "mixed": 1.1837263591587543, - "neutral": 77.24307775497437, - "negative": 5.440209060907364, - "positive": 16.132986545562744, - "sentiment": 55 - } - } - ], - [ - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-08-11T14:15:03", - "type": "star", - "sourceId": "ifsjqijctwcblkq", - "platform": "github" - } - ], - [ - { - "member": "Bertram Gilfoyle", - "timestamp": "2022-08-08T22:16:14", - "type": "follow", - "sourceId": "gqvcnhwpiljcpem", - "platform": "twitter" - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-08-02T02:33:44", - "type": "star", - "sourceId": "waauzpcaqthymgd", - "platform": "github" - } - ], - [ - { - "member": "Richard Hendricks", - "timestamp": "2022-08-10T14:41:01", - "type": "follow", - "sourceId": "nckdavdbwiyrsyy", - "platform": "twitter" - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-08-08T04:23:03", - "type": "star", - "sourceId": "pyunscvcavxejos", - "platform": "github" - } - ], - [ - { - "member": "Erlich Bachman", - "timestamp": "2022-09-03T15:23:19", - "type": "follow", - "sourceId": "wcvhgckawxbpxmm", - "platform": "twitter" - } - ], - [ - { - "member": "Big Head", - "timestamp": "2022-09-10T14:33:38", - "type": "star", - "sourceId": "rmmjuembpdrhvra", - "platform": "github" - } - ], - [ - { - "member": "Big Head", - "timestamp": "2022-08-05T11:25:42", - "type": "follow", - "sourceId": "qbmxfymcrafyodl", - "platform": "twitter" - } - ], - [ - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-08T05:34:35", - "type": "star", - "sourceId": "bsntydymakdvtty", - "platform": "github" - } - ], - [ - { - "member": "Dinesh Chugtai", - "timestamp": "2022-09-08T10:25:58", - "type": "follow", - "sourceId": "ntddgmiqhwvxmkg", - "platform": "twitter" - } - ], - [ - { - "member": "Peter Gregory", - "timestamp": "2022-09-23T12:43:50", - "type": "star", - "sourceId": "yjohjszuknzrubg", - "platform": "github" - } - ], - [ - { - "member": "Peter Gregory", - "timestamp": "2022-09-22T09:40:07", - "type": "follow", - "sourceId": "xhqvyrxpkomledo", - "platform": "twitter" - } - ], - [ - { - "member": "Monica Hall", - "timestamp": "2022-09-27T21:05:37", - "type": "star", - "sourceId": "dleudwbgscschid", - "platform": "github" - } - ], - [ - { - "member": "Monica Hall", - "timestamp": "2022-09-27T21:12:08", - "type": "follow", - "sourceId": "xhszjwjeqcwxqco", - "platform": "twitter" - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-12T12:11:42", - "type": "star", - "sourceId": "isobblmiltviqdg", - "platform": "github" - } - ], - [ - { - "member": "Jared Dunn", - "timestamp": "2022-09-03T22:02:25", - "type": "follow", - "sourceId": "hctnnzacgfmsogd", - "platform": "twitter" - } - ], - [ - { - "member": "Gavin Belson", - "timestamp": "2022-09-03T15:12:57", - "type": "star", - "sourceId": "hkvjhfhxbkbsvtu", - "platform": "github" - } - ], - [ - { - "member": "Gavin Belson", - "timestamp": "2022-09-14T08:57:04", - "type": "follow", - "sourceId": "gyufisjkamfjtsq", - "platform": "twitter" - } - ], - [ - { - "member": "Jian-Yang", - "timestamp": "2022-08-10T00:13:12", - "type": "star", - "sourceId": "levocnsqhmolnkg", - "platform": "github" - } - ], - [ - { - "member": "Jian-Yang", - "timestamp": "2022-08-09T12:55:53", - "type": "follow", - "sourceId": "naymkamhdxrygos", - "platform": "twitter" - } - ], - [ - { - "member": "Laurie Bream", - "timestamp": "2022-09-26T19:17:15", - "type": "star", - "sourceId": "vtmcpgaalmaghbr", - "platform": "github" - } - ], - [ - { - "member": "Laurie Bream", - "timestamp": "2022-09-27T09:17:43", - "type": "follow", - "sourceId": "pppsebevgltczzz", - "platform": "twitter" - } - ], - [ - { - "member": "Russ Hanneman", - "timestamp": "2022-09-08T01:05:02", - "type": "star", - "sourceId": "cssnvjtqaqhzmhn", - "platform": "github" - } - ], - [ - { - "member": "Russ Hanneman", - "timestamp": "2022-08-14T04:33:19", - "type": "follow", - "sourceId": "iigssbuolzqgytb", - "platform": "twitter" - } - ], - [ - { - "member": "Jack Barker", - "timestamp": "2022-09-13T04:45:58", - "type": "star", - "sourceId": "vstbgtzhpqbsrfj", - "platform": "github" - } - ], - [ - { - "member": "Jack Barker", - "timestamp": "2022-09-09T12:30:12", - "type": "follow", - "sourceId": "uoxoyphekweisdz", - "platform": "twitter" - } - ] - ] -} diff --git a/backend/src/database/initializers/seed-entities.ts b/backend/src/database/initializers/seed-entities.ts deleted file mode 100644 index 8a558226e3..0000000000 --- a/backend/src/database/initializers/seed-entities.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { getServiceLogger } from '@crowd/logging' -/** - * This script is responsible for seeding entity data to database. - * It has two modes through arguments. `all` OR `seederFileName` - * It can either run all entity seeders (updates and creates) - * using the `all` flag. - * Or it can selectively run a single seeder using `[seederFileName]` argument - * Examples: - * ts-node seed-entities all => Runs all possible entity seeders in the initializers/entity folder - * ts-node seed-entities conversations => Only runs the conversation seeder.(Useful in incremental db updates) - */ - -import conversations from './entities/2022-04-27-add-conversations' -import microservices from './entities/2022-04-05-add-microservices' -import { IS_DEV_ENV } from '../../conf/index' - -const log = getServiceLogger() - -const arg = process.argv[2] - -/** - * Seeds all entities. Intended to be used in github actions - * when creating :latest docker image - */ -async function seedAllEntities() { - if (!IS_DEV_ENV) { - throw new Error('This script is only allowed for development environment!') - } - - await conversations() - await microservices() -} - -/** - * This function is used to selectively run seeder functions - * Selection is sent using the command line argument to the script - * Intended to be used in staging/production environment for data changes - */ -async function seedSelected() { - // eslint-disable-next-line import/no-dynamic-require - const selectedInitializer = require(`./entities/${arg}`).default - await selectedInitializer() -} - -if (arg) { - if (arg === 'all') { - seedAllEntities() - } else { - seedSelected() - } -} else { - log.info( - 'This script needs an argument. To run all initializers `all`, or to run a specific initializer, `initializerName`', - ) -} diff --git a/backend/src/database/initializers/seed.ts b/backend/src/database/initializers/seed.ts deleted file mode 100644 index 481969f5e0..0000000000 --- a/backend/src/database/initializers/seed.ts +++ /dev/null @@ -1,128 +0,0 @@ -/** - * This script is responsible for seeding - * test data with single tenant, single user, - * members and activities using activityService.createWithMember - * tags, reports and widgets using service & repo functions - */ - -import dotenv from 'dotenv' -import dotenvExpand from 'dotenv-expand' -import { getServiceLogger } from '@crowd/logging' -import SequelizeTestUtils from '../utils/sequelizeTestUtils' -import ActivityService from '../../services/activityService' -import TagService from '../../services/tagService' -import MemberService from '../../services/memberService' -import WidgetRepository from '../repositories/widgetRepository' -import ReportRepository from '../repositories/reportRepository' - -const path = require('path') - -const environmentArg = process.argv[2] - -const envFile = environmentArg === 'dev' ? '.env' : `.env-${environmentArg}` - -const env = dotenv.config({ - path: path.resolve(__dirname, `../../../${envFile}`), -}) - -dotenvExpand.expand(env) - -const db = null - -const log = getServiceLogger() - -async function createSingleTenant() { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const activities = require('./activities.json') - - const as = new ActivityService(mockIRepositoryOptions) - const ts = new TagService(mockIRepositoryOptions) - const ms = new MemberService(mockIRepositoryOptions) - - log.info('Starting seeding the db...') - - // create activities with members - for (const activity of activities) { - if (activity.member.email !== null) { - await as.createWithMember(activity) - } - } - - const memberReferenceArray = (await ms.findAndCountAll({})).rows - - const tags = require('./tags.json') - - // create tags with member associations - for (const tag of tags) { - tag.members = [] - // select 0-5 members to associate with created tag - const selectXMembers = Math.floor(Math.random() * 5) - - for (let i = 0; i < selectXMembers; i++) { - const memberIdx = Math.floor(Math.random() * memberReferenceArray.length) - - // check member already added, if yes we don't need to readd - if (!(memberReferenceArray[memberIdx].id in tag.members)) { - tag.members.push(memberReferenceArray[memberIdx].id) - } - } - - await ts.create(tag) - } - - // create reports with widgets - const reports = require('./reports.json') - - for (const report of reports) { - // first create the widgets with empty reports - const widgetsArray: any = [] - for (const widget of report.widgets) { - widgetsArray.push(await WidgetRepository.create(widget, mockIRepositoryOptions)) - } - - report.widgets = widgetsArray.map((i) => i.id) - - // create report with widgets - await ReportRepository.create(report, mockIRepositoryOptions) - } - - // create widgets that don't have report relationship - const widgets = require('./widgets.json') - - for (const widget of widgets) { - await WidgetRepository.create(widget, mockIRepositoryOptions) - } - - log.info('Database seeded') - log.info(`User email: ${mockIRepositoryOptions.currentUser.email}`) - log.info('User password: 12345') - log.info(`Tenant id: ${mockIRepositoryOptions.currentTenant.id}`) - log.info( - `# of members added: ${await mockIRepositoryOptions.database.member.count({ - tenantId: mockIRepositoryOptions.currentTenant.id, - })}`, - ) - log.info( - `# of activities added:${await mockIRepositoryOptions.database.activity.count({ - tenantId: mockIRepositoryOptions.currentTenant.id, - })}`, - ) - log.info( - `# of tags added:${await mockIRepositoryOptions.database.tag.count({ - tenantId: mockIRepositoryOptions.currentTenant.id, - })}`, - ) - log.info( - `# of reports added:${await mockIRepositoryOptions.database.report.count({ - tenantId: mockIRepositoryOptions.currentTenant.id, - })}`, - ) - log.info( - `# of widgets added:${await mockIRepositoryOptions.database.widget.count({ - tenantId: mockIRepositoryOptions.currentTenant.id, - })}`, - ) -} - -createSingleTenant() diff --git a/backend/src/database/initializers/suggested-tasks.json b/backend/src/database/initializers/suggested-tasks.json deleted file mode 100644 index a0e5058b31..0000000000 --- a/backend/src/database/initializers/suggested-tasks.json +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "name": "Engage with relevant content", - "body": "Engage with at least 5 posts on Eagle Eye today" - }, - { - "name": "Reach out to influential contacts", - "body": "Connect with new contacts with over 10k followers" - }, - { - "name": "Reach out to poorly engaged contacts", - "body": "Connect with contacts with low activity in the last 30 days" - }, - { - "name": "Check for negative reactions", - "body": "React to activities with very negative sentiment" - }, - { - "name": "Setup your workpace integrations", - "body": "Connect with at least 2 data sources that are relevant to your community" - }, - { - "name": "Setup your team", - "body": "Invite colleagues to your community workspace" - } -] diff --git a/backend/src/database/initializers/tags.json b/backend/src/database/initializers/tags.json deleted file mode 100644 index 93d8747c0f..0000000000 --- a/backend/src/database/initializers/tags.json +++ /dev/null @@ -1,56 +0,0 @@ -[ - { - "name": "advocate" - }, - { - "name": "testing" - }, - { - "name": "Beta User" - }, - { - "name": "vue" - }, - { - "name": "new_tag" - }, - { - "name": "newer_tag" - }, - { - "name": "one_more" - }, - { - "name": "champion" - }, - { - "name": "spain" - }, - { - "name": "netherlands" - }, - { - "name": "contributor" - }, - { - "name": "employee" - }, - { - "name": "germany" - }, - { - "name": "PHP" - }, - { - "name": "NodeJS" - }, - { - "name": "Go" - }, - { - "name": "Python" - }, - { - "name": "DevRel" - } -] diff --git a/backend/src/database/initializers/twitterSourceIdsFixedTimestamps.ts b/backend/src/database/initializers/twitterSourceIdsFixedTimestamps.ts deleted file mode 100644 index dfe74294f0..0000000000 --- a/backend/src/database/initializers/twitterSourceIdsFixedTimestamps.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * This script is responsible for regenerating - * sourceIds for twitter follow activities that have timestamp > 1970-01-01 - */ - -import dotenv from 'dotenv' -import dotenvExpand from 'dotenv-expand' -import { getServiceLogger } from '@crowd/logging' -import { PlatformType } from '@crowd/types' -import ActivityService from '../../services/activityService' -import IntegrationService from '../../services/integrationService' -import TenantService from '../../services/tenantService' -import getUserContext from '../utils/getUserContext' -import { IntegrationServiceBase } from '../../serverless/integrations/services/integrationServiceBase' - -const path = require('path') - -const env = dotenv.config({ - path: path.resolve(__dirname, `../../../.env.staging`), -}) - -dotenvExpand.expand(env) - -const log = getServiceLogger() - -async function twitterFollowsFixSourceIdsWithTimestamp() { - const tenants = await TenantService._findAndCountAllForEveryUser({}) - - // for each tenant - for (const t of tenants.rows) { - const tenantId = t.id - // get user context - const userContext = await getUserContext(tenantId) - const integrationService = new IntegrationService(userContext) - - const twitterIntegration = ( - await integrationService.findAndCountAll({ filter: { platform: PlatformType.TWITTER } }) - ).rows[0] - - if (twitterIntegration) { - const actService = new ActivityService(userContext) - - // get activities where timestamp != 1970-01-01, we can query by > 2000-01-01 - const activities = await actService.findAndCountAll({ - filter: { type: 'follow', timestampRange: ['2000-01-01'] }, - }) - - for (const activity of activities.rows) { - log.info({ activity }, 'Activity') - // calculate sourceId with fixed timestamps - const sourceIdRegenerated = IntegrationServiceBase.generateSourceIdHash( - activity.communityMember.username.twitter, - 'follow', - '1970-01-01T00:00:00+00:00', - 'twitter', - ) - await actService.update(activity.id, { sourceId: sourceIdRegenerated }) - } - } - } -} - -twitterFollowsFixSourceIdsWithTimestamp() diff --git a/backend/src/database/initializers/widgets.json b/backend/src/database/initializers/widgets.json deleted file mode 100644 index 740bd655e2..0000000000 --- a/backend/src/database/initializers/widgets.json +++ /dev/null @@ -1,9186 +0,0 @@ -[ - { - "type": "number-activities", - "cache": [[], []], - "settings": null - }, - { - "type": "benchmark" - }, - { - "type": "inactive-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-13T11:49:20.149Z", - "start": "2022-01-06T00:00:00.000Z", - "end": "2022-01-13T00:00:00.000Z" - } - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-02-14T15:00:09.233Z", - "start": "2022-02-07T00:00:00.000Z", - "end": "2022-02-14T00:00:00.000Z" - } - }, - { - "type": "inactive-members-graph", - "cache": { - "x": [ - "2021-07-22T00:00:00", - "2021-07-23T00:00:00", - "2021-07-23T00:00:00", - "2021-07-23T00:00:00", - "2021-07-23T00:00:00", - "2021-07-23T00:00:00", - "2021-07-24T00:00:00", - "2021-07-25T00:00:00", - "2021-07-25T00:00:00", - "2021-07-25T00:00:00", - "2021-07-26T00:00:00", - "2021-07-26T00:00:00", - "2021-07-26T00:00:00", - "2021-07-26T00:00:00", - "2021-07-27T00:00:00", - "2021-07-28T00:00:00", - "2021-07-28T00:00:00", - "2021-07-28T00:00:00", - "2021-07-28T00:00:00", - "2021-07-28T00:00:00", - "2021-07-28T00:00:00", - "2021-07-29T00:00:00", - "2021-07-29T00:00:00", - "2021-07-29T00:00:00", - "2021-07-29T00:00:00", - "2021-07-29T00:00:00", - "2021-07-29T00:00:00", - "2021-07-29T00:00:00", - "2021-07-29T00:00:00", - "2021-07-30T00:00:00", - "2021-07-30T00:00:00", - "2021-07-31T00:00:00", - "2021-07-31T00:00:00", - "2021-07-31T00:00:00", - "2021-07-31T00:00:00", - "2021-08-01T00:00:00", - "2021-08-01T00:00:00", - "2021-08-01T00:00:00", - "2021-08-01T00:00:00", - "2021-08-01T00:00:00", - "2021-08-02T00:00:00", - "2021-08-02T00:00:00", - "2021-08-02T00:00:00", - "2021-08-02T00:00:00", - "2021-08-03T00:00:00", - "2021-08-03T00:00:00", - "2021-08-03T00:00:00", - "2021-08-03T00:00:00", - "2021-08-03T00:00:00", - "2021-08-03T00:00:00", - "2021-08-04T00:00:00", - "2021-08-04T00:00:00", - "2021-08-04T00:00:00", - "2021-08-04T00:00:00", - "2021-08-05T00:00:00", - "2021-08-06T00:00:00", - "2021-08-06T00:00:00", - "2021-08-06T00:00:00", - "2021-08-06T00:00:00", - "2021-08-07T00:00:00", - "2021-08-07T00:00:00", - "2021-08-08T00:00:00", - "2021-08-08T00:00:00", - "2021-08-09T00:00:00", - "2021-08-09T00:00:00", - "2021-08-09T00:00:00", - "2021-08-10T00:00:00", - "2021-08-10T00:00:00", - "2021-08-10T00:00:00", - "2021-08-10T00:00:00", - "2021-08-11T00:00:00", - "2021-08-11T00:00:00", - "2021-08-12T00:00:00", - "2021-08-12T00:00:00", - "2021-08-12T00:00:00", - "2021-08-13T00:00:00", - "2021-08-13T00:00:00", - "2021-08-13T00:00:00", - "2021-08-13T00:00:00", - "2021-08-14T00:00:00", - "2021-08-14T00:00:00", - "2021-08-14T00:00:00", - "2021-08-15T00:00:00", - "2021-08-15T00:00:00", - "2021-08-15T00:00:00", - "2021-08-15T00:00:00", - "2021-08-16T00:00:00", - "2021-08-16T00:00:00", - "2021-08-16T00:00:00", - "2021-08-18T00:00:00", - "2021-08-18T00:00:00", - "2021-08-18T00:00:00", - "2021-08-19T00:00:00", - "2021-08-19T00:00:00", - "2021-08-19T00:00:00", - "2021-08-19T00:00:00", - "2021-08-21T00:00:00", - "2021-08-22T00:00:00", - "2021-08-22T00:00:00", - "2021-08-22T00:00:00", - "2021-08-22T00:00:00", - "2021-08-23T00:00:00", - "2021-08-23T00:00:00", - "2021-08-23T00:00:00", - "2021-08-23T00:00:00", - "2021-08-24T00:00:00", - "2021-08-25T00:00:00", - "2021-08-25T00:00:00", - "2021-08-25T00:00:00", - "2021-08-25T00:00:00", - "2021-08-25T00:00:00", - "2021-08-25T00:00:00", - "2021-08-25T00:00:00", - "2021-08-26T00:00:00", - "2021-08-27T00:00:00", - "2021-08-28T00:00:00", - "2021-08-28T00:00:00", - "2021-08-28T00:00:00", - "2021-08-28T00:00:00", - "2021-08-29T00:00:00", - "2021-08-29T00:00:00", - "2021-08-29T00:00:00", - "2021-08-29T00:00:00", - "2021-08-29T00:00:00", - "2021-08-30T00:00:00", - "2021-08-30T00:00:00", - "2021-08-31T00:00:00", - "2021-08-31T00:00:00", - "2021-08-31T00:00:00", - "2021-08-31T00:00:00", - "2021-09-01T00:00:00", - "2021-09-01T00:00:00", - "2021-09-02T00:00:00", - "2021-09-02T00:00:00", - "2021-09-03T00:00:00", - "2021-09-03T00:00:00", - "2021-09-03T00:00:00", - "2021-09-03T00:00:00", - "2021-09-03T00:00:00", - "2021-09-05T00:00:00", - "2021-09-05T00:00:00", - "2021-09-05T00:00:00", - "2021-09-05T00:00:00", - "2021-09-06T00:00:00", - "2021-09-06T00:00:00", - "2021-09-06T00:00:00", - "2021-09-07T00:00:00", - "2021-09-07T00:00:00", - "2021-09-07T00:00:00", - "2021-09-07T00:00:00", - "2021-09-07T00:00:00", - "2021-09-07T00:00:00", - "2021-09-07T00:00:00", - "2021-09-08T00:00:00", - "2021-09-09T00:00:00", - "2021-09-09T00:00:00", - "2021-09-09T00:00:00", - "2021-09-09T00:00:00", - "2021-09-10T00:00:00", - "2021-09-10T00:00:00", - "2021-09-10T00:00:00", - "2021-09-10T00:00:00", - "2021-09-11T00:00:00", - "2021-09-11T00:00:00", - "2021-09-11T00:00:00", - "2021-09-11T00:00:00", - "2021-09-11T00:00:00", - "2021-09-12T00:00:00", - "2021-09-12T00:00:00", - "2021-09-12T00:00:00", - "2021-09-12T00:00:00", - "2021-09-12T00:00:00", - "2021-09-12T00:00:00", - "2021-09-12T00:00:00", - "2021-09-13T00:00:00", - "2021-09-13T00:00:00", - "2021-09-13T00:00:00", - "2021-09-13T00:00:00", - "2021-09-14T00:00:00", - "2021-09-14T00:00:00", - "2021-09-14T00:00:00", - "2021-09-14T00:00:00", - "2021-09-14T00:00:00", - "2021-09-15T00:00:00", - "2021-09-15T00:00:00", - "2021-09-15T00:00:00", - "2021-09-15T00:00:00", - "2021-09-15T00:00:00", - "2021-09-15T00:00:00", - "2021-09-16T00:00:00", - "2021-09-16T00:00:00", - "2021-09-16T00:00:00", - "2021-09-16T00:00:00", - "2021-09-17T00:00:00", - "2021-09-17T00:00:00", - "2021-09-17T00:00:00", - "2021-09-17T00:00:00", - "2021-09-17T00:00:00", - "2021-09-18T00:00:00", - "2021-09-18T00:00:00", - "2021-09-18T00:00:00", - "2021-09-19T00:00:00", - "2021-09-19T00:00:00", - "2021-09-19T00:00:00", - "2021-09-19T00:00:00", - "2021-09-21T00:00:00", - "2021-09-21T00:00:00", - "2021-09-21T00:00:00", - "2021-09-21T00:00:00", - "2021-09-22T00:00:00", - "2021-09-22T00:00:00", - "2021-09-22T00:00:00", - "2021-09-22T00:00:00", - "2021-09-22T00:00:00", - "2021-09-23T00:00:00", - "2021-09-23T00:00:00", - "2021-09-23T00:00:00", - "2021-09-23T00:00:00", - "2021-09-24T00:00:00", - "2021-09-24T00:00:00", - "2021-09-24T00:00:00", - "2021-09-25T00:00:00", - "2021-09-25T00:00:00", - "2021-09-27T00:00:00", - "2021-09-27T00:00:00", - "2021-09-27T00:00:00", - "2021-09-28T00:00:00", - "2021-09-28T00:00:00", - "2021-09-28T00:00:00", - "2021-09-29T00:00:00", - "2021-09-29T00:00:00", - "2021-09-30T00:00:00", - "2021-09-30T00:00:00", - "2021-09-30T00:00:00", - "2021-09-30T00:00:00", - "2021-09-30T00:00:00", - "2021-09-30T00:00:00", - "2021-10-01T00:00:00", - "2021-10-01T00:00:00", - "2021-10-01T00:00:00", - "2021-10-01T00:00:00", - "2021-10-01T00:00:00", - "2021-10-02T00:00:00", - "2021-10-02T00:00:00", - "2021-10-02T00:00:00", - "2021-10-03T00:00:00", - "2021-10-03T00:00:00", - "2021-10-03T00:00:00", - "2021-10-04T00:00:00", - "2021-10-04T00:00:00", - "2021-10-04T00:00:00", - "2021-10-04T00:00:00", - "2021-10-04T00:00:00", - "2021-10-05T00:00:00", - "2021-10-05T00:00:00", - "2021-10-06T00:00:00", - "2021-10-06T00:00:00", - "2021-10-06T00:00:00", - "2021-10-06T00:00:00", - "2021-10-07T00:00:00", - "2021-10-07T00:00:00", - "2021-10-07T00:00:00", - "2021-10-07T00:00:00", - "2021-10-08T00:00:00", - "2021-10-08T00:00:00", - "2021-10-08T00:00:00", - "2021-10-09T00:00:00", - "2021-10-09T00:00:00", - "2021-10-09T00:00:00", - "2021-10-09T00:00:00", - "2021-10-09T00:00:00", - "2021-10-10T00:00:00", - "2021-10-10T00:00:00", - "2021-10-11T00:00:00", - "2021-10-11T00:00:00", - "2021-10-11T00:00:00", - "2021-10-11T00:00:00", - "2021-10-12T00:00:00", - "2021-10-12T00:00:00", - "2021-10-12T00:00:00", - "2021-10-12T00:00:00", - "2021-10-12T00:00:00", - "2021-10-13T00:00:00", - "2021-10-13T00:00:00", - "2021-10-13T00:00:00", - "2021-10-13T00:00:00", - "2021-10-13T00:00:00", - "2021-10-14T00:00:00", - "2021-10-14T00:00:00", - "2021-10-14T00:00:00", - "2021-10-14T00:00:00" - ], - "y": [ - 1, 2, 2, 2, 2, 3, 3, 4, 5, 6, 6, 5, 5, 5, 4, 4, 5, 4, 3, 3, 2, 2, 3, 3, 4, 4, 4, 5, 6, 6, 7, - 7, 7, 7, 7, 6, 6, 6, 5, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, - 7, 8, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2 - ] - }, - "settings": null - }, - { - "type": "channel-distribution", - "cache": { - "x": ["discord", "twitter", "github", "apis"], - "y": [14, 12, 2, 13] - }, - "settings": null - }, - { - "type": "integrations" - }, - { - "type": "number-activities", - "cache": [0, 0], - "settings": null - }, - { - "type": "benchmark" - }, - { - "type": "newest-members" - }, - { - "type": "inactive-members", - "cache": [0, 0], - "settings": null - }, - { - "type": "inactive-members-graph" - }, - { - "type": "number-activities-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": null - }, - { - "type": "number-members" - }, - { - "type": "benchmark" - }, - { - "type": "inactive-members" - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": null - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "number-members-graph" - }, - { - "type": "number-activities", - "cache": [0, 0], - "settings": null - }, - { - "type": "number-members", - "cache": [0, 0], - "settings": null - }, - { - "type": "inactive-members-graph" - }, - { - "type": "latest-activities" - }, - { - "type": "builder" - }, - { - "type": "inactive-members", - "cache": [0, 0], - "settings": null - }, - { - "type": "newest-members" - }, - { - "type": "number-activities", - "cache": [0, 0], - "settings": null - }, - { - "type": "integrations" - }, - { - "type": "builder" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "newest-members" - }, - { - "type": "benchmark" - }, - { - "type": "integrations" - }, - { - "type": "channel-distribution" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "number-activities", - "cache": [0, 0], - "settings": null - }, - { - "type": "time-to-first-interaction-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": null - }, - { - "type": "time-to-first-interaction-graph", - "cache": { - "x": [ - "2021-07-23T00:00:00", - "2021-07-23T00:00:00", - "2021-07-24T00:00:00", - "2021-07-26T00:00:00", - "2021-07-26T00:00:00", - "2021-07-26T00:00:00", - "2021-07-26T00:00:00", - "2021-07-27T00:00:00", - "2021-07-28T00:00:00", - "2021-07-28T00:00:00", - "2021-07-28T00:00:00", - "2021-07-28T00:00:00", - "2021-07-28T00:00:00", - "2021-07-29T00:00:00", - "2021-07-29T00:00:00", - "2021-07-29T00:00:00", - "2021-07-29T00:00:00", - "2021-07-30T00:00:00", - "2021-07-31T00:00:00", - "2021-07-31T00:00:00", - "2021-07-31T00:00:00", - "2021-07-31T00:00:00", - "2021-08-01T00:00:00", - "2021-08-01T00:00:00", - "2021-08-01T00:00:00", - "2021-08-01T00:00:00", - "2021-08-02T00:00:00", - "2021-08-02T00:00:00", - "2021-08-03T00:00:00", - "2021-08-03T00:00:00", - "2021-08-03T00:00:00", - "2021-08-04T00:00:00", - "2021-08-04T00:00:00", - "2021-08-04T00:00:00", - "2021-08-04T00:00:00", - "2021-08-05T00:00:00", - "2021-08-06T00:00:00", - "2021-08-06T00:00:00", - "2021-08-06T00:00:00", - "2021-08-06T00:00:00", - "2021-08-07T00:00:00", - "2021-08-08T00:00:00", - "2021-08-08T00:00:00", - "2021-08-09T00:00:00", - "2021-08-09T00:00:00", - "2021-08-10T00:00:00", - "2021-08-10T00:00:00", - "2021-08-11T00:00:00", - "2021-08-12T00:00:00", - "2021-08-12T00:00:00", - "2021-08-13T00:00:00", - "2021-08-13T00:00:00", - "2021-08-13T00:00:00", - "2021-08-14T00:00:00", - "2021-08-14T00:00:00", - "2021-08-15T00:00:00", - "2021-08-15T00:00:00", - "2021-08-15T00:00:00", - "2021-08-16T00:00:00", - "2021-08-16T00:00:00", - "2021-08-16T00:00:00", - "2021-08-18T00:00:00", - "2021-08-18T00:00:00", - "2021-08-18T00:00:00", - "2021-08-19T00:00:00", - "2021-08-19T00:00:00", - "2021-08-19T00:00:00", - "2021-08-21T00:00:00", - "2021-08-22T00:00:00", - "2021-08-22T00:00:00", - "2021-08-22T00:00:00", - "2021-08-22T00:00:00", - "2021-08-23T00:00:00", - "2021-08-23T00:00:00", - "2021-08-23T00:00:00", - "2021-08-24T00:00:00", - "2021-08-25T00:00:00", - "2021-08-25T00:00:00", - "2021-08-25T00:00:00", - "2021-08-25T00:00:00", - "2021-08-25T00:00:00", - "2021-08-25T00:00:00", - "2021-08-25T00:00:00", - "2021-08-26T00:00:00", - "2021-08-27T00:00:00", - "2021-08-28T00:00:00", - "2021-08-28T00:00:00", - "2021-08-28T00:00:00", - "2021-08-29T00:00:00", - "2021-08-29T00:00:00", - "2021-08-29T00:00:00", - "2021-08-29T00:00:00", - "2021-08-29T00:00:00", - "2021-08-30T00:00:00", - "2021-08-30T00:00:00", - "2021-08-31T00:00:00", - "2021-08-31T00:00:00", - "2021-08-31T00:00:00", - "2021-08-31T00:00:00", - "2021-09-01T00:00:00", - "2021-09-01T00:00:00", - "2021-09-02T00:00:00", - "2021-09-02T00:00:00", - "2021-09-03T00:00:00", - "2021-09-03T00:00:00", - "2021-09-03T00:00:00", - "2021-09-03T00:00:00", - "2021-09-03T00:00:00", - "2021-09-05T00:00:00", - "2021-09-05T00:00:00", - "2021-09-05T00:00:00", - "2021-09-06T00:00:00", - "2021-09-06T00:00:00", - "2021-09-06T00:00:00", - "2021-09-07T00:00:00", - "2021-09-07T00:00:00", - "2021-09-07T00:00:00", - "2021-09-07T00:00:00", - "2021-09-07T00:00:00", - "2021-09-07T00:00:00", - "2021-09-08T00:00:00", - "2021-09-09T00:00:00", - "2021-09-09T00:00:00", - "2021-09-10T00:00:00", - "2021-09-10T00:00:00", - "2021-09-10T00:00:00", - "2021-09-10T00:00:00", - "2021-09-11T00:00:00", - "2021-09-11T00:00:00", - "2021-09-11T00:00:00", - "2021-09-11T00:00:00", - "2021-09-11T00:00:00", - "2021-09-12T00:00:00", - "2021-09-12T00:00:00", - "2021-09-12T00:00:00", - "2021-09-12T00:00:00", - "2021-09-12T00:00:00", - "2021-09-12T00:00:00", - "2021-09-12T00:00:00", - "2021-09-13T00:00:00", - "2021-09-13T00:00:00", - "2021-09-13T00:00:00", - "2021-09-13T00:00:00", - "2021-09-14T00:00:00", - "2021-09-14T00:00:00", - "2021-09-14T00:00:00", - "2021-09-14T00:00:00", - "2021-09-15T00:00:00", - "2021-09-15T00:00:00", - "2021-09-15T00:00:00", - "2021-09-15T00:00:00", - "2021-09-15T00:00:00", - "2021-09-15T00:00:00", - "2021-09-16T00:00:00", - "2021-09-16T00:00:00", - "2021-09-16T00:00:00", - "2021-09-16T00:00:00", - "2021-09-17T00:00:00", - "2021-09-17T00:00:00", - "2021-09-17T00:00:00", - "2021-09-17T00:00:00", - "2021-09-17T00:00:00", - "2021-09-18T00:00:00", - "2021-09-18T00:00:00", - "2021-09-18T00:00:00", - "2021-09-19T00:00:00", - "2021-09-19T00:00:00", - "2021-09-19T00:00:00", - "2021-09-19T00:00:00", - "2021-09-21T00:00:00", - "2021-09-21T00:00:00", - "2021-09-21T00:00:00", - "2021-09-21T00:00:00", - "2021-09-22T00:00:00", - "2021-09-22T00:00:00", - "2021-09-22T00:00:00", - "2021-09-22T00:00:00", - "2021-09-22T00:00:00", - "2021-09-23T00:00:00", - "2021-09-23T00:00:00", - "2021-09-23T00:00:00", - "2021-09-23T00:00:00", - "2021-09-24T00:00:00", - "2021-09-24T00:00:00", - "2021-09-24T00:00:00", - "2021-09-25T00:00:00", - "2021-09-27T00:00:00", - "2021-09-27T00:00:00", - "2021-09-28T00:00:00", - "2021-09-28T00:00:00", - "2021-09-29T00:00:00", - "2021-09-29T00:00:00", - "2021-09-30T00:00:00", - "2021-09-30T00:00:00", - "2021-09-30T00:00:00", - "2021-09-30T00:00:00", - "2021-09-30T00:00:00", - "2021-10-01T00:00:00", - "2021-10-01T00:00:00", - "2021-10-01T00:00:00", - "2021-10-01T00:00:00", - "2021-10-02T00:00:00", - "2021-10-02T00:00:00", - "2021-10-02T00:00:00", - "2021-10-03T00:00:00", - "2021-10-03T00:00:00", - "2021-10-03T00:00:00", - "2021-10-04T00:00:00", - "2021-10-04T00:00:00", - "2021-10-04T00:00:00", - "2021-10-05T00:00:00", - "2021-10-05T00:00:00", - "2021-10-06T00:00:00", - "2021-10-06T00:00:00", - "2021-10-06T00:00:00", - "2021-10-07T00:00:00", - "2021-10-07T00:00:00", - "2021-10-07T00:00:00", - "2021-10-08T00:00:00", - "2021-10-08T00:00:00", - "2021-10-08T00:00:00", - "2021-10-09T00:00:00", - "2021-10-09T00:00:00", - "2021-10-09T00:00:00", - "2021-10-09T00:00:00", - "2021-10-09T00:00:00", - "2021-10-10T00:00:00", - "2021-10-10T00:00:00", - "2021-10-11T00:00:00", - "2021-10-11T00:00:00", - "2021-10-11T00:00:00", - "2021-10-12T00:00:00", - "2021-10-12T00:00:00", - "2021-10-12T00:00:00", - "2021-10-12T00:00:00", - "2021-10-12T00:00:00", - "2021-10-13T00:00:00", - "2021-10-13T00:00:00", - "2021-10-13T00:00:00", - "2021-10-13T00:00:00", - "2021-10-14T00:00:00" - ], - "y": [ - 0, 0, 0, 0, 0.5, 0.428571428571429, 0.375, 0.777777777777778, 0.7, 0.818181818181818, - 1.08333333333333, 1, 1.14285714285714, 1.26666666666667, 1.5, 1.41176470588235, - 1.72222222222222, 2, 1.9, 2.19047619047619, 2.5, 2.39130434782609, 2.58333333333333, 2.48, - 2.73076923076923, 2.7037037037037, 2.92857142857143, 2.82758620689655, 2.73333333333333, - 3.03225806451613, 3.09375, 3.36363636363636, 3.26470588235294, 3.42857142857143, - 3.33333333333333, 3.24324324324324, 3.15789473684211, 3.41025641025641, 3.6, - 3.8780487804878, 4.07142857142857, 3.97674418604651, 4.20454545454545, 4.33333333333333, - 4.58695652173913, 4.72340425531915, 5.02083333333333, 4.91836734693878, 5.2, - 5.15686274509804, 5.09615384615385, 5.05660377358491, 5.33333333333333, 5.23636363636364, - 5.5, 5.40350877192982, 5.58620689655172, 5.76271186440678, 6.06666666666667, - 6.34426229508197, 6.62903225806452, 6.92063492063492, 6.8125, 7.06153846153846, - 7.04545454545455, 7.17910447761194, 7.44117647058824, 7.59420289855072, 7.81428571428571, - 8.14084507042254, 8.43055555555556, 8.73972602739726, 8.62162162162162, 8.50666666666667, - 8.57894736842105, 8.88311688311688, 9.15384615384615, 9.10126582278481, 9.1375, - 9.40740740740741, 9.60975609756097, 9.49397590361446, 9.38095238095238, 9.45882352941176, - 9.67441860465116, 9.6551724137931, 9.76136363636363, 10.0674157303371, 10.3555555555556, - 10.6483516483516, 10.945652173913, 11.2150537634409, 11.4574468085106, 11.5578947368421, - 11.8333333333333, 12.1134020618557, 12.3775510204082, 12.4747474747475, 12.35, - 12.5940594059406, 12.6862745098039, 12.9320388349515, 13, 12.8761904761905, - 13.1603773584906, 13.0373831775701, 12.9166666666667, 13.1284403669725, 13.0090909090909, - 13.2252252252252, 13.3392857142857, 13.5575221238938, 13.4385964912281, 13.3217391304348, - 13.4137931034483, 13.2991452991453, 13.4830508474576, 13.5210084033613, 13.7916666666667, - 14.0661157024793, 14.1639344262295, 14.0487804878049, 13.9354838709677, 14.16, - 14.4126984126984, 14.2992125984252, 14.515625, 14.7751937984496, 14.6615384615385, - 14.8015267175573, 14.8636363636364, 15.1353383458647, 15.3955223880597, 15.2814814814815, - 15.4191176470588, 15.5182481751825, 15.695652173913, 15.8992805755396, 16.0714285714286, - 16.3049645390071, 16.1901408450704, 16.3916083916084, 16.2777777777778, 16.5310344827586, - 16.7739726027397, 17, 17.1216216216216, 17.255033557047, 17.4533333333333, 17.5562913907285, - 17.7828947368421, 18.0196078431373, 17.9025974025974, 18.0967741935484, 18.3141025641026, - 18.5350318471338, 18.6962025316456, 18.8867924528302, 19.1125, 18.9937888198758, - 19.2283950617284, 19.1104294478528, 18.9939024390244, 19.1151515151515, 19.3012048192771, - 19.5149700598802, 19.7261904761905, 19.9408284023669, 20.1705882352941, 20.4035087719298, - 20.5058139534884, 20.7341040462428, 20.6149425287356, 20.7485714285714, 20.6306818181818, - 20.7570621468927, 20.6404494382022, 20.8715083798883, 20.9833333333333, 20.8674033149171, - 21.0824175824176, 20.9672131147541, 21.1902173913044, 21.4, 21.5860215053763, - 21.668449197861, 21.5531914893617, 21.6931216931217, 21.5789473684211, 21.7225130890052, - 21.9270833333333, 22.1295336787565, 22.3659793814433, 22.5692307692308, 22.75, - 22.9492385786802, 23.0757575757576, 23.2562814070352, 23.14, 23.3582089552239, - 23.5346534653465, 23.6305418719212, 23.7254901960784, 23.609756097561, 23.7378640776699, - 23.6231884057971, 23.8557692307692, 24.0813397129187, 24.3190476190476, 24.2037914691943, - 24.4433962264151, 24.6244131455399, 24.7616822429907, 24.9627906976744, 24.8472222222222, - 25.0506912442396, 25.2844036697248, 25.5068493150685, 25.3909090909091, 25.2760180995475, - 25.3828828828829, 25.6143497757848, 25.5, 25.3866666666667, 25.2743362831858, - 25.4757709251101, 25.6798245614035, 25.5676855895197, 25.7739130434783, 25.9177489177489, - 26.0775862068966, 26.2832618025751, 26.465811965812, 26.6893617021277, 26.8305084745763, - 27.0506329113924, 27.2521008403361, 27.1380753138075, 27.3208333333333, 27.5020746887967, - 27.7314049586777 - ] - }, - "settings": null - }, - { - "type": "benchmark", - "settings": { - "repositories": [ - { - "id": 322361358, - "value": "ego", - "label": "ego", - "active": true, - "editing": false, - "githubRepo": { - "id": 322361358, - "node_id": "MDEwOlJlcG9zaXRvcnkzMjIzNjEzNTg=", - "name": "ego", - "full_name": "edgelesssys/ego", - "private": false, - "owner": { - "login": "edgelesssys", - "id": 58512657, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjU4NTEyNjU3", - "avatar_url": "https://avatars.githubusercontent.com/u/58512657?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/edgelesssys", - "html_url": "https://github.com/edgelesssys", - "followers_url": "https://api.github.com/users/edgelesssys/followers", - "following_url": "https://api.github.com/users/edgelesssys/following{/other_user}", - "gists_url": "https://api.github.com/users/edgelesssys/gists{/gist_id}", - "starred_url": "https://api.github.com/users/edgelesssys/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/edgelesssys/subscriptions", - "organizations_url": "https://api.github.com/users/edgelesssys/orgs", - "repos_url": "https://api.github.com/users/edgelesssys/repos", - "events_url": "https://api.github.com/users/edgelesssys/events{/privacy}", - "received_events_url": "https://api.github.com/users/edgelesssys/received_events", - "type": "Organization", - "site_admin": false - }, - "html_url": "https://github.com/edgelesssys/ego", - "description": "EGo is an open-source SDK that enables you to develop your own confidential apps in the Go programming language.", - "fork": false, - "url": "https://api.github.com/repos/edgelesssys/ego", - "forks_url": "https://api.github.com/repos/edgelesssys/ego/forks", - "keys_url": "https://api.github.com/repos/edgelesssys/ego/keys{/key_id}", - "collaborators_url": "https://api.github.com/repos/edgelesssys/ego/collaborators{/collaborator}", - "teams_url": "https://api.github.com/repos/edgelesssys/ego/teams", - "hooks_url": "https://api.github.com/repos/edgelesssys/ego/hooks", - "issue_events_url": "https://api.github.com/repos/edgelesssys/ego/issues/events{/number}", - "events_url": "https://api.github.com/repos/edgelesssys/ego/events", - "assignees_url": "https://api.github.com/repos/edgelesssys/ego/assignees{/user}", - "branches_url": "https://api.github.com/repos/edgelesssys/ego/branches{/branch}", - "tags_url": "https://api.github.com/repos/edgelesssys/ego/tags", - "blobs_url": "https://api.github.com/repos/edgelesssys/ego/git/blobs{/sha}", - "git_tags_url": "https://api.github.com/repos/edgelesssys/ego/git/tags{/sha}", - "git_refs_url": "https://api.github.com/repos/edgelesssys/ego/git/refs{/sha}", - "trees_url": "https://api.github.com/repos/edgelesssys/ego/git/trees{/sha}", - "statuses_url": "https://api.github.com/repos/edgelesssys/ego/statuses/{sha}", - "languages_url": "https://api.github.com/repos/edgelesssys/ego/languages", - "stargazers_url": "https://api.github.com/repos/edgelesssys/ego/stargazers", - "contributors_url": "https://api.github.com/repos/edgelesssys/ego/contributors", - "subscribers_url": "https://api.github.com/repos/edgelesssys/ego/subscribers", - "subscription_url": "https://api.github.com/repos/edgelesssys/ego/subscription", - "commits_url": "https://api.github.com/repos/edgelesssys/ego/commits{/sha}", - "git_commits_url": "https://api.github.com/repos/edgelesssys/ego/git/commits{/sha}", - "comments_url": "https://api.github.com/repos/edgelesssys/ego/comments{/number}", - "issue_comment_url": "https://api.github.com/repos/edgelesssys/ego/issues/comments{/number}", - "contents_url": "https://api.github.com/repos/edgelesssys/ego/contents/{+path}", - "compare_url": "https://api.github.com/repos/edgelesssys/ego/compare/{base}...{head}", - "merges_url": "https://api.github.com/repos/edgelesssys/ego/merges", - "archive_url": "https://api.github.com/repos/edgelesssys/ego/{archive_format}{/ref}", - "downloads_url": "https://api.github.com/repos/edgelesssys/ego/downloads", - "issues_url": "https://api.github.com/repos/edgelesssys/ego/issues{/number}", - "pulls_url": "https://api.github.com/repos/edgelesssys/ego/pulls{/number}", - "milestones_url": "https://api.github.com/repos/edgelesssys/ego/milestones{/number}", - "notifications_url": "https://api.github.com/repos/edgelesssys/ego/notifications{?since,all,participating}", - "labels_url": "https://api.github.com/repos/edgelesssys/ego/labels{/name}", - "releases_url": "https://api.github.com/repos/edgelesssys/ego/releases{/id}", - "deployments_url": "https://api.github.com/repos/edgelesssys/ego/deployments", - "created_at": "2020-12-17T17:09:00Z", - "updated_at": "2021-10-28T13:03:19Z", - "pushed_at": "2021-10-28T13:03:16Z", - "git_url": "git://github.com/edgelesssys/ego.git", - "ssh_url": "git@github.com:edgelesssys/ego.git", - "clone_url": "https://github.com/edgelesssys/ego.git", - "svn_url": "https://github.com/edgelesssys/ego", - "homepage": "https://ego.dev", - "size": 1636, - "stargazers_count": 187, - "watchers_count": 187, - "language": "Go", - "has_issues": true, - "has_projects": false, - "has_downloads": true, - "has_wiki": false, - "has_pages": false, - "forks_count": 16, - "mirror_url": null, - "archived": false, - "disabled": false, - "open_issues_count": 4, - "license": { - "key": "mpl-2.0", - "name": "Mozilla Public License 2.0", - "spdx_id": "MPL-2.0", - "url": "https://api.github.com/licenses/mpl-2.0", - "node_id": "MDc6TGljZW5zZTE0" - }, - "allow_forking": true, - "is_template": false, - "topics": [ - "confidential-computing", - "confidential-microservices", - "enclave", - "golang", - "intel-sgx", - "sgx" - ], - "visibility": "public", - "forks": 16, - "open_issues": 4, - "watchers": 187, - "default_branch": "master", - "permissions": { - "admin": false, - "maintain": false, - "push": false, - "triage": false, - "pull": true - }, - "score": 1 - }, - "data": { - "2022-02-14": { - "date": "2022-02-14", - "value": 245 - }, - "2022-02-13": { - "date": "2022-02-13", - "value": 245 - }, - "2022-02-12": { - "date": "2022-02-12", - "value": 245 - }, - "2022-02-11": { - "date": "2022-02-11", - "value": 243 - }, - "2022-02-10": { - "date": "2022-02-10", - "value": 242 - }, - "2022-02-09": { - "date": "2022-02-09", - "value": 242 - }, - "2022-02-08": { - "date": "2022-02-08", - "value": 242 - }, - "2022-02-07": { - "date": "2022-02-07", - "value": 241 - }, - "2022-02-06": { - "date": "2022-02-06", - "value": 241 - }, - "2022-02-05": { - "date": "2022-02-05", - "value": 241 - }, - "2022-02-04": { - "date": "2022-02-04", - "value": 240 - }, - "2022-02-03": { - "date": "2022-02-03", - "value": 240 - }, - "2022-02-02": { - "date": "2022-02-02", - "value": 240 - }, - "2022-02-01": { - "date": "2022-02-01", - "value": 240 - }, - "2022-01-31": { - "date": "2022-01-31", - "value": 239 - }, - "2022-01-30": { - "date": "2022-01-30", - "value": 239 - }, - "2022-01-29": { - "date": "2022-01-29", - "value": 239 - }, - "2022-01-28": { - "date": "2022-01-28", - "value": 239 - }, - "2022-01-27": { - "date": "2022-01-27", - "value": 239 - }, - "2022-01-26": { - "date": "2022-01-26", - "value": 239 - }, - "2022-01-25": { - "date": "2022-01-25", - "value": 237 - }, - "2022-01-24": { - "date": "2022-01-24", - "value": 237 - }, - "2022-01-23": { - "date": "2022-01-23", - "value": 236 - }, - "2022-01-22": { - "date": "2022-01-22", - "value": 235 - }, - "2022-01-21": { - "date": "2022-01-21", - "value": 235 - }, - "2022-01-20": { - "date": "2022-01-20", - "value": 235 - }, - "2022-01-19": { - "date": "2022-01-19", - "value": 235 - }, - "2022-01-18": { - "date": "2022-01-18", - "value": 235 - }, - "2022-01-17": { - "date": "2022-01-17", - "value": 234 - }, - "2022-01-16": { - "date": "2022-01-16", - "value": 234 - }, - "2022-01-15": { - "date": "2022-01-15", - "value": 234 - }, - "2022-01-14": { - "date": "2022-01-14", - "value": 234 - }, - "2022-01-13": { - "date": "2022-01-13", - "value": 234 - }, - "2022-01-12": { - "date": "2022-01-12", - "value": 233 - }, - "2022-01-11": { - "date": "2022-01-11", - "value": 233 - }, - "2022-01-10": { - "date": "2022-01-10", - "value": 233 - }, - "2022-01-09": { - "date": "2022-01-09", - "value": 233 - }, - "2022-01-08": { - "date": "2022-01-08", - "value": 233 - }, - "2022-01-07": { - "date": "2022-01-07", - "value": 232 - }, - "2022-01-06": { - "date": "2022-01-06", - "value": 232 - }, - "2022-01-05": { - "date": "2022-01-05", - "value": 232 - }, - "2022-01-04": { - "date": "2022-01-04", - "value": 232 - }, - "2022-01-03": { - "date": "2022-01-03", - "value": 230 - }, - "2022-01-02": { - "date": "2022-01-02", - "value": 230 - }, - "2022-01-01": { - "date": "2022-01-01", - "value": 230 - }, - "2021-12-31": { - "date": "2021-12-31", - "value": 230 - }, - "2021-12-30": { - "date": "2021-12-30", - "value": 230 - }, - "2021-12-29": { - "date": "2021-12-29", - "value": 228 - }, - "2021-12-28": { - "date": "2021-12-28", - "value": 228 - }, - "2021-12-27": { - "date": "2021-12-27", - "value": 228 - }, - "2021-12-26": { - "date": "2021-12-26", - "value": 228 - }, - "2021-12-25": { - "date": "2021-12-25", - "value": 228 - }, - "2021-12-24": { - "date": "2021-12-24", - "value": 227 - }, - "2021-12-23": { - "date": "2021-12-23", - "value": 227 - }, - "2021-12-22": { - "date": "2021-12-22", - "value": 227 - }, - "2021-12-21": { - "date": "2021-12-21", - "value": 227 - }, - "2021-12-20": { - "date": "2021-12-20", - "value": 227 - }, - "2021-12-19": { - "date": "2021-12-19", - "value": 227 - }, - "2021-12-18": { - "date": "2021-12-18", - "value": 227 - }, - "2021-12-17": { - "date": "2021-12-17", - "value": 227 - }, - "2021-12-16": { - "date": "2021-12-16", - "value": 227 - }, - "2021-12-15": { - "date": "2021-12-15", - "value": 226 - }, - "2021-12-14": { - "date": "2021-12-14", - "value": 225 - }, - "2021-12-13": { - "date": "2021-12-13", - "value": 225 - }, - "2021-12-12": { - "date": "2021-12-12", - "value": 224 - }, - "2021-12-11": { - "date": "2021-12-11", - "value": 223 - }, - "2021-12-10": { - "date": "2021-12-10", - "value": 221 - }, - "2021-12-09": { - "date": "2021-12-09", - "value": 218 - }, - "2021-12-08": { - "date": "2021-12-08", - "value": 217 - }, - "2021-12-07": { - "date": "2021-12-07", - "value": 216 - }, - "2021-12-06": { - "date": "2021-12-06", - "value": 215 - }, - "2021-12-05": { - "date": "2021-12-05", - "value": 214 - }, - "2021-12-04": { - "date": "2021-12-04", - "value": 213 - }, - "2021-12-03": { - "date": "2021-12-03", - "value": 210 - }, - "2021-12-02": { - "date": "2021-12-02", - "value": 197 - }, - "2021-12-01": { - "date": "2021-12-01", - "value": 197 - }, - "2021-11-30": { - "date": "2021-11-30", - "value": 197 - }, - "2021-11-29": { - "date": "2021-11-29", - "value": 197 - }, - "2021-11-28": { - "date": "2021-11-28", - "value": 197 - }, - "2021-11-27": { - "date": "2021-11-27", - "value": 197 - }, - "2021-11-26": { - "date": "2021-11-26", - "value": 196 - }, - "2021-11-25": { - "date": "2021-11-25", - "value": 193 - }, - "2021-11-24": { - "date": "2021-11-24", - "value": 192 - }, - "2021-11-23": { - "date": "2021-11-23", - "value": 191 - }, - "2021-11-22": { - "date": "2021-11-22", - "value": 191 - }, - "2021-11-21": { - "date": "2021-11-21", - "value": 191 - }, - "2021-11-20": { - "date": "2021-11-20", - "value": 191 - }, - "2021-11-19": { - "date": "2021-11-19", - "value": 190 - }, - "2021-11-18": { - "date": "2021-11-18", - "value": 188 - }, - "2021-11-17": { - "date": "2021-11-17", - "value": 188 - }, - "2021-11-16": { - "date": "2021-11-16", - "value": 188 - }, - "2021-11-15": { - "date": "2021-11-15", - "value": 188 - }, - "2021-11-14": { - "date": "2021-11-14", - "value": 187 - } - } - }, - { - "id": 341208515, - "value": "edgelessdb", - "label": "edgelessdb", - "active": true, - "editing": false, - "githubRepo": { - "id": 341208515, - "node_id": "MDEwOlJlcG9zaXRvcnkzNDEyMDg1MTU=", - "name": "edgelessdb", - "full_name": "edgelesssys/edgelessdb", - "private": false, - "owner": { - "login": "edgelesssys", - "id": 58512657, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjU4NTEyNjU3", - "avatar_url": "https://avatars.githubusercontent.com/u/58512657?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/edgelesssys", - "html_url": "https://github.com/edgelesssys", - "followers_url": "https://api.github.com/users/edgelesssys/followers", - "following_url": "https://api.github.com/users/edgelesssys/following{/other_user}", - "gists_url": "https://api.github.com/users/edgelesssys/gists{/gist_id}", - "starred_url": "https://api.github.com/users/edgelesssys/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/edgelesssys/subscriptions", - "organizations_url": "https://api.github.com/users/edgelesssys/orgs", - "repos_url": "https://api.github.com/users/edgelesssys/repos", - "events_url": "https://api.github.com/users/edgelesssys/events{/privacy}", - "received_events_url": "https://api.github.com/users/edgelesssys/received_events", - "type": "Organization", - "site_admin": false - }, - "html_url": "https://github.com/edgelesssys/edgelessdb", - "description": "EdgelessDB is a MySQL-compatible database for confidential computing. It runs entirely inside a secure enclave and comes with advanced features for collaboration, recovery, and access control.", - "fork": false, - "url": "https://api.github.com/repos/edgelesssys/edgelessdb", - "forks_url": "https://api.github.com/repos/edgelesssys/edgelessdb/forks", - "keys_url": "https://api.github.com/repos/edgelesssys/edgelessdb/keys{/key_id}", - "collaborators_url": "https://api.github.com/repos/edgelesssys/edgelessdb/collaborators{/collaborator}", - "teams_url": "https://api.github.com/repos/edgelesssys/edgelessdb/teams", - "hooks_url": "https://api.github.com/repos/edgelesssys/edgelessdb/hooks", - "issue_events_url": "https://api.github.com/repos/edgelesssys/edgelessdb/issues/events{/number}", - "events_url": "https://api.github.com/repos/edgelesssys/edgelessdb/events", - "assignees_url": "https://api.github.com/repos/edgelesssys/edgelessdb/assignees{/user}", - "branches_url": "https://api.github.com/repos/edgelesssys/edgelessdb/branches{/branch}", - "tags_url": "https://api.github.com/repos/edgelesssys/edgelessdb/tags", - "blobs_url": "https://api.github.com/repos/edgelesssys/edgelessdb/git/blobs{/sha}", - "git_tags_url": "https://api.github.com/repos/edgelesssys/edgelessdb/git/tags{/sha}", - "git_refs_url": "https://api.github.com/repos/edgelesssys/edgelessdb/git/refs{/sha}", - "trees_url": "https://api.github.com/repos/edgelesssys/edgelessdb/git/trees{/sha}", - "statuses_url": "https://api.github.com/repos/edgelesssys/edgelessdb/statuses/{sha}", - "languages_url": "https://api.github.com/repos/edgelesssys/edgelessdb/languages", - "stargazers_url": "https://api.github.com/repos/edgelesssys/edgelessdb/stargazers", - "contributors_url": "https://api.github.com/repos/edgelesssys/edgelessdb/contributors", - "subscribers_url": "https://api.github.com/repos/edgelesssys/edgelessdb/subscribers", - "subscription_url": "https://api.github.com/repos/edgelesssys/edgelessdb/subscription", - "commits_url": "https://api.github.com/repos/edgelesssys/edgelessdb/commits{/sha}", - "git_commits_url": "https://api.github.com/repos/edgelesssys/edgelessdb/git/commits{/sha}", - "comments_url": "https://api.github.com/repos/edgelesssys/edgelessdb/comments{/number}", - "issue_comment_url": "https://api.github.com/repos/edgelesssys/edgelessdb/issues/comments{/number}", - "contents_url": "https://api.github.com/repos/edgelesssys/edgelessdb/contents/{+path}", - "compare_url": "https://api.github.com/repos/edgelesssys/edgelessdb/compare/{base}...{head}", - "merges_url": "https://api.github.com/repos/edgelesssys/edgelessdb/merges", - "archive_url": "https://api.github.com/repos/edgelesssys/edgelessdb/{archive_format}{/ref}", - "downloads_url": "https://api.github.com/repos/edgelesssys/edgelessdb/downloads", - "issues_url": "https://api.github.com/repos/edgelesssys/edgelessdb/issues{/number}", - "pulls_url": "https://api.github.com/repos/edgelesssys/edgelessdb/pulls{/number}", - "milestones_url": "https://api.github.com/repos/edgelesssys/edgelessdb/milestones{/number}", - "notifications_url": "https://api.github.com/repos/edgelesssys/edgelessdb/notifications{?since,all,participating}", - "labels_url": "https://api.github.com/repos/edgelesssys/edgelessdb/labels{/name}", - "releases_url": "https://api.github.com/repos/edgelesssys/edgelessdb/releases{/id}", - "deployments_url": "https://api.github.com/repos/edgelesssys/edgelessdb/deployments", - "created_at": "2021-02-22T13:24:30Z", - "updated_at": "2021-11-05T08:34:42Z", - "pushed_at": "2021-11-05T08:34:39Z", - "git_url": "git://github.com/edgelesssys/edgelessdb.git", - "ssh_url": "git@github.com:edgelesssys/edgelessdb.git", - "clone_url": "https://github.com/edgelesssys/edgelessdb.git", - "svn_url": "https://github.com/edgelesssys/edgelessdb", - "homepage": "https://edgeless.systems/products/edgelessdb", - "size": 434, - "stargazers_count": 71, - "watchers_count": 71, - "language": "Go", - "has_issues": true, - "has_projects": false, - "has_downloads": true, - "has_wiki": false, - "has_pages": false, - "forks_count": 6, - "mirror_url": null, - "archived": false, - "disabled": false, - "open_issues_count": 2, - "license": { - "key": "gpl-2.0", - "name": "GNU General Public License v2.0", - "spdx_id": "GPL-2.0", - "url": "https://api.github.com/licenses/gpl-2.0", - "node_id": "MDc6TGljZW5zZTg=" - }, - "allow_forking": true, - "is_template": false, - "topics": [ - "confidential-computing", - "database", - "enclave", - "mariadb", - "mysql", - "sgx", - "sql" - ], - "visibility": "public", - "forks": 6, - "open_issues": 2, - "watchers": 71, - "default_branch": "main", - "permissions": { - "admin": false, - "maintain": false, - "push": false, - "triage": false, - "pull": true - }, - "score": 1 - }, - "data": { - "2022-02-14": { - "date": "2022-02-14", - "value": 93 - }, - "2022-02-13": { - "date": "2022-02-13", - "value": 93 - }, - "2022-02-12": { - "date": "2022-02-12", - "value": 93 - }, - "2022-02-11": { - "date": "2022-02-11", - "value": 93 - }, - "2022-02-10": { - "date": "2022-02-10", - "value": 93 - }, - "2022-02-09": { - "date": "2022-02-09", - "value": 93 - }, - "2022-02-08": { - "date": "2022-02-08", - "value": 93 - }, - "2022-02-07": { - "date": "2022-02-07", - "value": 92 - }, - "2022-02-06": { - "date": "2022-02-06", - "value": 92 - }, - "2022-02-05": { - "date": "2022-02-05", - "value": 92 - }, - "2022-02-04": { - "date": "2022-02-04", - "value": 92 - }, - "2022-02-03": { - "date": "2022-02-03", - "value": 91 - }, - "2022-02-02": { - "date": "2022-02-02", - "value": 91 - }, - "2022-02-01": { - "date": "2022-02-01", - "value": 91 - }, - "2022-01-31": { - "date": "2022-01-31", - "value": 91 - }, - "2022-01-30": { - "date": "2022-01-30", - "value": 91 - }, - "2022-01-29": { - "date": "2022-01-29", - "value": 91 - }, - "2022-01-28": { - "date": "2022-01-28", - "value": 91 - }, - "2022-01-27": { - "date": "2022-01-27", - "value": 91 - }, - "2022-01-26": { - "date": "2022-01-26", - "value": 91 - }, - "2022-01-25": { - "date": "2022-01-25", - "value": 90 - }, - "2022-01-24": { - "date": "2022-01-24", - "value": 90 - }, - "2022-01-23": { - "date": "2022-01-23", - "value": 90 - }, - "2022-01-22": { - "date": "2022-01-22", - "value": 89 - }, - "2022-01-21": { - "date": "2022-01-21", - "value": 89 - }, - "2022-01-20": { - "date": "2022-01-20", - "value": 88 - }, - "2022-01-19": { - "date": "2022-01-19", - "value": 88 - }, - "2022-01-18": { - "date": "2022-01-18", - "value": 88 - }, - "2022-01-17": { - "date": "2022-01-17", - "value": 88 - }, - "2022-01-16": { - "date": "2022-01-16", - "value": 88 - }, - "2022-01-15": { - "date": "2022-01-15", - "value": 88 - }, - "2022-01-14": { - "date": "2022-01-14", - "value": 88 - }, - "2022-01-13": { - "date": "2022-01-13", - "value": 88 - }, - "2022-01-12": { - "date": "2022-01-12", - "value": 88 - }, - "2022-01-11": { - "date": "2022-01-11", - "value": 88 - }, - "2022-01-10": { - "date": "2022-01-10", - "value": 88 - }, - "2022-01-09": { - "date": "2022-01-09", - "value": 88 - }, - "2022-01-08": { - "date": "2022-01-08", - "value": 88 - }, - "2022-01-07": { - "date": "2022-01-07", - "value": 88 - }, - "2022-01-06": { - "date": "2022-01-06", - "value": 87 - }, - "2022-01-05": { - "date": "2022-01-05", - "value": 87 - }, - "2022-01-04": { - "date": "2022-01-04", - "value": 86 - }, - "2022-01-03": { - "date": "2022-01-03", - "value": 86 - }, - "2022-01-02": { - "date": "2022-01-02", - "value": 86 - }, - "2022-01-01": { - "date": "2022-01-01", - "value": 86 - }, - "2021-12-31": { - "date": "2021-12-31", - "value": 85 - }, - "2021-12-30": { - "date": "2021-12-30", - "value": 84 - }, - "2021-12-29": { - "date": "2021-12-29", - "value": 84 - }, - "2021-12-28": { - "date": "2021-12-28", - "value": 84 - }, - "2021-12-27": { - "date": "2021-12-27", - "value": 84 - }, - "2021-12-26": { - "date": "2021-12-26", - "value": 84 - }, - "2021-12-25": { - "date": "2021-12-25", - "value": 84 - }, - "2021-12-24": { - "date": "2021-12-24", - "value": 83 - }, - "2021-12-23": { - "date": "2021-12-23", - "value": 83 - }, - "2021-12-22": { - "date": "2021-12-22", - "value": 83 - }, - "2021-12-21": { - "date": "2021-12-21", - "value": 83 - }, - "2021-12-20": { - "date": "2021-12-20", - "value": 83 - }, - "2021-12-19": { - "date": "2021-12-19", - "value": 83 - }, - "2021-12-18": { - "date": "2021-12-18", - "value": 83 - }, - "2021-12-17": { - "date": "2021-12-17", - "value": 83 - }, - "2021-12-16": { - "date": "2021-12-16", - "value": 83 - }, - "2021-12-15": { - "date": "2021-12-15", - "value": 83 - }, - "2021-12-14": { - "date": "2021-12-14", - "value": 80 - }, - "2021-12-13": { - "date": "2021-12-13", - "value": 80 - }, - "2021-12-12": { - "date": "2021-12-12", - "value": 80 - }, - "2021-12-11": { - "date": "2021-12-11", - "value": 80 - }, - "2021-12-10": { - "date": "2021-12-10", - "value": 80 - }, - "2021-12-09": { - "date": "2021-12-09", - "value": 79 - }, - "2021-12-08": { - "date": "2021-12-08", - "value": 79 - }, - "2021-12-07": { - "date": "2021-12-07", - "value": 78 - }, - "2021-12-06": { - "date": "2021-12-06", - "value": 77 - }, - "2021-12-05": { - "date": "2021-12-05", - "value": 77 - }, - "2021-12-04": { - "date": "2021-12-04", - "value": 77 - }, - "2021-12-03": { - "date": "2021-12-03", - "value": 77 - }, - "2021-12-02": { - "date": "2021-12-02", - "value": 77 - }, - "2021-12-01": { - "date": "2021-12-01", - "value": 77 - }, - "2021-11-30": { - "date": "2021-11-30", - "value": 77 - }, - "2021-11-29": { - "date": "2021-11-29", - "value": 77 - }, - "2021-11-28": { - "date": "2021-11-28", - "value": 77 - }, - "2021-11-27": { - "date": "2021-11-27", - "value": 77 - }, - "2021-11-26": { - "date": "2021-11-26", - "value": 76 - }, - "2021-11-25": { - "date": "2021-11-25", - "value": 76 - }, - "2021-11-24": { - "date": "2021-11-24", - "value": 76 - }, - "2021-11-23": { - "date": "2021-11-23", - "value": 75 - }, - "2021-11-22": { - "date": "2021-11-22", - "value": 75 - }, - "2021-11-21": { - "date": "2021-11-21", - "value": 75 - }, - "2021-11-20": { - "date": "2021-11-20", - "value": 75 - }, - "2021-11-19": { - "date": "2021-11-19", - "value": 74 - }, - "2021-11-18": { - "date": "2021-11-18", - "value": 74 - }, - "2021-11-17": { - "date": "2021-11-17", - "value": 74 - }, - "2021-11-16": { - "date": "2021-11-16", - "value": 72 - }, - "2021-11-15": { - "date": "2021-11-15", - "value": 72 - }, - "2021-11-14": { - "date": "2021-11-14", - "value": 72 - } - } - }, - { - "id": 281696828, - "value": "marblerun", - "label": "marblerun", - "active": true, - "editing": false, - "githubRepo": { - "id": 281696828, - "node_id": "MDEwOlJlcG9zaXRvcnkyODE2OTY4Mjg=", - "name": "marblerun", - "full_name": "edgelesssys/marblerun", - "private": false, - "owner": { - "login": "edgelesssys", - "id": 58512657, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjU4NTEyNjU3", - "avatar_url": "https://avatars.githubusercontent.com/u/58512657?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/edgelesssys", - "html_url": "https://github.com/edgelesssys", - "followers_url": "https://api.github.com/users/edgelesssys/followers", - "following_url": "https://api.github.com/users/edgelesssys/following{/other_user}", - "gists_url": "https://api.github.com/users/edgelesssys/gists{/gist_id}", - "starred_url": "https://api.github.com/users/edgelesssys/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/edgelesssys/subscriptions", - "organizations_url": "https://api.github.com/users/edgelesssys/orgs", - "repos_url": "https://api.github.com/users/edgelesssys/repos", - "events_url": "https://api.github.com/users/edgelesssys/events{/privacy}", - "received_events_url": "https://api.github.com/users/edgelesssys/received_events", - "type": "Organization", - "site_admin": false - }, - "html_url": "https://github.com/edgelesssys/marblerun", - "description": "MarbleRun is the control plane for confidential computing. Deploy, scale, and verify your confidential microservices on vanilla Kubernetes. 100% Go, 100% cloud native, 100% confidential.", - "fork": false, - "url": "https://api.github.com/repos/edgelesssys/marblerun", - "forks_url": "https://api.github.com/repos/edgelesssys/marblerun/forks", - "keys_url": "https://api.github.com/repos/edgelesssys/marblerun/keys{/key_id}", - "collaborators_url": "https://api.github.com/repos/edgelesssys/marblerun/collaborators{/collaborator}", - "teams_url": "https://api.github.com/repos/edgelesssys/marblerun/teams", - "hooks_url": "https://api.github.com/repos/edgelesssys/marblerun/hooks", - "issue_events_url": "https://api.github.com/repos/edgelesssys/marblerun/issues/events{/number}", - "events_url": "https://api.github.com/repos/edgelesssys/marblerun/events", - "assignees_url": "https://api.github.com/repos/edgelesssys/marblerun/assignees{/user}", - "branches_url": "https://api.github.com/repos/edgelesssys/marblerun/branches{/branch}", - "tags_url": "https://api.github.com/repos/edgelesssys/marblerun/tags", - "blobs_url": "https://api.github.com/repos/edgelesssys/marblerun/git/blobs{/sha}", - "git_tags_url": "https://api.github.com/repos/edgelesssys/marblerun/git/tags{/sha}", - "git_refs_url": "https://api.github.com/repos/edgelesssys/marblerun/git/refs{/sha}", - "trees_url": "https://api.github.com/repos/edgelesssys/marblerun/git/trees{/sha}", - "statuses_url": "https://api.github.com/repos/edgelesssys/marblerun/statuses/{sha}", - "languages_url": "https://api.github.com/repos/edgelesssys/marblerun/languages", - "stargazers_url": "https://api.github.com/repos/edgelesssys/marblerun/stargazers", - "contributors_url": "https://api.github.com/repos/edgelesssys/marblerun/contributors", - "subscribers_url": "https://api.github.com/repos/edgelesssys/marblerun/subscribers", - "subscription_url": "https://api.github.com/repos/edgelesssys/marblerun/subscription", - "commits_url": "https://api.github.com/repos/edgelesssys/marblerun/commits{/sha}", - "git_commits_url": "https://api.github.com/repos/edgelesssys/marblerun/git/commits{/sha}", - "comments_url": "https://api.github.com/repos/edgelesssys/marblerun/comments{/number}", - "issue_comment_url": "https://api.github.com/repos/edgelesssys/marblerun/issues/comments{/number}", - "contents_url": "https://api.github.com/repos/edgelesssys/marblerun/contents/{+path}", - "compare_url": "https://api.github.com/repos/edgelesssys/marblerun/compare/{base}...{head}", - "merges_url": "https://api.github.com/repos/edgelesssys/marblerun/merges", - "archive_url": "https://api.github.com/repos/edgelesssys/marblerun/{archive_format}{/ref}", - "downloads_url": "https://api.github.com/repos/edgelesssys/marblerun/downloads", - "issues_url": "https://api.github.com/repos/edgelesssys/marblerun/issues{/number}", - "pulls_url": "https://api.github.com/repos/edgelesssys/marblerun/pulls{/number}", - "milestones_url": "https://api.github.com/repos/edgelesssys/marblerun/milestones{/number}", - "notifications_url": "https://api.github.com/repos/edgelesssys/marblerun/notifications{?since,all,participating}", - "labels_url": "https://api.github.com/repos/edgelesssys/marblerun/labels{/name}", - "releases_url": "https://api.github.com/repos/edgelesssys/marblerun/releases{/id}", - "deployments_url": "https://api.github.com/repos/edgelesssys/marblerun/deployments", - "created_at": "2020-07-22T14:16:01Z", - "updated_at": "2021-11-08T09:49:31Z", - "pushed_at": "2021-11-08T10:22:46Z", - "git_url": "git://github.com/edgelesssys/marblerun.git", - "ssh_url": "git@github.com:edgelesssys/marblerun.git", - "clone_url": "https://github.com/edgelesssys/marblerun.git", - "svn_url": "https://github.com/edgelesssys/marblerun", - "homepage": "https://marblerun.sh", - "size": 5203, - "stargazers_count": 140, - "watchers_count": 140, - "language": "Go", - "has_issues": true, - "has_projects": false, - "has_downloads": true, - "has_wiki": false, - "has_pages": false, - "forks_count": 17, - "mirror_url": null, - "archived": false, - "disabled": false, - "open_issues_count": 4, - "license": { - "key": "mpl-2.0", - "name": "Mozilla Public License 2.0", - "spdx_id": "MPL-2.0", - "url": "https://api.github.com/licenses/mpl-2.0", - "node_id": "MDc6TGljZW5zZTE0" - }, - "allow_forking": true, - "is_template": false, - "topics": [ - "confidential-computing", - "confidential-microservices", - "distributed-systems", - "enclave", - "golang", - "intel-sgx", - "kubernetes", - "microservice", - "service-mesh", - "sgx" - ], - "visibility": "public", - "forks": 17, - "open_issues": 4, - "watchers": 140, - "default_branch": "master", - "permissions": { - "admin": false, - "maintain": false, - "push": false, - "triage": false, - "pull": true - }, - "score": 1 - }, - "data": { - "2022-02-14": { - "date": "2022-02-14", - "value": 150 - }, - "2022-02-13": { - "date": "2022-02-13", - "value": 150 - }, - "2022-02-12": { - "date": "2022-02-12", - "value": 150 - }, - "2022-02-11": { - "date": "2022-02-11", - "value": 150 - }, - "2022-02-10": { - "date": "2022-02-10", - "value": 150 - }, - "2022-02-09": { - "date": "2022-02-09", - "value": 150 - }, - "2022-02-08": { - "date": "2022-02-08", - "value": 150 - }, - "2022-02-07": { - "date": "2022-02-07", - "value": 150 - }, - "2022-02-06": { - "date": "2022-02-06", - "value": 150 - }, - "2022-02-05": { - "date": "2022-02-05", - "value": 150 - }, - "2022-02-04": { - "date": "2022-02-04", - "value": 150 - }, - "2022-02-03": { - "date": "2022-02-03", - "value": 150 - }, - "2022-02-02": { - "date": "2022-02-02", - "value": 150 - }, - "2022-02-01": { - "date": "2022-02-01", - "value": 150 - }, - "2022-01-31": { - "date": "2022-01-31", - "value": 150 - }, - "2022-01-30": { - "date": "2022-01-30", - "value": 150 - }, - "2022-01-29": { - "date": "2022-01-29", - "value": 150 - }, - "2022-01-28": { - "date": "2022-01-28", - "value": 150 - }, - "2022-01-27": { - "date": "2022-01-27", - "value": 150 - }, - "2022-01-26": { - "date": "2022-01-26", - "value": 150 - }, - "2022-01-25": { - "date": "2022-01-25", - "value": 149 - }, - "2022-01-24": { - "date": "2022-01-24", - "value": 149 - }, - "2022-01-23": { - "date": "2022-01-23", - "value": 149 - }, - "2022-01-22": { - "date": "2022-01-22", - "value": 149 - }, - "2022-01-21": { - "date": "2022-01-21", - "value": 148 - }, - "2022-01-20": { - "date": "2022-01-20", - "value": 148 - }, - "2022-01-19": { - "date": "2022-01-19", - "value": 148 - }, - "2022-01-18": { - "date": "2022-01-18", - "value": 148 - }, - "2022-01-17": { - "date": "2022-01-17", - "value": 148 - }, - "2022-01-16": { - "date": "2022-01-16", - "value": 148 - }, - "2022-01-15": { - "date": "2022-01-15", - "value": 148 - }, - "2022-01-14": { - "date": "2022-01-14", - "value": 148 - }, - "2022-01-13": { - "date": "2022-01-13", - "value": 148 - }, - "2022-01-12": { - "date": "2022-01-12", - "value": 148 - }, - "2022-01-11": { - "date": "2022-01-11", - "value": 148 - }, - "2022-01-10": { - "date": "2022-01-10", - "value": 147 - }, - "2022-01-09": { - "date": "2022-01-09", - "value": 147 - }, - "2022-01-08": { - "date": "2022-01-08", - "value": 147 - }, - "2022-01-07": { - "date": "2022-01-07", - "value": 147 - }, - "2022-01-06": { - "date": "2022-01-06", - "value": 146 - }, - "2022-01-05": { - "date": "2022-01-05", - "value": 146 - }, - "2022-01-04": { - "date": "2022-01-04", - "value": 146 - }, - "2022-01-03": { - "date": "2022-01-03", - "value": 146 - }, - "2022-01-02": { - "date": "2022-01-02", - "value": 146 - }, - "2022-01-01": { - "date": "2022-01-01", - "value": 146 - }, - "2021-12-31": { - "date": "2021-12-31", - "value": 146 - }, - "2021-12-30": { - "date": "2021-12-30", - "value": 146 - }, - "2021-12-29": { - "date": "2021-12-29", - "value": 146 - }, - "2021-12-28": { - "date": "2021-12-28", - "value": 146 - }, - "2021-12-27": { - "date": "2021-12-27", - "value": 146 - }, - "2021-12-26": { - "date": "2021-12-26", - "value": 146 - }, - "2021-12-25": { - "date": "2021-12-25", - "value": 146 - }, - "2021-12-24": { - "date": "2021-12-24", - "value": 146 - }, - "2021-12-23": { - "date": "2021-12-23", - "value": 146 - }, - "2021-12-22": { - "date": "2021-12-22", - "value": 146 - }, - "2021-12-21": { - "date": "2021-12-21", - "value": 146 - }, - "2021-12-20": { - "date": "2021-12-20", - "value": 146 - }, - "2021-12-19": { - "date": "2021-12-19", - "value": 146 - }, - "2021-12-18": { - "date": "2021-12-18", - "value": 146 - }, - "2021-12-17": { - "date": "2021-12-17", - "value": 146 - }, - "2021-12-16": { - "date": "2021-12-16", - "value": 146 - }, - "2021-12-15": { - "date": "2021-12-15", - "value": 146 - }, - "2021-12-14": { - "date": "2021-12-14", - "value": 146 - }, - "2021-12-13": { - "date": "2021-12-13", - "value": 146 - }, - "2021-12-12": { - "date": "2021-12-12", - "value": 146 - }, - "2021-12-11": { - "date": "2021-12-11", - "value": 146 - }, - "2021-12-10": { - "date": "2021-12-10", - "value": 146 - }, - "2021-12-09": { - "date": "2021-12-09", - "value": 145 - }, - "2021-12-08": { - "date": "2021-12-08", - "value": 145 - }, - "2021-12-07": { - "date": "2021-12-07", - "value": 143 - }, - "2021-12-06": { - "date": "2021-12-06", - "value": 142 - }, - "2021-12-05": { - "date": "2021-12-05", - "value": 142 - }, - "2021-12-04": { - "date": "2021-12-04", - "value": 142 - }, - "2021-12-03": { - "date": "2021-12-03", - "value": 142 - }, - "2021-12-02": { - "date": "2021-12-02", - "value": 142 - }, - "2021-12-01": { - "date": "2021-12-01", - "value": 142 - }, - "2021-11-30": { - "date": "2021-11-30", - "value": 142 - }, - "2021-11-29": { - "date": "2021-11-29", - "value": 142 - }, - "2021-11-28": { - "date": "2021-11-28", - "value": 142 - }, - "2021-11-27": { - "date": "2021-11-27", - "value": 142 - }, - "2021-11-26": { - "date": "2021-11-26", - "value": 142 - }, - "2021-11-25": { - "date": "2021-11-25", - "value": 141 - }, - "2021-11-24": { - "date": "2021-11-24", - "value": 141 - }, - "2021-11-23": { - "date": "2021-11-23", - "value": 141 - }, - "2021-11-22": { - "date": "2021-11-22", - "value": 141 - }, - "2021-11-21": { - "date": "2021-11-21", - "value": 141 - }, - "2021-11-20": { - "date": "2021-11-20", - "value": 141 - }, - "2021-11-19": { - "date": "2021-11-19", - "value": 141 - }, - "2021-11-18": { - "date": "2021-11-18", - "value": 141 - }, - "2021-11-17": { - "date": "2021-11-17", - "value": 141 - }, - "2021-11-16": { - "date": "2021-11-16", - "value": 141 - }, - "2021-11-15": { - "date": "2021-11-15", - "value": 141 - }, - "2021-11-14": { - "date": "2021-11-14", - "value": 141 - } - } - }, - { - "id": 193215554, - "value": "n8n", - "label": "n8n", - "active": true, - "editing": false, - "githubRepo": { - "id": 193215554, - "node_id": "MDEwOlJlcG9zaXRvcnkxOTMyMTU1NTQ=", - "name": "n8n", - "full_name": "n8n-io/n8n", - "private": false, - "owner": { - "login": "n8n-io", - "id": 45487711, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjQ1NDg3NzEx", - "avatar_url": "https://avatars.githubusercontent.com/u/45487711?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/n8n-io", - "html_url": "https://github.com/n8n-io", - "followers_url": "https://api.github.com/users/n8n-io/followers", - "following_url": "https://api.github.com/users/n8n-io/following{/other_user}", - "gists_url": "https://api.github.com/users/n8n-io/gists{/gist_id}", - "starred_url": "https://api.github.com/users/n8n-io/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/n8n-io/subscriptions", - "organizations_url": "https://api.github.com/users/n8n-io/orgs", - "repos_url": "https://api.github.com/users/n8n-io/repos", - "events_url": "https://api.github.com/users/n8n-io/events{/privacy}", - "received_events_url": "https://api.github.com/users/n8n-io/received_events", - "type": "Organization", - "site_admin": false - }, - "html_url": "https://github.com/n8n-io/n8n", - "description": "Free and open fair-code licensed node based Workflow Automation Tool. Easily automate tasks across different services.", - "fork": false, - "url": "https://api.github.com/repos/n8n-io/n8n", - "forks_url": "https://api.github.com/repos/n8n-io/n8n/forks", - "keys_url": "https://api.github.com/repos/n8n-io/n8n/keys{/key_id}", - "collaborators_url": "https://api.github.com/repos/n8n-io/n8n/collaborators{/collaborator}", - "teams_url": "https://api.github.com/repos/n8n-io/n8n/teams", - "hooks_url": "https://api.github.com/repos/n8n-io/n8n/hooks", - "issue_events_url": "https://api.github.com/repos/n8n-io/n8n/issues/events{/number}", - "events_url": "https://api.github.com/repos/n8n-io/n8n/events", - "assignees_url": "https://api.github.com/repos/n8n-io/n8n/assignees{/user}", - "branches_url": "https://api.github.com/repos/n8n-io/n8n/branches{/branch}", - "tags_url": "https://api.github.com/repos/n8n-io/n8n/tags", - "blobs_url": "https://api.github.com/repos/n8n-io/n8n/git/blobs{/sha}", - "git_tags_url": "https://api.github.com/repos/n8n-io/n8n/git/tags{/sha}", - "git_refs_url": "https://api.github.com/repos/n8n-io/n8n/git/refs{/sha}", - "trees_url": "https://api.github.com/repos/n8n-io/n8n/git/trees{/sha}", - "statuses_url": "https://api.github.com/repos/n8n-io/n8n/statuses/{sha}", - "languages_url": "https://api.github.com/repos/n8n-io/n8n/languages", - "stargazers_url": "https://api.github.com/repos/n8n-io/n8n/stargazers", - "contributors_url": "https://api.github.com/repos/n8n-io/n8n/contributors", - "subscribers_url": "https://api.github.com/repos/n8n-io/n8n/subscribers", - "subscription_url": "https://api.github.com/repos/n8n-io/n8n/subscription", - "commits_url": "https://api.github.com/repos/n8n-io/n8n/commits{/sha}", - "git_commits_url": "https://api.github.com/repos/n8n-io/n8n/git/commits{/sha}", - "comments_url": "https://api.github.com/repos/n8n-io/n8n/comments{/number}", - "issue_comment_url": "https://api.github.com/repos/n8n-io/n8n/issues/comments{/number}", - "contents_url": "https://api.github.com/repos/n8n-io/n8n/contents/{+path}", - "compare_url": "https://api.github.com/repos/n8n-io/n8n/compare/{base}...{head}", - "merges_url": "https://api.github.com/repos/n8n-io/n8n/merges", - "archive_url": "https://api.github.com/repos/n8n-io/n8n/{archive_format}{/ref}", - "downloads_url": "https://api.github.com/repos/n8n-io/n8n/downloads", - "issues_url": "https://api.github.com/repos/n8n-io/n8n/issues{/number}", - "pulls_url": "https://api.github.com/repos/n8n-io/n8n/pulls{/number}", - "milestones_url": "https://api.github.com/repos/n8n-io/n8n/milestones{/number}", - "notifications_url": "https://api.github.com/repos/n8n-io/n8n/notifications{?since,all,participating}", - "labels_url": "https://api.github.com/repos/n8n-io/n8n/labels{/name}", - "releases_url": "https://api.github.com/repos/n8n-io/n8n/releases{/id}", - "deployments_url": "https://api.github.com/repos/n8n-io/n8n/deployments", - "created_at": "2019-06-22T09:24:21Z", - "updated_at": "2022-02-10T17:03:43Z", - "pushed_at": "2022-02-10T16:08:38Z", - "git_url": "git://github.com/n8n-io/n8n.git", - "ssh_url": "git@github.com:n8n-io/n8n.git", - "clone_url": "https://github.com/n8n-io/n8n.git", - "svn_url": "https://github.com/n8n-io/n8n", - "homepage": "https://n8n.io", - "size": 29237, - "stargazers_count": 20775, - "watchers_count": 20775, - "language": "TypeScript", - "has_issues": true, - "has_projects": true, - "has_downloads": true, - "has_wiki": true, - "has_pages": true, - "forks_count": 2214, - "mirror_url": null, - "archived": false, - "disabled": false, - "open_issues_count": 238, - "license": { - "key": "other", - "name": "Other", - "spdx_id": "NOASSERTION", - "url": null, - "node_id": "MDc6TGljZW5zZTA=" - }, - "allow_forking": true, - "is_template": false, - "topics": [ - "apis", - "automated", - "automation", - "cli", - "data-flow", - "development", - "docker", - "iaas", - "integration-framework", - "integrations", - "ipaas", - "low-code", - "low-code-development-platform", - "low-code-plattform", - "n8n", - "node", - "self-hosted", - "typescript", - "workflow", - "workflow-automation" - ], - "visibility": "public", - "forks": 2214, - "open_issues": 238, - "watchers": 20775, - "default_branch": "master", - "permissions": { - "admin": false, - "maintain": false, - "push": false, - "triage": false, - "pull": true - }, - "score": 1 - }, - "data": { - "2022-02-14": { - "date": "2022-02-14", - "value": 20834 - }, - "2022-02-13": { - "date": "2022-02-13", - "value": 20817 - }, - "2022-02-12": { - "date": "2022-02-12", - "value": 20802 - }, - "2022-02-11": { - "date": "2022-02-11", - "value": 20774 - }, - "2022-02-10": { - "date": "2022-02-10", - "value": 20748 - }, - "2022-02-09": { - "date": "2022-02-09", - "value": 20719 - }, - "2022-02-08": { - "date": "2022-02-08", - "value": 20562 - }, - "2022-02-07": { - "date": "2022-02-07", - "value": 20365 - }, - "2022-02-06": { - "date": "2022-02-06", - "value": 20258 - }, - "2022-02-05": { - "date": "2022-02-05", - "value": 20086 - }, - "2022-02-04": { - "date": "2022-02-04", - "value": 20028 - }, - "2022-02-03": { - "date": "2022-02-03", - "value": 20011 - }, - "2022-02-02": { - "date": "2022-02-02", - "value": 19957 - }, - "2022-02-01": { - "date": "2022-02-01", - "value": 19937 - }, - "2022-01-31": { - "date": "2022-01-31", - "value": 19927 - }, - "2022-01-30": { - "date": "2022-01-30", - "value": 19905 - }, - "2022-01-29": { - "date": "2022-01-29", - "value": 19884 - }, - "2022-01-28": { - "date": "2022-01-28", - "value": 19860 - }, - "2022-01-27": { - "date": "2022-01-27", - "value": 19835 - }, - "2022-01-26": { - "date": "2022-01-26", - "value": 19817 - }, - "2022-01-25": { - "date": "2022-01-25", - "value": 19789 - }, - "2022-01-24": { - "date": "2022-01-24", - "value": 19755 - }, - "2022-01-23": { - "date": "2022-01-23", - "value": 19742 - }, - "2022-01-22": { - "date": "2022-01-22", - "value": 19725 - }, - "2022-01-21": { - "date": "2022-01-21", - "value": 19715 - }, - "2022-01-20": { - "date": "2022-01-20", - "value": 19697 - }, - "2022-01-19": { - "date": "2022-01-19", - "value": 19665 - }, - "2022-01-18": { - "date": "2022-01-18", - "value": 19654 - }, - "2022-01-17": { - "date": "2022-01-17", - "value": 19631 - }, - "2022-01-16": { - "date": "2022-01-16", - "value": 19620 - }, - "2022-01-15": { - "date": "2022-01-15", - "value": 19596 - }, - "2022-01-14": { - "date": "2022-01-14", - "value": 19580 - }, - "2022-01-13": { - "date": "2022-01-13", - "value": 19562 - }, - "2022-01-12": { - "date": "2022-01-12", - "value": 19541 - }, - "2022-01-11": { - "date": "2022-01-11", - "value": 19522 - }, - "2022-01-10": { - "date": "2022-01-10", - "value": 19500 - }, - "2022-01-09": { - "date": "2022-01-09", - "value": 19481 - }, - "2022-01-08": { - "date": "2022-01-08", - "value": 19466 - }, - "2022-01-07": { - "date": "2022-01-07", - "value": 19443 - }, - "2022-01-06": { - "date": "2022-01-06", - "value": 19418 - }, - "2022-01-05": { - "date": "2022-01-05", - "value": 19395 - }, - "2022-01-04": { - "date": "2022-01-04", - "value": 19364 - }, - "2022-01-03": { - "date": "2022-01-03", - "value": 19341 - }, - "2022-01-02": { - "date": "2022-01-02", - "value": 19329 - }, - "2022-01-01": { - "date": "2022-01-01", - "value": 19324 - }, - "2021-12-31": { - "date": "2021-12-31", - "value": 19307 - }, - "2021-12-30": { - "date": "2021-12-30", - "value": 19292 - }, - "2021-12-29": { - "date": "2021-12-29", - "value": 19275 - }, - "2021-12-28": { - "date": "2021-12-28", - "value": 19252 - }, - "2021-12-27": { - "date": "2021-12-27", - "value": 19236 - }, - "2021-12-26": { - "date": "2021-12-26", - "value": 19228 - }, - "2021-12-25": { - "date": "2021-12-25", - "value": 19220 - }, - "2021-12-24": { - "date": "2021-12-24", - "value": 19210 - }, - "2021-12-23": { - "date": "2021-12-23", - "value": 19196 - }, - "2021-12-22": { - "date": "2021-12-22", - "value": 19183 - }, - "2021-12-21": { - "date": "2021-12-21", - "value": 19171 - }, - "2021-12-20": { - "date": "2021-12-20", - "value": 19157 - }, - "2021-12-19": { - "date": "2021-12-19", - "value": 19150 - }, - "2021-12-18": { - "date": "2021-12-18", - "value": 19145 - }, - "2021-12-17": { - "date": "2021-12-17", - "value": 19135 - }, - "2021-12-16": { - "date": "2021-12-16", - "value": 19124 - }, - "2021-12-15": { - "date": "2021-12-15", - "value": 19106 - }, - "2021-12-14": { - "date": "2021-12-14", - "value": 19095 - }, - "2021-12-13": { - "date": "2021-12-13", - "value": 19076 - }, - "2021-12-12": { - "date": "2021-12-12", - "value": 19052 - }, - "2021-12-11": { - "date": "2021-12-11", - "value": 19042 - }, - "2021-12-10": { - "date": "2021-12-10", - "value": 19028 - }, - "2021-12-09": { - "date": "2021-12-09", - "value": 19018 - }, - "2021-12-08": { - "date": "2021-12-08", - "value": 19007 - }, - "2021-12-07": { - "date": "2021-12-07", - "value": 18994 - }, - "2021-12-06": { - "date": "2021-12-06", - "value": 18983 - }, - "2021-12-05": { - "date": "2021-12-05", - "value": 18972 - }, - "2021-12-04": { - "date": "2021-12-04", - "value": 18962 - }, - "2021-12-03": { - "date": "2021-12-03", - "value": 18943 - }, - "2021-12-02": { - "date": "2021-12-02", - "value": 18922 - }, - "2021-12-01": { - "date": "2021-12-01", - "value": 18902 - }, - "2021-11-30": { - "date": "2021-11-30", - "value": 18884 - }, - "2021-11-29": { - "date": "2021-11-29", - "value": 18865 - }, - "2021-11-28": { - "date": "2021-11-28", - "value": 18851 - }, - "2021-11-27": { - "date": "2021-11-27", - "value": 18840 - }, - "2021-11-26": { - "date": "2021-11-26", - "value": 18825 - }, - "2021-11-25": { - "date": "2021-11-25", - "value": 18813 - }, - "2021-11-24": { - "date": "2021-11-24", - "value": 18797 - }, - "2021-11-23": { - "date": "2021-11-23", - "value": 18789 - }, - "2021-11-22": { - "date": "2021-11-22", - "value": 18766 - }, - "2021-11-21": { - "date": "2021-11-21", - "value": 18738 - }, - "2021-11-20": { - "date": "2021-11-20", - "value": 18730 - }, - "2021-11-19": { - "date": "2021-11-19", - "value": 18719 - }, - "2021-11-18": { - "date": "2021-11-18", - "value": 18699 - }, - "2021-11-17": { - "date": "2021-11-17", - "value": 18673 - }, - "2021-11-16": { - "date": "2021-11-16", - "value": 18658 - }, - "2021-11-15": { - "date": "2021-11-15", - "value": 18643 - }, - "2021-11-14": { - "date": "2021-11-14", - "value": 18642 - } - } - } - ], - "last_updated_at": "2022-02-14", - "timeframe": { - "label": "Last three months", - "value": "last_three_months", - "date": "2021-11-14" - } - } - }, - { - "type": "inactive-members", - "cache": [0, 0], - "settings": null - }, - { - "type": "number-members", - "cache": [0, 0], - "settings": null - }, - { - "type": "number-activities-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": null - }, - { - "type": "number-members-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": null - }, - { - "type": "latest-activities" - }, - { - "type": "number-activities" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "number-activities", - "cache": [0, 0], - "settings": null - }, - { - "type": "channel-distribution" - }, - { - "type": "time-to-first-interaction" - }, - { - "type": "builder" - }, - { - "type": "number-members" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "benchmark" - }, - { - "type": "number-activities-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": null - }, - { - "type": "channel-distribution" - }, - { - "type": "benchmark" - }, - { - "type": "integrations" - }, - { - "type": "number-activities-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": null - }, - { - "type": "number-activities-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": null - }, - { - "type": "number-members-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": null - }, - { - "type": "inactive-members", - "cache": [[], []], - "settings": null - }, - { - "type": "number-activities-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": null - }, - { - "type": "time-to-first-interaction", - "cache": [[], []], - "settings": null - }, - { - "type": "number-members", - "cache": [[], []], - "settings": null - }, - { - "type": "inactive-members-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": null - }, - { - "type": "number-activities", - "cache": [102, 0], - "settings": { - "last_computed_at": "2022-01-13T11:49:20.813Z", - "start": "2022-01-06T00:00:00.000Z", - "end": "2022-01-13T00:00:00.000Z" - } - }, - { - "type": "number-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-13T11:49:20.113Z", - "start": "2022-01-06T00:00:00.000Z", - "end": "2022-01-13T00:00:00.000Z" - } - }, - { - "type": "number-activities-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": { - "last_computed_at": "2022-01-13T11:49:19.632Z", - "start": "2022-01-06T00:00:00.000Z", - "end": "2022-01-13T00:00:00.000Z" - } - }, - { - "type": "number-members-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": { - "last_computed_at": "2022-01-13T11:49:20.106Z", - "start": "2022-01-06T00:00:00.000Z", - "end": "2022-01-13T00:00:00.000Z" - } - }, - { - "type": "builder" - }, - { - "type": "latest-activities" - }, - { - "type": "integrations" - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": null - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "time-to-first-interaction" - }, - { - "type": "number-members-graph" - }, - { - "type": "inactive-members", - "cache": [0, 0], - "settings": null - }, - { - "type": "number-members", - "cache": [0, 0], - "settings": null - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "benchmark" - }, - { - "type": "newest-members" - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": null - }, - { - "type": "number-members-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": null - }, - { - "type": "benchmark" - }, - { - "type": "latest-activities" - }, - { - "type": "number-members", - "cache": [0, 0], - "settings": null - }, - { - "type": "latest-activities" - }, - { - "type": "number-members", - "cache": [0, 0], - "settings": null - }, - { - "type": "number-members-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": null - }, - { - "type": "channel-distribution", - "cache": { - "x": [], - "y": [] - }, - "settings": null - }, - { - "type": "newest-members" - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": null - }, - { - "type": "inactive-members-graph" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "channel-distribution" - }, - { - "type": "latest-activities" - }, - { - "type": "builder" - }, - { - "type": "number-activities", - "cache": [0, 0], - "settings": null - }, - { - "type": "number-members", - "cache": [0, 0], - "settings": null - }, - { - "type": "number-members-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": null - }, - { - "type": "channel-distribution" - }, - { - "type": "benchmark" - }, - { - "type": "newest-members" - }, - { - "type": "integrations" - }, - { - "type": "builder" - }, - { - "type": "inactive-members" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "number-activities-graph" - }, - { - "type": "channel-distribution" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "number-activities-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": null - }, - { - "type": "number-members-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": null - }, - { - "type": "latest-activities" - }, - { - "type": "integrations" - }, - { - "type": "number-activities" - }, - { - "type": "number-activities-graph" - }, - { - "type": "channel-distribution" - }, - { - "type": "inactive-members", - "cache": [0, 0], - "settings": null - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "number-members-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": null - }, - { - "type": "newest-members" - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": null - }, - { - "type": "channel-distribution" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "inactive-members", - "cache": [0, 0], - "settings": null - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": null - }, - { - "type": "builder" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "integrations" - }, - { - "type": "builder" - }, - { - "type": "inactive-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-10T15:34:16.827Z", - "start": "2022-01-03T00:00:00.000Z", - "end": "2022-01-10T00:00:00.000Z" - } - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-02-16T07:16:02.866Z", - "start": "2022-02-09T00:00:00.000Z", - "end": "2022-02-16T00:00:00.000Z" - } - }, - { - "type": "newest-members" - }, - { - "type": "latest-activities" - }, - { - "type": "number-activities", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-10T15:34:16.785Z", - "start": "2022-01-03T00:00:00.000Z", - "end": "2022-01-10T00:00:00.000Z" - } - }, - { - "type": "number-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-10T15:34:16.774Z", - "start": "2022-01-03T00:00:00.000Z", - "end": "2022-01-10T00:00:00.000Z" - } - }, - { - "type": "number-members-graph", - "cache": { - "x": ["2021-12-06T00:00:00.000Z", "2021-12-21T00:00:00.000Z"], - "y": [1, 2] - }, - "settings": { - "last_computed_at": "2022-01-10T15:34:16.779Z", - "start": "2021-10-10T00:00:00.000Z", - "end": "2022-01-10T00:00:00.000Z" - } - }, - { - "type": "number-activities-graph", - "cache": { - "x": ["2021-12-06T00:00:00.000Z", "2021-12-21T00:00:00.000Z"], - "y": [1, 2] - }, - "settings": { - "last_computed_at": "2022-01-10T15:34:16.812Z", - "start": "2021-10-10T00:00:00.000Z", - "end": "2022-01-10T00:00:00.000Z" - } - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "channel-distribution" - }, - { - "type": "benchmark" - }, - { - "type": "inactive-members", - "cache": [0, 0], - "settings": null - }, - { - "type": "benchmark" - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": null - }, - { - "type": "newest-members" - }, - { - "type": "integrations" - }, - { - "type": "latest-activities" - }, - { - "type": "builder" - }, - { - "type": "number-members", - "cache": [0, 0], - "settings": null - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "number-activities-graph", - "cache": { - "x": ["2021-10-05T00:00:00"], - "y": [1] - }, - "settings": null - }, - { - "type": "number-activities", - "cache": [0, 0], - "settings": null - }, - { - "type": "channel-distribution" - }, - { - "type": "number-members-graph", - "cache": { - "x": ["2021-10-05T00:00:00"], - "y": [1] - }, - "settings": null - }, - { - "type": "inactive-members", - "cache": [0, 3], - "settings": { - "last_computed_at": "2022-01-14T09:38:18.825Z", - "start": "2022-01-07T00:00:00.000Z", - "end": "2022-01-14T00:00:00.000Z" - } - }, - { - "type": "newest-members" - }, - { - "type": "channel-distribution" - }, - { - "type": "integrations" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "number-activities", - "cache": [0, 29], - "settings": { - "last_computed_at": "2022-01-14T09:38:18.843Z", - "start": "2022-01-07T00:00:00.000Z", - "end": "2022-01-14T00:00:00.000Z" - } - }, - { - "type": "benchmark" - }, - { - "type": "number-members", - "cache": [0, 6], - "settings": { - "last_computed_at": "2022-01-14T09:38:18.824Z", - "start": "2022-01-07T00:00:00.000Z", - "end": "2022-01-14T00:00:00.000Z" - } - }, - { - "type": "time-to-first-interaction", - "cache": [1, 2], - "settings": { - "last_computed_at": "2022-02-17T10:44:41.342Z", - "start": "2022-02-10T00:00:00.000Z", - "end": "2022-02-17T00:00:00.000Z" - } - }, - { - "type": "number-members-graph", - "cache": { - "x": [ - "2021-10-14T00:00:00.000Z", - "2021-10-15T00:00:00.000Z", - "2021-10-16T00:00:00.000Z", - "2021-10-17T00:00:00.000Z", - "2021-10-18T00:00:00.000Z", - "2021-10-19T00:00:00.000Z", - "2021-10-20T00:00:00.000Z", - "2021-10-21T00:00:00.000Z", - "2021-10-22T00:00:00.000Z", - "2021-10-23T00:00:00.000Z", - "2021-10-24T00:00:00.000Z", - "2021-10-25T00:00:00.000Z", - "2021-10-26T00:00:00.000Z", - "2021-10-27T00:00:00.000Z", - "2021-10-28T00:00:00.000Z", - "2021-10-29T00:00:00.000Z", - "2021-10-30T00:00:00.000Z", - "2021-10-31T00:00:00.000Z", - "2021-11-01T00:00:00.000Z", - "2021-11-02T00:00:00.000Z", - "2021-11-03T00:00:00.000Z", - "2021-11-04T00:00:00.000Z", - "2021-11-05T00:00:00.000Z", - "2021-11-06T00:00:00.000Z", - "2021-11-07T00:00:00.000Z", - "2021-11-08T00:00:00.000Z", - "2021-11-09T00:00:00.000Z", - "2021-11-10T00:00:00.000Z", - "2021-11-11T00:00:00.000Z", - "2021-11-12T00:00:00.000Z", - "2021-11-13T00:00:00.000Z", - "2021-11-14T00:00:00.000Z", - "2021-11-15T00:00:00.000Z", - "2021-11-16T00:00:00.000Z", - "2021-11-17T00:00:00.000Z", - "2021-11-18T00:00:00.000Z", - "2021-11-19T00:00:00.000Z", - "2021-11-20T00:00:00.000Z", - "2021-11-21T00:00:00.000Z", - "2021-11-22T00:00:00.000Z", - "2021-11-23T00:00:00.000Z", - "2021-11-24T00:00:00.000Z", - "2021-11-25T00:00:00.000Z", - "2021-11-26T00:00:00.000Z", - "2021-11-27T00:00:00.000Z", - "2021-11-28T00:00:00.000Z", - "2021-11-29T00:00:00.000Z", - "2021-11-30T00:00:00.000Z", - "2021-12-01T00:00:00.000Z", - "2021-12-02T00:00:00.000Z", - "2021-12-03T00:00:00.000Z", - "2021-12-04T00:00:00.000Z", - "2021-12-05T00:00:00.000Z", - "2021-12-06T00:00:00.000Z", - "2021-12-07T00:00:00.000Z", - "2021-12-08T00:00:00.000Z", - "2021-12-09T00:00:00.000Z", - "2021-12-10T00:00:00.000Z", - "2021-12-11T00:00:00.000Z", - "2021-12-12T00:00:00.000Z", - "2021-12-13T00:00:00.000Z", - "2021-12-14T00:00:00.000Z", - "2021-12-15T00:00:00.000Z", - "2021-12-16T00:00:00.000Z", - "2021-12-17T00:00:00.000Z", - "2021-12-18T00:00:00.000Z", - "2021-12-19T00:00:00.000Z", - "2021-12-20T00:00:00.000Z", - "2021-12-21T00:00:00.000Z", - "2021-12-22T00:00:00.000Z", - "2021-12-23T00:00:00.000Z", - "2022-01-12T00:00:00.000Z", - "2022-01-13T00:00:00.000Z" - ], - "y": [ - 1, 13, 21, 29, 40, 47, 55, 58, 70, 75, 84, 92, 95, 104, 112, 114, 121, 125, 131, 136, 140, - 149, 152, 161, 168, 170, 172, 175, 181, 186, 186, 188, 195, 198, 200, 208, 213, 216, 225, - 232, 236, 243, 249, 254, 255, 262, 269, 272, 274, 279, 291, 297, 303, 306, 312, 315, 323, - 325, 331, 336, 343, 346, 351, 352, 352, 352, 352, 352, 352, 352, 352, 353, 358 - ] - }, - "settings": { - "last_computed_at": "2022-01-14T09:38:20.260Z", - "start": "2021-10-14T00:00:00.000Z", - "end": "2022-01-14T00:00:00.000Z" - } - }, - { - "type": "builder" - }, - { - "type": "latest-activities" - }, - { - "type": "number-activities-graph", - "cache": { - "x": [ - "2021-10-14T00:00:00.000Z", - "2021-10-15T00:00:00.000Z", - "2021-10-16T00:00:00.000Z", - "2021-10-17T00:00:00.000Z", - "2021-10-18T00:00:00.000Z", - "2021-10-19T00:00:00.000Z", - "2021-10-20T00:00:00.000Z", - "2021-10-21T00:00:00.000Z", - "2021-10-22T00:00:00.000Z", - "2021-10-23T00:00:00.000Z", - "2021-10-24T00:00:00.000Z", - "2021-10-25T00:00:00.000Z", - "2021-10-26T00:00:00.000Z", - "2021-10-27T00:00:00.000Z", - "2021-10-28T00:00:00.000Z", - "2021-10-29T00:00:00.000Z", - "2021-10-30T00:00:00.000Z", - "2021-10-31T00:00:00.000Z", - "2021-11-01T00:00:00.000Z", - "2021-11-02T00:00:00.000Z", - "2021-11-03T00:00:00.000Z", - "2021-11-04T00:00:00.000Z", - "2021-11-05T00:00:00.000Z", - "2021-11-06T00:00:00.000Z", - "2021-11-07T00:00:00.000Z", - "2021-11-08T00:00:00.000Z", - "2021-11-09T00:00:00.000Z", - "2021-11-10T00:00:00.000Z", - "2021-11-11T00:00:00.000Z", - "2021-11-12T00:00:00.000Z", - "2021-11-13T00:00:00.000Z", - "2021-11-14T00:00:00.000Z", - "2021-11-15T00:00:00.000Z", - "2021-11-16T00:00:00.000Z", - "2021-11-17T00:00:00.000Z", - "2021-11-18T00:00:00.000Z", - "2021-11-19T00:00:00.000Z", - "2021-11-20T00:00:00.000Z", - "2021-11-21T00:00:00.000Z", - "2021-11-22T00:00:00.000Z", - "2021-11-23T00:00:00.000Z", - "2021-11-24T00:00:00.000Z", - "2021-11-25T00:00:00.000Z", - "2021-11-26T00:00:00.000Z", - "2021-11-27T00:00:00.000Z", - "2021-11-28T00:00:00.000Z", - "2021-11-29T00:00:00.000Z", - "2021-11-30T00:00:00.000Z", - "2021-12-01T00:00:00.000Z", - "2021-12-02T00:00:00.000Z", - "2021-12-03T00:00:00.000Z", - "2021-12-04T00:00:00.000Z", - "2021-12-05T00:00:00.000Z", - "2021-12-06T00:00:00.000Z", - "2021-12-07T00:00:00.000Z", - "2021-12-08T00:00:00.000Z", - "2021-12-09T00:00:00.000Z", - "2021-12-10T00:00:00.000Z", - "2021-12-11T00:00:00.000Z", - "2021-12-12T00:00:00.000Z", - "2021-12-13T00:00:00.000Z", - "2021-12-14T00:00:00.000Z", - "2021-12-15T00:00:00.000Z", - "2021-12-16T00:00:00.000Z", - "2021-12-17T00:00:00.000Z", - "2021-12-18T00:00:00.000Z", - "2021-12-19T00:00:00.000Z", - "2021-12-20T00:00:00.000Z", - "2021-12-21T00:00:00.000Z", - "2021-12-22T00:00:00.000Z", - "2021-12-23T00:00:00.000Z", - "2022-01-12T00:00:00.000Z", - "2022-01-13T00:00:00.000Z" - ], - "y": [ - 1, 16, 28, 39, 52, 68, 82, 92, 115, 132, 150, 164, 183, 206, 232, 250, 275, 297, 321, 344, - 362, 397, 419, 451, 473, 499, 524, 554, 581, 621, 641, 673, 713, 744, 775, 810, 847, 879, - 925, 979, 1026, 1073, 1125, 1181, 1228, 1281, 1355, 1407, 1472, 1534, 1591, 1662, 1731, - 1818, 1896, 1979, 2062, 2172, 2249, 2351, 2437, 2532, 2618, 2728, 2834, 2939, 3041, 3147, - 3242, 3349, 3439, 3519, 3541 - ] - }, - "settings": { - "last_computed_at": "2022-01-14T09:38:20.297Z", - "start": "2021-10-14T00:00:00.000Z", - "end": "2022-01-14T00:00:00.000Z" - } - }, - { - "type": "inactive-members-graph" - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2021-11-30T13:44:33.601Z", - "start": "2021-11-23T00:00:00.000Z", - "end": "2021-11-30T00:00:00.000Z" - } - }, - { - "type": "inactive-members-graph" - }, - { - "type": "channel-distribution" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "builder" - }, - { - "type": "integrations" - }, - { - "type": "inactive-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2021-11-30T13:44:33.612Z", - "start": "2021-11-23T00:00:00.000Z", - "end": "2021-11-30T00:00:00.000Z" - } - }, - { - "type": "newest-members" - }, - { - "type": "benchmark" - }, - { - "type": "number-activities", - "cache": [0, 0], - "settings": { - "last_computed_at": "2021-11-30T13:44:33.617Z", - "start": "2021-11-23T00:00:00.000Z", - "end": "2021-11-30T00:00:00.000Z" - } - }, - { - "type": "number-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2021-11-30T13:44:33.666Z", - "start": "2021-11-23T00:00:00.000Z", - "end": "2021-11-30T00:00:00.000Z" - } - }, - { - "type": "latest-activities" - }, - { - "type": "number-members-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": { - "last_computed_at": "2021-11-30T13:44:33.658Z", - "start": "2021-08-30T00:00:00.000Z", - "end": "2021-11-30T00:00:00.000Z" - } - }, - { - "type": "number-activities-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": { - "last_computed_at": "2021-11-30T13:44:33.629Z", - "start": "2021-08-30T00:00:00.000Z", - "end": "2021-11-30T00:00:00.000Z" - } - }, - { - "type": "inactive-members", - "settings": { - "end": "2021-12-16 12:00:00", - "start": "2021-12-09 12:00:00", - "lastComputedAt": 1637875827631 - } - }, - { - "type": "benchmark" - }, - { - "type": "latest-activities" - }, - { - "type": "newest-members" - }, - { - "type": "builder" - }, - { - "type": "integrations" - }, - { - "type": "time-to-first-interaction", - "settings": { - "end": "2021-12-16 12:00:00", - "start": "2021-12-09 12:00:00", - "lastComputedAt": 1637875827630 - } - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "number-activities-graph", - "settings": { - "end": "2021-12-16 12:00:00", - "start": "2021-09-16 12:00:00", - "lastComputedAt": 1637875828011 - } - }, - { - "type": "inactive-members-graph" - }, - { - "type": "number-members", - "settings": { - "end": "2021-12-16 12:00:00", - "start": "2021-12-09 12:00:00", - "lastComputedAt": 1637875827649 - } - }, - { - "type": "number-activities", - "settings": { - "end": "2021-12-16 12:00:00", - "start": "2021-12-09 12:00:00", - "lastComputedAt": 1637875827657 - } - }, - { - "type": "number-members-graph", - "settings": { - "end": "2021-12-16 12:00:00", - "start": "2021-09-16 12:00:00", - "lastComputedAt": 1637875827627 - } - }, - { - "type": "channel-distribution" - }, - { - "type": "number-members-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": { - "last_computed_at": "2021-12-23T10:22:10.934Z", - "start": "2021-09-23T00:00:00.000Z", - "end": "2021-12-23T00:00:00.000Z" - } - }, - { - "type": "inactive-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2021-12-23T10:22:10.816Z", - "start": "2021-12-16T00:00:00.000Z", - "end": "2021-12-23T00:00:00.000Z" - } - }, - { - "type": "number-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2021-12-23T10:22:10.868Z", - "start": "2021-12-16T00:00:00.000Z", - "end": "2021-12-23T00:00:00.000Z" - } - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2021-12-23T10:22:10.886Z", - "start": "2021-12-16T00:00:00.000Z", - "end": "2021-12-23T00:00:00.000Z" - } - }, - { - "type": "integrations" - }, - { - "type": "latest-activities" - }, - { - "type": "number-activities-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": { - "last_computed_at": "2021-12-23T10:22:10.923Z", - "start": "2021-09-23T00:00:00.000Z", - "end": "2021-12-23T00:00:00.000Z" - } - }, - { - "type": "benchmark" - }, - { - "type": "newest-members" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "builder" - }, - { - "type": "channel-distribution" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "number-activities", - "cache": [0, 0], - "settings": { - "last_computed_at": "2021-12-23T10:22:10.913Z", - "start": "2021-12-16T00:00:00.000Z", - "end": "2021-12-23T00:00:00.000Z" - } - }, - { - "type": "inactive-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-04T17:32:58.033Z", - "start": "2021-12-28T00:00:00.000Z", - "end": "2022-01-04T00:00:00.000Z" - } - }, - { - "type": "number-activities", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-04T17:32:57.942Z", - "start": "2021-12-28T00:00:00.000Z", - "end": "2022-01-04T00:00:00.000Z" - } - }, - { - "type": "channel-distribution" - }, - { - "type": "integrations" - }, - { - "type": "latest-activities" - }, - { - "type": "benchmark" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-04T17:32:58.006Z", - "start": "2021-12-28T00:00:00.000Z", - "end": "2022-01-04T00:00:00.000Z" - } - }, - { - "type": "number-members-graph", - "cache": { - "x": [ - "2021-10-05T00:00:00.000Z", - "2021-10-08T00:00:00.000Z", - "2021-10-09T00:00:00.000Z", - "2021-10-11T00:00:00.000Z", - "2021-10-13T00:00:00.000Z", - "2021-10-14T00:00:00.000Z", - "2021-10-18T00:00:00.000Z", - "2021-10-20T00:00:00.000Z", - "2021-10-21T00:00:00.000Z", - "2021-10-22T00:00:00.000Z", - "2021-10-25T00:00:00.000Z", - "2021-10-28T00:00:00.000Z", - "2021-11-01T00:00:00.000Z", - "2021-11-03T00:00:00.000Z", - "2021-11-05T00:00:00.000Z", - "2021-11-06T00:00:00.000Z", - "2021-11-08T00:00:00.000Z", - "2021-11-09T00:00:00.000Z", - "2021-11-10T00:00:00.000Z", - "2021-11-11T00:00:00.000Z", - "2021-11-12T00:00:00.000Z", - "2021-11-15T00:00:00.000Z", - "2021-11-16T00:00:00.000Z", - "2021-11-17T00:00:00.000Z", - "2021-11-19T00:00:00.000Z", - "2021-11-20T00:00:00.000Z", - "2021-11-21T00:00:00.000Z", - "2021-11-23T00:00:00.000Z", - "2021-11-24T00:00:00.000Z", - "2021-11-29T00:00:00.000Z", - "2021-11-30T00:00:00.000Z", - "2021-12-08T00:00:00.000Z", - "2021-12-09T00:00:00.000Z", - "2021-12-13T00:00:00.000Z", - "2021-12-16T00:00:00.000Z", - "2021-12-17T00:00:00.000Z", - "2021-12-20T00:00:00.000Z", - "2021-12-21T00:00:00.000Z", - "2021-12-22T00:00:00.000Z", - "2022-01-03T00:00:00.000Z" - ], - "y": [ - 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, - 4, 4, 4, 5, 5, 5, 5, 5, 6 - ] - }, - "settings": { - "last_computed_at": "2022-01-04T17:32:58.077Z", - "start": "2021-10-04T00:00:00.000Z", - "end": "2022-01-04T00:00:00.000Z" - } - }, - { - "type": "builder" - }, - { - "type": "number-activities-graph", - "cache": { - "x": [ - "2021-10-05T00:00:00.000Z", - "2021-10-08T00:00:00.000Z", - "2021-10-09T00:00:00.000Z", - "2021-10-11T00:00:00.000Z", - "2021-10-13T00:00:00.000Z", - "2021-10-14T00:00:00.000Z", - "2021-10-18T00:00:00.000Z", - "2021-10-20T00:00:00.000Z", - "2021-10-21T00:00:00.000Z", - "2021-10-22T00:00:00.000Z", - "2021-10-25T00:00:00.000Z", - "2021-10-28T00:00:00.000Z", - "2021-11-01T00:00:00.000Z", - "2021-11-03T00:00:00.000Z", - "2021-11-05T00:00:00.000Z", - "2021-11-06T00:00:00.000Z", - "2021-11-08T00:00:00.000Z", - "2021-11-09T00:00:00.000Z", - "2021-11-10T00:00:00.000Z", - "2021-11-11T00:00:00.000Z", - "2021-11-12T00:00:00.000Z", - "2021-11-15T00:00:00.000Z", - "2021-11-16T00:00:00.000Z", - "2021-11-17T00:00:00.000Z", - "2021-11-19T00:00:00.000Z", - "2021-11-20T00:00:00.000Z", - "2021-11-21T00:00:00.000Z", - "2021-11-23T00:00:00.000Z", - "2021-11-24T00:00:00.000Z", - "2021-11-29T00:00:00.000Z", - "2021-11-30T00:00:00.000Z", - "2021-12-08T00:00:00.000Z", - "2021-12-09T00:00:00.000Z", - "2021-12-13T00:00:00.000Z", - "2021-12-16T00:00:00.000Z", - "2021-12-17T00:00:00.000Z", - "2021-12-20T00:00:00.000Z", - "2021-12-21T00:00:00.000Z", - "2021-12-22T00:00:00.000Z", - "2022-01-03T00:00:00.000Z" - ], - "y": [ - 1, 2, 3, 4, 6, 8, 10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 25, 26, 29, 33, 34, 35, - 36, 37, 38, 43, 44, 46, 48, 50, 51, 52, 53, 55, 57, 58, 59, 60 - ] - }, - "settings": { - "last_computed_at": "2022-01-04T17:32:58.016Z", - "start": "2021-10-04T00:00:00.000Z", - "end": "2022-01-04T00:00:00.000Z" - } - }, - { - "type": "newest-members" - }, - { - "type": "number-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-04T17:32:58.021Z", - "start": "2021-12-28T00:00:00.000Z", - "end": "2022-01-04T00:00:00.000Z" - } - }, - { - "type": "inactive-members-graph" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-11T15:12:17.958Z", - "start": "2022-01-04T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "number-activities-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": { - "last_computed_at": "2022-01-11T15:12:17.944Z", - "start": "2021-10-11T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "number-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-11T15:12:18.019Z", - "start": "2022-01-04T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "inactive-members-graph" - }, - { - "type": "channel-distribution" - }, - { - "type": "latest-activities" - }, - { - "type": "inactive-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-11T15:12:18.069Z", - "start": "2022-01-04T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "number-activities", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-11T15:12:17.911Z", - "start": "2022-01-04T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "integrations" - }, - { - "type": "builder" - }, - { - "type": "number-members-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": { - "last_computed_at": "2022-01-11T15:12:17.984Z", - "start": "2021-10-11T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "benchmark" - }, - { - "type": "newest-members" - }, - { - "type": "number-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-11T15:13:00.536Z", - "start": "2022-01-04T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "builder" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "number-activities-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": { - "last_computed_at": "2022-01-11T15:13:00.565Z", - "start": "2021-10-11T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "number-members-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": { - "last_computed_at": "2022-01-11T15:13:00.543Z", - "start": "2021-10-11T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "newest-members" - }, - { - "type": "number-activities", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-11T15:13:00.567Z", - "start": "2022-01-04T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "inactive-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-11T15:13:00.538Z", - "start": "2022-01-04T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-11T15:13:00.570Z", - "start": "2022-01-04T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "latest-activities" - }, - { - "type": "benchmark" - }, - { - "type": "integrations" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "channel-distribution" - }, - { - "type": "inactive-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-11T16:18:40.304Z", - "start": "2022-01-04T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "benchmark" - }, - { - "type": "number-members-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": { - "last_computed_at": "2022-01-11T16:18:40.367Z", - "start": "2021-10-11T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "latest-activities" - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-11T16:18:40.425Z", - "start": "2022-01-04T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "channel-distribution" - }, - { - "type": "newest-members" - }, - { - "type": "integrations" - }, - { - "type": "number-activities", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-11T16:18:40.346Z", - "start": "2022-01-04T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "builder" - }, - { - "type": "number-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-11T16:18:39.876Z", - "start": "2022-01-04T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "inactive-members-graph" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "number-activities-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": { - "last_computed_at": "2022-01-11T16:18:40.516Z", - "start": "2021-10-11T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "inactive-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-11T16:19:21.548Z", - "start": "2022-01-04T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "number-members-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": { - "last_computed_at": "2022-01-11T16:19:21.548Z", - "start": "2021-10-11T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-11T16:19:21.536Z", - "start": "2022-01-04T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "number-activities", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-11T16:19:21.540Z", - "start": "2022-01-04T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "benchmark" - }, - { - "type": "channel-distribution" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "latest-activities" - }, - { - "type": "number-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-11T16:19:21.544Z", - "start": "2022-01-04T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "newest-members" - }, - { - "type": "integrations" - }, - { - "type": "builder" - }, - { - "type": "number-activities-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": { - "last_computed_at": "2022-01-11T16:19:21.616Z", - "start": "2021-10-11T00:00:00.000Z", - "end": "2022-01-11T00:00:00.000Z" - } - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "benchmark", - "settings": { - "repositories": [ - { - "id": 56919458, - "value": "vue-multiselect", - "label": "vue-multiselect", - "active": true, - "editing": false, - "githubRepo": { - "id": 56919458, - "node_id": "MDEwOlJlcG9zaXRvcnk1NjkxOTQ1OA==", - "name": "vue-multiselect", - "full_name": "shentao/vue-multiselect", - "private": false, - "owner": { - "login": "shentao", - "id": 3737591, - "node_id": "MDQ6VXNlcjM3Mzc1OTE=", - "avatar_url": "https://avatars.githubusercontent.com/u/3737591?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/shentao", - "html_url": "https://github.com/shentao", - "followers_url": "https://api.github.com/users/shentao/followers", - "following_url": "https://api.github.com/users/shentao/following{/other_user}", - "gists_url": "https://api.github.com/users/shentao/gists{/gist_id}", - "starred_url": "https://api.github.com/users/shentao/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/shentao/subscriptions", - "organizations_url": "https://api.github.com/users/shentao/orgs", - "repos_url": "https://api.github.com/users/shentao/repos", - "events_url": "https://api.github.com/users/shentao/events{/privacy}", - "received_events_url": "https://api.github.com/users/shentao/received_events", - "type": "User", - "site_admin": false - }, - "html_url": "https://github.com/shentao/vue-multiselect", - "description": "Universal select/multiselect/tagging component for Vue.js", - "fork": false, - "url": "https://api.github.com/repos/shentao/vue-multiselect", - "forks_url": "https://api.github.com/repos/shentao/vue-multiselect/forks", - "keys_url": "https://api.github.com/repos/shentao/vue-multiselect/keys{/key_id}", - "collaborators_url": "https://api.github.com/repos/shentao/vue-multiselect/collaborators{/collaborator}", - "teams_url": "https://api.github.com/repos/shentao/vue-multiselect/teams", - "hooks_url": "https://api.github.com/repos/shentao/vue-multiselect/hooks", - "issue_events_url": "https://api.github.com/repos/shentao/vue-multiselect/issues/events{/number}", - "events_url": "https://api.github.com/repos/shentao/vue-multiselect/events", - "assignees_url": "https://api.github.com/repos/shentao/vue-multiselect/assignees{/user}", - "branches_url": "https://api.github.com/repos/shentao/vue-multiselect/branches{/branch}", - "tags_url": "https://api.github.com/repos/shentao/vue-multiselect/tags", - "blobs_url": "https://api.github.com/repos/shentao/vue-multiselect/git/blobs{/sha}", - "git_tags_url": "https://api.github.com/repos/shentao/vue-multiselect/git/tags{/sha}", - "git_refs_url": "https://api.github.com/repos/shentao/vue-multiselect/git/refs{/sha}", - "trees_url": "https://api.github.com/repos/shentao/vue-multiselect/git/trees{/sha}", - "statuses_url": "https://api.github.com/repos/shentao/vue-multiselect/statuses/{sha}", - "languages_url": "https://api.github.com/repos/shentao/vue-multiselect/languages", - "stargazers_url": "https://api.github.com/repos/shentao/vue-multiselect/stargazers", - "contributors_url": "https://api.github.com/repos/shentao/vue-multiselect/contributors", - "subscribers_url": "https://api.github.com/repos/shentao/vue-multiselect/subscribers", - "subscription_url": "https://api.github.com/repos/shentao/vue-multiselect/subscription", - "commits_url": "https://api.github.com/repos/shentao/vue-multiselect/commits{/sha}", - "git_commits_url": "https://api.github.com/repos/shentao/vue-multiselect/git/commits{/sha}", - "comments_url": "https://api.github.com/repos/shentao/vue-multiselect/comments{/number}", - "issue_comment_url": "https://api.github.com/repos/shentao/vue-multiselect/issues/comments{/number}", - "contents_url": "https://api.github.com/repos/shentao/vue-multiselect/contents/{+path}", - "compare_url": "https://api.github.com/repos/shentao/vue-multiselect/compare/{base}...{head}", - "merges_url": "https://api.github.com/repos/shentao/vue-multiselect/merges", - "archive_url": "https://api.github.com/repos/shentao/vue-multiselect/{archive_format}{/ref}", - "downloads_url": "https://api.github.com/repos/shentao/vue-multiselect/downloads", - "issues_url": "https://api.github.com/repos/shentao/vue-multiselect/issues{/number}", - "pulls_url": "https://api.github.com/repos/shentao/vue-multiselect/pulls{/number}", - "milestones_url": "https://api.github.com/repos/shentao/vue-multiselect/milestones{/number}", - "notifications_url": "https://api.github.com/repos/shentao/vue-multiselect/notifications{?since,all,participating}", - "labels_url": "https://api.github.com/repos/shentao/vue-multiselect/labels{/name}", - "releases_url": "https://api.github.com/repos/shentao/vue-multiselect/releases{/id}", - "deployments_url": "https://api.github.com/repos/shentao/vue-multiselect/deployments", - "created_at": "2016-04-23T13:02:33Z", - "updated_at": "2022-01-19T11:09:17Z", - "pushed_at": "2022-01-17T04:06:58Z", - "git_url": "git://github.com/shentao/vue-multiselect.git", - "ssh_url": "git@github.com:shentao/vue-multiselect.git", - "clone_url": "https://github.com/shentao/vue-multiselect.git", - "svn_url": "https://github.com/shentao/vue-multiselect", - "homepage": "https://vue-multiselect.js.org/", - "size": 13669, - "stargazers_count": 6021, - "watchers_count": 6021, - "language": "JavaScript", - "has_issues": true, - "has_projects": true, - "has_downloads": true, - "has_wiki": true, - "has_pages": true, - "forks_count": 916, - "mirror_url": null, - "archived": false, - "disabled": false, - "open_issues_count": 357, - "license": { - "key": "mit", - "name": "MIT License", - "spdx_id": "MIT", - "url": "https://api.github.com/licenses/mit", - "node_id": "MDc6TGljZW5zZTEz" - }, - "allow_forking": true, - "is_template": false, - "topics": ["component", "dropdown", "javascript", "select", "vue"], - "visibility": "public", - "forks": 916, - "open_issues": 357, - "watchers": 6021, - "default_branch": "master", - "permissions": { - "admin": false, - "maintain": false, - "push": false, - "triage": false, - "pull": true - }, - "score": 1 - }, - "data": { - "2022-01-26": { - "date": "2022-01-26", - "value": 6029 - }, - "2022-01-25": { - "date": "2022-01-25", - "value": 6029 - }, - "2022-01-24": { - "date": "2022-01-24", - "value": 6029 - }, - "2022-01-23": { - "date": "2022-01-23", - "value": 6026 - }, - "2022-01-22": { - "date": "2022-01-22", - "value": 6026 - }, - "2022-01-21": { - "date": "2022-01-21", - "value": 6022 - }, - "2022-01-20": { - "date": "2022-01-20", - "value": 6020 - }, - "2022-01-19": { - "date": "2022-01-19", - "value": 6018 - }, - "2022-01-18": { - "date": "2022-01-18", - "value": 6016 - }, - "2022-01-17": { - "date": "2022-01-17", - "value": 6015 - }, - "2022-01-16": { - "date": "2022-01-16", - "value": 6014 - }, - "2022-01-15": { - "date": "2022-01-15", - "value": 6011 - }, - "2022-01-14": { - "date": "2022-01-14", - "value": 6010 - }, - "2022-01-13": { - "date": "2022-01-13", - "value": 6008 - }, - "2022-01-12": { - "date": "2022-01-12", - "value": 6007 - }, - "2022-01-11": { - "date": "2022-01-11", - "value": 6005 - }, - "2022-01-10": { - "date": "2022-01-10", - "value": 6003 - }, - "2022-01-09": { - "date": "2022-01-09", - "value": 6003 - }, - "2022-01-08": { - "date": "2022-01-08", - "value": 6002 - }, - "2022-01-07": { - "date": "2022-01-07", - "value": 5999 - }, - "2022-01-06": { - "date": "2022-01-06", - "value": 5996 - }, - "2022-01-05": { - "date": "2022-01-05", - "value": 5995 - }, - "2022-01-04": { - "date": "2022-01-04", - "value": 5992 - }, - "2022-01-03": { - "date": "2022-01-03", - "value": 5991 - }, - "2022-01-02": { - "date": "2022-01-02", - "value": 5991 - }, - "2022-01-01": { - "date": "2022-01-01", - "value": 5991 - }, - "2021-12-31": { - "date": "2021-12-31", - "value": 5989 - }, - "2021-12-30": { - "date": "2021-12-30", - "value": 5988 - }, - "2021-12-29": { - "date": "2021-12-29", - "value": 5985 - }, - "2021-12-28": { - "date": "2021-12-28", - "value": 5985 - }, - "2021-12-27": { - "date": "2021-12-27", - "value": 5985 - }, - "2021-12-26": { - "date": "2021-12-26", - "value": 5985 - }, - "2021-12-25": { - "date": "2021-12-25", - "value": 5985 - }, - "2021-12-24": { - "date": "2021-12-24", - "value": 5984 - }, - "2021-12-23": { - "date": "2021-12-23", - "value": 5984 - }, - "2021-12-22": { - "date": "2021-12-22", - "value": 5980 - }, - "2021-12-21": { - "date": "2021-12-21", - "value": 5978 - }, - "2021-12-20": { - "date": "2021-12-20", - "value": 5978 - }, - "2021-12-19": { - "date": "2021-12-19", - "value": 5978 - }, - "2021-12-18": { - "date": "2021-12-18", - "value": 5978 - }, - "2021-12-17": { - "date": "2021-12-17", - "value": 5977 - }, - "2021-12-16": { - "date": "2021-12-16", - "value": 5974 - }, - "2021-12-15": { - "date": "2021-12-15", - "value": 5972 - }, - "2021-12-14": { - "date": "2021-12-14", - "value": 5970 - }, - "2021-12-13": { - "date": "2021-12-13", - "value": 5969 - }, - "2021-12-12": { - "date": "2021-12-12", - "value": 5969 - }, - "2021-12-11": { - "date": "2021-12-11", - "value": 5969 - }, - "2021-12-10": { - "date": "2021-12-10", - "value": 5967 - }, - "2021-12-09": { - "date": "2021-12-09", - "value": 5967 - }, - "2021-12-08": { - "date": "2021-12-08", - "value": 5966 - }, - "2021-12-07": { - "date": "2021-12-07", - "value": 5962 - }, - "2021-12-06": { - "date": "2021-12-06", - "value": 5958 - }, - "2021-12-05": { - "date": "2021-12-05", - "value": 5958 - }, - "2021-12-04": { - "date": "2021-12-04", - "value": 5957 - }, - "2021-12-03": { - "date": "2021-12-03", - "value": 5955 - }, - "2021-12-02": { - "date": "2021-12-02", - "value": 5955 - }, - "2021-12-01": { - "date": "2021-12-01", - "value": 5954 - }, - "2021-11-30": { - "date": "2021-11-30", - "value": 5953 - }, - "2021-11-29": { - "date": "2021-11-29", - "value": 5952 - }, - "2021-11-28": { - "date": "2021-11-28", - "value": 5951 - }, - "2021-11-27": { - "date": "2021-11-27", - "value": 5950 - }, - "2021-11-26": { - "date": "2021-11-26", - "value": 5947 - }, - "2021-11-25": { - "date": "2021-11-25", - "value": 5947 - }, - "2021-11-24": { - "date": "2021-11-24", - "value": 5942 - }, - "2021-11-23": { - "date": "2021-11-23", - "value": 5940 - }, - "2021-11-22": { - "date": "2021-11-22", - "value": 5938 - }, - "2021-11-21": { - "date": "2021-11-21", - "value": 5938 - }, - "2021-11-20": { - "date": "2021-11-20", - "value": 5938 - }, - "2021-11-19": { - "date": "2021-11-19", - "value": 5935 - }, - "2021-11-18": { - "date": "2021-11-18", - "value": 5931 - }, - "2021-11-17": { - "date": "2021-11-17", - "value": 5929 - }, - "2021-11-16": { - "date": "2021-11-16", - "value": 5927 - }, - "2021-11-15": { - "date": "2021-11-15", - "value": 5926 - }, - "2021-11-14": { - "date": "2021-11-14", - "value": 5925 - }, - "2021-11-13": { - "date": "2021-11-13", - "value": 5923 - }, - "2021-11-12": { - "date": "2021-11-12", - "value": 5923 - }, - "2021-11-11": { - "date": "2021-11-11", - "value": 5921 - }, - "2021-11-10": { - "date": "2021-11-10", - "value": 5919 - }, - "2021-11-09": { - "date": "2021-11-09", - "value": 5916 - }, - "2021-11-08": { - "date": "2021-11-08", - "value": 5912 - }, - "2021-11-07": { - "date": "2021-11-07", - "value": 5910 - }, - "2021-11-06": { - "date": "2021-11-06", - "value": 5909 - }, - "2021-11-05": { - "date": "2021-11-05", - "value": 5908 - }, - "2021-11-04": { - "date": "2021-11-04", - "value": 5908 - }, - "2021-11-03": { - "date": "2021-11-03", - "value": 5907 - }, - "2021-11-02": { - "date": "2021-11-02", - "value": 5905 - }, - "2021-11-01": { - "date": "2021-11-01", - "value": 5905 - }, - "2021-10-31": { - "date": "2021-10-31", - "value": 5905 - }, - "2021-10-30": { - "date": "2021-10-30", - "value": 5903 - }, - "2021-10-29": { - "date": "2021-10-29", - "value": 5903 - }, - "2021-10-28": { - "date": "2021-10-28", - "value": 5902 - }, - "2021-10-27": { - "date": "2021-10-27", - "value": 5902 - }, - "2021-10-26": { - "date": "2021-10-26", - "value": 5900 - } - }, - "color": "#20A215" - } - ], - "last_updated_at": "2022-01-26", - "timeframe": { - "label": "Last three months", - "value": "last_three_months", - "date": "2021-10-26" - } - } - }, - { - "type": "inactive-members", - "cache": [3, 5], - "settings": { - "last_computed_at": "2022-01-14T10:45:36.437Z", - "start": "2022-01-07T00:00:00.000Z", - "end": "2022-01-14T00:00:00.000Z" - } - }, - { - "type": "number-members-graph", - "cache": { - "x": [ - "2021-10-14T00:00:00.000Z", - "2021-10-15T00:00:00.000Z", - "2021-10-16T00:00:00.000Z", - "2021-10-17T00:00:00.000Z", - "2021-10-18T00:00:00.000Z", - "2021-10-19T00:00:00.000Z", - "2021-10-20T00:00:00.000Z", - "2021-10-21T00:00:00.000Z", - "2021-10-22T00:00:00.000Z", - "2021-10-23T00:00:00.000Z", - "2021-10-24T00:00:00.000Z", - "2021-10-25T00:00:00.000Z", - "2021-10-26T00:00:00.000Z", - "2021-10-27T00:00:00.000Z", - "2021-10-28T00:00:00.000Z", - "2021-10-29T00:00:00.000Z", - "2021-10-30T00:00:00.000Z", - "2021-10-31T00:00:00.000Z", - "2021-11-01T00:00:00.000Z", - "2021-11-02T00:00:00.000Z", - "2021-11-03T00:00:00.000Z", - "2021-11-04T00:00:00.000Z", - "2021-11-05T00:00:00.000Z", - "2021-11-06T00:00:00.000Z", - "2021-11-07T00:00:00.000Z", - "2021-11-08T00:00:00.000Z", - "2021-11-09T00:00:00.000Z", - "2021-11-10T00:00:00.000Z", - "2021-11-11T00:00:00.000Z", - "2021-11-12T00:00:00.000Z", - "2021-11-13T00:00:00.000Z", - "2021-11-14T00:00:00.000Z", - "2021-11-15T00:00:00.000Z", - "2021-11-16T00:00:00.000Z", - "2021-11-17T00:00:00.000Z", - "2021-11-18T00:00:00.000Z", - "2021-11-19T00:00:00.000Z", - "2021-11-20T00:00:00.000Z", - "2021-11-21T00:00:00.000Z", - "2021-11-22T00:00:00.000Z", - "2021-11-23T00:00:00.000Z", - "2021-11-24T00:00:00.000Z", - "2021-11-25T00:00:00.000Z", - "2021-11-26T00:00:00.000Z", - "2021-11-27T00:00:00.000Z", - "2021-11-28T00:00:00.000Z", - "2021-11-29T00:00:00.000Z", - "2021-11-30T00:00:00.000Z", - "2021-12-01T00:00:00.000Z", - "2021-12-02T00:00:00.000Z", - "2021-12-03T00:00:00.000Z", - "2021-12-04T00:00:00.000Z", - "2021-12-05T00:00:00.000Z", - "2021-12-06T00:00:00.000Z", - "2021-12-07T00:00:00.000Z", - "2021-12-08T00:00:00.000Z", - "2021-12-09T00:00:00.000Z", - "2021-12-10T00:00:00.000Z", - "2021-12-11T00:00:00.000Z", - "2021-12-12T00:00:00.000Z", - "2021-12-13T00:00:00.000Z", - "2021-12-14T00:00:00.000Z", - "2021-12-15T00:00:00.000Z", - "2021-12-16T00:00:00.000Z", - "2021-12-17T00:00:00.000Z", - "2021-12-18T00:00:00.000Z", - "2021-12-19T00:00:00.000Z", - "2021-12-20T00:00:00.000Z", - "2021-12-21T00:00:00.000Z", - "2021-12-22T00:00:00.000Z", - "2021-12-23T00:00:00.000Z", - "2021-12-29T00:00:00.000Z", - "2021-12-30T00:00:00.000Z", - "2022-01-03T00:00:00.000Z", - "2022-01-04T00:00:00.000Z", - "2022-01-05T00:00:00.000Z", - "2022-01-06T00:00:00.000Z", - "2022-01-08T00:00:00.000Z", - "2022-01-10T00:00:00.000Z", - "2022-01-11T00:00:00.000Z", - "2022-01-12T00:00:00.000Z" - ], - "y": [ - 1, 131, 148, 163, 169, 175, 180, 185, 188, 188, 192, 198, 200, 214, 215, 219, 221, 222, 225, - 229, 233, 235, 236, 237, 237, 239, 240, 242, 245, 257, 257, 257, 257, 258, 260, 261, 264, - 265, 269, 272, 276, 276, 277, 277, 278, 279, 283, 285, 286, 287, 289, 291, 292, 292, 292, - 292, 293, 293, 293, 293, 296, 296, 310, 310, 311, 314, 315, 317, 317, 317, 318, 319, 320, - 321, 321, 323, 325, 326, 327, 327, 328 - ] - }, - "settings": { - "last_computed_at": "2022-01-14T10:45:44.584Z", - "start": "2021-10-14T00:00:00.000Z", - "end": "2022-01-14T00:00:00.000Z" - } - }, - { - "type": "newest-members" - }, - { - "type": "latest-activities" - }, - { - "type": "channel-distribution" - }, - { - "type": "integrations" - }, - { - "type": "builder" - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-25T23:21:32.193Z", - "start": "2022-01-19T00:00:00.000Z", - "end": "2022-01-26T00:00:00.000Z" - } - }, - { - "type": "number-activities", - "cache": [17, 5], - "settings": { - "last_computed_at": "2022-01-14T10:45:36.778Z", - "start": "2022-01-07T00:00:00.000Z", - "end": "2022-01-14T00:00:00.000Z" - } - }, - { - "type": "number-members", - "cache": [3, 3], - "settings": { - "last_computed_at": "2022-01-14T10:45:37.631Z", - "start": "2022-01-07T00:00:00.000Z", - "end": "2022-01-14T00:00:00.000Z" - } - }, - { - "type": "inactive-members-graph" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "number-activities-graph", - "cache": { - "x": [ - "2021-10-14T00:00:00.000Z", - "2021-10-15T00:00:00.000Z", - "2021-10-16T00:00:00.000Z", - "2021-10-17T00:00:00.000Z", - "2021-10-18T00:00:00.000Z", - "2021-10-19T00:00:00.000Z", - "2021-10-20T00:00:00.000Z", - "2021-10-21T00:00:00.000Z", - "2021-10-22T00:00:00.000Z", - "2021-10-23T00:00:00.000Z", - "2021-10-24T00:00:00.000Z", - "2021-10-25T00:00:00.000Z", - "2021-10-26T00:00:00.000Z", - "2021-10-27T00:00:00.000Z", - "2021-10-28T00:00:00.000Z", - "2021-10-29T00:00:00.000Z", - "2021-10-30T00:00:00.000Z", - "2021-10-31T00:00:00.000Z", - "2021-11-01T00:00:00.000Z", - "2021-11-02T00:00:00.000Z", - "2021-11-03T00:00:00.000Z", - "2021-11-04T00:00:00.000Z", - "2021-11-05T00:00:00.000Z", - "2021-11-06T00:00:00.000Z", - "2021-11-07T00:00:00.000Z", - "2021-11-08T00:00:00.000Z", - "2021-11-09T00:00:00.000Z", - "2021-11-10T00:00:00.000Z", - "2021-11-11T00:00:00.000Z", - "2021-11-12T00:00:00.000Z", - "2021-11-13T00:00:00.000Z", - "2021-11-14T00:00:00.000Z", - "2021-11-15T00:00:00.000Z", - "2021-11-16T00:00:00.000Z", - "2021-11-17T00:00:00.000Z", - "2021-11-18T00:00:00.000Z", - "2021-11-19T00:00:00.000Z", - "2021-11-20T00:00:00.000Z", - "2021-11-21T00:00:00.000Z", - "2021-11-22T00:00:00.000Z", - "2021-11-23T00:00:00.000Z", - "2021-11-24T00:00:00.000Z", - "2021-11-25T00:00:00.000Z", - "2021-11-26T00:00:00.000Z", - "2021-11-27T00:00:00.000Z", - "2021-11-28T00:00:00.000Z", - "2021-11-29T00:00:00.000Z", - "2021-11-30T00:00:00.000Z", - "2021-12-01T00:00:00.000Z", - "2021-12-02T00:00:00.000Z", - "2021-12-03T00:00:00.000Z", - "2021-12-04T00:00:00.000Z", - "2021-12-05T00:00:00.000Z", - "2021-12-06T00:00:00.000Z", - "2021-12-07T00:00:00.000Z", - "2021-12-08T00:00:00.000Z", - "2021-12-09T00:00:00.000Z", - "2021-12-10T00:00:00.000Z", - "2021-12-11T00:00:00.000Z", - "2021-12-12T00:00:00.000Z", - "2021-12-13T00:00:00.000Z", - "2021-12-14T00:00:00.000Z", - "2021-12-15T00:00:00.000Z", - "2021-12-16T00:00:00.000Z", - "2021-12-17T00:00:00.000Z", - "2021-12-18T00:00:00.000Z", - "2021-12-19T00:00:00.000Z", - "2021-12-20T00:00:00.000Z", - "2021-12-21T00:00:00.000Z", - "2021-12-22T00:00:00.000Z", - "2021-12-23T00:00:00.000Z", - "2021-12-29T00:00:00.000Z", - "2021-12-30T00:00:00.000Z", - "2022-01-03T00:00:00.000Z", - "2022-01-04T00:00:00.000Z", - "2022-01-05T00:00:00.000Z", - "2022-01-06T00:00:00.000Z", - "2022-01-08T00:00:00.000Z", - "2022-01-10T00:00:00.000Z", - "2022-01-11T00:00:00.000Z", - "2022-01-12T00:00:00.000Z" - ], - "y": [ - 1, 284, 443, 616, 812, 940, 1093, 1245, 1396, 1530, 1638, 1937, 2214, 2616, 2786, 3071, - 3224, 3411, 3624, 3805, 4030, 4282, 4566, 4722, 5149, 5461, 5776, 5918, 6185, 6591, 6841, - 7137, 7310, 7599, 7850, 8019, 8280, 8522, 8846, 9018, 9280, 9526, 9730, 9889, 10183, 10342, - 10635, 10686, 10984, 11255, 11558, 11717, 11867, 12026, 12268, 12451, 12754, 13027, 13300, - 13516, 13579, 13748, 14014, 14311, 14468, 14883, 15054, 15220, 15477, 15566, 15567, 15568, - 15569, 15570, 15571, 15576, 15585, 15587, 15588, 15589, 15591 - ] - }, - "settings": { - "last_computed_at": "2022-01-14T10:45:44.552Z", - "start": "2021-10-14T00:00:00.000Z", - "end": "2022-01-14T00:00:00.000Z" - } - }, - { - "type": "inactive-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-12T11:45:28.654Z", - "start": "2022-01-05T00:00:00.000Z", - "end": "2022-01-12T00:00:00.000Z" - } - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-20T15:06:26.809Z", - "start": "2022-01-13T00:00:00.000Z", - "end": "2022-01-20T00:00:00.000Z" - } - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "number-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-12T11:45:28.808Z", - "start": "2022-01-05T00:00:00.000Z", - "end": "2022-01-12T00:00:00.000Z" - } - }, - { - "type": "number-activities", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-12T11:45:28.840Z", - "start": "2022-01-05T00:00:00.000Z", - "end": "2022-01-12T00:00:00.000Z" - } - }, - { - "type": "inactive-members-graph" - }, - { - "type": "integrations" - }, - { - "type": "number-members-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": { - "last_computed_at": "2022-01-12T11:45:28.806Z", - "start": "2021-10-12T00:00:00.000Z", - "end": "2022-01-12T00:00:00.000Z" - } - }, - { - "type": "latest-activities" - }, - { - "type": "builder" - }, - { - "type": "number-activities-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": { - "last_computed_at": "2022-01-12T11:45:28.818Z", - "start": "2021-10-12T00:00:00.000Z", - "end": "2022-01-12T00:00:00.000Z" - } - }, - { - "type": "newest-members" - }, - { - "type": "benchmark" - }, - { - "type": "channel-distribution" - }, - { - "type": "benchmark", - "settings": { - "repositories": [ - { - "id": 341208515, - "value": "edgelessdb", - "label": "edgelessdb", - "active": true, - "editing": false, - "githubRepo": { - "id": 341208515, - "node_id": "MDEwOlJlcG9zaXRvcnkzNDEyMDg1MTU=", - "name": "edgelessdb", - "full_name": "edgelesssys/edgelessdb", - "private": false, - "owner": { - "login": "edgelesssys", - "id": 58512657, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjU4NTEyNjU3", - "avatar_url": "https://avatars.githubusercontent.com/u/58512657?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/edgelesssys", - "html_url": "https://github.com/edgelesssys", - "followers_url": "https://api.github.com/users/edgelesssys/followers", - "following_url": "https://api.github.com/users/edgelesssys/following{/other_user}", - "gists_url": "https://api.github.com/users/edgelesssys/gists{/gist_id}", - "starred_url": "https://api.github.com/users/edgelesssys/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/edgelesssys/subscriptions", - "organizations_url": "https://api.github.com/users/edgelesssys/orgs", - "repos_url": "https://api.github.com/users/edgelesssys/repos", - "events_url": "https://api.github.com/users/edgelesssys/events{/privacy}", - "received_events_url": "https://api.github.com/users/edgelesssys/received_events", - "type": "Organization", - "site_admin": false - }, - "html_url": "https://github.com/edgelesssys/edgelessdb", - "description": "EdgelessDB is a MySQL-compatible database for confidential computing. It runs entirely inside a secure enclave and comes with advanced features for collaboration, recovery, and access control.", - "fork": false, - "url": "https://api.github.com/repos/edgelesssys/edgelessdb", - "forks_url": "https://api.github.com/repos/edgelesssys/edgelessdb/forks", - "keys_url": "https://api.github.com/repos/edgelesssys/edgelessdb/keys{/key_id}", - "collaborators_url": "https://api.github.com/repos/edgelesssys/edgelessdb/collaborators{/collaborator}", - "teams_url": "https://api.github.com/repos/edgelesssys/edgelessdb/teams", - "hooks_url": "https://api.github.com/repos/edgelesssys/edgelessdb/hooks", - "issue_events_url": "https://api.github.com/repos/edgelesssys/edgelessdb/issues/events{/number}", - "events_url": "https://api.github.com/repos/edgelesssys/edgelessdb/events", - "assignees_url": "https://api.github.com/repos/edgelesssys/edgelessdb/assignees{/user}", - "branches_url": "https://api.github.com/repos/edgelesssys/edgelessdb/branches{/branch}", - "tags_url": "https://api.github.com/repos/edgelesssys/edgelessdb/tags", - "blobs_url": "https://api.github.com/repos/edgelesssys/edgelessdb/git/blobs{/sha}", - "git_tags_url": "https://api.github.com/repos/edgelesssys/edgelessdb/git/tags{/sha}", - "git_refs_url": "https://api.github.com/repos/edgelesssys/edgelessdb/git/refs{/sha}", - "trees_url": "https://api.github.com/repos/edgelesssys/edgelessdb/git/trees{/sha}", - "statuses_url": "https://api.github.com/repos/edgelesssys/edgelessdb/statuses/{sha}", - "languages_url": "https://api.github.com/repos/edgelesssys/edgelessdb/languages", - "stargazers_url": "https://api.github.com/repos/edgelesssys/edgelessdb/stargazers", - "contributors_url": "https://api.github.com/repos/edgelesssys/edgelessdb/contributors", - "subscribers_url": "https://api.github.com/repos/edgelesssys/edgelessdb/subscribers", - "subscription_url": "https://api.github.com/repos/edgelesssys/edgelessdb/subscription", - "commits_url": "https://api.github.com/repos/edgelesssys/edgelessdb/commits{/sha}", - "git_commits_url": "https://api.github.com/repos/edgelesssys/edgelessdb/git/commits{/sha}", - "comments_url": "https://api.github.com/repos/edgelesssys/edgelessdb/comments{/number}", - "issue_comment_url": "https://api.github.com/repos/edgelesssys/edgelessdb/issues/comments{/number}", - "contents_url": "https://api.github.com/repos/edgelesssys/edgelessdb/contents/{+path}", - "compare_url": "https://api.github.com/repos/edgelesssys/edgelessdb/compare/{base}...{head}", - "merges_url": "https://api.github.com/repos/edgelesssys/edgelessdb/merges", - "archive_url": "https://api.github.com/repos/edgelesssys/edgelessdb/{archive_format}{/ref}", - "downloads_url": "https://api.github.com/repos/edgelesssys/edgelessdb/downloads", - "issues_url": "https://api.github.com/repos/edgelesssys/edgelessdb/issues{/number}", - "pulls_url": "https://api.github.com/repos/edgelesssys/edgelessdb/pulls{/number}", - "milestones_url": "https://api.github.com/repos/edgelesssys/edgelessdb/milestones{/number}", - "notifications_url": "https://api.github.com/repos/edgelesssys/edgelessdb/notifications{?since,all,participating}", - "labels_url": "https://api.github.com/repos/edgelesssys/edgelessdb/labels{/name}", - "releases_url": "https://api.github.com/repos/edgelesssys/edgelessdb/releases{/id}", - "deployments_url": "https://api.github.com/repos/edgelesssys/edgelessdb/deployments", - "created_at": "2021-02-22T13:24:30Z", - "updated_at": "2022-01-06T11:27:56Z", - "pushed_at": "2021-12-17T12:07:53Z", - "git_url": "git://github.com/edgelesssys/edgelessdb.git", - "ssh_url": "git@github.com:edgelesssys/edgelessdb.git", - "clone_url": "https://github.com/edgelesssys/edgelessdb.git", - "svn_url": "https://github.com/edgelesssys/edgelessdb", - "homepage": "https://edgeless.systems/products/edgelessdb", - "size": 438, - "stargazers_count": 88, - "watchers_count": 88, - "language": "Go", - "has_issues": true, - "has_projects": false, - "has_downloads": true, - "has_wiki": false, - "has_pages": false, - "forks_count": 7, - "mirror_url": null, - "archived": false, - "disabled": false, - "open_issues_count": 4, - "license": { - "key": "gpl-2.0", - "name": "GNU General Public License v2.0", - "spdx_id": "GPL-2.0", - "url": "https://api.github.com/licenses/gpl-2.0", - "node_id": "MDc6TGljZW5zZTg=" - }, - "allow_forking": true, - "is_template": false, - "topics": [ - "confidential-computing", - "database", - "enclave", - "mariadb", - "mysql", - "sgx", - "sql" - ], - "visibility": "public", - "forks": 7, - "open_issues": 4, - "watchers": 88, - "default_branch": "main", - "permissions": { - "admin": false, - "maintain": false, - "push": false, - "triage": false, - "pull": true - }, - "score": 1 - }, - "data": { - "2022-02-17": { - "date": "2022-02-17", - "value": 94 - }, - "2022-02-16": { - "date": "2022-02-16", - "value": 94 - }, - "2022-02-15": { - "date": "2022-02-15", - "value": 93 - }, - "2022-02-14": { - "date": "2022-02-14", - "value": 93 - }, - "2022-02-13": { - "date": "2022-02-13", - "value": 93 - }, - "2022-02-12": { - "date": "2022-02-12", - "value": 93 - }, - "2022-02-11": { - "date": "2022-02-11", - "value": 93 - }, - "2022-02-10": { - "date": "2022-02-10", - "value": 93 - }, - "2022-02-09": { - "date": "2022-02-09", - "value": 93 - }, - "2022-02-08": { - "date": "2022-02-08", - "value": 93 - }, - "2022-02-07": { - "date": "2022-02-07", - "value": 92 - }, - "2022-02-06": { - "date": "2022-02-06", - "value": 92 - }, - "2022-02-05": { - "date": "2022-02-05", - "value": 92 - }, - "2022-02-04": { - "date": "2022-02-04", - "value": 92 - }, - "2022-02-03": { - "date": "2022-02-03", - "value": 91 - }, - "2022-02-02": { - "date": "2022-02-02", - "value": 91 - }, - "2022-02-01": { - "date": "2022-02-01", - "value": 91 - }, - "2022-01-31": { - "date": "2022-01-31", - "value": 91 - }, - "2022-01-30": { - "date": "2022-01-30", - "value": 91 - }, - "2022-01-29": { - "date": "2022-01-29", - "value": 91 - }, - "2022-01-28": { - "date": "2022-01-28", - "value": 91 - }, - "2022-01-27": { - "date": "2022-01-27", - "value": 91 - }, - "2022-01-26": { - "date": "2022-01-26", - "value": 91 - }, - "2022-01-25": { - "date": "2022-01-25", - "value": 90 - }, - "2022-01-24": { - "date": "2022-01-24", - "value": 90 - }, - "2022-01-23": { - "date": "2022-01-23", - "value": 90 - }, - "2022-01-22": { - "date": "2022-01-22", - "value": 89 - }, - "2022-01-21": { - "date": "2022-01-21", - "value": 89 - }, - "2022-01-20": { - "date": "2022-01-20", - "value": 88 - }, - "2022-01-19": { - "date": "2022-01-19", - "value": 88 - }, - "2022-01-18": { - "date": "2022-01-18", - "value": 88 - }, - "2022-01-17": { - "date": "2022-01-17", - "value": 88 - }, - "2022-01-16": { - "date": "2022-01-16", - "value": 88 - }, - "2022-01-15": { - "date": "2022-01-15", - "value": 88 - }, - "2022-01-14": { - "date": "2022-01-14", - "value": 88 - }, - "2022-01-13": { - "date": "2022-01-13", - "value": 88 - }, - "2022-01-12": { - "date": "2022-01-12", - "value": 88 - }, - "2022-01-11": { - "date": "2022-01-11", - "value": 88 - }, - "2022-01-10": { - "date": "2022-01-10", - "value": 88 - }, - "2022-01-09": { - "date": "2022-01-09", - "value": 88 - }, - "2022-01-08": { - "date": "2022-01-08", - "value": 88 - }, - "2022-01-07": { - "date": "2022-01-07", - "value": 88 - }, - "2022-01-06": { - "date": "2022-01-06", - "value": 87 - }, - "2022-01-05": { - "date": "2022-01-05", - "value": 87 - }, - "2022-01-04": { - "date": "2022-01-04", - "value": 86 - }, - "2022-01-03": { - "date": "2022-01-03", - "value": 86 - }, - "2022-01-02": { - "date": "2022-01-02", - "value": 86 - }, - "2022-01-01": { - "date": "2022-01-01", - "value": 86 - }, - "2021-12-31": { - "date": "2021-12-31", - "value": 85 - }, - "2021-12-30": { - "date": "2021-12-30", - "value": 84 - }, - "2021-12-29": { - "date": "2021-12-29", - "value": 84 - }, - "2021-12-28": { - "date": "2021-12-28", - "value": 84 - }, - "2021-12-27": { - "date": "2021-12-27", - "value": 84 - }, - "2021-12-26": { - "date": "2021-12-26", - "value": 84 - }, - "2021-12-25": { - "date": "2021-12-25", - "value": 84 - }, - "2021-12-24": { - "date": "2021-12-24", - "value": 83 - }, - "2021-12-23": { - "date": "2021-12-23", - "value": 83 - }, - "2021-12-22": { - "date": "2021-12-22", - "value": 83 - }, - "2021-12-21": { - "date": "2021-12-21", - "value": 83 - }, - "2021-12-20": { - "date": "2021-12-20", - "value": 83 - }, - "2021-12-19": { - "date": "2021-12-19", - "value": 83 - }, - "2021-12-18": { - "date": "2021-12-18", - "value": 83 - }, - "2021-12-17": { - "date": "2021-12-17", - "value": 83 - }, - "2021-12-16": { - "date": "2021-12-16", - "value": 83 - }, - "2021-12-15": { - "date": "2021-12-15", - "value": 83 - }, - "2021-12-14": { - "date": "2021-12-14", - "value": 80 - }, - "2021-12-13": { - "date": "2021-12-13", - "value": 80 - }, - "2021-12-12": { - "date": "2021-12-12", - "value": 80 - }, - "2021-12-11": { - "date": "2021-12-11", - "value": 80 - }, - "2021-12-10": { - "date": "2021-12-10", - "value": 80 - }, - "2021-12-09": { - "date": "2021-12-09", - "value": 79 - }, - "2021-12-08": { - "date": "2021-12-08", - "value": 79 - }, - "2021-12-07": { - "date": "2021-12-07", - "value": 78 - }, - "2021-12-06": { - "date": "2021-12-06", - "value": 77 - }, - "2021-12-05": { - "date": "2021-12-05", - "value": 77 - }, - "2021-12-04": { - "date": "2021-12-04", - "value": 77 - }, - "2021-12-03": { - "date": "2021-12-03", - "value": 77 - }, - "2021-12-02": { - "date": "2021-12-02", - "value": 77 - }, - "2021-12-01": { - "date": "2021-12-01", - "value": 77 - }, - "2021-11-30": { - "date": "2021-11-30", - "value": 77 - }, - "2021-11-29": { - "date": "2021-11-29", - "value": 77 - }, - "2021-11-28": { - "date": "2021-11-28", - "value": 77 - }, - "2021-11-27": { - "date": "2021-11-27", - "value": 77 - }, - "2021-11-26": { - "date": "2021-11-26", - "value": 76 - }, - "2021-11-25": { - "date": "2021-11-25", - "value": 76 - }, - "2021-11-24": { - "date": "2021-11-24", - "value": 76 - }, - "2021-11-23": { - "date": "2021-11-23", - "value": 75 - }, - "2021-11-22": { - "date": "2021-11-22", - "value": 75 - }, - "2021-11-21": { - "date": "2021-11-21", - "value": 75 - }, - "2021-11-20": { - "date": "2021-11-20", - "value": 75 - }, - "2021-11-19": { - "date": "2021-11-19", - "value": 74 - }, - "2021-11-18": { - "date": "2021-11-18", - "value": 74 - }, - "2021-11-17": { - "date": "2021-11-17", - "value": 74 - } - } - }, - { - "id": 389910135, - "value": "awesome-community-building", - "label": "awesome-community-building", - "active": true, - "editing": false, - "githubRepo": { - "id": 389910135, - "node_id": "MDEwOlJlcG9zaXRvcnkzODk5MTAxMzU=", - "name": "awesome-community-building", - "full_name": "CrowdDevHQ/awesome-community-building", - "private": false, - "owner": { - "login": "CrowdDevHQ", - "id": 85551972, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy", - "avatar_url": "https://avatars.githubusercontent.com/u/85551972?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/CrowdDevHQ", - "html_url": "https://github.com/CrowdDevHQ", - "followers_url": "https://api.github.com/users/CrowdDevHQ/followers", - "following_url": "https://api.github.com/users/CrowdDevHQ/following{/other_user}", - "gists_url": "https://api.github.com/users/CrowdDevHQ/gists{/gist_id}", - "starred_url": "https://api.github.com/users/CrowdDevHQ/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/CrowdDevHQ/subscriptions", - "organizations_url": "https://api.github.com/users/CrowdDevHQ/orgs", - "repos_url": "https://api.github.com/users/CrowdDevHQ/repos", - "events_url": "https://api.github.com/users/CrowdDevHQ/events{/privacy}", - "received_events_url": "https://api.github.com/users/CrowdDevHQ/received_events", - "type": "Organization", - "site_admin": false - }, - "html_url": "https://github.com/CrowdDevHQ/awesome-community-building", - "description": "A curated list of awesome resources on building developer communities. 🥑", - "fork": false, - "url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building", - "forks_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/forks", - "keys_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/keys{/key_id}", - "collaborators_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/collaborators{/collaborator}", - "teams_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/teams", - "hooks_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/hooks", - "issue_events_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/issues/events{/number}", - "events_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/events", - "assignees_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/assignees{/user}", - "branches_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/branches{/branch}", - "tags_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/tags", - "blobs_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/git/blobs{/sha}", - "git_tags_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/git/tags{/sha}", - "git_refs_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/git/refs{/sha}", - "trees_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/git/trees{/sha}", - "statuses_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/statuses/{sha}", - "languages_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/languages", - "stargazers_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/stargazers", - "contributors_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/contributors", - "subscribers_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/subscribers", - "subscription_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/subscription", - "commits_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/commits{/sha}", - "git_commits_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/git/commits{/sha}", - "comments_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/comments{/number}", - "issue_comment_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/issues/comments{/number}", - "contents_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/contents/{+path}", - "compare_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/compare/{base}...{head}", - "merges_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/merges", - "archive_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/{archive_format}{/ref}", - "downloads_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/downloads", - "issues_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/issues{/number}", - "pulls_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/pulls{/number}", - "milestones_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/milestones{/number}", - "notifications_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/notifications{?since,all,participating}", - "labels_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/labels{/name}", - "releases_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/releases{/id}", - "deployments_url": "https://api.github.com/repos/CrowdDevHQ/awesome-community-building/deployments", - "created_at": "2021-07-27T08:43:58Z", - "updated_at": "2022-01-03T18:09:50Z", - "pushed_at": "2021-11-22T10:48:14Z", - "git_url": "git://github.com/CrowdDevHQ/awesome-community-building.git", - "ssh_url": "git@github.com:CrowdDevHQ/awesome-community-building.git", - "clone_url": "https://github.com/CrowdDevHQ/awesome-community-building.git", - "svn_url": "https://github.com/CrowdDevHQ/awesome-community-building", - "homepage": null, - "size": 4, - "stargazers_count": 6, - "watchers_count": 6, - "language": null, - "has_issues": true, - "has_projects": true, - "has_downloads": true, - "has_wiki": true, - "has_pages": false, - "forks_count": 1, - "mirror_url": null, - "archived": false, - "disabled": false, - "open_issues_count": 0, - "license": null, - "allow_forking": true, - "is_template": false, - "topics": [], - "visibility": "public", - "forks": 1, - "open_issues": 0, - "watchers": 6, - "default_branch": "main", - "permissions": { - "admin": true, - "maintain": true, - "push": true, - "triage": true, - "pull": true - }, - "score": 1 - }, - "data": { - "2022-02-17": { - "date": "2022-02-17", - "value": 6 - }, - "2022-02-16": { - "date": "2022-02-16", - "value": 6 - }, - "2022-02-15": { - "date": "2022-02-15", - "value": 6 - }, - "2022-02-14": { - "date": "2022-02-14", - "value": 5 - }, - "2022-02-13": { - "date": "2022-02-13", - "value": 5 - }, - "2022-02-12": { - "date": "2022-02-12", - "value": 5 - }, - "2022-02-11": { - "date": "2022-02-11", - "value": 5 - }, - "2022-02-10": { - "date": "2022-02-10", - "value": 5 - }, - "2022-02-09": { - "date": "2022-02-09", - "value": 5 - }, - "2022-02-08": { - "date": "2022-02-08", - "value": 5 - }, - "2022-02-07": { - "date": "2022-02-07", - "value": 5 - }, - "2022-02-06": { - "date": "2022-02-06", - "value": 5 - }, - "2022-02-05": { - "date": "2022-02-05", - "value": 5 - }, - "2022-02-04": { - "date": "2022-02-04", - "value": 5 - }, - "2022-02-03": { - "date": "2022-02-03", - "value": 5 - }, - "2022-02-02": { - "date": "2022-02-02", - "value": 5 - }, - "2022-02-01": { - "date": "2022-02-01", - "value": 5 - }, - "2022-01-31": { - "date": "2022-01-31", - "value": 5 - }, - "2022-01-30": { - "date": "2022-01-30", - "value": 5 - }, - "2022-01-29": { - "date": "2022-01-29", - "value": 5 - }, - "2022-01-28": { - "date": "2022-01-28", - "value": 5 - }, - "2022-01-27": { - "date": "2022-01-27", - "value": 5 - }, - "2022-01-26": { - "date": "2022-01-26", - "value": 5 - }, - "2022-01-25": { - "date": "2022-01-25", - "value": 5 - }, - "2022-01-24": { - "date": "2022-01-24", - "value": 5 - }, - "2022-01-23": { - "date": "2022-01-23", - "value": 5 - }, - "2022-01-22": { - "date": "2022-01-22", - "value": 5 - }, - "2022-01-21": { - "date": "2022-01-21", - "value": 5 - }, - "2022-01-20": { - "date": "2022-01-20", - "value": 5 - }, - "2022-01-19": { - "date": "2022-01-19", - "value": 5 - }, - "2022-01-18": { - "date": "2022-01-18", - "value": 5 - }, - "2022-01-17": { - "date": "2022-01-17", - "value": 5 - }, - "2022-01-16": { - "date": "2022-01-16", - "value": 5 - }, - "2022-01-15": { - "date": "2022-01-15", - "value": 5 - }, - "2022-01-14": { - "date": "2022-01-14", - "value": 5 - }, - "2022-01-13": { - "date": "2022-01-13", - "value": 5 - }, - "2022-01-12": { - "date": "2022-01-12", - "value": 5 - }, - "2022-01-11": { - "date": "2022-01-11", - "value": 5 - }, - "2022-01-10": { - "date": "2022-01-10", - "value": 5 - }, - "2022-01-09": { - "date": "2022-01-09", - "value": 5 - }, - "2022-01-08": { - "date": "2022-01-08", - "value": 5 - }, - "2022-01-07": { - "date": "2022-01-07", - "value": 5 - }, - "2022-01-06": { - "date": "2022-01-06", - "value": 5 - }, - "2022-01-05": { - "date": "2022-01-05", - "value": 5 - }, - "2022-01-04": { - "date": "2022-01-04", - "value": 5 - }, - "2022-01-03": { - "date": "2022-01-03", - "value": 3 - }, - "2022-01-02": { - "date": "2022-01-02", - "value": 3 - }, - "2022-01-01": { - "date": "2022-01-01", - "value": 3 - }, - "2021-12-31": { - "date": "2021-12-31", - "value": 3 - }, - "2021-12-30": { - "date": "2021-12-30", - "value": 3 - }, - "2021-12-29": { - "date": "2021-12-29", - "value": 3 - }, - "2021-12-28": { - "date": "2021-12-28", - "value": 3 - }, - "2021-12-27": { - "date": "2021-12-27", - "value": 3 - }, - "2021-12-26": { - "date": "2021-12-26", - "value": 3 - }, - "2021-12-25": { - "date": "2021-12-25", - "value": 3 - }, - "2021-12-24": { - "date": "2021-12-24", - "value": 3 - }, - "2021-12-23": { - "date": "2021-12-23", - "value": 3 - }, - "2021-12-22": { - "date": "2021-12-22", - "value": 3 - }, - "2021-12-21": { - "date": "2021-12-21", - "value": 3 - }, - "2021-12-20": { - "date": "2021-12-20", - "value": 3 - }, - "2021-12-19": { - "date": "2021-12-19", - "value": 3 - }, - "2021-12-18": { - "date": "2021-12-18", - "value": 3 - }, - "2021-12-17": { - "date": "2021-12-17", - "value": 3 - }, - "2021-12-16": { - "date": "2021-12-16", - "value": 3 - }, - "2021-12-15": { - "date": "2021-12-15", - "value": 3 - }, - "2021-12-14": { - "date": "2021-12-14", - "value": 3 - }, - "2021-12-13": { - "date": "2021-12-13", - "value": 3 - }, - "2021-12-12": { - "date": "2021-12-12", - "value": 3 - }, - "2021-12-11": { - "date": "2021-12-11", - "value": 3 - }, - "2021-12-10": { - "date": "2021-12-10", - "value": 3 - }, - "2021-12-09": { - "date": "2021-12-09", - "value": 3 - }, - "2021-12-08": { - "date": "2021-12-08", - "value": 3 - }, - "2021-12-07": { - "date": "2021-12-07", - "value": 3 - }, - "2021-12-06": { - "date": "2021-12-06", - "value": 3 - }, - "2021-12-05": { - "date": "2021-12-05", - "value": 3 - }, - "2021-12-04": { - "date": "2021-12-04", - "value": 3 - }, - "2021-12-03": { - "date": "2021-12-03", - "value": 3 - }, - "2021-12-02": { - "date": "2021-12-02", - "value": 3 - }, - "2021-12-01": { - "date": "2021-12-01", - "value": 3 - }, - "2021-11-30": { - "date": "2021-11-30", - "value": 2 - }, - "2021-11-29": { - "date": "2021-11-29", - "value": 2 - }, - "2021-11-28": { - "date": "2021-11-28", - "value": 2 - }, - "2021-11-27": { - "date": "2021-11-27", - "value": 2 - }, - "2021-11-26": { - "date": "2021-11-26", - "value": 2 - }, - "2021-11-25": { - "date": "2021-11-25", - "value": 2 - }, - "2021-11-24": { - "date": "2021-11-24", - "value": 2 - }, - "2021-11-23": { - "date": "2021-11-23", - "value": 2 - }, - "2021-11-22": { - "date": "2021-11-22", - "value": 2 - }, - "2021-11-21": { - "date": "2021-11-21", - "value": 2 - }, - "2021-11-20": { - "date": "2021-11-20", - "value": 2 - }, - "2021-11-19": { - "date": "2021-11-19", - "value": 2 - }, - "2021-11-18": { - "date": "2021-11-18", - "value": 2 - }, - "2021-11-17": { - "date": "2021-11-17", - "value": 2 - } - }, - "color": "#331717" - }, - { - "id": 267045750, - "value": "edgelessrt", - "label": "edgelessrt", - "active": true, - "editing": false, - "githubRepo": { - "id": 267045750, - "node_id": "MDEwOlJlcG9zaXRvcnkyNjcwNDU3NTA=", - "name": "edgelessrt", - "full_name": "edgelesssys/edgelessrt", - "private": false, - "owner": { - "login": "edgelesssys", - "id": 58512657, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjU4NTEyNjU3", - "avatar_url": "https://avatars.githubusercontent.com/u/58512657?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/edgelesssys", - "html_url": "https://github.com/edgelesssys", - "followers_url": "https://api.github.com/users/edgelesssys/followers", - "following_url": "https://api.github.com/users/edgelesssys/following{/other_user}", - "gists_url": "https://api.github.com/users/edgelesssys/gists{/gist_id}", - "starred_url": "https://api.github.com/users/edgelesssys/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/edgelesssys/subscriptions", - "organizations_url": "https://api.github.com/users/edgelesssys/orgs", - "repos_url": "https://api.github.com/users/edgelesssys/repos", - "events_url": "https://api.github.com/users/edgelesssys/events{/privacy}", - "received_events_url": "https://api.github.com/users/edgelesssys/received_events", - "type": "Organization", - "site_admin": false - }, - "html_url": "https://github.com/edgelesssys/edgelessrt", - "description": "Edgeless RT is an SDK and a runtime for Intel SGX. It combines top-notch Go support with simplicity, robustness and a small TCB. Developing confidential microservices has never been easier! C++17 and Rust (experimental) are also supported.", - "fork": false, - "url": "https://api.github.com/repos/edgelesssys/edgelessrt", - "forks_url": "https://api.github.com/repos/edgelesssys/edgelessrt/forks", - "keys_url": "https://api.github.com/repos/edgelesssys/edgelessrt/keys{/key_id}", - "collaborators_url": "https://api.github.com/repos/edgelesssys/edgelessrt/collaborators{/collaborator}", - "teams_url": "https://api.github.com/repos/edgelesssys/edgelessrt/teams", - "hooks_url": "https://api.github.com/repos/edgelesssys/edgelessrt/hooks", - "issue_events_url": "https://api.github.com/repos/edgelesssys/edgelessrt/issues/events{/number}", - "events_url": "https://api.github.com/repos/edgelesssys/edgelessrt/events", - "assignees_url": "https://api.github.com/repos/edgelesssys/edgelessrt/assignees{/user}", - "branches_url": "https://api.github.com/repos/edgelesssys/edgelessrt/branches{/branch}", - "tags_url": "https://api.github.com/repos/edgelesssys/edgelessrt/tags", - "blobs_url": "https://api.github.com/repos/edgelesssys/edgelessrt/git/blobs{/sha}", - "git_tags_url": "https://api.github.com/repos/edgelesssys/edgelessrt/git/tags{/sha}", - "git_refs_url": "https://api.github.com/repos/edgelesssys/edgelessrt/git/refs{/sha}", - "trees_url": "https://api.github.com/repos/edgelesssys/edgelessrt/git/trees{/sha}", - "statuses_url": "https://api.github.com/repos/edgelesssys/edgelessrt/statuses/{sha}", - "languages_url": "https://api.github.com/repos/edgelesssys/edgelessrt/languages", - "stargazers_url": "https://api.github.com/repos/edgelesssys/edgelessrt/stargazers", - "contributors_url": "https://api.github.com/repos/edgelesssys/edgelessrt/contributors", - "subscribers_url": "https://api.github.com/repos/edgelesssys/edgelessrt/subscribers", - "subscription_url": "https://api.github.com/repos/edgelesssys/edgelessrt/subscription", - "commits_url": "https://api.github.com/repos/edgelesssys/edgelessrt/commits{/sha}", - "git_commits_url": "https://api.github.com/repos/edgelesssys/edgelessrt/git/commits{/sha}", - "comments_url": "https://api.github.com/repos/edgelesssys/edgelessrt/comments{/number}", - "issue_comment_url": "https://api.github.com/repos/edgelesssys/edgelessrt/issues/comments{/number}", - "contents_url": "https://api.github.com/repos/edgelesssys/edgelessrt/contents/{+path}", - "compare_url": "https://api.github.com/repos/edgelesssys/edgelessrt/compare/{base}...{head}", - "merges_url": "https://api.github.com/repos/edgelesssys/edgelessrt/merges", - "archive_url": "https://api.github.com/repos/edgelesssys/edgelessrt/{archive_format}{/ref}", - "downloads_url": "https://api.github.com/repos/edgelesssys/edgelessrt/downloads", - "issues_url": "https://api.github.com/repos/edgelesssys/edgelessrt/issues{/number}", - "pulls_url": "https://api.github.com/repos/edgelesssys/edgelessrt/pulls{/number}", - "milestones_url": "https://api.github.com/repos/edgelesssys/edgelessrt/milestones{/number}", - "notifications_url": "https://api.github.com/repos/edgelesssys/edgelessrt/notifications{?since,all,participating}", - "labels_url": "https://api.github.com/repos/edgelesssys/edgelessrt/labels{/name}", - "releases_url": "https://api.github.com/repos/edgelesssys/edgelessrt/releases{/id}", - "deployments_url": "https://api.github.com/repos/edgelesssys/edgelessrt/deployments", - "created_at": "2020-05-26T13:09:17Z", - "updated_at": "2022-01-06T06:11:55Z", - "pushed_at": "2022-01-14T10:45:52Z", - "git_url": "git://github.com/edgelesssys/edgelessrt.git", - "ssh_url": "git@github.com:edgelesssys/edgelessrt.git", - "clone_url": "https://github.com/edgelesssys/edgelessrt.git", - "svn_url": "https://github.com/edgelesssys/edgelessrt", - "homepage": "https://edgeless.systems", - "size": 98955, - "stargazers_count": 101, - "watchers_count": 101, - "language": "C++", - "has_issues": true, - "has_projects": false, - "has_downloads": true, - "has_wiki": false, - "has_pages": false, - "forks_count": 14, - "mirror_url": null, - "archived": false, - "disabled": false, - "open_issues_count": 1, - "license": { - "key": "mit", - "name": "MIT License", - "spdx_id": "MIT", - "url": "https://api.github.com/licenses/mit", - "node_id": "MDc6TGljZW5zZTEz" - }, - "allow_forking": true, - "is_template": false, - "topics": [ - "confidential-computing", - "confidential-microservices", - "enclave", - "golang", - "intel-sgx", - "rust", - "sgx", - "trusted-execution-environment" - ], - "visibility": "public", - "forks": 14, - "open_issues": 1, - "watchers": 101, - "default_branch": "master", - "permissions": { - "admin": false, - "maintain": false, - "push": false, - "triage": false, - "pull": true - }, - "score": 1 - }, - "data": { - "2022-02-17": { - "date": "2022-02-17", - "value": 101 - }, - "2022-02-16": { - "date": "2022-02-16", - "value": 101 - }, - "2022-02-15": { - "date": "2022-02-15", - "value": 101 - }, - "2022-02-14": { - "date": "2022-02-14", - "value": 101 - }, - "2022-02-13": { - "date": "2022-02-13", - "value": 101 - }, - "2022-02-12": { - "date": "2022-02-12", - "value": 101 - }, - "2022-02-11": { - "date": "2022-02-11", - "value": 101 - }, - "2022-02-10": { - "date": "2022-02-10", - "value": 101 - }, - "2022-02-09": { - "date": "2022-02-09", - "value": 101 - }, - "2022-02-08": { - "date": "2022-02-08", - "value": 101 - }, - "2022-02-07": { - "date": "2022-02-07", - "value": 101 - }, - "2022-02-06": { - "date": "2022-02-06", - "value": 101 - }, - "2022-02-05": { - "date": "2022-02-05", - "value": 101 - }, - "2022-02-04": { - "date": "2022-02-04", - "value": 101 - }, - "2022-02-03": { - "date": "2022-02-03", - "value": 101 - }, - "2022-02-02": { - "date": "2022-02-02", - "value": 101 - }, - "2022-02-01": { - "date": "2022-02-01", - "value": 101 - }, - "2022-01-31": { - "date": "2022-01-31", - "value": 101 - }, - "2022-01-30": { - "date": "2022-01-30", - "value": 101 - }, - "2022-01-29": { - "date": "2022-01-29", - "value": 101 - }, - "2022-01-28": { - "date": "2022-01-28", - "value": 101 - }, - "2022-01-27": { - "date": "2022-01-27", - "value": 101 - }, - "2022-01-26": { - "date": "2022-01-26", - "value": 101 - }, - "2022-01-25": { - "date": "2022-01-25", - "value": 101 - }, - "2022-01-24": { - "date": "2022-01-24", - "value": 101 - }, - "2022-01-23": { - "date": "2022-01-23", - "value": 101 - }, - "2022-01-22": { - "date": "2022-01-22", - "value": 101 - }, - "2022-01-21": { - "date": "2022-01-21", - "value": 101 - }, - "2022-01-20": { - "date": "2022-01-20", - "value": 101 - }, - "2022-01-19": { - "date": "2022-01-19", - "value": 101 - }, - "2022-01-18": { - "date": "2022-01-18", - "value": 101 - }, - "2022-01-17": { - "date": "2022-01-17", - "value": 101 - }, - "2022-01-16": { - "date": "2022-01-16", - "value": 101 - }, - "2022-01-15": { - "date": "2022-01-15", - "value": 101 - }, - "2022-01-14": { - "date": "2022-01-14", - "value": 101 - }, - "2022-01-13": { - "date": "2022-01-13", - "value": 101 - }, - "2022-01-12": { - "date": "2022-01-12", - "value": 101 - }, - "2022-01-11": { - "date": "2022-01-11", - "value": 101 - }, - "2022-01-10": { - "date": "2022-01-10", - "value": 101 - }, - "2022-01-09": { - "date": "2022-01-09", - "value": 101 - }, - "2022-01-08": { - "date": "2022-01-08", - "value": 101 - }, - "2022-01-07": { - "date": "2022-01-07", - "value": 101 - }, - "2022-01-06": { - "date": "2022-01-06", - "value": 100 - }, - "2022-01-05": { - "date": "2022-01-05", - "value": 100 - }, - "2022-01-04": { - "date": "2022-01-04", - "value": 100 - }, - "2022-01-03": { - "date": "2022-01-03", - "value": 99 - }, - "2022-01-02": { - "date": "2022-01-02", - "value": 99 - }, - "2022-01-01": { - "date": "2022-01-01", - "value": 99 - }, - "2021-12-31": { - "date": "2021-12-31", - "value": 99 - }, - "2021-12-30": { - "date": "2021-12-30", - "value": 99 - }, - "2021-12-29": { - "date": "2021-12-29", - "value": 99 - }, - "2021-12-28": { - "date": "2021-12-28", - "value": 99 - }, - "2021-12-27": { - "date": "2021-12-27", - "value": 99 - }, - "2021-12-26": { - "date": "2021-12-26", - "value": 99 - }, - "2021-12-25": { - "date": "2021-12-25", - "value": 99 - }, - "2021-12-24": { - "date": "2021-12-24", - "value": 99 - }, - "2021-12-23": { - "date": "2021-12-23", - "value": 99 - }, - "2021-12-22": { - "date": "2021-12-22", - "value": 99 - }, - "2021-12-21": { - "date": "2021-12-21", - "value": 99 - }, - "2021-12-20": { - "date": "2021-12-20", - "value": 98 - }, - "2021-12-19": { - "date": "2021-12-19", - "value": 98 - }, - "2021-12-18": { - "date": "2021-12-18", - "value": 98 - }, - "2021-12-17": { - "date": "2021-12-17", - "value": 98 - }, - "2021-12-16": { - "date": "2021-12-16", - "value": 98 - }, - "2021-12-15": { - "date": "2021-12-15", - "value": 98 - }, - "2021-12-14": { - "date": "2021-12-14", - "value": 98 - }, - "2021-12-13": { - "date": "2021-12-13", - "value": 98 - }, - "2021-12-12": { - "date": "2021-12-12", - "value": 98 - }, - "2021-12-11": { - "date": "2021-12-11", - "value": 98 - }, - "2021-12-10": { - "date": "2021-12-10", - "value": 98 - }, - "2021-12-09": { - "date": "2021-12-09", - "value": 98 - }, - "2021-12-08": { - "date": "2021-12-08", - "value": 98 - }, - "2021-12-07": { - "date": "2021-12-07", - "value": 98 - }, - "2021-12-06": { - "date": "2021-12-06", - "value": 98 - }, - "2021-12-05": { - "date": "2021-12-05", - "value": 98 - }, - "2021-12-04": { - "date": "2021-12-04", - "value": 98 - }, - "2021-12-03": { - "date": "2021-12-03", - "value": 98 - }, - "2021-12-02": { - "date": "2021-12-02", - "value": 98 - }, - "2021-12-01": { - "date": "2021-12-01", - "value": 98 - }, - "2021-11-30": { - "date": "2021-11-30", - "value": 98 - }, - "2021-11-29": { - "date": "2021-11-29", - "value": 98 - }, - "2021-11-28": { - "date": "2021-11-28", - "value": 98 - }, - "2021-11-27": { - "date": "2021-11-27", - "value": 98 - }, - "2021-11-26": { - "date": "2021-11-26", - "value": 98 - }, - "2021-11-25": { - "date": "2021-11-25", - "value": 97 - }, - "2021-11-24": { - "date": "2021-11-24", - "value": 97 - }, - "2021-11-23": { - "date": "2021-11-23", - "value": 97 - }, - "2021-11-22": { - "date": "2021-11-22", - "value": 97 - }, - "2021-11-21": { - "date": "2021-11-21", - "value": 97 - }, - "2021-11-20": { - "date": "2021-11-20", - "value": 97 - }, - "2021-11-19": { - "date": "2021-11-19", - "value": 97 - }, - "2021-11-18": { - "date": "2021-11-18", - "value": 97 - }, - "2021-11-17": { - "date": "2021-11-17", - "value": 97 - } - }, - "color": "#4D2929" - } - ], - "last_updated_at": "2022-02-17", - "timeframe": { - "label": "Last three months", - "value": "last_three_months", - "date": "2021-11-17" - } - } - }, - { - "type": "number-members-graph", - "cache": { - "x": [ - "2021-10-14T00:00:00.000Z", - "2021-10-15T00:00:00.000Z", - "2021-10-16T00:00:00.000Z", - "2021-10-17T00:00:00.000Z", - "2021-10-18T00:00:00.000Z", - "2021-10-19T00:00:00.000Z", - "2021-10-20T00:00:00.000Z", - "2021-10-21T00:00:00.000Z", - "2021-10-22T00:00:00.000Z", - "2021-10-23T00:00:00.000Z", - "2021-10-25T00:00:00.000Z", - "2021-10-26T00:00:00.000Z", - "2021-10-27T00:00:00.000Z", - "2021-10-28T00:00:00.000Z", - "2021-10-29T00:00:00.000Z", - "2021-11-01T00:00:00.000Z", - "2021-11-02T00:00:00.000Z", - "2021-11-03T00:00:00.000Z", - "2021-11-04T00:00:00.000Z", - "2021-11-05T00:00:00.000Z", - "2021-11-06T00:00:00.000Z", - "2021-11-07T00:00:00.000Z", - "2021-11-08T00:00:00.000Z", - "2021-11-09T00:00:00.000Z", - "2021-11-10T00:00:00.000Z", - "2021-11-11T00:00:00.000Z", - "2021-11-12T00:00:00.000Z", - "2021-11-13T00:00:00.000Z", - "2021-11-14T00:00:00.000Z", - "2021-11-15T00:00:00.000Z", - "2021-11-16T00:00:00.000Z", - "2021-11-17T00:00:00.000Z", - "2021-11-18T00:00:00.000Z", - "2021-11-19T00:00:00.000Z", - "2021-11-20T00:00:00.000Z", - "2021-11-21T00:00:00.000Z", - "2021-11-22T00:00:00.000Z", - "2021-11-23T00:00:00.000Z", - "2021-11-24T00:00:00.000Z", - "2021-11-25T00:00:00.000Z", - "2021-11-26T00:00:00.000Z", - "2021-11-27T00:00:00.000Z", - "2021-11-28T00:00:00.000Z", - "2021-11-29T00:00:00.000Z", - "2021-11-30T00:00:00.000Z", - "2021-12-01T00:00:00.000Z", - "2021-12-02T00:00:00.000Z", - "2021-12-03T00:00:00.000Z", - "2021-12-05T00:00:00.000Z", - "2021-12-06T00:00:00.000Z", - "2021-12-07T00:00:00.000Z", - "2021-12-08T00:00:00.000Z", - "2021-12-09T00:00:00.000Z", - "2021-12-10T00:00:00.000Z", - "2021-12-11T00:00:00.000Z", - "2021-12-12T00:00:00.000Z", - "2021-12-13T00:00:00.000Z", - "2021-12-14T00:00:00.000Z", - "2021-12-15T00:00:00.000Z", - "2021-12-16T00:00:00.000Z", - "2021-12-17T00:00:00.000Z", - "2021-12-18T00:00:00.000Z", - "2021-12-19T00:00:00.000Z", - "2021-12-20T00:00:00.000Z", - "2021-12-21T00:00:00.000Z", - "2021-12-22T00:00:00.000Z", - "2021-12-23T00:00:00.000Z", - "2021-12-24T00:00:00.000Z", - "2021-12-25T00:00:00.000Z", - "2021-12-26T00:00:00.000Z", - "2021-12-27T00:00:00.000Z", - "2021-12-28T00:00:00.000Z", - "2021-12-29T00:00:00.000Z", - "2021-12-30T00:00:00.000Z", - "2021-12-31T00:00:00.000Z", - "2022-01-01T00:00:00.000Z", - "2022-01-02T00:00:00.000Z", - "2022-01-03T00:00:00.000Z", - "2022-01-04T00:00:00.000Z", - "2022-01-05T00:00:00.000Z", - "2022-01-06T00:00:00.000Z", - "2022-01-07T00:00:00.000Z", - "2022-01-08T00:00:00.000Z", - "2022-01-09T00:00:00.000Z", - "2022-01-10T00:00:00.000Z", - "2022-01-11T00:00:00.000Z", - "2022-01-12T00:00:00.000Z", - "2022-01-13T00:00:00.000Z" - ], - "y": [ - 1, 2, 5, 5, 5, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 9, 9, 11, 11, 11, 12, 13, 13, - 14, 15, 16, 16, 16, 16, 18, 18, 19, 20, 20, 20, 20, 20, 20, 21, 22, 22, 23, 24, 24, 24, 24, - 24, 25, 25, 26, 27, 28, 28, 28, 29, 29, 29, 29, 29, 29, 30, 30, 31, 31, 33, 33, 34, 34, 34, - 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35 - ] - }, - "settings": { - "last_computed_at": "2022-01-14T10:58:17.393Z", - "start": "2021-10-14T00:00:00.000Z", - "end": "2022-01-14T00:00:00.000Z" - } - }, - { - "type": "inactive-members", - "cache": [3, 5], - "settings": { - "last_computed_at": "2022-01-14T10:58:17.152Z", - "start": "2022-01-07T00:00:00.000Z", - "end": "2022-01-14T00:00:00.000Z" - } - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-02-17T12:08:11.126Z", - "start": "2022-02-10T00:00:00.000Z", - "end": "2022-02-17T00:00:00.000Z" - } - }, - { - "type": "channel-distribution" - }, - { - "type": "latest-activities" - }, - { - "type": "newest-members" - }, - { - "type": "number-activities", - "cache": [155, 131], - "settings": { - "last_computed_at": "2022-01-14T10:58:17.045Z", - "start": "2022-01-07T00:00:00.000Z", - "end": "2022-01-14T00:00:00.000Z" - } - }, - { - "type": "number-members", - "cache": [1, 11], - "settings": { - "last_computed_at": "2022-01-14T10:58:17.085Z", - "start": "2022-01-07T00:00:00.000Z", - "end": "2022-01-14T00:00:00.000Z" - } - }, - { - "type": "integrations" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "builder" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "number-activities-graph", - "cache": { - "x": [ - "2021-10-14T00:00:00.000Z", - "2021-10-15T00:00:00.000Z", - "2021-10-16T00:00:00.000Z", - "2021-10-17T00:00:00.000Z", - "2021-10-18T00:00:00.000Z", - "2021-10-19T00:00:00.000Z", - "2021-10-20T00:00:00.000Z", - "2021-10-21T00:00:00.000Z", - "2021-10-22T00:00:00.000Z", - "2021-10-23T00:00:00.000Z", - "2021-10-25T00:00:00.000Z", - "2021-10-26T00:00:00.000Z", - "2021-10-27T00:00:00.000Z", - "2021-10-28T00:00:00.000Z", - "2021-10-29T00:00:00.000Z", - "2021-11-01T00:00:00.000Z", - "2021-11-02T00:00:00.000Z", - "2021-11-03T00:00:00.000Z", - "2021-11-04T00:00:00.000Z", - "2021-11-05T00:00:00.000Z", - "2021-11-06T00:00:00.000Z", - "2021-11-07T00:00:00.000Z", - "2021-11-08T00:00:00.000Z", - "2021-11-09T00:00:00.000Z", - "2021-11-10T00:00:00.000Z", - "2021-11-11T00:00:00.000Z", - "2021-11-12T00:00:00.000Z", - "2021-11-13T00:00:00.000Z", - "2021-11-14T00:00:00.000Z", - "2021-11-15T00:00:00.000Z", - "2021-11-16T00:00:00.000Z", - "2021-11-17T00:00:00.000Z", - "2021-11-18T00:00:00.000Z", - "2021-11-19T00:00:00.000Z", - "2021-11-20T00:00:00.000Z", - "2021-11-21T00:00:00.000Z", - "2021-11-22T00:00:00.000Z", - "2021-11-23T00:00:00.000Z", - "2021-11-24T00:00:00.000Z", - "2021-11-25T00:00:00.000Z", - "2021-11-26T00:00:00.000Z", - "2021-11-27T00:00:00.000Z", - "2021-11-28T00:00:00.000Z", - "2021-11-29T00:00:00.000Z", - "2021-11-30T00:00:00.000Z", - "2021-12-01T00:00:00.000Z", - "2021-12-02T00:00:00.000Z", - "2021-12-03T00:00:00.000Z", - "2021-12-05T00:00:00.000Z", - "2021-12-06T00:00:00.000Z", - "2021-12-07T00:00:00.000Z", - "2021-12-08T00:00:00.000Z", - "2021-12-09T00:00:00.000Z", - "2021-12-10T00:00:00.000Z", - "2021-12-11T00:00:00.000Z", - "2021-12-12T00:00:00.000Z", - "2021-12-13T00:00:00.000Z", - "2021-12-14T00:00:00.000Z", - "2021-12-15T00:00:00.000Z", - "2021-12-16T00:00:00.000Z", - "2021-12-17T00:00:00.000Z", - "2021-12-18T00:00:00.000Z", - "2021-12-19T00:00:00.000Z", - "2021-12-20T00:00:00.000Z", - "2021-12-21T00:00:00.000Z", - "2021-12-22T00:00:00.000Z", - "2021-12-23T00:00:00.000Z", - "2021-12-24T00:00:00.000Z", - "2021-12-25T00:00:00.000Z", - "2021-12-26T00:00:00.000Z", - "2021-12-27T00:00:00.000Z", - "2021-12-28T00:00:00.000Z", - "2021-12-29T00:00:00.000Z", - "2021-12-30T00:00:00.000Z", - "2021-12-31T00:00:00.000Z", - "2022-01-01T00:00:00.000Z", - "2022-01-02T00:00:00.000Z", - "2022-01-03T00:00:00.000Z", - "2022-01-04T00:00:00.000Z", - "2022-01-05T00:00:00.000Z", - "2022-01-06T00:00:00.000Z", - "2022-01-07T00:00:00.000Z", - "2022-01-08T00:00:00.000Z", - "2022-01-09T00:00:00.000Z", - "2022-01-10T00:00:00.000Z", - "2022-01-11T00:00:00.000Z", - "2022-01-12T00:00:00.000Z", - "2022-01-13T00:00:00.000Z" - ], - "y": [ - 1, 5, 25, 26, 33, 40, 51, 53, 55, 63, 64, 83, 94, 101, 104, 108, 111, 114, 115, 123, 126, - 128, 130, 151, 162, 168, 173, 177, 178, 194, 212, 214, 218, 234, 237, 240, 246, 259, 281, - 293, 308, 309, 310, 312, 338, 342, 345, 349, 351, 352, 356, 361, 364, 388, 395, 396, 399, - 407, 419, 426, 432, 443, 450, 455, 466, 471, 490, 548, 555, 561, 567, 574, 577, 586, 594, - 600, 606, 612, 631, 653, 730, 749, 764, 774, 778, 795, 809, 872 - ] - }, - "settings": { - "last_computed_at": "2022-01-14T10:58:17.440Z", - "start": "2021-10-14T00:00:00.000Z", - "end": "2022-01-14T00:00:00.000Z" - } - }, - { - "type": "inactive-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-12T16:49:14.400Z", - "start": "2022-01-05T00:00:00.000Z", - "end": "2022-01-12T00:00:00.000Z" - } - }, - { - "type": "number-activities", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-12T16:49:14.910Z", - "start": "2022-01-05T00:00:00.000Z", - "end": "2022-01-12T00:00:00.000Z" - } - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "integrations" - }, - { - "type": "latest-activities" - }, - { - "type": "number-members", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-12T16:49:15.013Z", - "start": "2022-01-05T00:00:00.000Z", - "end": "2022-01-12T00:00:00.000Z" - } - }, - { - "type": "benchmark" - }, - { - "type": "channel-distribution" - }, - { - "type": "number-activities-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": { - "last_computed_at": "2022-01-12T16:49:14.309Z", - "start": "2021-10-12T00:00:00.000Z", - "end": "2022-01-12T00:00:00.000Z" - } - }, - { - "type": "number-members-graph", - "cache": { - "x": [], - "y": [] - }, - "settings": { - "last_computed_at": "2022-01-12T16:49:14.325Z", - "start": "2021-10-12T00:00:00.000Z", - "end": "2022-01-12T00:00:00.000Z" - } - }, - { - "type": "builder" - }, - { - "type": "newest-members" - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-12T16:49:14.910Z", - "start": "2022-01-05T00:00:00.000Z", - "end": "2022-01-12T00:00:00.000Z" - } - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-25T10:18:55.331Z", - "start": "2022-01-18T00:00:00.000Z", - "end": "2022-01-25T00:00:00.000Z" - } - }, - { - "type": "inactive-members", - "cache": [2, 4], - "settings": { - "last_computed_at": "2022-01-13T11:51:34.942Z", - "start": "2022-01-06T00:00:00.000Z", - "end": "2022-01-13T00:00:00.000Z" - } - }, - { - "type": "number-activities", - "cache": [71, 81], - "settings": { - "last_computed_at": "2022-01-13T11:51:35.738Z", - "start": "2022-01-06T00:00:00.000Z", - "end": "2022-01-13T00:00:00.000Z" - } - }, - { - "type": "number-activities-graph", - "cache": { - "x": [ - "2022-01-06T00:00:00.000Z", - "2022-01-07T00:00:00.000Z", - "2022-01-08T00:00:00.000Z", - "2022-01-09T00:00:00.000Z", - "2022-01-10T00:00:00.000Z", - "2022-01-11T00:00:00.000Z", - "2022-01-12T00:00:00.000Z" - ], - "y": [1, 15, 27, 37, 53, 62, 74] - }, - "settings": { - "last_computed_at": "2022-01-13T11:51:35.734Z", - "start": "2022-01-06T00:00:00.000Z", - "end": "2022-01-13T00:00:00.000Z" - } - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "number-members", - "cache": [0, 1], - "settings": { - "last_computed_at": "2022-01-13T11:51:35.640Z", - "start": "2022-01-06T00:00:00.000Z", - "end": "2022-01-13T00:00:00.000Z" - } - }, - { - "type": "integrations" - }, - { - "type": "builder" - }, - { - "type": "newest-members" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "latest-activities" - }, - { - "type": "channel-distribution" - }, - { - "type": "benchmark" - }, - { - "type": "number-members-graph", - "cache": { - "x": [ - "2022-01-06T00:00:00.000Z", - "2022-01-07T00:00:00.000Z", - "2022-01-08T00:00:00.000Z", - "2022-01-09T00:00:00.000Z", - "2022-01-10T00:00:00.000Z", - "2022-01-11T00:00:00.000Z", - "2022-01-12T00:00:00.000Z" - ], - "y": [1, 12, 19, 24, 30, 33, 34] - }, - "settings": { - "last_computed_at": "2022-01-13T11:51:35.770Z", - "start": "2022-01-06T00:00:00.000Z", - "end": "2022-01-13T00:00:00.000Z" - } - }, - { - "type": "inactive-members" - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-14T11:46:50.598Z", - "start": "2022-01-07T00:00:00.000Z", - "end": "2022-01-14T00:00:00.000Z" - } - }, - { - "type": "number-activities" - }, - { - "type": "number-members" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "number-activities-graph" - }, - { - "type": "number-members-graph" - }, - { - "type": "channel-distribution" - }, - { - "type": "benchmark" - }, - { - "type": "newest-members" - }, - { - "type": "latest-activities" - }, - { - "type": "integrations" - }, - { - "type": "builder" - }, - { - "type": "inactive-members" - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-14T11:52:42.202Z", - "start": "2022-01-07T00:00:00.000Z", - "end": "2022-01-14T00:00:00.000Z" - } - }, - { - "type": "number-activities" - }, - { - "type": "number-members" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "number-activities-graph" - }, - { - "type": "number-members-graph" - }, - { - "type": "channel-distribution" - }, - { - "type": "benchmark" - }, - { - "type": "latest-activities" - }, - { - "type": "newest-members" - }, - { - "type": "integrations" - }, - { - "type": "builder" - }, - { - "type": "time-to-first-interaction" - }, - { - "type": "number-members" - }, - { - "type": "inactive-members" - }, - { - "type": "number-activities" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "number-activities-graph" - }, - { - "type": "number-members-graph" - }, - { - "type": "channel-distribution" - }, - { - "type": "benchmark" - }, - { - "type": "latest-activities" - }, - { - "type": "newest-members" - }, - { - "type": "integrations" - }, - { - "type": "builder" - }, - { - "type": "inactive-members" - }, - { - "type": "time-to-first-interaction" - }, - { - "type": "number-activities" - }, - { - "type": "number-members" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "number-activities-graph" - }, - { - "type": "number-members-graph" - }, - { - "type": "channel-distribution" - }, - { - "type": "benchmark" - }, - { - "type": "latest-activities" - }, - { - "type": "newest-members" - }, - { - "type": "integrations" - }, - { - "type": "builder" - }, - { - "type": "inactive-members" - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-02-14T10:11:35.523Z", - "start": "2022-02-07T00:00:00.000Z", - "end": "2022-02-14T00:00:00.000Z" - } - }, - { - "type": "number-activities" - }, - { - "type": "number-members" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "number-activities-graph" - }, - { - "type": "channel-distribution" - }, - { - "type": "number-members-graph" - }, - { - "type": "benchmark" - }, - { - "type": "latest-activities" - }, - { - "type": "newest-members" - }, - { - "type": "integrations" - }, - { - "type": "builder" - }, - { - "type": "inactive-members" - }, - { - "type": "time-to-first-interaction", - "cache": [1, 0], - "settings": { - "last_computed_at": "2022-02-17T11:24:42.425Z", - "start": "2022-02-10T00:00:00.000Z", - "end": "2022-02-17T00:00:00.000Z" - } - }, - { - "type": "number-activities" - }, - { - "type": "number-members" - }, - { - "type": "number-activities-graph" - }, - { - "type": "number-members-graph" - }, - { - "type": "benchmark", - "settings": { - "repositories": [ - { - "id": 56919458, - "value": "vue-multiselect", - "label": "vue-multiselect", - "active": true, - "editing": false, - "githubRepo": { - "id": 56919458, - "node_id": "MDEwOlJlcG9zaXRvcnk1NjkxOTQ1OA==", - "name": "vue-multiselect", - "full_name": "shentao/vue-multiselect", - "private": false, - "owner": { - "login": "shentao", - "id": 3737591, - "node_id": "MDQ6VXNlcjM3Mzc1OTE=", - "avatar_url": "https://avatars.githubusercontent.com/u/3737591?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/shentao", - "html_url": "https://github.com/shentao", - "followers_url": "https://api.github.com/users/shentao/followers", - "following_url": "https://api.github.com/users/shentao/following{/other_user}", - "gists_url": "https://api.github.com/users/shentao/gists{/gist_id}", - "starred_url": "https://api.github.com/users/shentao/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/shentao/subscriptions", - "organizations_url": "https://api.github.com/users/shentao/orgs", - "repos_url": "https://api.github.com/users/shentao/repos", - "events_url": "https://api.github.com/users/shentao/events{/privacy}", - "received_events_url": "https://api.github.com/users/shentao/received_events", - "type": "User", - "site_admin": false - }, - "html_url": "https://github.com/shentao/vue-multiselect", - "description": "Universal select/multiselect/tagging component for Vue.js", - "fork": false, - "url": "https://api.github.com/repos/shentao/vue-multiselect", - "forks_url": "https://api.github.com/repos/shentao/vue-multiselect/forks", - "keys_url": "https://api.github.com/repos/shentao/vue-multiselect/keys{/key_id}", - "collaborators_url": "https://api.github.com/repos/shentao/vue-multiselect/collaborators{/collaborator}", - "teams_url": "https://api.github.com/repos/shentao/vue-multiselect/teams", - "hooks_url": "https://api.github.com/repos/shentao/vue-multiselect/hooks", - "issue_events_url": "https://api.github.com/repos/shentao/vue-multiselect/issues/events{/number}", - "events_url": "https://api.github.com/repos/shentao/vue-multiselect/events", - "assignees_url": "https://api.github.com/repos/shentao/vue-multiselect/assignees{/user}", - "branches_url": "https://api.github.com/repos/shentao/vue-multiselect/branches{/branch}", - "tags_url": "https://api.github.com/repos/shentao/vue-multiselect/tags", - "blobs_url": "https://api.github.com/repos/shentao/vue-multiselect/git/blobs{/sha}", - "git_tags_url": "https://api.github.com/repos/shentao/vue-multiselect/git/tags{/sha}", - "git_refs_url": "https://api.github.com/repos/shentao/vue-multiselect/git/refs{/sha}", - "trees_url": "https://api.github.com/repos/shentao/vue-multiselect/git/trees{/sha}", - "statuses_url": "https://api.github.com/repos/shentao/vue-multiselect/statuses/{sha}", - "languages_url": "https://api.github.com/repos/shentao/vue-multiselect/languages", - "stargazers_url": "https://api.github.com/repos/shentao/vue-multiselect/stargazers", - "contributors_url": "https://api.github.com/repos/shentao/vue-multiselect/contributors", - "subscribers_url": "https://api.github.com/repos/shentao/vue-multiselect/subscribers", - "subscription_url": "https://api.github.com/repos/shentao/vue-multiselect/subscription", - "commits_url": "https://api.github.com/repos/shentao/vue-multiselect/commits{/sha}", - "git_commits_url": "https://api.github.com/repos/shentao/vue-multiselect/git/commits{/sha}", - "comments_url": "https://api.github.com/repos/shentao/vue-multiselect/comments{/number}", - "issue_comment_url": "https://api.github.com/repos/shentao/vue-multiselect/issues/comments{/number}", - "contents_url": "https://api.github.com/repos/shentao/vue-multiselect/contents/{+path}", - "compare_url": "https://api.github.com/repos/shentao/vue-multiselect/compare/{base}...{head}", - "merges_url": "https://api.github.com/repos/shentao/vue-multiselect/merges", - "archive_url": "https://api.github.com/repos/shentao/vue-multiselect/{archive_format}{/ref}", - "downloads_url": "https://api.github.com/repos/shentao/vue-multiselect/downloads", - "issues_url": "https://api.github.com/repos/shentao/vue-multiselect/issues{/number}", - "pulls_url": "https://api.github.com/repos/shentao/vue-multiselect/pulls{/number}", - "milestones_url": "https://api.github.com/repos/shentao/vue-multiselect/milestones{/number}", - "notifications_url": "https://api.github.com/repos/shentao/vue-multiselect/notifications{?since,all,participating}", - "labels_url": "https://api.github.com/repos/shentao/vue-multiselect/labels{/name}", - "releases_url": "https://api.github.com/repos/shentao/vue-multiselect/releases{/id}", - "deployments_url": "https://api.github.com/repos/shentao/vue-multiselect/deployments", - "created_at": "2016-04-23T13:02:33Z", - "updated_at": "2022-01-19T11:09:17Z", - "pushed_at": "2022-01-17T04:06:58Z", - "git_url": "git://github.com/shentao/vue-multiselect.git", - "ssh_url": "git@github.com:shentao/vue-multiselect.git", - "clone_url": "https://github.com/shentao/vue-multiselect.git", - "svn_url": "https://github.com/shentao/vue-multiselect", - "homepage": "https://vue-multiselect.js.org/", - "size": 13669, - "stargazers_count": 6021, - "watchers_count": 6021, - "language": "JavaScript", - "has_issues": true, - "has_projects": true, - "has_downloads": true, - "has_wiki": true, - "has_pages": true, - "forks_count": 916, - "mirror_url": null, - "archived": false, - "disabled": false, - "open_issues_count": 357, - "license": { - "key": "mit", - "name": "MIT License", - "spdx_id": "MIT", - "url": "https://api.github.com/licenses/mit", - "node_id": "MDc6TGljZW5zZTEz" - }, - "allow_forking": true, - "is_template": false, - "topics": ["component", "dropdown", "javascript", "select", "vue"], - "visibility": "public", - "forks": 916, - "open_issues": 357, - "watchers": 6021, - "default_branch": "master", - "permissions": { - "admin": false, - "maintain": false, - "push": false, - "triage": false, - "pull": true - }, - "score": 1 - }, - "color": "#29AE1D", - "data": { - "2022-02-17": { - "date": "2022-02-17", - "value": 6049 - }, - "2022-02-16": { - "date": "2022-02-16", - "value": 6047 - }, - "2022-02-15": { - "date": "2022-02-15", - "value": 6046 - }, - "2022-02-14": { - "date": "2022-02-14", - "value": 6043 - }, - "2022-02-13": { - "date": "2022-02-13", - "value": 6042 - }, - "2022-02-12": { - "date": "2022-02-12", - "value": 6042 - }, - "2022-02-11": { - "date": "2022-02-11", - "value": 6040 - }, - "2022-02-10": { - "date": "2022-02-10", - "value": 6037 - }, - "2022-02-09": { - "date": "2022-02-09", - "value": 6035 - }, - "2022-02-08": { - "date": "2022-02-08", - "value": 6034 - }, - "2022-02-07": { - "date": "2022-02-07", - "value": 6034 - }, - "2022-02-06": { - "date": "2022-02-06", - "value": 6034 - }, - "2022-02-05": { - "date": "2022-02-05", - "value": 6032 - }, - "2022-02-04": { - "date": "2022-02-04", - "value": 6032 - }, - "2022-02-03": { - "date": "2022-02-03", - "value": 6030 - }, - "2022-02-02": { - "date": "2022-02-02", - "value": 6029 - }, - "2022-02-01": { - "date": "2022-02-01", - "value": 6029 - }, - "2022-01-31": { - "date": "2022-01-31", - "value": 6028 - }, - "2022-01-30": { - "date": "2022-01-30", - "value": 6026 - }, - "2022-01-29": { - "date": "2022-01-29", - "value": 6026 - }, - "2022-01-28": { - "date": "2022-01-28", - "value": 6025 - }, - "2022-01-27": { - "date": "2022-01-27", - "value": 6024 - }, - "2022-01-26": { - "date": "2022-01-26", - "value": 6023 - }, - "2022-01-25": { - "date": "2022-01-25", - "value": 6022 - }, - "2022-01-24": { - "date": "2022-01-24", - "value": 6022 - }, - "2022-01-23": { - "date": "2022-01-23", - "value": 6020 - }, - "2022-01-22": { - "date": "2022-01-22", - "value": 6019 - }, - "2022-01-21": { - "date": "2022-01-21", - "value": 6015 - }, - "2022-01-20": { - "date": "2022-01-20", - "value": 6013 - }, - "2022-01-19": { - "date": "2022-01-19", - "value": 6011 - }, - "2022-01-18": { - "date": "2022-01-18", - "value": 6009 - }, - "2022-01-17": { - "date": "2022-01-17", - "value": 6008 - }, - "2022-01-16": { - "date": "2022-01-16", - "value": 6008 - }, - "2022-01-15": { - "date": "2022-01-15", - "value": 6004 - }, - "2022-01-14": { - "date": "2022-01-14", - "value": 6003 - }, - "2022-01-13": { - "date": "2022-01-13", - "value": 6001 - }, - "2022-01-12": { - "date": "2022-01-12", - "value": 6000 - }, - "2022-01-11": { - "date": "2022-01-11", - "value": 5998 - }, - "2022-01-10": { - "date": "2022-01-10", - "value": 5996 - }, - "2022-01-09": { - "date": "2022-01-09", - "value": 5996 - }, - "2022-01-08": { - "date": "2022-01-08", - "value": 5995 - }, - "2022-01-07": { - "date": "2022-01-07", - "value": 5992 - }, - "2022-01-06": { - "date": "2022-01-06", - "value": 5989 - }, - "2022-01-05": { - "date": "2022-01-05", - "value": 5988 - }, - "2022-01-04": { - "date": "2022-01-04", - "value": 5985 - }, - "2022-01-03": { - "date": "2022-01-03", - "value": 5984 - }, - "2022-01-02": { - "date": "2022-01-02", - "value": 5984 - }, - "2022-01-01": { - "date": "2022-01-01", - "value": 5984 - }, - "2021-12-31": { - "date": "2021-12-31", - "value": 5982 - }, - "2021-12-30": { - "date": "2021-12-30", - "value": 5981 - }, - "2021-12-29": { - "date": "2021-12-29", - "value": 5979 - }, - "2021-12-28": { - "date": "2021-12-28", - "value": 5978 - }, - "2021-12-27": { - "date": "2021-12-27", - "value": 5978 - }, - "2021-12-26": { - "date": "2021-12-26", - "value": 5978 - }, - "2021-12-25": { - "date": "2021-12-25", - "value": 5978 - }, - "2021-12-24": { - "date": "2021-12-24", - "value": 5977 - }, - "2021-12-23": { - "date": "2021-12-23", - "value": 5977 - }, - "2021-12-22": { - "date": "2021-12-22", - "value": 5973 - }, - "2021-12-21": { - "date": "2021-12-21", - "value": 5971 - }, - "2021-12-20": { - "date": "2021-12-20", - "value": 5971 - }, - "2021-12-19": { - "date": "2021-12-19", - "value": 5971 - }, - "2021-12-18": { - "date": "2021-12-18", - "value": 5971 - }, - "2021-12-17": { - "date": "2021-12-17", - "value": 5970 - }, - "2021-12-16": { - "date": "2021-12-16", - "value": 5967 - }, - "2021-12-15": { - "date": "2021-12-15", - "value": 5965 - }, - "2021-12-14": { - "date": "2021-12-14", - "value": 5963 - }, - "2021-12-13": { - "date": "2021-12-13", - "value": 5963 - }, - "2021-12-12": { - "date": "2021-12-12", - "value": 5962 - }, - "2021-12-11": { - "date": "2021-12-11", - "value": 5962 - }, - "2021-12-10": { - "date": "2021-12-10", - "value": 5960 - }, - "2021-12-09": { - "date": "2021-12-09", - "value": 5960 - }, - "2021-12-08": { - "date": "2021-12-08", - "value": 5959 - }, - "2021-12-07": { - "date": "2021-12-07", - "value": 5955 - }, - "2021-12-06": { - "date": "2021-12-06", - "value": 5951 - }, - "2021-12-05": { - "date": "2021-12-05", - "value": 5951 - }, - "2021-12-04": { - "date": "2021-12-04", - "value": 5950 - }, - "2021-12-03": { - "date": "2021-12-03", - "value": 5948 - }, - "2021-12-02": { - "date": "2021-12-02", - "value": 5948 - }, - "2021-12-01": { - "date": "2021-12-01", - "value": 5947 - }, - "2021-11-30": { - "date": "2021-11-30", - "value": 5947 - }, - "2021-11-29": { - "date": "2021-11-29", - "value": 5946 - }, - "2021-11-28": { - "date": "2021-11-28", - "value": 5945 - }, - "2021-11-27": { - "date": "2021-11-27", - "value": 5944 - }, - "2021-11-26": { - "date": "2021-11-26", - "value": 5941 - }, - "2021-11-25": { - "date": "2021-11-25", - "value": 5941 - }, - "2021-11-24": { - "date": "2021-11-24", - "value": 5936 - }, - "2021-11-23": { - "date": "2021-11-23", - "value": 5934 - }, - "2021-11-22": { - "date": "2021-11-22", - "value": 5933 - }, - "2021-11-21": { - "date": "2021-11-21", - "value": 5932 - }, - "2021-11-20": { - "date": "2021-11-20", - "value": 5932 - }, - "2021-11-19": { - "date": "2021-11-19", - "value": 5930 - }, - "2021-11-18": { - "date": "2021-11-18", - "value": 5925 - }, - "2021-11-17": { - "date": "2021-11-17", - "value": 5923 - } - } - } - ], - "last_updated_at": "2022-02-17", - "timeframe": { - "label": "Last three months", - "value": "last_three_months", - "date": "2021-11-17" - } - } - }, - { - "type": "latest-activities" - }, - { - "type": "integrations" - }, - { - "type": "newest-members" - }, - { - "type": "builder" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "channel-distribution" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "number-members" - }, - { - "type": "number-activities-graph" - }, - { - "type": "time-to-first-interaction", - "cache": [3, 3], - "settings": { - "last_computed_at": "2022-01-20T17:18:35.921Z", - "start": "2022-01-13T00:00:00.000Z", - "end": "2022-01-20T00:00:00.000Z" - } - }, - { - "type": "number-members-graph" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "inactive-members" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "benchmark", - "settings": { - "repositories": [ - { - "id": 26390092, - "value": "deepstream.io", - "label": "deepstream.io", - "active": true, - "editing": false, - "githubRepo": { - "id": 26390092, - "node_id": "MDEwOlJlcG9zaXRvcnkyNjM5MDA5Mg==", - "name": "deepstream.io", - "full_name": "deepstreamIO/deepstream.io", - "private": false, - "owner": { - "login": "deepstreamIO", - "id": 9024218, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjkwMjQyMTg=", - "avatar_url": "https://avatars.githubusercontent.com/u/9024218?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/deepstreamIO", - "html_url": "https://github.com/deepstreamIO", - "followers_url": "https://api.github.com/users/deepstreamIO/followers", - "following_url": "https://api.github.com/users/deepstreamIO/following{/other_user}", - "gists_url": "https://api.github.com/users/deepstreamIO/gists{/gist_id}", - "starred_url": "https://api.github.com/users/deepstreamIO/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/deepstreamIO/subscriptions", - "organizations_url": "https://api.github.com/users/deepstreamIO/orgs", - "repos_url": "https://api.github.com/users/deepstreamIO/repos", - "events_url": "https://api.github.com/users/deepstreamIO/events{/privacy}", - "received_events_url": "https://api.github.com/users/deepstreamIO/received_events", - "type": "Organization", - "site_admin": false - }, - "html_url": "https://github.com/deepstreamIO/deepstream.io", - "description": "deepstream.io server", - "fork": false, - "url": "https://api.github.com/repos/deepstreamIO/deepstream.io", - "forks_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/forks", - "keys_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/keys{/key_id}", - "collaborators_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/collaborators{/collaborator}", - "teams_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/teams", - "hooks_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/hooks", - "issue_events_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/issues/events{/number}", - "events_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/events", - "assignees_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/assignees{/user}", - "branches_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/branches{/branch}", - "tags_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/tags", - "blobs_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/git/blobs{/sha}", - "git_tags_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/git/tags{/sha}", - "git_refs_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/git/refs{/sha}", - "trees_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/git/trees{/sha}", - "statuses_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/statuses/{sha}", - "languages_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/languages", - "stargazers_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/stargazers", - "contributors_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/contributors", - "subscribers_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/subscribers", - "subscription_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/subscription", - "commits_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/commits{/sha}", - "git_commits_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/git/commits{/sha}", - "comments_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/comments{/number}", - "issue_comment_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/issues/comments{/number}", - "contents_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/contents/{+path}", - "compare_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/compare/{base}...{head}", - "merges_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/merges", - "archive_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/{archive_format}{/ref}", - "downloads_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/downloads", - "issues_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/issues{/number}", - "pulls_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/pulls{/number}", - "milestones_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/milestones{/number}", - "notifications_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/notifications{?since,all,participating}", - "labels_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/labels{/name}", - "releases_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/releases{/id}", - "deployments_url": "https://api.github.com/repos/deepstreamIO/deepstream.io/deployments", - "created_at": "2014-11-09T08:30:01Z", - "updated_at": "2022-01-18T13:10:13Z", - "pushed_at": "2022-01-02T03:19:39Z", - "git_url": "git://github.com/deepstreamIO/deepstream.io.git", - "ssh_url": "git@github.com:deepstreamIO/deepstream.io.git", - "clone_url": "https://github.com/deepstreamIO/deepstream.io.git", - "svn_url": "https://github.com/deepstreamIO/deepstream.io", - "homepage": "http://deepstream.io/", - "size": 10866, - "stargazers_count": 6952, - "watchers_count": 6952, - "language": "TypeScript", - "has_issues": true, - "has_projects": true, - "has_downloads": true, - "has_wiki": true, - "has_pages": false, - "forks_count": 399, - "mirror_url": null, - "archived": false, - "disabled": false, - "open_issues_count": 6, - "license": { - "key": "mit", - "name": "MIT License", - "spdx_id": "MIT", - "url": "https://api.github.com/licenses/mit", - "node_id": "MDc6TGljZW5zZTEz" - }, - "allow_forking": true, - "is_template": false, - "topics": [ - "authentication", - "datasync", - "deepstream", - "permissions", - "pubsub", - "realtime", - "rpc", - "typescript", - "websocket" - ], - "visibility": "public", - "forks": 399, - "open_issues": 6, - "watchers": 6952, - "default_branch": "master", - "score": 1 - }, - "data": { - "2022-01-20": { - "date": "2022-01-20", - "value": 6952 - }, - "2022-01-19": { - "date": "2022-01-19", - "value": 6952 - }, - "2022-01-18": { - "date": "2022-01-18", - "value": 6950 - }, - "2022-01-17": { - "date": "2022-01-17", - "value": 6950 - }, - "2022-01-16": { - "date": "2022-01-16", - "value": 6950 - }, - "2022-01-15": { - "date": "2022-01-15", - "value": 6950 - }, - "2022-01-14": { - "date": "2022-01-14", - "value": 6950 - }, - "2022-01-13": { - "date": "2022-01-13", - "value": 6950 - }, - "2022-01-12": { - "date": "2022-01-12", - "value": 6950 - }, - "2022-01-11": { - "date": "2022-01-11", - "value": 6949 - }, - "2022-01-10": { - "date": "2022-01-10", - "value": 6949 - }, - "2022-01-09": { - "date": "2022-01-09", - "value": 6949 - }, - "2022-01-08": { - "date": "2022-01-08", - "value": 6949 - }, - "2022-01-07": { - "date": "2022-01-07", - "value": 6949 - }, - "2022-01-06": { - "date": "2022-01-06", - "value": 6949 - }, - "2022-01-05": { - "date": "2022-01-05", - "value": 6948 - }, - "2022-01-04": { - "date": "2022-01-04", - "value": 6948 - }, - "2022-01-03": { - "date": "2022-01-03", - "value": 6948 - }, - "2022-01-02": { - "date": "2022-01-02", - "value": 6948 - }, - "2022-01-01": { - "date": "2022-01-01", - "value": 6948 - }, - "2021-12-31": { - "date": "2021-12-31", - "value": 6948 - }, - "2021-12-30": { - "date": "2021-12-30", - "value": 6948 - }, - "2021-12-29": { - "date": "2021-12-29", - "value": 6947 - }, - "2021-12-28": { - "date": "2021-12-28", - "value": 6946 - }, - "2021-12-27": { - "date": "2021-12-27", - "value": 6945 - }, - "2021-12-26": { - "date": "2021-12-26", - "value": 6943 - }, - "2021-12-25": { - "date": "2021-12-25", - "value": 6942 - }, - "2021-12-24": { - "date": "2021-12-24", - "value": 6941 - }, - "2021-12-23": { - "date": "2021-12-23", - "value": 6941 - }, - "2021-12-22": { - "date": "2021-12-22", - "value": 6940 - }, - "2021-12-21": { - "date": "2021-12-21", - "value": 6940 - }, - "2021-12-20": { - "date": "2021-12-20", - "value": 6940 - }, - "2021-12-19": { - "date": "2021-12-19", - "value": 6940 - }, - "2021-12-18": { - "date": "2021-12-18", - "value": 6940 - }, - "2021-12-17": { - "date": "2021-12-17", - "value": 6940 - }, - "2021-12-16": { - "date": "2021-12-16", - "value": 6940 - }, - "2021-12-15": { - "date": "2021-12-15", - "value": 6940 - }, - "2021-12-14": { - "date": "2021-12-14", - "value": 6939 - }, - "2021-12-13": { - "date": "2021-12-13", - "value": 6938 - }, - "2021-12-12": { - "date": "2021-12-12", - "value": 6938 - }, - "2021-12-11": { - "date": "2021-12-11", - "value": 6938 - }, - "2021-12-10": { - "date": "2021-12-10", - "value": 6937 - }, - "2021-12-09": { - "date": "2021-12-09", - "value": 6937 - }, - "2021-12-08": { - "date": "2021-12-08", - "value": 6936 - }, - "2021-12-07": { - "date": "2021-12-07", - "value": 6934 - }, - "2021-12-06": { - "date": "2021-12-06", - "value": 6934 - }, - "2021-12-05": { - "date": "2021-12-05", - "value": 6934 - }, - "2021-12-04": { - "date": "2021-12-04", - "value": 6934 - }, - "2021-12-03": { - "date": "2021-12-03", - "value": 6933 - }, - "2021-12-02": { - "date": "2021-12-02", - "value": 6933 - }, - "2021-12-01": { - "date": "2021-12-01", - "value": 6931 - }, - "2021-11-30": { - "date": "2021-11-30", - "value": 6930 - }, - "2021-11-29": { - "date": "2021-11-29", - "value": 6929 - }, - "2021-11-28": { - "date": "2021-11-28", - "value": 6929 - }, - "2021-11-27": { - "date": "2021-11-27", - "value": 6929 - }, - "2021-11-26": { - "date": "2021-11-26", - "value": 6929 - }, - "2021-11-25": { - "date": "2021-11-25", - "value": 6928 - }, - "2021-11-24": { - "date": "2021-11-24", - "value": 6928 - }, - "2021-11-23": { - "date": "2021-11-23", - "value": 6928 - }, - "2021-11-22": { - "date": "2021-11-22", - "value": 6928 - }, - "2021-11-21": { - "date": "2021-11-21", - "value": 6928 - }, - "2021-11-20": { - "date": "2021-11-20", - "value": 6926 - }, - "2021-11-19": { - "date": "2021-11-19", - "value": 6925 - }, - "2021-11-18": { - "date": "2021-11-18", - "value": 6925 - }, - "2021-11-17": { - "date": "2021-11-17", - "value": 6925 - }, - "2021-11-16": { - "date": "2021-11-16", - "value": 6924 - }, - "2021-11-15": { - "date": "2021-11-15", - "value": 6923 - }, - "2021-11-14": { - "date": "2021-11-14", - "value": 6923 - }, - "2021-11-13": { - "date": "2021-11-13", - "value": 6923 - }, - "2021-11-12": { - "date": "2021-11-12", - "value": 6923 - }, - "2021-11-11": { - "date": "2021-11-11", - "value": 6923 - }, - "2021-11-10": { - "date": "2021-11-10", - "value": 6923 - }, - "2021-11-09": { - "date": "2021-11-09", - "value": 6922 - }, - "2021-11-08": { - "date": "2021-11-08", - "value": 6922 - }, - "2021-11-07": { - "date": "2021-11-07", - "value": 6922 - }, - "2021-11-06": { - "date": "2021-11-06", - "value": 6922 - }, - "2021-11-05": { - "date": "2021-11-05", - "value": 6922 - }, - "2021-11-04": { - "date": "2021-11-04", - "value": 6920 - }, - "2021-11-03": { - "date": "2021-11-03", - "value": 6920 - }, - "2021-11-02": { - "date": "2021-11-02", - "value": 6920 - }, - "2021-11-01": { - "date": "2021-11-01", - "value": 6920 - }, - "2021-10-31": { - "date": "2021-10-31", - "value": 6919 - }, - "2021-10-30": { - "date": "2021-10-30", - "value": 6919 - }, - "2021-10-29": { - "date": "2021-10-29", - "value": 6916 - }, - "2021-10-28": { - "date": "2021-10-28", - "value": 6915 - }, - "2021-10-27": { - "date": "2021-10-27", - "value": 6914 - }, - "2021-10-26": { - "date": "2021-10-26", - "value": 6913 - }, - "2021-10-25": { - "date": "2021-10-25", - "value": 6913 - }, - "2021-10-24": { - "date": "2021-10-24", - "value": 6913 - }, - "2021-10-23": { - "date": "2021-10-23", - "value": 6913 - }, - "2021-10-22": { - "date": "2021-10-22", - "value": 6912 - }, - "2021-10-21": { - "date": "2021-10-21", - "value": 6912 - }, - "2021-10-20": { - "date": "2021-10-20", - "value": 6911 - } - }, - "color": "#F98181" - }, - { - "id": 337414495, - "value": "nhost", - "label": "nhost", - "active": true, - "editing": false, - "githubRepo": { - "id": 337414495, - "node_id": "MDEwOlJlcG9zaXRvcnkzMzc0MTQ0OTU=", - "name": "nhost", - "full_name": "nhost/nhost", - "private": false, - "owner": { - "login": "nhost", - "id": 48448799, - "node_id": "MDEyOk9yZ2FuaXphdGlvbjQ4NDQ4Nzk5", - "avatar_url": "https://avatars.githubusercontent.com/u/48448799?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/nhost", - "html_url": "https://github.com/nhost", - "followers_url": "https://api.github.com/users/nhost/followers", - "following_url": "https://api.github.com/users/nhost/following{/other_user}", - "gists_url": "https://api.github.com/users/nhost/gists{/gist_id}", - "starred_url": "https://api.github.com/users/nhost/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/nhost/subscriptions", - "organizations_url": "https://api.github.com/users/nhost/orgs", - "repos_url": "https://api.github.com/users/nhost/repos", - "events_url": "https://api.github.com/users/nhost/events{/privacy}", - "received_events_url": "https://api.github.com/users/nhost/received_events", - "type": "Organization", - "site_admin": false - }, - "html_url": "https://github.com/nhost/nhost", - "description": "Serverless backend for web and mobile apps.", - "fork": false, - "url": "https://api.github.com/repos/nhost/nhost", - "forks_url": "https://api.github.com/repos/nhost/nhost/forks", - "keys_url": "https://api.github.com/repos/nhost/nhost/keys{/key_id}", - "collaborators_url": "https://api.github.com/repos/nhost/nhost/collaborators{/collaborator}", - "teams_url": "https://api.github.com/repos/nhost/nhost/teams", - "hooks_url": "https://api.github.com/repos/nhost/nhost/hooks", - "issue_events_url": "https://api.github.com/repos/nhost/nhost/issues/events{/number}", - "events_url": "https://api.github.com/repos/nhost/nhost/events", - "assignees_url": "https://api.github.com/repos/nhost/nhost/assignees{/user}", - "branches_url": "https://api.github.com/repos/nhost/nhost/branches{/branch}", - "tags_url": "https://api.github.com/repos/nhost/nhost/tags", - "blobs_url": "https://api.github.com/repos/nhost/nhost/git/blobs{/sha}", - "git_tags_url": "https://api.github.com/repos/nhost/nhost/git/tags{/sha}", - "git_refs_url": "https://api.github.com/repos/nhost/nhost/git/refs{/sha}", - "trees_url": "https://api.github.com/repos/nhost/nhost/git/trees{/sha}", - "statuses_url": "https://api.github.com/repos/nhost/nhost/statuses/{sha}", - "languages_url": "https://api.github.com/repos/nhost/nhost/languages", - "stargazers_url": "https://api.github.com/repos/nhost/nhost/stargazers", - "contributors_url": "https://api.github.com/repos/nhost/nhost/contributors", - "subscribers_url": "https://api.github.com/repos/nhost/nhost/subscribers", - "subscription_url": "https://api.github.com/repos/nhost/nhost/subscription", - "commits_url": "https://api.github.com/repos/nhost/nhost/commits{/sha}", - "git_commits_url": "https://api.github.com/repos/nhost/nhost/git/commits{/sha}", - "comments_url": "https://api.github.com/repos/nhost/nhost/comments{/number}", - "issue_comment_url": "https://api.github.com/repos/nhost/nhost/issues/comments{/number}", - "contents_url": "https://api.github.com/repos/nhost/nhost/contents/{+path}", - "compare_url": "https://api.github.com/repos/nhost/nhost/compare/{base}...{head}", - "merges_url": "https://api.github.com/repos/nhost/nhost/merges", - "archive_url": "https://api.github.com/repos/nhost/nhost/{archive_format}{/ref}", - "downloads_url": "https://api.github.com/repos/nhost/nhost/downloads", - "issues_url": "https://api.github.com/repos/nhost/nhost/issues{/number}", - "pulls_url": "https://api.github.com/repos/nhost/nhost/pulls{/number}", - "milestones_url": "https://api.github.com/repos/nhost/nhost/milestones{/number}", - "notifications_url": "https://api.github.com/repos/nhost/nhost/notifications{?since,all,participating}", - "labels_url": "https://api.github.com/repos/nhost/nhost/labels{/name}", - "releases_url": "https://api.github.com/repos/nhost/nhost/releases{/id}", - "deployments_url": "https://api.github.com/repos/nhost/nhost/deployments", - "created_at": "2021-02-09T13:33:34Z", - "updated_at": "2022-01-20T10:02:04Z", - "pushed_at": "2022-01-20T11:31:23Z", - "git_url": "git://github.com/nhost/nhost.git", - "ssh_url": "git@github.com:nhost/nhost.git", - "clone_url": "https://github.com/nhost/nhost.git", - "svn_url": "https://github.com/nhost/nhost", - "homepage": "https://nhost.io", - "size": 5433, - "stargazers_count": 1426, - "watchers_count": 1426, - "language": "TypeScript", - "has_issues": true, - "has_projects": true, - "has_downloads": true, - "has_wiki": true, - "has_pages": false, - "forks_count": 72, - "mirror_url": null, - "archived": false, - "disabled": false, - "open_issues_count": 27, - "license": { - "key": "mit", - "name": "MIT License", - "spdx_id": "MIT", - "url": "https://api.github.com/licenses/mit", - "node_id": "MDc6TGljZW5zZTEz" - }, - "allow_forking": true, - "is_template": false, - "topics": [ - "authentication", - "backend", - "database", - "flutter", - "graphql", - "hasura", - "postgres", - "serverless", - "serverless-functions", - "storage" - ], - "visibility": "public", - "forks": 72, - "open_issues": 27, - "watchers": 1426, - "default_branch": "main", - "score": 1 - }, - "data": { - "2022-01-20": { - "date": "2022-01-20", - "value": 1423 - }, - "2022-01-19": { - "date": "2022-01-19", - "value": 1418 - }, - "2022-01-18": { - "date": "2022-01-18", - "value": 1417 - }, - "2022-01-17": { - "date": "2022-01-17", - "value": 1413 - }, - "2022-01-16": { - "date": "2022-01-16", - "value": 1407 - }, - "2022-01-15": { - "date": "2022-01-15", - "value": 1407 - }, - "2022-01-14": { - "date": "2022-01-14", - "value": 1400 - }, - "2022-01-13": { - "date": "2022-01-13", - "value": 1399 - }, - "2022-01-12": { - "date": "2022-01-12", - "value": 1392 - }, - "2022-01-11": { - "date": "2022-01-11", - "value": 1391 - }, - "2022-01-10": { - "date": "2022-01-10", - "value": 1387 - }, - "2022-01-09": { - "date": "2022-01-09", - "value": 1382 - }, - "2022-01-08": { - "date": "2022-01-08", - "value": 1379 - }, - "2022-01-07": { - "date": "2022-01-07", - "value": 1375 - }, - "2022-01-06": { - "date": "2022-01-06", - "value": 1371 - }, - "2022-01-05": { - "date": "2022-01-05", - "value": 1369 - }, - "2022-01-04": { - "date": "2022-01-04", - "value": 1365 - }, - "2022-01-03": { - "date": "2022-01-03", - "value": 1362 - }, - "2022-01-02": { - "date": "2022-01-02", - "value": 1361 - }, - "2022-01-01": { - "date": "2022-01-01", - "value": 1361 - }, - "2021-12-31": { - "date": "2021-12-31", - "value": 1358 - }, - "2021-12-30": { - "date": "2021-12-30", - "value": 1358 - }, - "2021-12-29": { - "date": "2021-12-29", - "value": 1355 - }, - "2021-12-28": { - "date": "2021-12-28", - "value": 1355 - }, - "2021-12-27": { - "date": "2021-12-27", - "value": 1353 - }, - "2021-12-26": { - "date": "2021-12-26", - "value": 1350 - }, - "2021-12-25": { - "date": "2021-12-25", - "value": 1349 - }, - "2021-12-24": { - "date": "2021-12-24", - "value": 1348 - }, - "2021-12-23": { - "date": "2021-12-23", - "value": 1347 - }, - "2021-12-22": { - "date": "2021-12-22", - "value": 1346 - }, - "2021-12-21": { - "date": "2021-12-21", - "value": 1342 - }, - "2021-12-20": { - "date": "2021-12-20", - "value": 1341 - }, - "2021-12-19": { - "date": "2021-12-19", - "value": 1339 - }, - "2021-12-18": { - "date": "2021-12-18", - "value": 1336 - }, - "2021-12-17": { - "date": "2021-12-17", - "value": 1330 - }, - "2021-12-16": { - "date": "2021-12-16", - "value": 1326 - }, - "2021-12-15": { - "date": "2021-12-15", - "value": 1324 - }, - "2021-12-14": { - "date": "2021-12-14", - "value": 1322 - }, - "2021-12-13": { - "date": "2021-12-13", - "value": 1316 - }, - "2021-12-12": { - "date": "2021-12-12", - "value": 1315 - }, - "2021-12-11": { - "date": "2021-12-11", - "value": 1312 - }, - "2021-12-10": { - "date": "2021-12-10", - "value": 1307 - }, - "2021-12-09": { - "date": "2021-12-09", - "value": 1300 - }, - "2021-12-08": { - "date": "2021-12-08", - "value": 1297 - }, - "2021-12-07": { - "date": "2021-12-07", - "value": 1289 - }, - "2021-12-06": { - "date": "2021-12-06", - "value": 1285 - }, - "2021-12-05": { - "date": "2021-12-05", - "value": 1282 - }, - "2021-12-04": { - "date": "2021-12-04", - "value": 1279 - }, - "2021-12-03": { - "date": "2021-12-03", - "value": 1276 - }, - "2021-12-02": { - "date": "2021-12-02", - "value": 1273 - }, - "2021-12-01": { - "date": "2021-12-01", - "value": 1262 - }, - "2021-11-30": { - "date": "2021-11-30", - "value": 1254 - }, - "2021-11-29": { - "date": "2021-11-29", - "value": 1252 - }, - "2021-11-28": { - "date": "2021-11-28", - "value": 1248 - }, - "2021-11-27": { - "date": "2021-11-27", - "value": 1244 - }, - "2021-11-26": { - "date": "2021-11-26", - "value": 1239 - }, - "2021-11-25": { - "date": "2021-11-25", - "value": 1238 - }, - "2021-11-24": { - "date": "2021-11-24", - "value": 1235 - }, - "2021-11-23": { - "date": "2021-11-23", - "value": 1230 - }, - "2021-11-22": { - "date": "2021-11-22", - "value": 1229 - }, - "2021-11-21": { - "date": "2021-11-21", - "value": 1226 - }, - "2021-11-20": { - "date": "2021-11-20", - "value": 1222 - }, - "2021-11-19": { - "date": "2021-11-19", - "value": 1219 - }, - "2021-11-18": { - "date": "2021-11-18", - "value": 1216 - }, - "2021-11-17": { - "date": "2021-11-17", - "value": 1207 - }, - "2021-11-16": { - "date": "2021-11-16", - "value": 1200 - }, - "2021-11-15": { - "date": "2021-11-15", - "value": 1198 - }, - "2021-11-14": { - "date": "2021-11-14", - "value": 1195 - }, - "2021-11-13": { - "date": "2021-11-13", - "value": 1189 - }, - "2021-11-12": { - "date": "2021-11-12", - "value": 1187 - }, - "2021-11-11": { - "date": "2021-11-11", - "value": 1182 - }, - "2021-11-10": { - "date": "2021-11-10", - "value": 1169 - }, - "2021-11-09": { - "date": "2021-11-09", - "value": 1164 - }, - "2021-11-08": { - "date": "2021-11-08", - "value": 1149 - }, - "2021-11-07": { - "date": "2021-11-07", - "value": 1137 - }, - "2021-11-06": { - "date": "2021-11-06", - "value": 1128 - }, - "2021-11-05": { - "date": "2021-11-05", - "value": 1122 - }, - "2021-11-04": { - "date": "2021-11-04", - "value": 1111 - }, - "2021-11-03": { - "date": "2021-11-03", - "value": 1092 - }, - "2021-11-02": { - "date": "2021-11-02", - "value": 1075 - }, - "2021-11-01": { - "date": "2021-11-01", - "value": 1049 - }, - "2021-10-31": { - "date": "2021-10-31", - "value": 1014 - }, - "2021-10-30": { - "date": "2021-10-30", - "value": 1000 - }, - "2021-10-29": { - "date": "2021-10-29", - "value": 998 - }, - "2021-10-28": { - "date": "2021-10-28", - "value": 987 - }, - "2021-10-27": { - "date": "2021-10-27", - "value": 965 - }, - "2021-10-26": { - "date": "2021-10-26", - "value": 942 - }, - "2021-10-25": { - "date": "2021-10-25", - "value": 928 - }, - "2021-10-24": { - "date": "2021-10-24", - "value": 913 - }, - "2021-10-23": { - "date": "2021-10-23", - "value": 899 - }, - "2021-10-22": { - "date": "2021-10-22", - "value": 890 - }, - "2021-10-21": { - "date": "2021-10-21", - "value": 882 - }, - "2021-10-20": { - "date": "2021-10-20", - "value": 871 - } - }, - "color": "#15FF00" - } - ], - "last_updated_at": "2022-01-20", - "timeframe": { - "label": "Last three months", - "value": "last_three_months", - "date": "2021-10-20" - } - } - }, - { - "type": "builder" - }, - { - "type": "newest-members" - }, - { - "type": "channel-distribution" - }, - { - "type": "number-activities" - }, - { - "type": "integrations" - }, - { - "type": "latest-activities" - }, - { - "type": "inactive-members" - }, - { - "type": "builder" - }, - { - "type": "integrations" - }, - { - "type": "time-to-first-interaction-graph" - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-20T12:08:03.168Z", - "start": "2022-01-13T00:00:00.000Z", - "end": "2022-01-20T00:00:00.000Z" - } - }, - { - "type": "number-activities" - }, - { - "type": "number-members" - }, - { - "type": "number-activities-graph" - }, - { - "type": "inactive-members-graph" - }, - { - "type": "number-members-graph" - }, - { - "type": "channel-distribution" - }, - { - "type": "benchmark" - }, - { - "type": "newest-members" - }, - { - "type": "latest-activities" - }, - { - "type": "inactive-members" - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-25T16:47:57.727Z", - "start": "2022-01-18T00:00:00.000Z", - "end": "2022-01-25T00:00:00.000Z" - } - }, - { - "type": "benchmark" - }, - { - "type": "number-activities-graph" - }, - { - "type": "newest-members" - }, - { - "type": "number-activities" - }, - { - "type": "number-members" - }, - { - "type": "builder" - }, - { - "type": "integrations" - }, - { - "type": "latest-activities" - }, - { - "type": "number-members-graph" - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-01-27T10:11:38.938Z", - "start": "2022-01-20T00:00:00.000Z", - "end": "2022-01-27T00:00:00.000Z" - } - }, - { - "type": "number-activities" - }, - { - "type": "builder" - }, - { - "type": "integrations" - }, - { - "type": "latest-activities" - }, - { - "type": "number-members-graph" - }, - { - "type": "number-members" - }, - { - "type": "benchmark" - }, - { - "type": "newest-members" - }, - { - "type": "inactive-members" - }, - { - "type": "number-activities-graph" - }, - { - "type": "latest-activities" - }, - { - "type": "time-to-first-interaction", - "cache": [0, 1], - "settings": { - "last_computed_at": "2022-02-14T08:41:38.812Z", - "start": "2022-02-07T00:00:00.000Z", - "end": "2022-02-14T00:00:00.000Z" - } - }, - { - "type": "number-members" - }, - { - "type": "inactive-members" - }, - { - "type": "number-activities-graph" - }, - { - "type": "number-members-graph" - }, - { - "type": "builder" - }, - { - "type": "benchmark" - }, - { - "type": "newest-members" - }, - { - "type": "number-activities" - }, - { - "type": "integrations" - }, - { - "type": "number-members" - }, - { - "type": "inactive-members" - }, - { - "type": "latest-activities" - }, - { - "type": "number-activities-graph" - }, - { - "type": "benchmark" - }, - { - "type": "number-activities" - }, - { - "type": "newest-members" - }, - { - "type": "builder" - }, - { - "type": "time-to-first-interaction", - "cache": [0, 0], - "settings": { - "last_computed_at": "2022-02-15T23:21:35.145Z", - "start": "2022-02-09T00:00:00.000Z", - "end": "2022-02-16T00:00:00.000Z" - } - }, - { - "type": "number-members-graph" - }, - { - "type": "integrations" - } -] diff --git a/backend/src/database/migrations/R__array_accum.sql b/backend/src/database/migrations/R__array_accum.sql index 89dc71b789..8c9b24adeb 100644 --- a/backend/src/database/migrations/R__array_accum.sql +++ b/backend/src/database/migrations/R__array_accum.sql @@ -1,5 +1,5 @@ -CREATE AGGREGATE array_accum (ANYARRAY) ( +CREATE OR REPLACE AGGREGATE array_accum (anycompatiblearray) ( SFUNC = array_cat, - STYPE = ANYARRAY, + STYPE = anycompatiblearray, INITCOND = '{}' - ); +); diff --git a/backend/src/database/migrations/R__cubejs-materialized-views.sql b/backend/src/database/migrations/R__cubejs-materialized-views.sql deleted file mode 100644 index efd002d274..0000000000 --- a/backend/src/database/migrations/R__cubejs-materialized-views.sql +++ /dev/null @@ -1,69 +0,0 @@ -DROP MATERIALIZED VIEW IF EXISTS mv_members_cube; -CREATE MATERIALIZED VIEW IF NOT EXISTS mv_members_cube AS -SELECT - m.id, - m."tenantId", - m."joinedAt" AS "joinedAt", - m.score, - COALESCE(m.attributes->'location'->>'default', '')::VARCHAR(256) AS location, - COALESCE((m.attributes->'isBot'->>'default')::BOOLEAN, FALSE) as "isBot", - COALESCE((m.attributes->'isTeamMember'->>'default')::BOOLEAN, FALSE) as "isTeamMember", - COALESCE((m.attributes->'isOrganization'->>'default')::BOOLEAN, FALSE) as "isOrganization", - FLOOR(EXTRACT(EPOCH FROM m."joinedAt"))::BIGINT AS "joinedAtUnixTs" -FROM members m -; - -DROP MATERIALIZED VIEW IF EXISTS mv_activities_cube; -CREATE MATERIALIZED VIEW IF NOT EXISTS mv_activities_cube AS -SELECT - a.id, - a."isContribution", - a."tenantId", - a."memberId", - (a.platform)::VARCHAR(24), - (a.channel)::VARCHAR(256), - a.timestamp, - (a.type)::VARCHAR(256), - CASE - WHEN a.sentiment->>'sentiment' is null THEN 'no data' - WHEN (a.sentiment->>'sentiment')::integer < 34 THEN 'negative' - WHEN (a.sentiment->>'sentiment')::integer > 66 THEN 'positive' - ELSE 'neutral' - END::VARCHAR(8) AS "sentimentMood", - a."organizationId", - a."segmentId", - a."conversationId" -FROM activities a -WHERE a."deletedAt" IS NULL -; - -DROP MATERIALIZED VIEW IF EXISTS mv_organizations_cube; -CREATE MATERIALIZED VIEW IF NOT EXISTS mv_organizations_cube AS -SELECT - o.id, - o."tenantId", - o."createdAt", - MIN(m."joinedAt") AS "earliestJoinedAt" -FROM organizations o -JOIN "memberOrganizations" mo ON o.id = mo."organizationId" -JOIN members m ON mo."memberId" = m.id -JOIN activities a ON o.id = a."organizationId" -GROUP BY o.id -; - -DROP MATERIALIZED VIEW IF EXISTS mv_segments_cube; -CREATE MATERIALIZED VIEW IF NOT EXISTS mv_segments_cube AS -SELECT - id, - name -FROM segments -; - -CREATE INDEX IF NOT EXISTS mv_members_cube_tenant ON mv_members_cube ("tenantId"); -CREATE INDEX IF NOT EXISTS mv_activities_cube_timestamp ON mv_activities_cube (timestamp); -CREATE INDEX IF NOT EXISTS mv_activities_cube_org_id ON mv_activities_cube ("organizationId"); - -CREATE UNIQUE INDEX IF NOT EXISTS mv_members_cube_id ON mv_members_cube (id); -CREATE UNIQUE INDEX IF NOT EXISTS mv_activities_cube_id ON mv_activities_cube (id); -CREATE UNIQUE INDEX IF NOT EXISTS mv_organizations_cube_id ON mv_organizations_cube (id); -CREATE UNIQUE INDEX IF NOT EXISTS mv_segments_cube_id ON mv_segments_cube (id); diff --git a/backend/src/database/migrations/R__memberActivityAggregatesMVs.sql b/backend/src/database/migrations/R__memberActivityAggregatesMVs.sql deleted file mode 100644 index 050805b503..0000000000 --- a/backend/src/database/migrations/R__memberActivityAggregatesMVs.sql +++ /dev/null @@ -1,54 +0,0 @@ -DROP MATERIALIZED VIEW "memberActivityAggregatesMVs"; - -CREATE MATERIALIZED VIEW "memberActivityAggregatesMVs" AS -WITH - identities AS ( - SELECT - mi."memberId", - mi."segmentId", - ARRAY_AGG(DISTINCT mi.platform) AS identities, - JSONB_OBJECT_AGG(mi.platform, mi.usernames) AS username - FROM ( - SELECT - "memberId", - platform, - "segmentId", - ARRAY_AGG(username) AS usernames - FROM ( - SELECT - mi."memberId", - mi.platform, - mi.username, - mi."createdAt", - ms."segmentId", - ROW_NUMBER() - OVER (PARTITION BY - mi."memberId", - mi.platform, - ms."segmentId" - ORDER BY mi."createdAt" DESC) = 1 AS is_latest - FROM "memberIdentities" mi - JOIN "memberSegments" ms ON ms."memberId" = mi."memberId" - ) sub - WHERE is_latest - GROUP BY "memberId", platform, "segmentId" - ) mi - GROUP BY mi."memberId", "segmentId" - ) -SELECT - m.id, - MAX(a."timestamp") AS "lastActive", - i.identities, - i.username, - i."segmentId", - COUNT(a.id) AS "activityCount", - ARRAY_AGG(DISTINCT CONCAT(a.platform, ':', a.type)) FILTER (WHERE a.platform IS NOT NULL) AS "activityTypes", - ARRAY_AGG(DISTINCT a.platform) FILTER (WHERE a.platform IS NOT NULL) AS "activeOn", - COUNT(DISTINCT a."timestamp"::DATE) AS "activeDaysCount", - ROUND(AVG((a.sentiment ->> 'sentiment')::NUMERIC), 2) AS "averageSentiment" -FROM members m -JOIN identities i ON m.id = i."memberId" -LEFT JOIN activities a ON m.id = a."memberId" AND a."deletedAt" IS NULL -GROUP BY m.id, i.identities, i.username, i."segmentId"; - -CREATE UNIQUE INDEX ix_memberactivityaggregatesmvs_memberid_segmentid ON "memberActivityAggregatesMVs" (id, "segmentId"); diff --git a/backend/src/database/migrations/R__memberEnrichmentMaterializedViews.sql b/backend/src/database/migrations/R__memberEnrichmentMaterializedViews.sql new file mode 100644 index 0000000000..016e0395d2 --- /dev/null +++ b/backend/src/database/migrations/R__memberEnrichmentMaterializedViews.sql @@ -0,0 +1,976 @@ +-- global member activity counts +drop materialized view if exists "membersGlobalActivityCount" cascade; +create materialized view "membersGlobalActivityCount" as +select msa."memberId", + sum(msa."activityCount") AS total_count +from "memberSegmentsAgg" msa +where msa."segmentId" IN ( + select id + from segments + where "grandparentId" is not null + and "parentId" is not null + ) +group by msa."memberId" +order by sum(msa."activityCount") desc; +create unique index ix_member_global_activity_count_member_id on "membersGlobalActivityCount" ("memberId"); +create index ix_member_global_activity_count on "membersGlobalActivityCount" (total_count); + +-- member enrichment monitoring (total) +drop materialized view if exists "memberEnrichmentMonitoringTotal"; +create materialized view "memberEnrichmentMonitoringTotal" as +with all_members as ( + select count(*) as count from members +), +total_enrichable_members as ( + with enrichable_in_at_least_one_source as ( + select mem.id + from members mem + inner join "memberIdentities" mi on mem.id = mi."memberId" and mi.verified + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mem.id + left join "memberEnrichmentCache" mec on mec."memberId" = mem.id + where ( + (mi.verified and + ((mi.type = 'username' AND mi.platform = 'github') OR (mi.type = 'email'))) + OR + ("membersGlobalActivityCount".total_count > 10 AND mi.type = 'email' and mi.verified) + OR + ( + ("membersGlobalActivityCount".total_count > 500) AND + (mem."displayName" like '% %') AND + (mem.attributes -> 'location' ->> 'default' is not null and + mem.attributes -> 'location' ->> 'default' <> '') AND + ( + (mem.attributes -> 'websiteUrl' ->> 'default' is not null and + mem.attributes -> 'websiteUrl' ->> 'default' <> '') OR + (mi.verified AND mi.type = 'username' and mi.platform = 'github') OR + (mi.verified AND mi.type = 'email') + ) + ) + OR + ((mi.verified AND mi.type = 'username' and mi.platform = 'linkedin')) + ) + group by mem.id + order by mem.id desc) + select count(*) as count from enrichable_in_at_least_one_source + ), + attempted_to_enrich_among_enrichable_members as ( + with enrichable_by_clearbit as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + where mec.source = 'clearbit' + and mec."memberId" in ( + select distinct mi."memberId" + from "memberIdentities" mi + where mi.verified and mi.type = 'email' + ) + ), + enrichable_by_progai as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + where mec.source = 'progai' + and mec."memberId" in ( + select distinct mi."memberId" + from "memberIdentities" mi + where mi.verified + and mi.type = 'username' + and mi.platform = 'github' + ) + ), + enrichable_by_serp as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + where mec.source = 'serp' + and mec."memberId" in ( + select distinct mem.id as "memberId" + from members mem + inner join "memberIdentities" mi on mem.id = mi."memberId" and mi.verified + inner join "membersGlobalActivityCount" + on "membersGlobalActivityCount"."memberId" = mem.id + where + ("membersGlobalActivityCount".total_count > 500 and + (mem."displayName" like '% %') and + (mem.attributes -> 'location' ->> 'default' is not null and + mem.attributes -> 'location' ->> 'default' <> '') and + ((mem.attributes -> 'websiteUrl' ->> 'default' is not null and + mem.attributes -> 'websiteUrl' ->> 'default' <> '') OR + (mi.verified and mi.type = 'username' and mi.platform = 'github') OR + (mi.verified and mi.type = 'email'))) + group by mem.id + order by mem.id desc + ) + ), + enrichable_by_progai_linkedin_scraper as ( + with clearbit_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + where mec.source = 'clearbit' + and mec.data is not null + and mec.data -> 'linkedin' ->> 'handle' is not null), + progai_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + where mec.source = 'progai' + and mec.data is not null + and mec.data ->> 'linkedin_url' is not null), + serp_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + where mec.source = 'serp' + and mec.data is not null + ), + existing_verified_linkedin_identities as ( + select distinct mi."memberId" + from "memberIdentities" mi + where mi.platform = 'linkedin' and mi.verified + group by mi."memberId"), + all_unique_members_enrichable_by_progai_linkedin_scraper as ( + select "memberId" from clearbit_linkedin_profiles + union + select "memberId" from progai_linkedin_profiles + union + select "memberId" from serp_linkedin_profiles + union + select "memberId" from existing_verified_linkedin_identities) + select distinct "memberId" + from all_unique_members_enrichable_by_progai_linkedin_scraper + ), + enrichable_by_crustdata_linkedin_scraper as ( + with clearbit_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mec."memberId" + where "membersGlobalActivityCount".total_count > 1000 + and mec.source = 'clearbit' + and mec.data is not null + and mec.data -> 'linkedin' ->> 'handle' is not null), + progai_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mec."memberId" + where "membersGlobalActivityCount".total_count > 1000 + and mec.source = 'progai' + and mec.data is not null + and mec.data ->> 'linkedin_url' is not null), + serp_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mec."memberId" + where "membersGlobalActivityCount".total_count > 1000 + and mec.source = 'serp' + and mec.data is not null), + existing_verified_linkedin_identities as ( + select distinct mi."memberId" + from "memberIdentities" mi + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mi."memberId" + where "membersGlobalActivityCount".total_count > 1000 + and mi.verified + and mi.platform = 'linkedin' + group by mi."memberId"), + all_unique_members_enrichable_by_crustdata as ( + select "memberId" from clearbit_linkedin_profiles + union + select "memberId" from progai_linkedin_profiles + union + select "memberId" from serp_linkedin_profiles + union + select "memberId" from existing_verified_linkedin_identities) + select distinct "memberId" from all_unique_members_enrichable_by_crustdata + ), + unique_members as ( + select "memberId" from enrichable_by_clearbit + union + select "memberId" from enrichable_by_progai + union + select "memberId" from enrichable_by_serp + union + select "memberId" from enrichable_by_progai_linkedin_scraper + union + select "memberId" from enrichable_by_crustdata_linkedin_scraper) + select count(distinct "memberId") as count + from unique_members) + SELECT (SELECT count FROM all_members) as "totalMembers", + (SELECT count FROM total_enrichable_members) as "enrichableMembers", + (SELECT count FROM attempted_to_enrich_among_enrichable_members) as "attemptedToEnrich"; + + +-- member enrichment monitoring (clearbit) +drop materialized view if exists "memberEnrichmentMonitoringClearbit"; +create materialized view "memberEnrichmentMonitoringClearbit" as +with clearbit_enrichable_members as ( + select count(distinct mi."memberId") as count + from "memberIdentities" mi + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mi."memberId" + where mi.verified and mi.type = 'email' and "membersGlobalActivityCount".total_count > 10 +), +attempted_to_enrich_among_clearbit_enrichable_members as ( + select count(distinct mec."memberId") as count + from "memberEnrichmentCache" mec + where mec."memberId" in ( + select distinct mi."memberId" + from "memberIdentities" mi + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mi."memberId" + where mi.verified and mi.type = 'email' and "membersGlobalActivityCount".total_count > 10 + ) +), +clearbit_hit_count_among_attempted as ( + select count(*) as count + from "memberEnrichmentCache" mec + where mec.data is not null + and mec.source = 'clearbit' + and mec."memberId" in ( + select distinct mi."memberId" + from "memberIdentities" mi + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mi."memberId" + where mi.verified and mi.type = 'email' and "membersGlobalActivityCount".total_count > 10 + ) +) +select + (select count from clearbit_enrichable_members) as "enrichableMembers", + (select count from attempted_to_enrich_among_clearbit_enrichable_members) as "attemptedToEnrich", + case when (select count from clearbit_enrichable_members)::numeric = 0 then 0 else + (round((select count from attempted_to_enrich_among_clearbit_enrichable_members)::numeric / + (select count from clearbit_enrichable_members)::numeric * 100, 2)) end as progress, + ((select count from clearbit_hit_count_among_attempted)) as "hitCount", + case when (select count from attempted_to_enrich_among_clearbit_enrichable_members)::numeric = 0 then 0 else + (round((select count from clearbit_hit_count_among_attempted)::numeric / + (select count from attempted_to_enrich_among_clearbit_enrichable_members)::numeric * 100, + 2)) end as "hitRate"; + + +-- member enrichment monitoring (crustdata) +drop materialized view if exists "memberEnrichmentMonitoringCrustdata"; +create materialized view "memberEnrichmentMonitoringCrustdata" as +with crustdata_members_with_scrapable_profiles AS ( + with clearbit_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mec."memberId" + where "membersGlobalActivityCount".total_count > 1000 + and mec.source = 'clearbit' + and mec.data is not null + and mec.data -> 'linkedin' ->> 'handle' is not null), + progai_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mec."memberId" + where "membersGlobalActivityCount".total_count > 1000 + and mec.source = 'progai' + and mec.data is not null + and mec.data ->> 'linkedin_url' is not null), + serp_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mec."memberId" + where "membersGlobalActivityCount".total_count > 1000 + and mec.source = 'serp' + and mec.data is not null), + existing_verified_linkedin_identities as ( + select distinct mi."memberId" + from "memberIdentities" mi + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mi."memberId" + where "membersGlobalActivityCount".total_count > 1000 + and mi.verified + and mi.platform = 'linkedin' + group by mi."memberId"), + unique_members AS ( + select "memberId" from clearbit_linkedin_profiles + union + select "memberId" from progai_linkedin_profiles + union + select "memberId" from serp_linkedin_profiles + union + select "memberId" from existing_verified_linkedin_identities) +select count(distinct "memberId") as count +from unique_members), + crustdata_attempted_to_scrape_among_scrapable_profiles as ( + select count(*) as count + from "memberEnrichmentCache" mec + where mec.source = 'crustdata' + and mec."memberId" in ( + with clearbit_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mec."memberId" + where "membersGlobalActivityCount".total_count > 1000 + and mec.source = 'clearbit' + and mec.data is not null + and mec.data -> 'linkedin' ->> 'handle' is not null), + progai_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mec."memberId" + where "membersGlobalActivityCount".total_count > 1000 + and mec.source = 'progai' + and mec.data is not null + and mec.data ->> 'linkedin_url' is not null), + serp_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mec."memberId" + where "membersGlobalActivityCount".total_count > 1000 + and mec.source = 'serp' + and mec.data is not null), + existing_verified_linkedin_identities as ( + select distinct mi."memberId" + from "memberIdentities" mi + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mi."memberId" + where "membersGlobalActivityCount".total_count > 1000 + and mi.verified + and mi.platform = 'linkedin' + group by mi."memberId"), + unique_members as ( + select "memberId" from clearbit_linkedin_profiles + union + select "memberId" from progai_linkedin_profiles + union + select "memberId" from serp_linkedin_profiles + union + select "memberId" from existing_verified_linkedin_identities) + select distinct "memberId" from unique_members)), + crustdata_hit_count_among_attempted as ( + select count(*) as count + from "memberEnrichmentCache" mec + where mec.source = 'crustdata' + and mec.data is not null + and mec."memberId" in ( + with clearbit_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mec."memberId" + where "membersGlobalActivityCount".total_count > 1000 + and mec.source = 'clearbit' + and mec.data is not null + and mec.data -> 'linkedin' ->> 'handle' is not null), + progai_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mec."memberId" + where "membersGlobalActivityCount".total_count > 1000 + and mec.source = 'progai' + and mec.data is not null + and mec.data ->> 'linkedin_url' is not null), + serp_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mec."memberId" + where "membersGlobalActivityCount".total_count > 1000 + and mec.source = 'serp' + and mec.data is not null), + existing_verified_linkedin_identities as ( + select distinct mi."memberId" + from "memberIdentities" mi + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mi."memberId" + where "membersGlobalActivityCount".total_count > 1000 + and mi.verified + and mi.platform = 'linkedin' + group by mi."memberId"), + unique_members as ( + select "memberId" from clearbit_linkedin_profiles + union + select "memberId" from progai_linkedin_profiles + union + select "memberId" from serp_linkedin_profiles + union + select "memberId" from existing_verified_linkedin_identities) +select distinct "memberId" from unique_members)) +select + (select count from crustdata_members_with_scrapable_profiles) as "membersWithScrapableProfiles", + (select count from crustdata_attempted_to_scrape_among_scrapable_profiles) as "attemptedToScrape", + case when (select count from crustdata_members_with_scrapable_profiles)::numeric = 0 then 0 else + (round((select count from crustdata_attempted_to_scrape_among_scrapable_profiles)::numeric / + (select count from crustdata_members_with_scrapable_profiles)::numeric * 100, 2)) end as progress, + ((select count from crustdata_hit_count_among_attempted)) as "hitCount", + case when (select count from crustdata_attempted_to_scrape_among_scrapable_profiles)::numeric = 0 then 0 else + (round((select count from crustdata_hit_count_among_attempted)::numeric / + (select count from crustdata_attempted_to_scrape_among_scrapable_profiles)::numeric * 100, + 2)) end as "hitRate"; + +-- member enrichment monitoring (progai-github) +drop materialized view if exists "memberEnrichmentMonitoringProgaiGithub"; +create materialized view "memberEnrichmentMonitoringProgaiGithub" as +with progai_enrichable_members as ( + select count(distinct mi."memberId") as count + from "memberIdentities" mi + where mi.verified + and mi.type = 'username' and mi.platform = 'github' +), +attempted_to_enrich_among_progai_enrichable_members as ( + select count(distinct mec."memberId") as count + from "memberEnrichmentCache" mec + where mec."memberId" in ( + select distinct mi."memberId" + from "memberIdentities" mi + where mi.verified + and mi.type = 'username' and mi.platform = 'github' + ) +), +progai_hit_count_among_attempted as ( + select count(*) as count + from "memberEnrichmentCache" mec + where mec.data is not null and mec.source = 'progai' and mec."memberId" in ( + select distinct mi."memberId" + from "memberIdentities" mi + where mi.verified + and mi.type = 'username' and mi.platform = 'github' + ) +) +select + (select count from progai_enrichable_members) as "enrichableMembers", + (select count from attempted_to_enrich_among_progai_enrichable_members) as "attemptedToEnrich", + case when (select count from progai_enrichable_members)::numeric = 0 then 0 else + round((select count from attempted_to_enrich_among_progai_enrichable_members)::numeric / (select count from progai_enrichable_members)::numeric * 100, 2) end as progress, + (select count from progai_hit_count_among_attempted) as "hitCount", + case when (select count from attempted_to_enrich_among_progai_enrichable_members)::numeric = 0 then 0 else + round((select count from progai_hit_count_among_attempted)::numeric / (select count from attempted_to_enrich_among_progai_enrichable_members)::numeric * 100, 2) end as "hitRate"; + +-- member enrichment monitoring (progai-linkedin) +drop materialized view if exists "memberEnrichmentMonitoringProgaiLinkedin"; +create materialized view "memberEnrichmentMonitoringProgaiLinkedin" as +with progai_linkedin_members_with_scrapable_profiles as ( + with clearbit_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + where mec.source = 'clearbit' + and mec.data is not null + and mec.data->'linkedin'->>'handle' is not null + ), + progai_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + where mec.source = 'progai' + and mec.data is not null + and mec.data->>'linkedin_url' is not null + ), + serp_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + where mec.source = 'serp' + and mec.data is not null + ), + existing_verified_linkedin_identities as ( + select distinct mi."memberId" + from "memberIdentities" mi + where mi.platform = 'linkedin' + and mi.verified + group by mi."memberId" + ), + unique_members as ( + select "memberId" from clearbit_linkedin_profiles + union + select "memberId" from progai_linkedin_profiles + union + select "memberId" from serp_linkedin_profiles + union + select "memberId" from existing_verified_linkedin_identities + ) + select count(distinct "memberId") as count from unique_members +), +progai_linkedin_attempted_to_scrape_among_scrapable_profiles as ( + select count(*) as count from "memberEnrichmentCache" mec where mec.source= 'progai-linkedin-scraper' + and mec."memberId" in ( + with clearbit_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + where mec.source = 'clearbit' + and mec.data is not null + and mec.data->'linkedin'->>'handle' is not null + ), + progai_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + where mec.source = 'progai' + and mec.data is not null + and mec.data->>'linkedin_url' is not null + ), + serp_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + where mec.source = 'serp' + and mec.data is not null + ), + existing_verified_linkedin_identities as ( + select distinct mi."memberId" + from "memberIdentities" mi + where mi.platform = 'linkedin' + and mi.verified + group by mi."memberId" + ), + unique_members as ( + select "memberId" from clearbit_linkedin_profiles + union + select "memberId" from progai_linkedin_profiles + union + select "memberId" from serp_linkedin_profiles + union + select "memberId" from existing_verified_linkedin_identities + ) + select distinct "memberId" from unique_members + ) +), +progai_linkedin_scraper_hit_count_among_attempted as ( + select count(*) as count from "memberEnrichmentCache" mec where mec.source= 'progai-linkedin-scraper' + and mec.data is not null and mec."memberId" in ( + with clearbit_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + where mec.source = 'clearbit' + and mec.data is not null + and mec.data->'linkedin'->>'handle' is not null + ), + progai_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + where mec.source = 'progai' + and mec.data is not null + and mec.data->>'linkedin_url' is not null + ), + serp_linkedin_profiles as ( + select distinct mec."memberId" + from "memberEnrichmentCache" mec + where mec.source = 'serp' and mec.data is not null + ), + existing_verified_linkedin_identities as ( + select distinct mi."memberId" + from "memberIdentities" mi + where mi.platform = 'linkedin' + and mi.verified + group by mi."memberId" + ), + unique_members as ( + select "memberId" from clearbit_linkedin_profiles + union + select "memberId" from progai_linkedin_profiles + union + select "memberId" from serp_linkedin_profiles + union + select "memberId" from existing_verified_linkedin_identities + ) + select distinct "memberId" from unique_members + ) +) +select + (select count from progai_linkedin_members_with_scrapable_profiles) as "membersWithScrapableProfiles", + (select count from progai_linkedin_attempted_to_scrape_among_scrapable_profiles) as "attemptedToScrape", + case when (select count from progai_linkedin_members_with_scrapable_profiles)::numeric = 0 then 0 else + (round((select count from progai_linkedin_attempted_to_scrape_among_scrapable_profiles)::numeric / + (select count from progai_linkedin_members_with_scrapable_profiles)::numeric * 100, 2)) end as progress, + ((select count from progai_linkedin_scraper_hit_count_among_attempted)) as "hitCount", + case when (select count from progai_linkedin_attempted_to_scrape_among_scrapable_profiles)::numeric = 0 then 0 else + (round((select count from progai_linkedin_scraper_hit_count_among_attempted)::numeric / + (select count from progai_linkedin_attempted_to_scrape_among_scrapable_profiles)::numeric * 100, + 2)) end as "hitRate"; + +-- member enrichment monitoring (serp) +drop materialized view if exists "memberEnrichmentMonitoringSerp"; +create materialized view "memberEnrichmentMonitoringSerp" as +with serp_enrichable_members as ( + select count(distinct mem.id) as count from members mem + join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mem.id + join "memberIdentities" mi on mi."memberId" = mem.id + where + ("membersGlobalActivityCount".total_count > 500) and + (mem."displayName" like '% %') and + (mem.attributes->'location'->>'default' is not null and mem.attributes->'location'->>'default' <> '') and + ((mem.attributes->'websiteUrl'->>'default' is not null and mem.attributes->'websiteUrl'->>'default' <> '') or + (mi.verified and mi.type = 'username' and mi.platform = 'github') or + (mi.verified and mi.type = 'email') + ) +), +attempted_to_enrich_among_serp_enrichable_members as ( + select count(*) as count + from "memberEnrichmentCache" mec + where mec.source = 'serp' and mec."memberId" in ( + select distinct mem.id + from members mem + join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mem.id + join "memberIdentities" mi on mi."memberId" = mem.id + where + ("membersGlobalActivityCount".total_count > 500) and + (mem."displayName" like '% %') and + (mem.attributes->'location'->>'default' is not null and mem.attributes->'location'->>'default' <> '') and + ((mem.attributes->'websiteUrl'->>'default' is not null and mem.attributes->'websiteUrl'->>'default' <> '') or + (mi.verified and mi.type = 'username' and mi.platform = 'github') or + (mi.verified and mi.type = 'email')) + ) +), +serp_hit_count_among_attempted as ( + select count(*) as count + from "memberEnrichmentCache" mec + where mec.data is not null + and mec.source = 'serp' + and mec."memberId" in ( + select distinct mem.id from members mem + join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mem.id + join "memberIdentities" mi on mi."memberId" = mem.id + where + ("membersGlobalActivityCount".total_count > 500) and + (mem."displayName" like '% %') and + (mem.attributes->'location'->>'default' is not null and mem.attributes->'location'->>'default' <> '') and + ((mem.attributes->'websiteUrl'->>'default' is not null and mem.attributes->'websiteUrl'->>'default' <> '') or + (mi.verified and mi.type = 'username' and mi.platform = 'github') or + (mi.verified and mi.type = 'email') + ) + ) +) +select + (select count from serp_enrichable_members) as "enrichableMembers", + (select count from attempted_to_enrich_among_serp_enrichable_members) as "attemptedToEnrich", + case when (select count from serp_enrichable_members)::numeric = 0 then 0 else + round((select count from attempted_to_enrich_among_serp_enrichable_members)::numeric / (select count from serp_enrichable_members)::numeric * 100, 2) end as progress, + (select count from serp_hit_count_among_attempted) as "hitCount", + case when (select count from attempted_to_enrich_among_serp_enrichable_members)::numeric = 0 then 0 else + round((select count from serp_hit_count_among_attempted)::numeric / (select count from attempted_to_enrich_among_serp_enrichable_members)::numeric * 100, 2) end as "hitRate"; + + +-- member enrichment monitoring (entity updates) +drop materialized view if exists "memberEnrichmentMonitoringEntityUpdates"; +create materialized view "memberEnrichmentMonitoringEntityUpdates" as +with enriched_total as ( + WITH total_members as ( + select count(*) as count + from "memberEnrichments" + WHERE "lastUpdatedAt" is not null + ), + members_with_more_than_1000_activity as ( + select count(*) as count + from "memberEnrichments" m + LEFT JOIN "membersGlobalActivityCount" act_count + ON act_count."memberId" = m."memberId" + WHERE act_count.total_count > 1000 + AND m."lastUpdatedAt" is not null + ), + members_with_more_than_100_activity as ( + select count(*) as count + from "memberEnrichments" m + LEFT JOIN "membersGlobalActivityCount" act_count + ON act_count."memberId" = m."memberId" + WHERE act_count.total_count > 100 and act_count.total_count < 1000 + AND m."lastUpdatedAt" is not null + ), + members_with_more_than_10_activity as ( + select count(*) as count + from "memberEnrichments" m + LEFT JOIN "membersGlobalActivityCount" act_count + ON act_count."memberId" = m."memberId" + WHERE act_count.total_count > 10 and act_count.total_count < 100 + AND m."lastUpdatedAt" is not null + ) + select + (select count from total_members) as "enriched_today_total", + (select count from members_with_more_than_1000_activity) as "enriched_today_members_more_than_1000_activity", + (select count from members_with_more_than_100_activity) as "enriched_today_members_more_than_100_activity", + (select count from members_with_more_than_10_activity) as "enriched_today_members_more_than_10_activity" +), +enriched_today as ( + WITH total_members as ( + select count(*) as count + from "memberEnrichments" + WHERE "lastUpdatedAt" >= date_trunc('day', now()) + ), + members_with_more_than_1000_activity as ( + select count(*) as count + from "memberEnrichments" m + LEFT JOIN "membersGlobalActivityCount" act_count + ON act_count."memberId" = m."memberId" + WHERE act_count.total_count > 1000 + AND m."lastUpdatedAt" >= date_trunc('day', now()) + ), + members_with_more_than_100_activity as ( + select count(*) as count + from "memberEnrichments" m + LEFT JOIN "membersGlobalActivityCount" act_count + ON act_count."memberId" = m."memberId" + WHERE act_count.total_count > 100 and act_count.total_count < 1000 + AND m."lastUpdatedAt" >= date_trunc('day', now()) + ), + members_with_more_than_10_activity as ( + select count(*) as count + from "memberEnrichments" m + LEFT JOIN "membersGlobalActivityCount" act_count + ON act_count."memberId" = m."memberId" + WHERE act_count.total_count > 10 and act_count.total_count < 100 + AND m."lastUpdatedAt" >= date_trunc('day', now()) + ) + select + (select count from total_members) as "enriched_today_total", + (select count from members_with_more_than_1000_activity) as "enriched_today_members_more_than_1000_activity", + (select count from members_with_more_than_100_activity) as "enriched_today_members_more_than_100_activity", + (select count from members_with_more_than_10_activity) as "enriched_today_members_more_than_10_activity" +), +enriched_since_yesterday as ( + WITH total_members as ( + select count(*) as count + from "memberEnrichments" + WHERE "lastUpdatedAt" >= date_trunc('day', now() - interval '1 day') + ), + members_with_more_than_1000_activity as ( + select count(*) as count + from "memberEnrichments" m + LEFT JOIN "membersGlobalActivityCount" act_count + ON act_count."memberId" = m."memberId" + WHERE act_count.total_count > 1000 + AND m."lastUpdatedAt" >= date_trunc('day', now() - interval '1 day') + ), + members_with_more_than_100_activity as ( + select count(*) as count + from "memberEnrichments" m + LEFT JOIN "membersGlobalActivityCount" act_count + ON act_count."memberId" = m."memberId" + WHERE act_count.total_count > 100 and act_count.total_count < 1000 + AND m."lastUpdatedAt" >= date_trunc('day', now() - interval '1 day') + ), + members_with_more_than_10_activity as ( + select count(*) as count + from "memberEnrichments" m + LEFT JOIN "membersGlobalActivityCount" act_count + ON act_count."memberId" = m."memberId" + WHERE act_count.total_count > 10 and act_count.total_count < 100 + AND m."lastUpdatedAt" >= date_trunc('day', now() - interval '1 day') + ) + select + (select count from total_members) as "enriched_since_yesterday_total", + (select count from members_with_more_than_1000_activity) as "enriched_since_yesterday_members_more_than_1000_activity", + (select count from members_with_more_than_100_activity) as "enriched_since_yesterday_members_more_than_100_activity", + (select count from members_with_more_than_10_activity) as "enriched_since_yesterday_members_more_than_10_activity" +), +enriched_in_last_30days as ( + WITH total_members as ( + select count(*) as count + from "memberEnrichments" + WHERE "lastUpdatedAt" >= now() - interval '30 days' + ), + members_with_more_than_1000_activity as ( + select count(*) as count + from "memberEnrichments" m + LEFT JOIN "membersGlobalActivityCount" act_count + ON act_count."memberId" = m."memberId" + WHERE act_count.total_count > 1000 + AND m."lastUpdatedAt" >= now() - interval '30 days' + ), + members_with_more_than_100_activity as ( + select count(*) as count + from "memberEnrichments" m + LEFT JOIN "membersGlobalActivityCount" act_count + ON act_count."memberId" = m."memberId" + WHERE act_count.total_count > 100 and act_count.total_count < 1000 + AND m."lastUpdatedAt" >= now() - interval '30 days' + ), + members_with_more_than_10_activity as ( + select count(*) as count + from "memberEnrichments" m + LEFT JOIN "membersGlobalActivityCount" act_count + ON act_count."memberId" = m."memberId" + WHERE act_count.total_count > 10 and act_count.total_count < 100 + AND m."lastUpdatedAt" >= now() - interval '30 days' + ) + select + (select count from total_members) as "enriched_in30d_total", + (select count from members_with_more_than_1000_activity) as "enriched_in30d_members_more_than_1000_activity", + (select count from members_with_more_than_100_activity) as "enriched_in30d_members_more_than_100_activity", + (select count from members_with_more_than_10_activity) as "enriched_in30d_members_more_than_10_activity" +), +last_enriched_3_profiles as ( + select 'https://cm.lfx.dev/people/' || m."memberId" || '?projectGroup=dc48fac5-b31a-4659-ac99-60eb52a1082a' as profiles + from "memberEnrichments" m + where exists ( + select 1 from "memberEnrichmentCache" mec + where mec."memberId" = m."memberId" + and data is not null + ) + and m."lastUpdatedAt" is not null + order by m."lastUpdatedAt" desc + limit 3 +), +last_enriched_3_profiles_with_more_than_1000_activities as ( + select 'https://cm.lfx.dev/people/' || m."memberId" || '?projectGroup=dc48fac5-b31a-4659-ac99-60eb52a1082a' as profiles + from "memberEnrichments" m + left join "membersGlobalActivityCount" act_count on act_count."memberId" = m."memberId" + where act_count.total_count > 1000 + and exists ( + select 1 from "memberEnrichmentCache" mec + where mec."memberId" = m."memberId" + and data is not null + ) + and m."lastUpdatedAt" is not null + order by m."lastUpdatedAt" desc + limit 3 +), +last_enriched_3_profiles_with_more_than_100_activities as ( + select 'https://cm.lfx.dev/people/' || m."memberId" || '?projectGroup=dc48fac5-b31a-4659-ac99-60eb52a1082a' as profiles + from "memberEnrichments" m + left join "membersGlobalActivityCount" act_count on act_count."memberId" = m."memberId" + where act_count.total_count > 100 and act_count.total_count < 1000 + and exists ( + select 1 from "memberEnrichmentCache" mec + where mec."memberId" = m."memberId" + and data is not null + ) + and m."lastUpdatedAt" is not null + order by m."lastUpdatedAt" desc + limit 3 +), +last_enriched_3_profiles_with_more_than_10_activities as ( + select 'https://cm.lfx.dev/people/' || m."memberId" || '?projectGroup=dc48fac5-b31a-4659-ac99-60eb52a1082a' as profiles + from "memberEnrichments" m + left join "membersGlobalActivityCount" act_count on act_count."memberId" = m."memberId" + where act_count.total_count > 10 and act_count.total_count < 100 + and exists ( + select 1 from "memberEnrichmentCache" mec + where mec."memberId" = m."memberId" + and data is not null + ) + and m."lastUpdatedAt" is not null + order by m."lastUpdatedAt" desc + limit 3 +), +oldest_created_at_of_enrichable_member as ( + with enrichable_in_at_least_one_source as ( + select mem.id, mem."createdAt", max("membersGlobalActivityCount".total_count) as total_count + from members mem + inner join "memberIdentities" mi on mem.id = mi."memberId" and mi.verified + left join "membersGlobalActivityCount" on "membersGlobalActivityCount"."memberId" = mem.id + left join "memberEnrichmentCache" mec on mec."memberId" = mem.id + where ( + (mi.verified and + ((mi.type = 'username' AND mi.platform = 'github') OR (mi.type = 'email'))) + OR + ("membersGlobalActivityCount".total_count > 10 AND mi.type = 'email' and mi.verified) + OR + ( + ("membersGlobalActivityCount".total_count > 500) AND + (mem."displayName" like '% %') AND + (mem.attributes -> 'location' ->> 'default' is not null and + mem.attributes -> 'location' ->> 'default' <> '') AND + ( + (mem.attributes -> 'websiteUrl' ->> 'default' is not null and + mem.attributes -> 'websiteUrl' ->> 'default' <> '') OR + (mi.verified AND mi.type = 'username' and mi.platform = 'github') OR + (mi.verified AND mi.type = 'email') + ) + ) + OR + ((mi.verified AND mi.type = 'username' and mi.platform = 'linkedin')) + ) + group by mem.id + order by mem.id desc) + select + (select min("createdAt") as "date" from enrichable_in_at_least_one_source) as total, + (select min("createdAt") as "date" from enrichable_in_at_least_one_source where total_count > 1000) as members_with_more_than_1000_activities, + (select min("createdAt") as "date" from enrichable_in_at_least_one_source where total_count > 100 and total_count < 1000) as members_with_more_than_100_activities, + (select min("createdAt") as "date" from enrichable_in_at_least_one_source where total_count > 10 and total_count < 100) as members_with_more_than_10_activities +) + select + (select enriched_today_total from enriched_total) as "enrichedTotalAll", + (select enriched_today_members_more_than_1000_activity from enriched_total) as "enrichedTotal1000", + (select enriched_today_members_more_than_100_activity from enriched_total) as "enrichedTotal100", + (select enriched_today_members_more_than_10_activity from enriched_total) as "enrichedTotal10", + + (select enriched_today_total from enriched_today) as "enrichedTodayAll", + (select enriched_today_members_more_than_1000_activity from enriched_today) as "enrichedToday1000", + (select enriched_today_members_more_than_100_activity from enriched_today) as "enrichedToday100", + (select enriched_today_members_more_than_10_activity from enriched_today) as "enrichedToday10", + + (select enriched_since_yesterday_total from enriched_since_yesterday) as "enrichedSinceYesterdayAll", + (select enriched_since_yesterday_members_more_than_1000_activity from enriched_since_yesterday) as "enrichedSinceYesterday1000", + (select enriched_since_yesterday_members_more_than_100_activity from enriched_since_yesterday) as "enrichedSinceYesterday100", + (select enriched_since_yesterday_members_more_than_10_activity from enriched_since_yesterday) as "enrichedSinceYesterday10", + + (select enriched_in30d_total from enriched_in_last_30days) as "enrichedInLast30DaysAll", + (select enriched_in30d_members_more_than_1000_activity from enriched_in_last_30days) as "enrichedInLast30Days1000", + (select enriched_in30d_members_more_than_100_activity from enriched_in_last_30days) as "enrichedInLast30Days100", + (select enriched_in30d_members_more_than_10_activity from enriched_in_last_30days) as "enrichedInLast30Days10", + + (select array_agg(profiles) from last_enriched_3_profiles ) as "lastEnriched3ProfilesAll", + (select array_agg(profiles) from last_enriched_3_profiles_with_more_than_1000_activities ) as "lastEnriched3Profiles1000", + (select array_agg(profiles) from last_enriched_3_profiles_with_more_than_100_activities ) as "lastEnriched3Profiles100", + (select array_agg(profiles) from last_enriched_3_profiles_with_more_than_10_activities ) as "lastEnriched3Profiles10", + + (select total from oldest_created_at_of_enrichable_member) as "oldestCreatedAtInEnrichableMembersAll", + (select members_with_more_than_1000_activities from oldest_created_at_of_enrichable_member) as "oldestCreatedAtInEnrichableMembers1000", + (select members_with_more_than_100_activities from oldest_created_at_of_enrichable_member) as "oldestCreatedAtInEnrichableMembers100", + (select members_with_more_than_10_activities from oldest_created_at_of_enrichable_member) as "oldestCreatedAtInEnrichableMembers10"; + + +-- member enrichment monitoring (LLM queries) +drop materialized view if exists "memberEnrichmentMonitoringLLMQueries"; +create materialized view "memberEnrichmentMonitoringLLMQueries" as +with check_profile_belongs_to_member_with_llm as ( + with total_times_called as ( + select count(*) as count + from "llmPromptHistory" + where type = 'member_enrichment_find_related_linkedin_profiles' + ), + averages as ( + select avg("inputTokenCount") as "inputToken", + sum("inputTokenCount") as "inputTokenTotal", + avg("outputTokenCount") as "outputToken", + sum("outputTokenCount") as "outputTokenTotal", + avg("responseTimeSeconds") as "responseTimeSeconds" + from "llmPromptHistory" + where type = 'member_enrichment_find_related_linkedin_profiles' + ) + select + (select count from total_times_called) as "total_times_called", + (select ("inputToken" * 0.003 + "outputToken" * 0.015) / 1000 from averages) as "average_cost", + (select "responseTimeSeconds" from averages) as "average_response_time", + (select ("inputTokenTotal" * 0.003 + "outputTokenTotal" * 0.015) / 1000 from averages) as "total_cost" +), +squash_multiple_value_attributes_with_llm as ( + with total_times_called as ( + select count(*) as count + from "llmPromptHistory" + where type = 'member_enrichment_squash_multiple_value_attributes' + ), + averages as ( + select avg("inputTokenCount") as "inputToken", + sum("inputTokenCount") as "inputTokenTotal", + avg("outputTokenCount") as "outputToken", + sum("outputTokenCount") as "outputTokenTotal", + avg("responseTimeSeconds") as "responseTimeSeconds" + from "llmPromptHistory" + where type = 'member_enrichment_squash_multiple_value_attributes' + ) + select + (select count from total_times_called) as "total_times_called", + (select ("inputToken" * 0.003 + "outputToken" * 0.015) / 1000 from averages) as "average_cost", + (select "responseTimeSeconds" from averages) as "average_response_time", + (select ("inputTokenTotal" * 0.003 + "outputTokenTotal" * 0.015) / 1000 from averages) as "total_cost" +), +squash_work_experiences_with_llm as ( + with total_times_called as ( + select count(*) as count + from "llmPromptHistory" + where type = 'member_enrichment_squash_work_experiences_from_multiple_sources' + ), + averages as ( + select avg("inputTokenCount") as "inputToken", + sum("inputTokenCount") as "inputTokenTotal", + avg("outputTokenCount") as "outputToken", + sum("outputTokenCount") as "outputTokenTotal", + avg("responseTimeSeconds") as "responseTimeSeconds" + from "llmPromptHistory" + where type = 'member_enrichment_squash_work_experiences_from_multiple_sources' + ) + select + (select count from total_times_called) as "total_times_called", + (select ("inputToken" * 0.003 + "outputToken" * 0.015) / 1000 from averages) as "average_cost", + (select "responseTimeSeconds" from averages) as "average_response_time", + (select ("inputTokenTotal" * 0.003 + "outputTokenTotal" * 0.015) / 1000 from averages) as "total_cost" +) + select + (select total_times_called from check_profile_belongs_to_member_with_llm) as "checkProfileBelongsToMemberTotalTimesCalled", + (select round(average_cost, 5) from check_profile_belongs_to_member_with_llm) as "checkProfileBelongsToMemberAvgCostPerRequest", + (select round(average_response_time, 2) from check_profile_belongs_to_member_with_llm) as "checkProfileBelongsToMemberAvgResponseTime", + (select round(total_cost, 2) from check_profile_belongs_to_member_with_llm) as "checkProfileBelongsToMemberTotalCost", + + (select total_times_called from squash_multiple_value_attributes_with_llm) as "squashAttributesTotalTimesCalled", + (select round(average_cost, 5) from squash_multiple_value_attributes_with_llm) as "squashAttributesAvgCostPerRequest", + (select round(average_response_time, 2) from squash_multiple_value_attributes_with_llm) as "squashAttributesAvgResponseTime", + (select round(total_cost, 2) from squash_multiple_value_attributes_with_llm) as "squashAttributesTotalCost", + + (select total_times_called from squash_work_experiences_with_llm) as "squashWorkExperiencesTotalTimesCalled", + (select round(average_cost, 5) from squash_work_experiences_with_llm) as "squashWorkExperiencesAvgCostPerRequest", + (select round(average_response_time, 2) from squash_work_experiences_with_llm) as "squashWorkExperiencesAvgResponseTime", + (select round(total_cost, 2) from squash_work_experiences_with_llm) as "squashWorkExperiencesTotalCost"; + + diff --git a/backend/src/database/migrations/R__member_segments_mv.sql b/backend/src/database/migrations/R__member_segments_mv.sql new file mode 100644 index 0000000000..9109ee5f93 --- /dev/null +++ b/backend/src/database/migrations/R__member_segments_mv.sql @@ -0,0 +1,2 @@ +-- member_segments_mv is no longer needed — replaced by memberSegmentsAgg table (CM-1008) +drop materialized view if exists member_segments_mv; \ No newline at end of file diff --git a/backend/src/database/migrations/R__organizationEnrichmentMaterializedViews.sql b/backend/src/database/migrations/R__organizationEnrichmentMaterializedViews.sql new file mode 100644 index 0000000000..636840f648 --- /dev/null +++ b/backend/src/database/migrations/R__organizationEnrichmentMaterializedViews.sql @@ -0,0 +1,22 @@ +-- drop old materialized view if exists +drop materialized view if exists "organizationsGlobalActivityCount" cascade; + +-- create the materialized view +create materialized view "organizationsGlobalActivityCount" as +select + osa."organizationId", + sum(osa."activityCount") as total_count_estimate +from "organizationSegmentsAgg" osa +where osa."segmentId" in ( + select id + from segments + where "grandparentId" is not null + and "parentId" is not null +) +group by osa."organizationId" +order by sum(osa."activityCount") desc; + +create unique index ix_organization_global_activity_count_organization_id + on "organizationsGlobalActivityCount" ("organizationId"); + +create index ix_organization_global_activity_count_estimate on "organizationsGlobalActivityCount" (total_count_estimate); diff --git a/backend/src/database/migrations/R__organization_segments_mv.sql b/backend/src/database/migrations/R__organization_segments_mv.sql new file mode 100644 index 0000000000..50857cd231 --- /dev/null +++ b/backend/src/database/migrations/R__organization_segments_mv.sql @@ -0,0 +1,2 @@ +-- organization_segments_mv is no longer needed — replaced by organizationSegmentsAgg table (CM-1008) +drop materialized view if exists organization_segments_mv; diff --git a/backend/src/database/migrations/U1667915166__multipleTaskAssignees.sql b/backend/src/database/migrations/U1667915166__multipleTaskAssignees.sql deleted file mode 100644 index 0fee277d2e..0000000000 --- a/backend/src/database/migrations/U1667915166__multipleTaskAssignees.sql +++ /dev/null @@ -1,8 +0,0 @@ -DROP TABLE public."taskAssignees"; - -ALTER TABLE public.tasks ADD "assignedToId" uuid NULL; - -ALTER TABLE public.tasks ADD CONSTRAINT "tasks_assignedToId_fkey" FOREIGN KEY ("assignedToId") REFERENCES public.users(id) ON DELETE SET NULL ON UPDATE CASCADE; - --- remove tasks.type -ALTER TABLE tasks DROP COLUMN "type"; diff --git a/backend/src/database/migrations/U1668524084__averageSentimentCalculationFix.sql b/backend/src/database/migrations/U1668524084__averageSentimentCalculationFix.sql deleted file mode 100644 index 48096e7c58..0000000000 --- a/backend/src/database/migrations/U1668524084__averageSentimentCalculationFix.sql +++ /dev/null @@ -1,29 +0,0 @@ -drop materialized view "memberActivityAggregatesMVs"; - -create materialized view "memberActivityAggregatesMVs" as -( -select m.id, - max(a.timestamp) as "lastActive", - count(a.id) as "activityCount", - array_agg( - distinct (a.platform) - ) filter ( - where - a.platform is not null - ) AS "activeOn", - ROUND( - AVG( - COALESCE( - a."sentiment" ->> 'sentiment', - '0' - ):: float - ):: numeric, - 2 - ) AS "averageSentiment" -from members m - left outer join activities a on m.id = a."memberId" and a."deletedAt" is null -group by m.id - ); - -create unique index ix_memberactivityaggregatesmvs_memberid - on "memberActivityAggregatesMVs" (id); \ No newline at end of file diff --git a/backend/src/database/migrations/U1668615936__averageSentimentExcludeZeroFix.sql b/backend/src/database/migrations/U1668615936__averageSentimentExcludeZeroFix.sql deleted file mode 100644 index e5b39ab742..0000000000 --- a/backend/src/database/migrations/U1668615936__averageSentimentExcludeZeroFix.sql +++ /dev/null @@ -1,36 +0,0 @@ -drop materialized view "memberActivityAggregatesMVs"; - -create materialized view "memberActivityAggregatesMVs" as -( - select - m.id, - max(a.timestamp) as "lastActive", - count(a.id) as "activityCount", - array_agg( - distinct (a.platform) - ) filter ( - where - a.platform is not null - ) AS "activeOn", - ROUND( - AVG(CASE - WHEN - COALESCE( - a."sentiment" ->> 'sentiment', - '0' - ):: float <> 0 THEN COALESCE( - a."sentiment" ->> 'sentiment', - '0' - ):: float - ELSE NULL end - ):: numeric, - 2 - ) AS "averageSentiment" - from members m - left outer join activities a on m.id = a."memberId" and a."deletedAt" is null - group by m.id -); - - -create unique index ix_memberactivityaggregatesmvs_memberid - on "memberActivityAggregatesMVs" (id); \ No newline at end of file diff --git a/backend/src/database/migrations/U1669396956__addLocationWebsiteAndGithubToOrganizations.sql b/backend/src/database/migrations/U1669396956__addLocationWebsiteAndGithubToOrganizations.sql deleted file mode 100644 index d895256dce..0000000000 --- a/backend/src/database/migrations/U1669396956__addLocationWebsiteAndGithubToOrganizations.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE public.organizations DROP COLUMN "location"; -ALTER TABLE public.organizations DROP COLUMN "github"; -ALTER TABLE public.organizations DROP COLUMN "website"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1669737732__organizationCacheEnrichedBoolean.sql b/backend/src/database/migrations/U1669737732__organizationCacheEnrichedBoolean.sql deleted file mode 100644 index df48593e89..0000000000 --- a/backend/src/database/migrations/U1669737732__organizationCacheEnrichedBoolean.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE public."organizationCaches" DROP COLUMN "enriched"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1669802927__organizationCacheMissingFields.sql b/backend/src/database/migrations/U1669802927__organizationCacheMissingFields.sql deleted file mode 100644 index 055dfb8d60..0000000000 --- a/backend/src/database/migrations/U1669802927__organizationCacheMissingFields.sql +++ /dev/null @@ -1,6 +0,0 @@ -ALTER TABLE public."organizationCaches" DROP COLUMN "location"; -ALTER TABLE public."organizationCaches" DROP COLUMN "github"; -ALTER TABLE public."organizationCaches" DROP COLUMN "website"; - -create unique index organizations_url_tenant_id - on "organizations" (url,"tenantId"); \ No newline at end of file diff --git a/backend/src/database/migrations/U1669832400__organizationCacheIndexUpdates.sql b/backend/src/database/migrations/U1669832400__organizationCacheIndexUpdates.sql deleted file mode 100644 index 4e181014cf..0000000000 --- a/backend/src/database/migrations/U1669832400__organizationCacheIndexUpdates.sql +++ /dev/null @@ -1,4 +0,0 @@ -create unique index organization_caches_url - on "organizationCaches" (url); - -drop index public.organization_caches_name; \ No newline at end of file diff --git a/backend/src/database/migrations/U1670422647__addEmailSentAtToIntegrations.sql b/backend/src/database/migrations/U1670422647__addEmailSentAtToIntegrations.sql deleted file mode 100644 index a67112b9b3..0000000000 --- a/backend/src/database/migrations/U1670422647__addEmailSentAtToIntegrations.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE public.integrations DROP COLUMN "emailSentAt"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1671451319__incoming-webhooks.sql b/backend/src/database/migrations/U1671451319__incoming-webhooks.sql deleted file mode 100644 index e72a978aed..0000000000 --- a/backend/src/database/migrations/U1671451319__incoming-webhooks.sql +++ /dev/null @@ -1 +0,0 @@ -drop table "incomingWebhooks"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1672933725__eagleEyeContentsAddExactMatchKeyword.sql b/backend/src/database/migrations/U1672933725__eagleEyeContentsAddExactMatchKeyword.sql deleted file mode 100644 index bf02192db4..0000000000 --- a/backend/src/database/migrations/U1672933725__eagleEyeContentsAddExactMatchKeyword.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE public."eagleEyeContents" DROP COLUMN "exactKeywords"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1673002934__automation-eventid-index.sql b/backend/src/database/migrations/U1673002934__automation-eventid-index.sql deleted file mode 100644 index c59d495cb6..0000000000 --- a/backend/src/database/migrations/U1673002934__automation-eventid-index.sql +++ /dev/null @@ -1 +0,0 @@ -drop index "automationExecutions_automationId_eventId"; diff --git a/backend/src/database/migrations/U1673347364__activityTypesForMembers.sql b/backend/src/database/migrations/U1673347364__activityTypesForMembers.sql deleted file mode 100644 index 4e86e0f2ca..0000000000 --- a/backend/src/database/migrations/U1673347364__activityTypesForMembers.sql +++ /dev/null @@ -1,42 +0,0 @@ -drop materialized view "memberActivityAggregatesMVs"; - -create materialized view "memberActivityAggregatesMVs" as -( - select - m.id, - max(a.timestamp) as "lastActive", - count(a.id) as "activityCount", - array_agg( - distinct (concat(a.platform,':',a.type)) - ) filter ( - where - a.platform is not null - ) AS "activityTypes", - array_agg( - distinct (a.platform) - ) filter ( - where - a.platform is not null - ) AS "activeOn", - ROUND( - AVG(CASE - WHEN - COALESCE( - a."sentiment" ->> 'sentiment', - '0' - ):: float <> 0 THEN COALESCE( - a."sentiment" ->> 'sentiment', - '0' - ):: float - ELSE NULL end - ):: numeric, - 2 - ) AS "averageSentiment" - from members m - left outer join activities a on m.id = a."memberId" and a."deletedAt" is null - group by m.id -); - - -create unique index ix_memberactivityaggregatesmvs_memberid - on "memberActivityAggregatesMVs" (id); \ No newline at end of file diff --git a/backend/src/database/migrations/U1673527315__templateReportUpdates.sql b/backend/src/database/migrations/U1673527315__templateReportUpdates.sql deleted file mode 100644 index 6da68a2b84..0000000000 --- a/backend/src/database/migrations/U1673527315__templateReportUpdates.sql +++ /dev/null @@ -1,21 +0,0 @@ -ALTER TABLE public.reports DROP COLUMN "isTemplate"; - -drop materialized view "memberActivityAggregatesMVs"; - -create materialized view "memberActivityAggregatesMVs" as -SELECT m.id, - max(a."timestamp") AS "lastActive", - count(a.id) AS "activityCount", - array_agg(DISTINCT a.platform) FILTER (WHERE a.platform IS NOT NULL) AS "activeOn", - round(avg( - CASE - WHEN (a.sentiment ->> 'sentiment'::text) IS NOT NULL - THEN (a.sentiment ->> 'sentiment'::text)::double precision - ELSE NULL::double precision - END)::numeric, 2) AS "averageSentiment" -FROM members m - LEFT JOIN activities a ON m.id = a."memberId" AND a."deletedAt" IS NULL -GROUP BY m.id; - -create unique index ix_memberactivityaggregatesmvs_memberid - on "memberActivityAggregatesMVs" (id); \ No newline at end of file diff --git a/backend/src/database/migrations/U1673857723__multiSelectAndEnrichment.sql b/backend/src/database/migrations/U1673857723__multiSelectAndEnrichment.sql deleted file mode 100644 index 26a6a69856..0000000000 --- a/backend/src/database/migrations/U1673857723__multiSelectAndEnrichment.sql +++ /dev/null @@ -1,11 +0,0 @@ -ALTER TABLE public."memberAttributeSettings" DROP COLUMN options; - -ALTER TABLE public."members" DROP COLUMN "isEnriched"; - -ALTER TABLE publoc."settings" -ALTER COLUMN "attributeSettings" SET DEFAULT '{"priorities": ["custom", "twitter", "github", "devto", "slack", "discord", "crowd"]}'::jsonb; - -ALTER TYPE public."memberAttributeSettings_type" RENAME TO "memberAttributeSettings_type_old"; -CREATE TYPE "enum_memberAttributeSettings_type" AS ENUM ('boolean', 'number', 'email', 'string', 'url', 'date'); -ALTER TABLE public."memberAttributeSettings" ALTER COLUMN type TYPE "memberAttributeSettings_type" USING type::text::"memberAttributeSettings_type"; -DROP TYPE "memberAttributeSettings_type_old"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1674038525__specialMemberAttribute.sql b/backend/src/database/migrations/U1674038525__specialMemberAttribute.sql deleted file mode 100644 index 7fa261d235..0000000000 --- a/backend/src/database/migrations/U1674038525__specialMemberAttribute.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TYPE public."memberAttributeSettings_type" RENAME TO "memberAttributeSettings_type_old"; -CREATE TYPE "enum_memberAttributeSettings_type" AS ENUM ('boolean', 'number', 'email', 'string', 'url', 'date', 'multiSelect'); -ALTER TABLE public."memberAttributeSettings" ALTER COLUMN type TYPE "memberAttributeSettings_type" USING type::text::"memberAttributeSettings_type"; -DROP TYPE "memberAttributeSettings_type_old"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1674065473__isEnrichedToLastEnriched.sql b/backend/src/database/migrations/U1674065473__isEnrichedToLastEnriched.sql deleted file mode 100644 index f365a5a726..0000000000 --- a/backend/src/database/migrations/U1674065473__isEnrichedToLastEnriched.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE public."members" ADD COLUMN "isEnriched" BOOLEAN DEFAULT false; -ALTER TABLE public."members" DROP COLUMN "lastEnriched"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1674113789__contributionsToMembers.sql b/backend/src/database/migrations/U1674113789__contributionsToMembers.sql deleted file mode 100644 index 3b6d57b7f3..0000000000 --- a/backend/src/database/migrations/U1674113789__contributionsToMembers.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE public."members" -DROP COLUMN contributions; \ No newline at end of file diff --git a/backend/src/database/migrations/U1674663701__memberEnrichmentCache.sql b/backend/src/database/migrations/U1674663701__memberEnrichmentCache.sql deleted file mode 100644 index 23b8c7fe18..0000000000 --- a/backend/src/database/migrations/U1674663701__memberEnrichmentCache.sql +++ /dev/null @@ -1 +0,0 @@ -drop table "memberEnrichmentCache"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1675166504__weeklyAnalyticsEmailsHistory.sql b/backend/src/database/migrations/U1675166504__weeklyAnalyticsEmailsHistory.sql deleted file mode 100644 index 24dd92f889..0000000000 --- a/backend/src/database/migrations/U1675166504__weeklyAnalyticsEmailsHistory.sql +++ /dev/null @@ -1 +0,0 @@ -drop table "weeklyAnalyticsEmailsHistory"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1675259471__eagleEyeActions.sql b/backend/src/database/migrations/U1675259471__eagleEyeActions.sql deleted file mode 100644 index 30b8b2c037..0000000000 --- a/backend/src/database/migrations/U1675259471__eagleEyeActions.sql +++ /dev/null @@ -1,59 +0,0 @@ -DROP TABLE IF EXISTS "eagleEyeContents"; - -DROP TYPE "eagleEyeContents_actions_type"; - -create table "eagleEyeContents"; -( - id uuid not null primary key, - "sourceId" text not null, - "vectorId" text not null, - status varchar(255) default NULL::character varying, - title text not null, - username text not null, - url text not null, - text text, - timestamp timestamp with time zone not null, - platform text not null, - keywords text [], - "similarityScore" double precision, - "userAttributes" jsonb, - "postAttributes" jsonb, - "importHash" varchar(255), - "createdAt" timestamp with time zone not null, - "updatedAt" timestamp with time zone not null, - "deletedAt" timestamp with time zone, - "tenantId" uuid not null references tenants on update cascade, - "createdById" uuid references users on update cascade on delete - set null, - "updatedById" uuid references users on update cascade on delete - set null, - "exactKeywords" text [] -); - -alter table "eagleEyeContents" owner to postgres; - -create index discord on "eagleEyeContents" ("vectorId", status); - -create index members_email_tenant_id on "eagleEyeContents" (id) -where ("deletedAt" IS NULL); - -create index members_joined_at_tenant_id on "eagleEyeContents" (id) -where ("deletedAt" IS NULL); - -create index members_username on "eagleEyeContents" using gin (id); - -create index slack on "eagleEyeContents" (id); - -create index twitter on "eagleEyeContents" (id); - -create unique index eagle_eye_contents_import_hash_tenant_id on "eagleEyeContents" ("importHash", "tenantId") -where ("deletedAt" IS NULL); - -create index eagle_eye_contents_platform_tenant_id_timestamp on "eagleEyeContents" (platform, "tenantId", timestamp) -where ("deletedAt" IS NULL); - -create index eagle_eye_contents_status_tenant_id_timestamp on "eagleEyeContents" (status, "tenantId", timestamp) -where ("deletedAt" IS NULL); - -create index eagle_eye_contents_tenant_id_timestamp on "eagleEyeContents" ("tenantId", timestamp) -where ("deletedAt" IS NULL); \ No newline at end of file diff --git a/backend/src/database/migrations/U1675702339__eagleEyeSettings.sql b/backend/src/database/migrations/U1675702339__eagleEyeSettings.sql deleted file mode 100644 index e3a0f4fe1e..0000000000 --- a/backend/src/database/migrations/U1675702339__eagleEyeSettings.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE public."users" -DROP COLUMN "eagleEyeSettings"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1677503916__activity-source-index.sql b/backend/src/database/migrations/U1677503916__activity-source-index.sql deleted file mode 100644 index 4f6190858c..0000000000 --- a/backend/src/database/migrations/U1677503916__activity-source-index.sql +++ /dev/null @@ -1 +0,0 @@ -drop index create unique index ix_unique_activities_tenantId_platform_type_sourceId; \ No newline at end of file diff --git a/backend/src/database/migrations/U1677836093__addReasonForUsingCrowd.sql b/backend/src/database/migrations/U1677836093__addReasonForUsingCrowd.sql deleted file mode 100644 index 3396f9f7b1..0000000000 --- a/backend/src/database/migrations/U1677836093__addReasonForUsingCrowd.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE public.tenants DROP COLUMN "reasonForUsingCrowd"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1678183368__add-isTeamOrganization.sql b/backend/src/database/migrations/U1678183368__add-isTeamOrganization.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1678449419__customActivityTypes.sql b/backend/src/database/migrations/U1678449419__customActivityTypes.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1678698341__identities-to-member-materialized-view.sql b/backend/src/database/migrations/U1678698341__identities-to-member-materialized-view.sql deleted file mode 100644 index d3fc3866c7..0000000000 --- a/backend/src/database/migrations/U1678698341__identities-to-member-materialized-view.sql +++ /dev/null @@ -1,28 +0,0 @@ -drop materialized view "memberActivityAggregatesMVs"; - -create materialized view "memberActivityAggregatesMVs" as -select m.id, - max(a."timestamp") as "lastActive", - count(a.id) as "activityCount", - array_agg( - distinct (concat(a.platform, ':', a.type)) - ) filter ( - where - a.platform is not null - ) as "activityTypes", - array_agg(distinct a.platform) filter (where a.platform is not null) as "activeOn", - count(distinct a.timestamp::date) as "activeDaysCount", - round(avg( - case - when (a.sentiment ->> 'sentiment'::text) is not null - then (a.sentiment ->> 'sentiment'::text)::double precision - else null::double precision - end)::numeric, 2) as "averageSentiment" -from members m - left join activities a on m.id = a."memberId" and a."deletedAt" is null -group by m.id; - -create unique index ix_memberactivityaggregatesmvs_memberid - on "memberActivityAggregatesMVs" (id); - - diff --git a/backend/src/database/migrations/U1678725444__add-eagleEyePlan-enum.sql b/backend/src/database/migrations/U1678725444__add-eagleEyePlan-enum.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1679059477__memberEmailArray.sql b/backend/src/database/migrations/U1679059477__memberEmailArray.sql deleted file mode 100644 index 1ebe59586b..0000000000 --- a/backend/src/database/migrations/U1679059477__memberEmailArray.sql +++ /dev/null @@ -1,5 +0,0 @@ -alter table members - alter column emails type text using coalesce(emails[1],''); - -ALTER TABLE members -RENAME COLUMN emails TO email; \ No newline at end of file diff --git a/backend/src/database/migrations/U1679646421__renameisKeyActionToisContribution.sql b/backend/src/database/migrations/U1679646421__renameisKeyActionToisContribution.sql deleted file mode 100644 index a375537acd..0000000000 --- a/backend/src/database/migrations/U1679646421__renameisKeyActionToisContribution.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE activities -RENAME COLUMN "isContribution" TO "isKeyAction"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1679825091__stream-delaying.sql b/backend/src/database/migrations/U1679825091__stream-delaying.sql deleted file mode 100644 index 282e3af1b0..0000000000 --- a/backend/src/database/migrations/U1679825091__stream-delaying.sql +++ /dev/null @@ -1,2 +0,0 @@ -drop table "integrationStreams"; -drop table "integrationRuns"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1680601075__member-identities.sql b/backend/src/database/migrations/U1680601075__member-identities.sql deleted file mode 100644 index aef8729afc..0000000000 --- a/backend/src/database/migrations/U1680601075__member-identities.sql +++ /dev/null @@ -1,4 +0,0 @@ -drop table "memberIdentities"; -drop function trigger_set_updated_at; -alter table activities - drop column username; \ No newline at end of file diff --git a/backend/src/database/migrations/U1680601431__addActivityToSettings.sql b/backend/src/database/migrations/U1680601431__addActivityToSettings.sql deleted file mode 100644 index 07352ec3a6..0000000000 --- a/backend/src/database/migrations/U1680601431__addActivityToSettings.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE public."settings" DROP COLUMN "activityChannels"; diff --git a/backend/src/database/migrations/U1680699288__addObjectMemberActivities.sql b/backend/src/database/migrations/U1680699288__addObjectMemberActivities.sql deleted file mode 100644 index 1ad07c1482..0000000000 --- a/backend/src/database/migrations/U1680699288__addObjectMemberActivities.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE "activities" DROP COLUMN "objectMemberId"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1681467668__member-identities-alter-tables.sql b/backend/src/database/migrations/U1681467668__member-identities-alter-tables.sql deleted file mode 100644 index d3b5a612b7..0000000000 --- a/backend/src/database/migrations/U1681467668__member-identities-alter-tables.sql +++ /dev/null @@ -1,33 +0,0 @@ -alter table members - rename column "usernameOld" to username; - -alter table members - alter column username set not null; - -drop materialized view "memberActivityAggregatesMVs"; - -create materialized view "memberActivityAggregatesMVs" as -select m.id, - max(a."timestamp") as "lastActive", - array(select jsonb_object_keys(m.username)) as identities, - count(a.id) as "activityCount", - array_agg( - distinct (concat(a.platform, ':', a.type)) - ) filter ( - where - a.platform is not null - ) as "activityTypes", - array_agg(distinct a.platform) filter (where a.platform is not null) as "activeOn", - count(distinct a.timestamp::date) as "activeDaysCount", - round(avg( - case - when (a.sentiment ->> 'sentiment'::text) is not null - then (a.sentiment ->> 'sentiment'::text)::double precision - else null::double precision - end)::numeric, 2) as "averageSentiment" -from members m - left join activities a on m.id = a."memberId" and a."deletedAt" is null -group by m.id; - -create unique index ix_memberactivityaggregatesmvs_memberid - on "memberActivityAggregatesMVs" (id); diff --git a/backend/src/database/migrations/U1681815008__addObjectMemberUsernameToActivities.sql b/backend/src/database/migrations/U1681815008__addObjectMemberUsernameToActivities.sql deleted file mode 100644 index ab3945d30d..0000000000 --- a/backend/src/database/migrations/U1681815008__addObjectMemberUsernameToActivities.sql +++ /dev/null @@ -1,2 +0,0 @@ -alter table activities - drop column "objectMemberUsername"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1681893786__member-extra-identities.sql b/backend/src/database/migrations/U1681893786__member-extra-identities.sql deleted file mode 100644 index 7ef1d0c997..0000000000 --- a/backend/src/database/migrations/U1681893786__member-extra-identities.sql +++ /dev/null @@ -1,2 +0,0 @@ -alter table members - drop column "weakIdentities"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1682098472__fuzzy-postgres.sql b/backend/src/database/migrations/U1682098472__fuzzy-postgres.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1682100889__members-to-merge-similarity.sql b/backend/src/database/migrations/U1682100889__members-to-merge-similarity.sql deleted file mode 100644 index 43de808459..0000000000 --- a/backend/src/database/migrations/U1682100889__members-to-merge-similarity.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Remove the 'similarity' column from the memberToMerge table -ALTER TABLE "memberToMerge" DROP COLUMN similarity; \ No newline at end of file diff --git a/backend/src/database/migrations/U1682249232__missing-indexes.sql b/backend/src/database/migrations/U1682249232__missing-indexes.sql deleted file mode 100644 index 11699077ae..0000000000 --- a/backend/src/database/migrations/U1682249232__missing-indexes.sql +++ /dev/null @@ -1,57 +0,0 @@ -drop index "ix_integrationRuns_tenantId"; -drop index "ix_integrationRuns_integrationId"; -drop index "ix_integrationRuns_microserviceId"; - -drop index "ix_integrationStreams_tenantId"; -drop index "ix_integrationStreams_integrationId"; -drop index "ix_integrationStreams_microserviceId"; - -drop index "ix_memberIdentities_tenantId"; - -drop index "ix_conversations_tenantId"; - -drop index "ix_organizations_tenantId"; - -drop index "ix_automationExecutions_tenantId"; - -drop index "ix_memberOrganizations_organizationId"; - -drop index "ix_widgets_tenantId"; -drop index "ix_widgets_reportId"; - -drop index "ix_memberAttributeSettings_tenantId"; - -drop index "ix_integrations_tenantId"; - -drop index "ix_tasks_tenantId"; - -drop index "ix_settings_tenantId"; - -drop index "ix_tenantUsers_userId"; -drop index "ix_tenantUsers_tenantId"; - -drop index "ix_reports_tenantId"; - -drop index "ix_microservices_tenantId"; - -drop index "ix_memberTags_tagId"; - -drop index "ix_eagleEyeContents_tenantId"; - -drop index "ix_eagleEyeActions_tenantId"; -drop index "ix_eagleEyeActions_actionById"; -drop index "ix_eagleEyeActions_contentId"; - -drop index "ix_tags_tenantId"; - -drop index "ix_notes_tenantId"; - -drop index "ix_memberNotes_noteId"; - -drop index "ix_memberTasks_taskId"; - -drop index "ix_taskAssignees_userId"; - -drop index "ix_files_tenantId"; - -drop index "ix_activityTasks_taskId"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1682340963__optimized-members-mv.sql b/backend/src/database/migrations/U1682340963__optimized-members-mv.sql deleted file mode 100644 index 9c2712ef27..0000000000 --- a/backend/src/database/migrations/U1682340963__optimized-members-mv.sql +++ /dev/null @@ -1,41 +0,0 @@ -drop materialized view "memberActivityAggregatesMVs"; - -create materialized view "memberActivityAggregatesMVs" as -with identities as (select "memberId", - array_agg(distinct platform) as identities, - jsonb_object_agg(platform, latest_username) as username, - jsonb_object_agg(platform, usernames) as "newUsername" - from (select "memberId", - platform, - first_value(username) - over (partition by "memberId", platform order by "createdAt" desc) as latest_username, - jsonb_agg(username) over (partition by "memberId", platform) as usernames - from "memberIdentities") ranked - group by "memberId") -select m.id, - max(a."timestamp") as "lastActive", - i.identities, - i.username, - i."newUsername", - count(a.id) as "activityCount", - array_agg( - distinct (concat(a.platform, ':', a.type)) - ) filter ( - where - a.platform is not null - ) as "activityTypes", - array_agg(distinct a.platform) filter (where a.platform is not null) as "activeOn", - count(distinct a.timestamp::date) as "activeDaysCount", - round(avg( - case - when (a.sentiment ->> 'sentiment'::text) is not null - then (a.sentiment ->> 'sentiment'::text)::double precision - else null::double precision - end)::numeric, 2) as "averageSentiment" -from members m - inner join identities i on m.id = i."memberId" - left join activities a on m.id = a."memberId" and a."deletedAt" is null -group by m.id, i.identities, i.username, i."newUsername"; - -create unique index ix_memberactivityaggregatesmvs_memberid - on "memberActivityAggregatesMVs" (id); \ No newline at end of file diff --git a/backend/src/database/migrations/U1682354033__multiple-usernames-per-platform.sql b/backend/src/database/migrations/U1682354033__multiple-usernames-per-platform.sql deleted file mode 100644 index ca22dc7073..0000000000 --- a/backend/src/database/migrations/U1682354033__multiple-usernames-per-platform.sql +++ /dev/null @@ -1,42 +0,0 @@ -drop materialized view "memberActivityAggregatesMVs"; - -create materialized view "memberActivityAggregatesMVs" as -with identities as (select mi."memberId", - array_agg(distinct mi.platform) as identities, - jsonb_object_agg(mi.platform, mi.latest_username) as username, - jsonb_object_agg(mi.platform, mi.usernames) as "newUsername" - from (select "memberId", - platform, - max(username) filter (where is_latest) as latest_username, - array_agg(username) as usernames - from (select "memberId", - platform, - username, - "createdAt", - row_number() over (partition by "memberId", platform order by "createdAt" desc) = - 1 as is_latest - from "memberIdentities") sub - group by "memberId", platform) mi - group by mi."memberId") -select m.id, - max(a."timestamp") as "lastActive", - i.identities, - i.username, - i."newUsername", - count(a.id) as "activityCount", - array_agg(distinct concat(a.platform, ':', a.type)) filter (where a.platform is not null) as "activityTypes", - array_agg(distinct a.platform) filter (where a.platform is not null) as "activeOn", - count(distinct a."timestamp"::date) as "activeDaysCount", - round(avg( - case - when (a.sentiment ->> 'sentiment') is not null - then (a.sentiment ->> 'sentiment')::double precision - else null::double precision - end)::numeric, 2) as "averageSentiment" -from members m - join identities i on m.id = i."memberId" - left join activities a on m.id = a."memberId" and a."deletedAt" is null -group by m.id, i.identities, i.username, i."newUsername"; - -create unique index ix_memberactivityaggregatesmvs_memberid - on "memberActivityAggregatesMVs" (id); \ No newline at end of file diff --git a/backend/src/database/migrations/U1682512853__addAutomationName.sql b/backend/src/database/migrations/U1682512853__addAutomationName.sql deleted file mode 100644 index 4676e1f281..0000000000 --- a/backend/src/database/migrations/U1682512853__addAutomationName.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE public."automations" DROP COLUMN "name"; diff --git a/backend/src/database/migrations/U1682865647__enrichOrganizationCacheWithMissingFields.sql b/backend/src/database/migrations/U1682865647__enrichOrganizationCacheWithMissingFields.sql deleted file mode 100644 index e40d254a96..0000000000 --- a/backend/src/database/migrations/U1682865647__enrichOrganizationCacheWithMissingFields.sql +++ /dev/null @@ -1,13 +0,0 @@ -ALTER TABLE public."organizationCaches" DROP COLUMN "lastEnrichedAt"; -ALTER TABLE public."organizationCaches" DROP COLUMN "employeeCountByCountry"; -ALTER TABLE public."organizationCaches" DROP COLUMN "type"; -ALTER TABLE public."organizationCaches" DROP COLUMN "geoLocation"; -ALTER TABLE public."organizationCaches" DROP COLUMN "size"; -ALTER TABLE public."organizationCaches" DROP COLUMN "ticker"; -ALTER TABLE public."organizationCaches" DROP COLUMN "headline"; -ALTER TABLE public."organizationCaches" DROP COLUMN "profiles"; -ALTER TABLE public."organizationCaches" DROP COLUMN "naics"; -ALTER TABLE public."organizationCaches" DROP COLUMN "address"; -ALTER TABLE public."organizationCaches" DROP COLUMN "industry"; -ALTER TABLE public."organizationCaches" DROP COLUMN "founded"; -ALTER TABLE public."organizationCaches" DROP COLUMN "location"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1682865663__enrichOrganizationWithMissingFields.sql b/backend/src/database/migrations/U1682865663__enrichOrganizationWithMissingFields.sql deleted file mode 100644 index a84f84c86f..0000000000 --- a/backend/src/database/migrations/U1682865663__enrichOrganizationWithMissingFields.sql +++ /dev/null @@ -1,13 +0,0 @@ -ALTER TABLE public."organizations" DROP COLUMN "lastEnrichedAt"; -ALTER TABLE public."organizations" DROP COLUMN "employeeCountByCountry"; -ALTER TABLE public."organizations" DROP COLUMN "type"; -ALTER TABLE public."organizations" DROP COLUMN "geoLocation"; -ALTER TABLE public."organizations" DROP COLUMN "size"; -ALTER TABLE public."organizations" DROP COLUMN "ticker"; -ALTER TABLE public."organizations" DROP COLUMN "headline"; -ALTER TABLE public."organizations" DROP COLUMN "profiles"; -ALTER TABLE public."organizations" DROP COLUMN "naics"; -ALTER TABLE public."organizations" DROP COLUMN "address"; -ALTER TABLE public."organizations" DROP COLUMN "industry"; -ALTER TABLE public."organizations" DROP COLUMN "founded"; -ALTER TABLE public."organizations" DROP COLUMN "location"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1683096071__addSlackHookToSettings.sql b/backend/src/database/migrations/U1683096071__addSlackHookToSettings.sql deleted file mode 100644 index 769de5c891..0000000000 --- a/backend/src/database/migrations/U1683096071__addSlackHookToSettings.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE public."settings" DROP COLUMN "slackWebHook"; diff --git a/backend/src/database/migrations/U1683110675__segments.sql b/backend/src/database/migrations/U1683110675__segments.sql deleted file mode 100644 index 02998960f6..0000000000 --- a/backend/src/database/migrations/U1683110675__segments.sql +++ /dev/null @@ -1,22 +0,0 @@ - -drop table "segments" -ALTER TABLE "activities" DROP COLUMN "segmentId"; - -ALTER TABLE "integrations" DROP COLUMN "segmentId"; - -ALTER TABLE "conversations" DROP COLUMN "segmentId"; - -ALTER TABLE "tags" DROP COLUMN "segmentId"; - -ALTER TABLE "tasks" DROP COLUMN "segmentId"; - -ALTER TABLE "reports" DROP COLUMN "segmentId"; - -ALTER TABLE "widgets" DROP COLUMN "segmentId"; - -DROP TABLE public."memberSegments"; - -DROP TABLE public."organizationSegments"; - - -DROP type public."segmentsStatus_type"; diff --git a/backend/src/database/migrations/U1683277017__missing-members-indexes.sql b/backend/src/database/migrations/U1683277017__missing-members-indexes.sql deleted file mode 100644 index c894947a56..0000000000 --- a/backend/src/database/migrations/U1683277017__missing-members-indexes.sql +++ /dev/null @@ -1,17 +0,0 @@ -drop index "ix_members_displayName"; - -drop index "ix_members_reach_total"; - -drop index "ix_members_score"; - -drop index "ix_members_numberOfOpenSourceContributions"; - -drop index "ix_members_lastEnriched"; - -drop index "ix_members_updatedAt"; - -drop index "ix_members_attributes_isOrganization"; - -drop index "ix_members_attributes_isTeamMember"; - -drop index "ix_members_attributes_isBot"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1683652780__customActivityTypesKeys.sql b/backend/src/database/migrations/U1683652780__customActivityTypesKeys.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1683792045__lowerCaseActivityTypeAndPlatform.sql b/backend/src/database/migrations/U1683792045__lowerCaseActivityTypeAndPlatform.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1684150090__new-integration-pipeline.sql b/backend/src/database/migrations/U1684150090__new-integration-pipeline.sql deleted file mode 100644 index a244ad26e2..0000000000 --- a/backend/src/database/migrations/U1684150090__new-integration-pipeline.sql +++ /dev/null @@ -1 +0,0 @@ -drop schema integration cascade; \ No newline at end of file diff --git a/backend/src/database/migrations/U1684497543__init-default-segments.sql b/backend/src/database/migrations/U1684497543__init-default-segments.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1685449426__incomingWebhooksRetry.sql b/backend/src/database/migrations/U1685449426__incomingWebhooksRetry.sql deleted file mode 100644 index 104ba2f0fc..0000000000 --- a/backend/src/database/migrations/U1685449426__incomingWebhooksRetry.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE public."incomingWebhooks" DROP COLUMN "retries"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1685627171__organizationDisplayName.sql b/backend/src/database/migrations/U1685627171__organizationDisplayName.sql deleted file mode 100644 index 28e6452a43..0000000000 --- a/backend/src/database/migrations/U1685627171__organizationDisplayName.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE public."organizations" DROP COLUMN "displayName"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1686135802__missing-integration-table-indexes.sql b/backend/src/database/migrations/U1686135802__missing-integration-table-indexes.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1686154722__activities-unique-index-segment.sql b/backend/src/database/migrations/U1686154722__activities-unique-index-segment.sql deleted file mode 100644 index 8731ed7aff..0000000000 --- a/backend/src/database/migrations/U1686154722__activities-unique-index-segment.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX ix_unique_activities_tenantid_platform_type_sourceid_segmentid; -CREATE UNIQUE INDEX ix_unique_activities_tenantid_platform_type_sourceid ON activities ("tenantId", platform, type, "sourceId"); \ No newline at end of file diff --git a/backend/src/database/migrations/U1686173729__activityOrganizations.sql b/backend/src/database/migrations/U1686173729__activityOrganizations.sql deleted file mode 100644 index 0fa6b8d5a1..0000000000 --- a/backend/src/database/migrations/U1686173729__activityOrganizations.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE public."activities" DROP COLUMN "organizationId"; - -DROP TABLE "memberSegmentAffiliations"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1686663853__finalize-default-segments.sql b/backend/src/database/migrations/U1686663853__finalize-default-segments.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1686825939__better-segment-slug-uniqueness.sql b/backend/src/database/migrations/U1686825939__better-segment-slug-uniqueness.sql deleted file mode 100644 index 9b34f4b6d2..0000000000 --- a/backend/src/database/migrations/U1686825939__better-segment-slug-uniqueness.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX "segments_slug_pSlug_gpSlugNull_tenantId_idx"; -DROP INDEX "segments_slug_pSlugNull_gpSlugNull_tenantId_idx"; diff --git a/backend/src/database/migrations/U1686900115__sync-timestamp.sql b/backend/src/database/migrations/U1686900115__sync-timestamp.sql deleted file mode 100644 index 723d530b8c..0000000000 --- a/backend/src/database/migrations/U1686900115__sync-timestamp.sql +++ /dev/null @@ -1,5 +0,0 @@ -alter table members - drop column "searchSyncedAt"; - -alter table activities - drop column "searchSyncedAt"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1687188570__tags-without-segments.sql b/backend/src/database/migrations/U1687188570__tags-without-segments.sql deleted file mode 100644 index 56534ea7f8..0000000000 --- a/backend/src/database/migrations/U1687188570__tags-without-segments.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE "tags" ADD COLUMN "segmentId" uuid; diff --git a/backend/src/database/migrations/U1687336284__drop-conversation-slug-index.sql b/backend/src/database/migrations/U1687336284__drop-conversation-slug-index.sql deleted file mode 100644 index 3a3b7c07b2..0000000000 --- a/backend/src/database/migrations/U1687336284__drop-conversation-slug-index.sql +++ /dev/null @@ -1 +0,0 @@ -create index conversations_slug_tenant_id on conversations (slug, "tenantId"); \ No newline at end of file diff --git a/backend/src/database/migrations/U1687353040__tenant-wide-template-reports.sql b/backend/src/database/migrations/U1687353040__tenant-wide-template-reports.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1687366317__migrate-activity-types-channels-to-segments.sql b/backend/src/database/migrations/U1687366317__migrate-activity-types-channels-to-segments.sql deleted file mode 100644 index e19001b731..0000000000 --- a/backend/src/database/migrations/U1687366317__migrate-activity-types-channels-to-segments.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE settings ADD COLUMN "customActivityTypes" JSONB DEFAULT '{}'::JSONB; -ALTER TABLE settings ADD COLUMN "activityChannels" JSONB DEFAULT '{}'::JSONB; - diff --git a/backend/src/database/migrations/U1687374592__fixActivitiesOrganizationIdFKeyConstraint.sql b/backend/src/database/migrations/U1687374592__fixActivitiesOrganizationIdFKeyConstraint.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1687608847__missing-indexes.sql b/backend/src/database/migrations/U1687608847__missing-indexes.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1687886017__addEnterprisePlan.sql b/backend/src/database/migrations/U1687886017__addEnterprisePlan.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1688480428__missing-indexes.sql b/backend/src/database/migrations/U1688480428__missing-indexes.sql deleted file mode 100644 index 0f3ac14cb3..0000000000 --- a/backend/src/database/migrations/U1688480428__missing-indexes.sql +++ /dev/null @@ -1,8 +0,0 @@ -drop index if exists "ix_activities_organizationId"; -drop index if exists "ix_segments_tenantId"; -drop index if exists "ix_memberSegments_segmentId"; -drop index if exists "ix_memberSegments_tenantId"; -drop index if exists "ix_organizationSegments_tenantId"; -drop index if exists "ix_organizationSegments_segmentId"; -drop index if exists "ix_memberSegmentAffiliations_organizationId"; -drop index if exists "ix_memberSegmentAffiliations_segmentId"; diff --git a/backend/src/database/migrations/U1688636697__employment-history.sql b/backend/src/database/migrations/U1688636697__employment-history.sql deleted file mode 100644 index 3ed96a0207..0000000000 --- a/backend/src/database/migrations/U1688636697__employment-history.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE "memberOrganizations" DROP COLUMN "dateStart"; -ALTER TABLE "memberOrganizations" DROP COLUMN "dateEnd"; -ALTER TABLE "memberOrganizations" DROP COLUMN "title"; diff --git a/backend/src/database/migrations/U1688653004__convert-employment-history.sql b/backend/src/database/migrations/U1688653004__convert-employment-history.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1689066813__support-webhooks-in-new-int-framework.sql b/backend/src/database/migrations/U1689066813__support-webhooks-in-new-int-framework.sql deleted file mode 100644 index 15e3888ec3..0000000000 --- a/backend/src/database/migrations/U1689066813__support-webhooks-in-new-int-framework.sql +++ /dev/null @@ -1,20 +0,0 @@ -alter table integration.streams - drop constraint "streams_webhookId_fkey", - drop column "webhookId", - drop constraint "streams_runId_fkey", - alter column "runId" set not null, - add constraint "streams_runId_fkey" foreign key ("runId") references integration.runs (id) on delete cascade; - -alter table integration."apiData" - drop constraint "apiData_webhookId_fkey", - drop column "webhookId", - drop constraint "apiData_runId_fkey", - alter column "runId" set not null, - add constraint "apiData_runId_fkey" foreign key ("runId") references integration.runs (id) on delete cascade; - -alter table integration.results - drop constraint "results_webhookId_fkey", - drop column "webhookId", - drop constraint "results_runId_fkey", - alter column "runId" set not null, - add constraint "results_runId_fkey" foreign key ("runId") references integration.runs (id) on delete cascade; diff --git a/backend/src/database/migrations/U1689175138__employment-history-segments.sql b/backend/src/database/migrations/U1689175138__employment-history-segments.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1689235178__addTermsAndPrivacyColumn.sql b/backend/src/database/migrations/U1689235178__addTermsAndPrivacyColumn.sql deleted file mode 100644 index 54d92a34c9..0000000000 --- a/backend/src/database/migrations/U1689235178__addTermsAndPrivacyColumn.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE public."users" DROP COLUMN "acceptedTermsAndPrivacy"; diff --git a/backend/src/database/migrations/U1689846910__affiliations-primary-key.sql b/backend/src/database/migrations/U1689846910__affiliations-primary-key.sql deleted file mode 100644 index 8200542b97..0000000000 --- a/backend/src/database/migrations/U1689846910__affiliations-primary-key.sql +++ /dev/null @@ -1,14 +0,0 @@ -ALTER TABLE "memberOrganizations" DROP CONSTRAINT "memberOrganizations_pkey"; -ALTER TABLE "memberOrganizations" DROP COLUMN "id"; -DROP INDEX "memberOrganizations_unique"; - -DELETE -FROM "memberOrganizations" -WHERE ctid NOT IN ( - SELECT MIN(ctid) - FROM "memberOrganizations" - GROUP BY "memberId", "organizationId" -); - -ALTER TABLE "memberOrganizations" ADD PRIMARY KEY ("memberId", "organizationId"); - diff --git a/backend/src/database/migrations/U1689891964__addOrganizationsAttributes.sql b/backend/src/database/migrations/U1689891964__addOrganizationsAttributes.sql deleted file mode 100644 index 064f9dccc3..0000000000 --- a/backend/src/database/migrations/U1689891964__addOrganizationsAttributes.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE DROP COLUMN "attributes"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1690191156__addScalePlan.sql b/backend/src/database/migrations/U1690191156__addScalePlan.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1690192894__fix-grandparent-name.sql b/backend/src/database/migrations/U1690192894__fix-grandparent-name.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1690797541__fix-duplicate-work-experiences.sql b/backend/src/database/migrations/U1690797541__fix-duplicate-work-experiences.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1690873474__optimize-integration-tables.sql b/backend/src/database/migrations/U1690873474__optimize-integration-tables.sql deleted file mode 100644 index 11f5f59318..0000000000 --- a/backend/src/database/migrations/U1690873474__optimize-integration-tables.sql +++ /dev/null @@ -1,88 +0,0 @@ --- integration.results - -create index if not exists "ix_integration_results_webhookId" on integration.results ("webhookId"); -create index if not exists "ix_integration_results_updatedAt" on integration.results ("updatedAt"); -create index if not exists "ix_integration_results_tenantId" on integration.results ("tenantId"); -create index if not exists "ix_integration_results_streamId" on integration.results ("streamId"); -create index if not exists "ix_integration_results_processedAt" on integration.results ("processedAt"); -create index if not exists "ix_integration_results_microserviceId" on integration.results ("microserviceId"); -create index if not exists "ix_integration_results_integrationId" on integration.results ("integrationId"); -create index if not exists "ix_integration_results_apiDataId" on integration.results ("apiDataId"); - -alter table integration.results - add constraint "results_webhookId_fkey" foreign key ("webhookId") references "incomingWebhooks" (id); - -alter table integration.results - add constraint "results_streamId_fkey" foreign key ("streamId") references integration.streams (id); - -alter table integration.results - add constraint "results_runId_fkey" foreign key ("runId") references integration.runs (id); - -alter table integration.results - add constraint "results_microserviceId_fkey" foreign key ("microserviceId") references microservices (id); - -alter table integration.results - add constraint "results_integrationId_fkey" foreign key ("integrationId") references integrations (id); - -alter table integration.results - add constraint "results_apiDataId_fkey" foreign key ("apiDataId") references integration."apiData" (id); - --- integration.apiData -create index if not exists "ix_integration_apiData_createdAt" on integration."apiData"("createdAt"); -create index if not exists "ix_integration_apiData_integrationId" on integration."apiData"("integrationId"); -create index if not exists "ix_integration_apiData_microserviceId" on integration."apiData"("microserviceId"); -create index if not exists "ix_integration_apiData_processedAt" on integration."apiData"("processedAt"); -create index if not exists "ix_integration_apiData_runId" on integration."apiData"("runId"); -create index if not exists "ix_integration_apiData_state" on integration."apiData"("state"); -create index if not exists "ix_integration_apiData_streamId" on integration."apiData"("streamId"); - -alter table integration."apiData" - add constraint "apiData_webhookId_fkey" foreign key ("webhookId") references "incomingWebhooks" (id); - -alter table integration."apiData" - add constraint "apiData_streamId_fkey" foreign key ("streamId") references integration.streams (id); - -alter table integration."apiData" - add constraint "apiData_runId_fkey" foreign key ("runId") references integration.runs (id); - -alter table integration."apiData" - add constraint "apiData_microserviceId_fkey" foreign key ("microserviceId") references microservices (id); - -alter table integration."apiData" - add constraint "apiData_integrationId_fkey" foreign key ("integrationId") references integrations (id); - --- integration.streams -drop index if exists "ix_integration_streams_delayedUntil"; -drop index if exists "ix_integration_streams_webhookId"; -create index if not exists "ix_integration_streams_tenantId" on integration.streams ("tenantId"); -create index if not exists "ix_integration_streams_processedAt" on integration.streams ("processedAt"); -create index if not exists "ix_integration_streams_parentId" on integration.streams ("parentId"); -create index if not exists "ix_integration_streams_microserviceId" on integration.streams ("microserviceId"); -create index if not exists "ix_integration_streams_integrationId" on integration.streams ("integrationId"); - -alter table integration.streams - add constraint "streams_webhookId_fkey" foreign key ("webhookId") references "incomingWebhooks" (id); - -alter table integration.streams - add constraint "streams_runId_fkey" foreign key ("runId") references integration.runs (id); - -alter table integration.streams - add constraint "streams_parentId_fkey" foreign key ("parentId") references integration.streams (id); - -alter table integration.streams - add constraint "streams_microserviceId_fkey" foreign key ("microserviceId") references microservices (id); - -alter table integration.streams - add constraint "streams_integrationId_fkey" foreign key ("integrationId") references integrations (id); - --- integration.runs -drop index if exists "ix_integration_runs_delayedUntil"; -create index if not exists "ix_integration_runs_tenantId" on integration.runs("tenantId"); -create index if not exists "ix_integration_runs_processedAt" on integration.runs("processedAt"); -create index if not exists "ix_integration_runs_microserviceId" on integration.runs("microserviceId"); - -alter table integration.runs - add constraint "runs_microserviceId_fkey" foreign key ("microserviceId") references microservices (id); - -alter table integration.streams - add constraint "runs_integrationId_fkey" foreign key ("integrationId") references integrations (id); \ No newline at end of file diff --git a/backend/src/database/migrations/U1691074653__syncRemoteTables.sql b/backend/src/database/migrations/U1691074653__syncRemoteTables.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1691078055__add-unique-index-to-organization-members.sql b/backend/src/database/migrations/U1691078055__add-unique-index-to-organization-members.sql deleted file mode 100644 index ef1ce46728..0000000000 --- a/backend/src/database/migrations/U1691078055__add-unique-index-to-organization-members.sql +++ /dev/null @@ -1 +0,0 @@ -DROP INDEX IF EXISTS ix_unique_member_org_no_date_nulls; diff --git a/backend/src/database/migrations/U1691152131__syncRemoteTablesMissingIndexes.sql b/backend/src/database/migrations/U1691152131__syncRemoteTablesMissingIndexes.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1691485202__organization-sync-timestamp.sql b/backend/src/database/migrations/U1691485202__organization-sync-timestamp.sql deleted file mode 100644 index 6deaed4307..0000000000 --- a/backend/src/database/migrations/U1691485202__organization-sync-timestamp.sql +++ /dev/null @@ -1,2 +0,0 @@ -alter table organizations - drop column "searchSyncedAt"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1691493013__past-activity-affiliation.sql b/backend/src/database/migrations/U1691493013__past-activity-affiliation.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1691569430__org_members_3rd_unique_index.sql b/backend/src/database/migrations/U1691569430__org_members_3rd_unique_index.sql deleted file mode 100644 index 25b008e3b0..0000000000 --- a/backend/src/database/migrations/U1691569430__org_members_3rd_unique_index.sql +++ /dev/null @@ -1 +0,0 @@ -DROP INDEX IF EXISTS ix_unique_member_org_no_end_date_nulls; diff --git a/backend/src/database/migrations/U1691658076__segment-affiliation-dates.sql b/backend/src/database/migrations/U1691658076__segment-affiliation-dates.sql deleted file mode 100644 index fc17ffbcb7..0000000000 --- a/backend/src/database/migrations/U1691658076__segment-affiliation-dates.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE "memberSegmentAffiliations" ADD CONSTRAINT "memberSegmentAffiliations_memberId_segmentId_key" UNIQUE ("memberId", "segmentId"); - -ALTER TABLE "memberSegmentAffiliations" DROP COLUMN "dateStart"; -ALTER TABLE "memberSegmentAffiliations" DROP COLUMN "dateEnd"; - diff --git a/backend/src/database/migrations/U1691667297__restore-work-experience.sql b/backend/src/database/migrations/U1691667297__restore-work-experience.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1692128732__past-activity-affilation-v2.sql b/backend/src/database/migrations/U1692128732__past-activity-affilation-v2.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1692786381__unique-activity-index.sql b/backend/src/database/migrations/U1692786381__unique-activity-index.sql deleted file mode 100644 index 1c5fa7cd74..0000000000 --- a/backend/src/database/migrations/U1692786381__unique-activity-index.sql +++ /dev/null @@ -1 +0,0 @@ -DROP INDEX CONCURRENTLY IF EXISTS activities_tenant_segment_source_id_idx; diff --git a/backend/src/database/migrations/U1692796226__addOrganizationPremiumDataPoints.sql b/backend/src/database/migrations/U1692796226__addOrganizationPremiumDataPoints.sql deleted file mode 100644 index 9643f7851d..0000000000 --- a/backend/src/database/migrations/U1692796226__addOrganizationPremiumDataPoints.sql +++ /dev/null @@ -1,20 +0,0 @@ -ALTER TABLE public."organizations" DROP COLUMN "affiliatedProfiles"; -ALTER TABLE public."organizations" DROP COLUMN "allSubsidiaries"; -ALTER TABLE public."organizations" DROP COLUMN "alternativeDomains"; -ALTER TABLE public."organizations" DROP COLUMN "alternativeNames"; -ALTER TABLE public."organizations" DROP COLUMN "averageEmployeeTenure"; -ALTER TABLE public."organizations" DROP COLUMN "averageTenureByLevel"; -ALTER TABLE public."organizations" DROP COLUMN "averageTenureByRole"; -ALTER TABLE public."organizations" DROP COLUMN "directSubsidiaries"; -ALTER TABLE public."organizations" DROP COLUMN "employeeChurnRate"; -ALTER TABLE public."organizations" DROP COLUMN "employeeCountByMonth"; -ALTER TABLE public."organizations" DROP COLUMN "employeeGrowthRate"; -ALTER TABLE public."organizations" DROP COLUMN "employeeCountByMonthByLevel"; -ALTER TABLE public."organizations" DROP COLUMN "employeeCountByMonthByRole"; -ALTER TABLE public."organizations" DROP COLUMN "gicsSector"; -ALTER TABLE public."organizations" DROP COLUMN "grossAdditionsByMonth"; -ALTER TABLE public."organizations" DROP COLUMN "grossDeparturesByMonth"; -ALTER TABLE public."organizations" DROP COLUMN "ultimateParent"; -ALTER TABLE public."organizations" DROP COLUMN "immediateParent"; -ALTER TABLE public."organizations" ADD COLUMN "parentUrl" TEXT NULL; -ALTER TABLE public."organizationCaches" ADD COLUMN "parentUrl" TEXT NULL; \ No newline at end of file diff --git a/backend/src/database/migrations/U1692800745__index-subprojects-by-tenant.sql b/backend/src/database/migrations/U1692800745__index-subprojects-by-tenant.sql deleted file mode 100644 index 7fca8ad6b2..0000000000 --- a/backend/src/database/migrations/U1692800745__index-subprojects-by-tenant.sql +++ /dev/null @@ -1 +0,0 @@ -DROP INDEX IF EXISTS segments_tenant_subprojects; diff --git a/backend/src/database/migrations/U1692803447__conversation-slug-index.sql b/backend/src/database/migrations/U1692803447__conversation-slug-index.sql deleted file mode 100644 index cb087c4ca8..0000000000 --- a/backend/src/database/migrations/U1692803447__conversation-slug-index.sql +++ /dev/null @@ -1 +0,0 @@ -DROP INDEX CONCURRENTLY IF EXISTS conversations_tenant_segment_slug; diff --git a/backend/src/database/migrations/U1692866303__segmentActivityChannels.sql b/backend/src/database/migrations/U1692866303__segmentActivityChannels.sql deleted file mode 100644 index b60485818b..0000000000 --- a/backend/src/database/migrations/U1692866303__segmentActivityChannels.sql +++ /dev/null @@ -1 +0,0 @@ -drop table if exists "public"."segmentActivityChannels" cascade; \ No newline at end of file diff --git a/backend/src/database/migrations/U1693120114__trackManuallyCreatedOrgsAndMembers.sql b/backend/src/database/migrations/U1693120114__trackManuallyCreatedOrgsAndMembers.sql deleted file mode 100644 index 52fcaaae59..0000000000 --- a/backend/src/database/migrations/U1693120114__trackManuallyCreatedOrgsAndMembers.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE public."organizations" DROP COLUMN "manuallyCreated"; -ALTER TABLE public."members" DROP COLUMN "manuallyCreated"; -ALTER TABLE public."organizationCaches" DROP COLUMN "manuallyCreated"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1693124397__searchSyncedAt-index-fix.sql b/backend/src/database/migrations/U1693124397__searchSyncedAt-index-fix.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1693388845__better-affiliations-source.sql b/backend/src/database/migrations/U1693388845__better-affiliations-source.sql deleted file mode 100644 index 774bbc9e02..0000000000 --- a/backend/src/database/migrations/U1693388845__better-affiliations-source.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE "memberOrganizations" DROP COLUMN source; -ALTER TABLE "memberOrganizations" DROP COLUMN "deletedAt"; diff --git a/backend/src/database/migrations/U1693899411__organizationIdentities.sql b/backend/src/database/migrations/U1693899411__organizationIdentities.sql deleted file mode 100644 index f660dd7a01..0000000000 --- a/backend/src/database/migrations/U1693899411__organizationIdentities.sql +++ /dev/null @@ -1 +0,0 @@ -drop table if exists "public"."organizationIdentities"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1694165658__repo-layer.sql b/backend/src/database/migrations/U1694165658__repo-layer.sql deleted file mode 100644 index c10a79760c..0000000000 --- a/backend/src/database/migrations/U1694165658__repo-layer.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS "githubRepos"; diff --git a/backend/src/database/migrations/U1694533080__normalizeOrgWebsiteColumn.sql b/backend/src/database/migrations/U1694533080__normalizeOrgWebsiteColumn.sql deleted file mode 100644 index 69b25db020..0000000000 --- a/backend/src/database/migrations/U1694533080__normalizeOrgWebsiteColumn.sql +++ /dev/null @@ -1 +0,0 @@ --- it's a destructive operation in up migration, so we can't really undo it \ No newline at end of file diff --git a/backend/src/database/migrations/U1694713629__git-as-integration-results.sql b/backend/src/database/migrations/U1694713629__git-as-integration-results.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1694760454__segment-activity-channels-missing-index.sql b/backend/src/database/migrations/U1694760454__segment-activity-channels-missing-index.sql deleted file mode 100644 index 35ef6cfc58..0000000000 --- a/backend/src/database/migrations/U1694760454__segment-activity-channels-missing-index.sql +++ /dev/null @@ -1 +0,0 @@ -drop index if exists "ix_segmentActivityChannels_segmentId_platform"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1695294574__init-github-repos.sql b/backend/src/database/migrations/U1695294574__init-github-repos.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1695657964__customViews.sql b/backend/src/database/migrations/U1695657964__customViews.sql deleted file mode 100644 index c2cef02832..0000000000 --- a/backend/src/database/migrations/U1695657964__customViews.sql +++ /dev/null @@ -1,7 +0,0 @@ -drop table "customViewOrders"; - -drop table "customViews"; - -drop type "customViewVisibility"; - -drop function if exists create_custom_view_and_order; \ No newline at end of file diff --git a/backend/src/database/migrations/U1695667633__entitySegmentsReplicationIdentitiesFix.sql b/backend/src/database/migrations/U1695667633__entitySegmentsReplicationIdentitiesFix.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1695669657__organizationsAddWebsiteUniqueIdx.sql b/backend/src/database/migrations/U1695669657__organizationsAddWebsiteUniqueIdx.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1695885001__lastSyncedPayloadForSyncRemoteTables.sql b/backend/src/database/migrations/U1695885001__lastSyncedPayloadForSyncRemoteTables.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1695972877__organizationMergeSuggestions.sql b/backend/src/database/migrations/U1695972877__organizationMergeSuggestions.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/database/migrations/U1673446803__tenantStripePaymentFields.sql b/backend/src/database/migrations/U1779280838__exclusion-reason-to-project-catalog.sql similarity index 100% rename from backend/src/database/migrations/U1673446803__tenantStripePaymentFields.sql rename to backend/src/database/migrations/U1779280838__exclusion-reason-to-project-catalog.sql diff --git a/backend/src/database/migrations/U1780555634__update-osps-catalog-id.sql b/backend/src/database/migrations/U1780555634__update-osps-catalog-id.sql new file mode 100644 index 0000000000..9b776c5bbe --- /dev/null +++ b/backend/src/database/migrations/U1780555634__update-osps-catalog-id.sql @@ -0,0 +1,3 @@ +UPDATE "securityInsightsEvaluationSuites" +SET "catalogId" = 'OSPS_B' +WHERE "catalogId" = 'osps-baseline-2026-02'; diff --git a/backend/src/database/migrations/V1687261112__addEnterprisePlan.sql b/backend/src/database/migrations/V1687261112__addEnterprisePlan.sql new file mode 100644 index 0000000000..7d4413a942 --- /dev/null +++ b/backend/src/database/migrations/V1687261112__addEnterprisePlan.sql @@ -0,0 +1 @@ +ALTER TYPE tenant_plans_type ADD VALUE 'Enterprise'; \ No newline at end of file diff --git a/backend/src/database/migrations/V1687886017__addEnterprisePlan.sql b/backend/src/database/migrations/V1687886017__addEnterprisePlan.sql deleted file mode 100644 index 8056dbdf02..0000000000 --- a/backend/src/database/migrations/V1687886017__addEnterprisePlan.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TYPE tenant_plans_type ADD VALUE 'Enterprise'; diff --git a/backend/src/database/migrations/V1689937957__admin-segments.sql b/backend/src/database/migrations/V1689937957__admin-segments.sql new file mode 100644 index 0000000000..11ec818ff9 --- /dev/null +++ b/backend/src/database/migrations/V1689937957__admin-segments.sql @@ -0,0 +1 @@ +ALTER TABLE "tenantUsers" ADD COLUMN "adminSegments" UUID[] DEFAULT '{}'::UUID[] NOT NULL; diff --git a/backend/src/database/migrations/V1699357748__org-merge-status.sql b/backend/src/database/migrations/V1699357748__org-merge-status.sql new file mode 100644 index 0000000000..c72e446c64 --- /dev/null +++ b/backend/src/database/migrations/V1699357748__org-merge-status.sql @@ -0,0 +1 @@ +ALTER TABLE "organizationToMerge" ADD COLUMN status VARCHAR(16) NOT NULL DEFAULT 'ready'; diff --git a/backend/src/database/migrations/V1699459698__merge-actions.sql b/backend/src/database/migrations/V1699459698__merge-actions.sql new file mode 100644 index 0000000000..f50452bccf --- /dev/null +++ b/backend/src/database/migrations/V1699459698__merge-actions.sql @@ -0,0 +1,13 @@ +CREATE TABLE "mergeActions" ( + id UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + "tenantId" UUID NOT NULL REFERENCES tenants (id) ON DELETE CASCADE, + type VARCHAR(16) NOT NULL, -- org or member + "primaryId" UUID NOT NULL, + "secondaryId" UUID NOT NULL, + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + "state" VARCHAR(16) NOT NULL, -- pending, in-progress, done + UNIQUE ("tenantId", type, "primaryId", "secondaryId") +); + +CREATE INDEX "mergeActions_main_idx" ON "mergeActions" (type, "primaryId", "secondaryId"); diff --git a/backend/src/database/migrations/V1699505061__organizationsViewed.sql b/backend/src/database/migrations/V1699505061__organizationsViewed.sql new file mode 100644 index 0000000000..71effb30a2 --- /dev/null +++ b/backend/src/database/migrations/V1699505061__organizationsViewed.sql @@ -0,0 +1 @@ +ALTER TABLE public."settings" ADD COLUMN "organizationsViewed" bool; diff --git a/backend/src/database/migrations/V1699626027__add-retries-to-results.sql b/backend/src/database/migrations/V1699626027__add-retries-to-results.sql new file mode 100644 index 0000000000..6d3e50c827 --- /dev/null +++ b/backend/src/database/migrations/V1699626027__add-retries-to-results.sql @@ -0,0 +1,3 @@ +ALTER TABLE integration.results +ADD COLUMN retries INT, +ADD COLUMN "delayedUntil" TIMESTAMP with time zone NULL; diff --git a/backend/src/database/migrations/V1699934020__contactsViewed.sql b/backend/src/database/migrations/V1699934020__contactsViewed.sql new file mode 100644 index 0000000000..7bda7b8dc0 --- /dev/null +++ b/backend/src/database/migrations/V1699934020__contactsViewed.sql @@ -0,0 +1 @@ +ALTER TABLE public."settings" ADD COLUMN "contactsViewed" bool; diff --git a/backend/src/database/migrations/V1701080323__tenant-priority-level.sql b/backend/src/database/migrations/V1701080323__tenant-priority-level.sql new file mode 100644 index 0000000000..5b7461bdbe --- /dev/null +++ b/backend/src/database/migrations/V1701080323__tenant-priority-level.sql @@ -0,0 +1,2 @@ +alter table tenants + add column "priorityLevel" varchar(255); \ No newline at end of file diff --git a/backend/src/database/migrations/V1701175795__member-emails-index.sql b/backend/src/database/migrations/V1701175795__member-emails-index.sql new file mode 100644 index 0000000000..107470c44b --- /dev/null +++ b/backend/src/database/migrations/V1701175795__member-emails-index.sql @@ -0,0 +1 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS members_tenant_emails on members USING GIN (emails array_ops); diff --git a/backend/src/database/migrations/V1702035391__track-manual-org-changes.sql b/backend/src/database/migrations/V1702035391__track-manual-org-changes.sql new file mode 100644 index 0000000000..72653660ae --- /dev/null +++ b/backend/src/database/migrations/V1702035391__track-manual-org-changes.sql @@ -0,0 +1,2 @@ +alter table organizations + add column "manuallyChangedFields" text[] null; diff --git a/backend/src/database/migrations/V1702983814__channel-index.sql b/backend/src/database/migrations/V1702983814__channel-index.sql new file mode 100644 index 0000000000..5169a84f5e --- /dev/null +++ b/backend/src/database/migrations/V1702983814__channel-index.sql @@ -0,0 +1,3 @@ +DROP INDEX CONCURRENTLY IF EXISTS activities_tenant_segment_source_id_idx; +DROP INDEX CONCURRENTLY IF EXISTS ix_unique_activities_tenantid_platform_type_sourceid_segmentid; +CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS ix_unique_activities_tenantid_platform_type_sourceid_segmentid_channel ON activities ("tenantId", platform, type, "sourceId", "segmentId", channel) \ No newline at end of file diff --git a/backend/src/database/migrations/V1703748504__org-identities.sql b/backend/src/database/migrations/V1703748504__org-identities.sql new file mode 100644 index 0000000000..967bad1e5d --- /dev/null +++ b/backend/src/database/migrations/V1703748504__org-identities.sql @@ -0,0 +1,131 @@ +create table "organizationCacheIdentities" ( + id uuid not null, + name text not null, + website text null, + + primary key (id, name), + foreign key (id) references "organizationCaches" (id) +); + +create index "ix_organizationCacheIdentities_name" on "organizationCacheIdentities" (name); +create index "ix_organizationCacheIdentities_website" on "organizationCacheIdentities" (website); + +alter table "organizationCaches" + rename column name to "oldName"; + +alter table "organizationCaches" + rename column website to "oldWebsite"; + +alter table "organizationCaches" + alter column "oldName" drop not null, + alter column "oldWebsite" drop not null; + +-- fill organizationCacheIdentities table with existing data (for caches without website because for those we need a better logic) +insert into "organizationCacheIdentities"(id, name) +select id, "oldName" +from "organizationCaches" +where "oldWebsite" is null; + +-- fill organizationCacheIdentities table with caches that are the only one in the group by website +insert into "organizationCacheIdentities"(id, name, website) +with data as (select count(id) as ids, "oldWebsite" + from "organizationCaches" + where "oldWebsite" is not null + group by "oldWebsite" + having count(id) = 1) +select oc.id, oc."oldName", oc."oldWebsite" +from "organizationCaches" oc + inner join data d on oc."oldWebsite" = d."oldWebsite"; + +create table "organizationCacheLinks" ( + "organizationCacheId" uuid not null, + "organizationId" uuid not null, + + primary key ("organizationCacheId", "organizationId"), + foreign key ("organizationCacheId") references "organizationCaches" (id), + foreign key ("organizationId") references organizations (id) +); + +-- fill the new organizationCacheLinks table +-- first the ones with website match +insert into "organizationCacheLinks"("organizationCacheId", "organizationId") +select oc.id, o.id +from organizations o + inner join "organizationCaches" oc on oc."oldWebsite" = o.website +where o.website is not null + and not exists (select 1 from "organizationCacheLinks" where "organizationId" = o.id); + +-- then the ones with name match +insert into "organizationCacheLinks"("organizationCacheId", "organizationId") +select oc.id, o.id +from organizations o + inner join "organizationCaches" oc on oc."oldName" = o."displayName" +where o."displayName" is not null + and not exists (select 1 from "organizationCacheLinks" where "organizationId" = o.id); + +-- then the ones that are left +do +$$ + declare + cache_id uuid; + org organizations%rowtype; + begin + for org in select * + from organizations o + where not exists (select 1 from "organizationCacheLinks" where "organizationId" = o.id) + and o."displayName" is not null + and length(trim(o."displayName")) > 0 + loop + -- check if we already created a cache + if org.website is not null then + cache_id := (select id from "organizationCaches" where "oldWebsite" = org.website limit 1); + end if; + + if cache_id is null then + cache_id := (select id from "organizationCaches" where "oldName" = org."displayName" limit 1); + end if; + + if cache_id is null then + -- generate id + cache_id := (select uuid_generate_v1()); + -- insert organizationsCaches row + insert into "organizationCaches" (id, description, emails, "phoneNumbers", logo, tags, twitter, linkedin, crunchbase, employees, "revenueRange", "importHash", "createdAt", "updatedAt", location, github, "employeeCountByCountry", type, "geoLocation", size, ticker, headline, profiles, naics, address, industry, founded, "manuallyCreated", "oldName", "oldWebsite") + values (cache_id, org.description, org.emails, org."phoneNumbers", org.logo, org.tags, org.twitter, org.linkedin, org.crunchbase, org.employees, org."revenueRange", org."importHash", org."createdAt", org."createdAt", org.location, org.github, org."employeeCountByCountry", org.type, org."geoLocation", org.size, org.ticker, org.headline, org.profiles, org.naics, org.address, org.industry, org.founded, org."manuallyCreated", org."displayName", org.website); + -- insert organizationCacheIdentities row + insert into "organizationCacheIdentities"(id, name, website) + values (cache_id, org."displayName", org.website); + end if; + + -- update create link between organizations and organizationCaches tables + insert into "organizationCacheLinks"("organizationCacheId", "organizationId") values (cache_id, org.id); + end loop; + end; +$$; + +-- naics column in organizationCaches and organizations is of type jsonb[] +-- which is not really useful for us since we are not using array operators on it to query it anyway +-- it's also much harder to safely update/insert this using a raw query +-- so we are going to change both columns to just a regular jsonb that will contain an array of objects +alter table "organizationCaches" + add column new_naics jsonb; + +update "organizationCaches" +set new_naics = to_jsonb(naics); + +alter table "organizationCaches" + drop column naics; + +alter table "organizationCaches" + rename column new_naics to naics; + +alter table organizations + add column new_naics jsonb; + +update organizations +set new_naics = to_jsonb(naics); + +alter table organizations + drop column naics; + +alter table organizations + rename column new_naics to naics; \ No newline at end of file diff --git a/backend/src/database/migrations/V1704204841__memberMergeSuggestionsAddActivityEstimate.sql b/backend/src/database/migrations/V1704204841__memberMergeSuggestionsAddActivityEstimate.sql new file mode 100644 index 0000000000..85da83ad38 --- /dev/null +++ b/backend/src/database/migrations/V1704204841__memberMergeSuggestionsAddActivityEstimate.sql @@ -0,0 +1,2 @@ +alter table "memberToMerge" +add column "activityEstimate" int null; \ No newline at end of file diff --git a/backend/src/database/migrations/V1704365919__fix-member-joined-at.sql b/backend/src/database/migrations/V1704365919__fix-member-joined-at.sql new file mode 100644 index 0000000000..ab41e713c2 --- /dev/null +++ b/backend/src/database/migrations/V1704365919__fix-member-joined-at.sql @@ -0,0 +1,36 @@ +DO $$ +DECLARE + _member_id UUID; + _first_acitivity TIMESTAMP; +BEGIN + FOR _member_id IN + SELECT id + FROM members + WHERE EXTRACT(YEAR FROM "joinedAt") = 1970 -- those who have the wrong joinedAt + AND EXISTS ( -- yet have at least one activity with a non-1970 timestamp + SELECT 1 + FROM activities a + WHERE a."memberId" = members.id + AND EXTRACT(YEAR FROM a.timestamp) != 1970 + ) + LOOP + RAISE NOTICE 'member_id: %', _member_id; + + -- find the actual first non-1970 activity timestamp + SELECT MIN(a.timestamp) INTO _first_acitivity + FROM activities a + WHERE EXTRACT(YEAR FROM a.timestamp) != 1970 + AND a."memberId" = _member_id; + + IF _first_acitivity IS NULL THEN + CONTINUE; + END IF; + + RAISE NOTICE 'first_acitivity: %', _first_acitivity; + + UPDATE members + SET "joinedAt" = _first_acitivity + WHERE id = _member_id; + END LOOP; +END; +$$; diff --git a/backend/src/database/migrations/V1704395824__memberMergeSuggestionsLastGeneratedAt.sql b/backend/src/database/migrations/V1704395824__memberMergeSuggestionsLastGeneratedAt.sql new file mode 100644 index 0000000000..0d89f2095c --- /dev/null +++ b/backend/src/database/migrations/V1704395824__memberMergeSuggestionsLastGeneratedAt.sql @@ -0,0 +1,2 @@ +alter table "tenants" +add column "memberMergeSuggestionsLastGeneratedAt" timestamp with time zone null; \ No newline at end of file diff --git a/backend/src/database/migrations/V1704923216__dashboardCacheLastRefreshedAt.sql b/backend/src/database/migrations/V1704923216__dashboardCacheLastRefreshedAt.sql new file mode 100644 index 0000000000..9eae562ae9 --- /dev/null +++ b/backend/src/database/migrations/V1704923216__dashboardCacheLastRefreshedAt.sql @@ -0,0 +1,2 @@ +alter table "segments" +add column "dashboardCacheLastRefreshedAt" timestamp with time zone null; \ No newline at end of file diff --git a/backend/src/database/migrations/V1705060063__org-cache-identities-unique-key.sql b/backend/src/database/migrations/V1705060063__org-cache-identities-unique-key.sql new file mode 100644 index 0000000000..d43c4cd069 --- /dev/null +++ b/backend/src/database/migrations/V1705060063__org-cache-identities-unique-key.sql @@ -0,0 +1,2 @@ +alter table "organizationCacheIdentities" + add constraint ix_unique_website_name unique (name, website); \ No newline at end of file diff --git a/backend/src/database/migrations/V1706543934__memberMergeBackupsForUnmerge.sql b/backend/src/database/migrations/V1706543934__memberMergeBackupsForUnmerge.sql new file mode 100644 index 0000000000..c8a2c8775f --- /dev/null +++ b/backend/src/database/migrations/V1706543934__memberMergeBackupsForUnmerge.sql @@ -0,0 +1,8 @@ +ALTER TABLE public."mergeActions" +ADD COLUMN "unmergeBackup" jsonb null; + +alter table members + add column "manuallyChangedFields" text[] null; + +update "mergeActions" +set state = 'merged' where state = 'done'; \ No newline at end of file diff --git a/backend/src/database/migrations/V1706692519__org-cache-identity-unique-name.sql b/backend/src/database/migrations/V1706692519__org-cache-identity-unique-name.sql new file mode 100644 index 0000000000..8f6a2fb825 --- /dev/null +++ b/backend/src/database/migrations/V1706692519__org-cache-identity-unique-name.sql @@ -0,0 +1,5 @@ +alter table "organizationCacheIdentities" + drop constraint ix_unique_website_name; + +alter table "organizationCacheIdentities" + add constraint ix_unique_name unique (name); \ No newline at end of file diff --git a/backend/src/database/migrations/V1706788189__org-cache-enrichement.sql b/backend/src/database/migrations/V1706788189__org-cache-enrichement.sql new file mode 100644 index 0000000000..5c7f207516 --- /dev/null +++ b/backend/src/database/migrations/V1706788189__org-cache-enrichement.sql @@ -0,0 +1,36 @@ +alter table "organizationCaches" + add column if not exists "affiliatedProfiles" text[]; +alter table "organizationCaches" + add column if not exists "allSubsidiaries" text[]; +alter table "organizationCaches" + add column if not exists "alternativeDomains" text[]; +alter table "organizationCaches" + add column if not exists "alternativeNames" text[]; +alter table "organizationCaches" + add column if not exists "averageEmployeeTenure" double precision; +alter table "organizationCaches" + add column if not exists "averageTenureByLevel" jsonb; +alter table "organizationCaches" + add column if not exists "averageTenureByRole" jsonb; +alter table "organizationCaches" + add column if not exists "directSubsidiaries" text[]; +alter table "organizationCaches" + add column if not exists "employeeChurnRate" jsonb; +alter table "organizationCaches" + add column if not exists "employeeCountByMonth" jsonb; +alter table "organizationCaches" + add column if not exists "employeeGrowthRate" jsonb; +alter table "organizationCaches" + add column if not exists "employeeCountByMonthByLevel" jsonb; +alter table "organizationCaches" + add column if not exists "employeeCountByMonthByRole" jsonb; +alter table "organizationCaches" + add column if not exists "gicsSector" text; +alter table "organizationCaches" + add column if not exists "grossAdditionsByMonth" jsonb; +alter table "organizationCaches" + add column if not exists "grossDeparturesByMonth" jsonb; +alter table "organizationCaches" + add column if not exists "ultimateParent" text; +alter table "organizationCaches" + add column if not exists "immediateParent" text; \ No newline at end of file diff --git a/backend/src/database/migrations/V1708935697__apiData-indexes.sql b/backend/src/database/migrations/V1708935697__apiData-indexes.sql new file mode 100644 index 0000000000..061d90f6f5 --- /dev/null +++ b/backend/src/database/migrations/V1708935697__apiData-indexes.sql @@ -0,0 +1,6 @@ +create index if not exists idx_apidata_state_delayeduntil on integration."apiData" (state, "delayedUntil") + where state = 'delayed'; + +create index if not exists idx_apidata_state on integration."apiData" (state); + +create index if not exists idx_apidata_webhookid_updatedat on integration."apiData" ("webhookId", "updatedAt" desc); diff --git a/backend/src/database/migrations/V1709219531__audit-logs.sql b/backend/src/database/migrations/V1709219531__audit-logs.sql new file mode 100644 index 0000000000..78c76aa18a --- /dev/null +++ b/backend/src/database/migrations/V1709219531__audit-logs.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS "auditLogAction" ( + id UUID NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(), + timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "userId" UUID NOT NULL, + "ipAddress" VARCHAR(255), + "userAgent" VARCHAR(255), + "requestId" UUID NOT NULL, + "actionType" VARCHAR(255) NOT NULL, + success BOOLEAN NOT NULL, + "entityId" UUID NOT NULL, + "oldState" JSONB NOT NULL, + "newState" JSONB NOT NULL, + "diff" JSONB NOT NULL +); + +CREATE INDEX IF NOT EXISTS "auditLogAction_userId" ON "auditLogAction" ("userId"); +CREATE INDEX IF NOT EXISTS "auditLogAction_entityId" ON "auditLogAction" ("entityId"); diff --git a/backend/src/database/migrations/V1709228609__indexed-entities.sql b/backend/src/database/migrations/V1709228609__indexed-entities.sql new file mode 100644 index 0000000000..1bd9867ee3 --- /dev/null +++ b/backend/src/database/migrations/V1709228609__indexed-entities.sql @@ -0,0 +1,11 @@ +create table if not exists indexed_entities ( + type varchar(255) not null, + entity_id uuid not null, + tenant_id uuid not null, + indexed_at timestamptz not null default now(), + + primary key (type, entity_id) +); + +create index if not exists ix_indexed_entities_tenant on indexed_entities (tenant_id); +create index if not exists ix_indexed_entities_type on indexed_entities (type); \ No newline at end of file diff --git a/backend/src/database/migrations/V1709276597__member-identity-types.sql b/backend/src/database/migrations/V1709276597__member-identity-types.sql new file mode 100644 index 0000000000..6a16779575 --- /dev/null +++ b/backend/src/database/migrations/V1709276597__member-identity-types.sql @@ -0,0 +1,24 @@ +alter table "memberIdentities" + add column type varchar(255), + add column verified boolean not null default true; + +alter table "memberIdentities" + rename column username to value; + +update "memberIdentities" +set type = 'username'; + +alter table "memberIdentities" + alter column type set not null; + +alter table "memberIdentities" + drop constraint "memberIdentities_platform_username_tenantId_key"; + +drop index if exists "memberIdentities_platform_username_tenantId_key"; +create unique index if not exists "uix_memberIdentities_platform_value_type_tenantId_verified" on "memberIdentities" (platform, value, type, "tenantId", verified) where verified = true; + +alter table members + rename column emails to "oldEmails"; + +alter table members + rename column "weakIdentities" to "oldWeakIdentities"; \ No newline at end of file diff --git a/backend/src/database/migrations/V1709793196__addAffiliationsLastChecked.sql b/backend/src/database/migrations/V1709793196__addAffiliationsLastChecked.sql new file mode 100644 index 0000000000..f6c99f5c7a --- /dev/null +++ b/backend/src/database/migrations/V1709793196__addAffiliationsLastChecked.sql @@ -0,0 +1,2 @@ +alter table "tenants" +add column "affiliationsLastCheckedAt" timestamp with time zone null; \ No newline at end of file diff --git a/backend/src/database/migrations/V1710166967__remove-apidata.sql b/backend/src/database/migrations/V1710166967__remove-apidata.sql new file mode 100644 index 0000000000..a5b370fb9f --- /dev/null +++ b/backend/src/database/migrations/V1710166967__remove-apidata.sql @@ -0,0 +1,4 @@ +alter table integration.results + drop column if exists "apiDataId"; + +drop table if exists integration."apiData"; \ No newline at end of file diff --git a/backend/src/database/migrations/V1710494493__remove-old-member-columns.sql b/backend/src/database/migrations/V1710494493__remove-old-member-columns.sql new file mode 100644 index 0000000000..f69294897b --- /dev/null +++ b/backend/src/database/migrations/V1710494493__remove-old-member-columns.sql @@ -0,0 +1,9 @@ +alter table members + drop column if exists "usernameOld", + drop column if exists type, + drop column if exists info, + drop column if exists "crowdInfo", + drop column if exists bio, + drop column if exists organisation, + drop column if exists location, + drop column if exists signals; \ No newline at end of file diff --git a/backend/src/database/migrations/V1711630211__fix-member-identities-pk.sql b/backend/src/database/migrations/V1711630211__fix-member-identities-pk.sql new file mode 100644 index 0000000000..c438f3093d --- /dev/null +++ b/backend/src/database/migrations/V1711630211__fix-member-identities-pk.sql @@ -0,0 +1,5 @@ +alter table "memberIdentities" + drop constraint "memberIdentities_pkey"; + +alter table "memberIdentities" + add primary key ("memberId", platform, value, type); \ No newline at end of file diff --git a/backend/src/database/migrations/V1712904840__drop-search-synced-at-columns.sql b/backend/src/database/migrations/V1712904840__drop-search-synced-at-columns.sql new file mode 100644 index 0000000000..70e6988223 --- /dev/null +++ b/backend/src/database/migrations/V1712904840__drop-search-synced-at-columns.sql @@ -0,0 +1,8 @@ +alter table members + drop column "searchSyncedAt"; + +alter table organizations + drop column "searchSyncedAt"; + +alter table activities + drop column "searchSyncedAt"; \ No newline at end of file diff --git a/backend/src/database/migrations/V1712914859__organizationMergeSuggestionsLastGeneratedAt.sql b/backend/src/database/migrations/V1712914859__organizationMergeSuggestionsLastGeneratedAt.sql new file mode 100644 index 0000000000..a8722a338b --- /dev/null +++ b/backend/src/database/migrations/V1712914859__organizationMergeSuggestionsLastGeneratedAt.sql @@ -0,0 +1,2 @@ +alter table "tenants" +add column "organizationMergeSuggestionsLastGeneratedAt" timestamp with time zone null; \ No newline at end of file diff --git a/backend/src/database/migrations/V1713259747__member-identities-index.sql b/backend/src/database/migrations/V1713259747__member-identities-index.sql new file mode 100644 index 0000000000..0dee958de5 --- /dev/null +++ b/backend/src/database/migrations/V1713259747__member-identities-index.sql @@ -0,0 +1 @@ +CREATE INDEX IF NOT EXISTS ix_memberIdentities_tenantId_platform_value_type ON "memberIdentities" ("tenantId", platform, value, type); diff --git a/backend/src/database/migrations/V1713437271__activities-segment-org-index.sql b/backend/src/database/migrations/V1713437271__activities-segment-org-index.sql new file mode 100644 index 0000000000..66ca3eca05 --- /dev/null +++ b/backend/src/database/migrations/V1713437271__activities-segment-org-index.sql @@ -0,0 +1 @@ +CREATE INDEX IF NOT EXISTS activities_segment_organizations ON activities ("segmentId", "organizationId"); diff --git a/backend/src/database/migrations/V1715071755__rawSuggestionTables.sql b/backend/src/database/migrations/V1715071755__rawSuggestionTables.sql new file mode 100644 index 0000000000..b22ea2bafc --- /dev/null +++ b/backend/src/database/migrations/V1715071755__rawSuggestionTables.sql @@ -0,0 +1,21 @@ +create table "memberToMergeRaw" ( + "createdAt" timestamp with time zone not null, + "updatedAt" timestamp with time zone not null, + "memberId" uuid not null constraint "memberToMergeRaw_memberId_fkey1" references members on update cascade on delete cascade, + "toMergeId" uuid not null constraint "memberToMergeRaw_toMergeId_fkey1" references members on update cascade on delete cascade, + similarity double precision, + "activityEstimate" integer, + constraint "memberToMergeRaw_pkey1" primary key ("memberId", "toMergeId") +); +create index "ix_memberToMergeRaw_toMergeId" on "memberToMergeRaw" ("toMergeId"); + +create table "organizationToMergeRaw" ( + "createdAt" timestamp with time zone not null, + "updatedAt" timestamp with time zone not null, + "organizationId" uuid not null references organizations on update cascade on delete cascade, + "toMergeId" uuid not null references organizations on update cascade on delete cascade, + similarity double precision, + status varchar(16) default 'ready'::character varying not null, + constraint "organizationToMergeRaw_pkey" primary key ("organizationId", "toMergeId") +); +create index "ix_organizationToMergeRaw_toMergeId" on "organizationToMergeRaw" ("toMergeId"); \ No newline at end of file diff --git a/backend/src/database/migrations/V1715088946__org-segment-aggregates.sql b/backend/src/database/migrations/V1715088946__org-segment-aggregates.sql new file mode 100644 index 0000000000..0de2aa4094 --- /dev/null +++ b/backend/src/database/migrations/V1715088946__org-segment-aggregates.sql @@ -0,0 +1,76 @@ +CREATE TABLE "organizationSegmentsAgg" ( + "id" UUID PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(), + "organizationId" UUID NOT NULL, + "segmentId" UUID NOT NULL, + "tenantId" UUID NOT NULL, + "joinedAt" TIMESTAMP WITH TIME ZONE NOT NULL, + "lastActive" TIMESTAMP WITH TIME ZONE NOT NULL, + "activeOn" TEXT[] NOT NULL, + "activityCount" BIGINT NOT NULL, + "memberCount" BIGINT NOT NULL, + UNIQUE ("organizationId", "segmentId") +); + +INSERT INTO "organizationSegmentsAgg" +WITH + segments_with_children AS ( + SELECT + pg.id AS segment_id, + 'project-group' AS segment_type, + sp.id AS subproject + FROM segments pg + JOIN segments p ON p."parentSlug" = pg.slug AND p."grandparentSlug" IS NULL + JOIN segments sp ON sp."parentSlug" = p.slug AND sp."grandparentSlug" = p."parentSlug" + WHERE pg."parentSlug" IS NULL + AND pg."grandparentSlug" IS NULL + + UNION ALL + + SELECT + p.id AS segment_id, + 'project' AS segment_type, + sp.id AS subproject + FROM segments p + JOIN segments sp ON sp."parentSlug" = p.slug AND sp."grandparentSlug" = p."parentSlug" + WHERE p."grandparentSlug" IS NULL + + UNION ALL + + SELECT + sp.id AS segment_id, + 'subproject' AS segment_type, + sp.id AS subproject + FROM segments sp + WHERE sp."parentSlug" IS NOT NULL AND sp."grandparentSlug" IS NOT NULL + ), + member_data as ( + select a."segmentId", + a."organizationId", + ARRAY_AGG(DISTINCT a."memberId") AS "memberIds", + count(distinct a."memberId") as "memberCount", + count(distinct a.id) as "activityCount", + case + when array_agg(distinct a.platform) = array [null] then array []::text[] + else array_agg(distinct a.platform) end as "activeOn", + max(a.timestamp) as "lastActive", + min(a.timestamp) filter ( where a.timestamp <> '1970-01-01T00:00:00.000Z') as "joinedAt" + FROM activities a + group by a."segmentId", a."organizationId" + ) +SELECT + gen_random_uuid(), + o."id", + s.segment_id, + o."tenantId", + COALESCE(MIN(md."joinedAt"), '1970-01-01') AS "joinedAt", + MAX(md."lastActive") AS "lastActive", + ARRAY_AGG(DISTINCT active_on.item) AS "activeOn", + SUM(md."activityCount") AS "activityCount", + COUNT(DISTINCT member_ids.item) AS "memberCount" +FROM organizations o +JOIN member_data md ON o."id" = md."organizationId" +JOIN segments_with_children s ON s.subproject = md."segmentId" +LEFT JOIN LATERAL (SELECT unnest(md."activeOn") as item) active_on ON TRUE +LEFT JOIN LATERAL (SELECT unnest(md."memberIds") as item) member_ids ON TRUE +GROUP BY o."id", s.segment_id, o."tenantId" +; diff --git a/backend/src/database/migrations/V1715593593__deprecated-activities.sql b/backend/src/database/migrations/V1715593593__deprecated-activities.sql new file mode 100644 index 0000000000..9c2bf46645 --- /dev/null +++ b/backend/src/database/migrations/V1715593593__deprecated-activities.sql @@ -0,0 +1,5 @@ +-- alter table activities +-- rename to old_activities; + +-- alter table conversations +-- rename to old_conversations; \ No newline at end of file diff --git a/backend/src/database/migrations/V1715788342__memberIdentitiesLowerValueIndex.sql b/backend/src/database/migrations/V1715788342__memberIdentitiesLowerValueIndex.sql new file mode 100644 index 0000000000..13e7a73b57 --- /dev/null +++ b/backend/src/database/migrations/V1715788342__memberIdentitiesLowerValueIndex.sql @@ -0,0 +1 @@ +create index if not exists ix_memberidentities_tenantid_platform_lowervalue_type on "memberIdentities" ("tenantId", platform, lower(value), type); \ No newline at end of file diff --git a/backend/src/database/migrations/V1716186809__remove-org-cache.sql b/backend/src/database/migrations/V1716186809__remove-org-cache.sql new file mode 100644 index 0000000000..3d46c445e3 --- /dev/null +++ b/backend/src/database/migrations/V1716186809__remove-org-cache.sql @@ -0,0 +1,8 @@ +alter table "organizationCacheLinks" + rename to "old_organizationCacheLinks"; + +alter table "organizationCacheIdentities" + rename to "old_organizationCacheIdentities"; + +alter table "organizationCaches" + rename to "old_organizationCaches"; \ No newline at end of file diff --git a/backend/src/database/migrations/V1716452766__deprecate-cubejs.sql b/backend/src/database/migrations/V1716452766__deprecate-cubejs.sql new file mode 100644 index 0000000000..c50ee2053b --- /dev/null +++ b/backend/src/database/migrations/V1716452766__deprecate-cubejs.sql @@ -0,0 +1,7 @@ +DROP TABLE IF EXISTS widgets CASCADE; +DROP TABLE IF EXISTS reports CASCADE; + +DROP MATERIALIZED VIEW IF EXISTS mv_activities_cube CASCADE; +DROP MATERIALIZED VIEW IF EXISTS mv_organizations_cube CASCADE; +DROP MATERIALIZED VIEW IF EXISTS mv_segments_cube CASCADE; +DROP MATERIALIZED VIEW IF EXISTS mv_members_cube CASCADE; diff --git a/backend/src/database/migrations/V1716470828__addActionByToMergeActions.sql b/backend/src/database/migrations/V1716470828__addActionByToMergeActions.sql new file mode 100644 index 0000000000..4660441694 --- /dev/null +++ b/backend/src/database/migrations/V1716470828__addActionByToMergeActions.sql @@ -0,0 +1,5 @@ +ALTER TABLE "mergeActions" +ADD COLUMN "actionBy" uuid DEFAULT null; + +ALTER TABLE "mergeActions" +ADD CONSTRAINT "mergeActions_actionBy_fkey" FOREIGN KEY ("actionBy") REFERENCES public.users(id) ON DELETE NO ACTION ON UPDATE NO ACTION; \ No newline at end of file diff --git a/backend/src/database/migrations/V1716910730__lfx-memberships.sql b/backend/src/database/migrations/V1716910730__lfx-memberships.sql new file mode 100644 index 0000000000..c5298dbe53 --- /dev/null +++ b/backend/src/database/migrations/V1716910730__lfx-memberships.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS "lfxMemberships" ( + "id" UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + "createdAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + "tenantId" UUID NOT NULL REFERENCES "tenants" ("id") ON DELETE CASCADE, + "organizationId" UUID REFERENCES "organizations" ("id") ON DELETE CASCADE, + "segmentId" UUID REFERENCES "segments" ("id") ON DELETE CASCADE, + "accountName" TEXT NOT NULL, + "parentAccount" TEXT, + "project" TEXT, + "productName" TEXT NOT NULL, + "purchaseHistoryName" TEXT NOT NULL, + "installDate" DATE NOT NULL, + "usageEndDate" DATE NOT NULL, + "status" TEXT NOT NULL, + "priceCurrency" TEXT NOT NULL, + "price" INTEGER NOT NULL, + "productFamily" TEXT NOT NULL, + "tier" TEXT NOT NULL, + "accountDomain" TEXT NOT NULL, + "domainAlias" TEXT [], + UNIQUE ("tenantId", "segmentId", "accountName") +); diff --git a/backend/src/database/migrations/V1717160507__dropForeignKeysInOldOrganizationTables.sql b/backend/src/database/migrations/V1717160507__dropForeignKeysInOldOrganizationTables.sql new file mode 100644 index 0000000000..67758d9994 --- /dev/null +++ b/backend/src/database/migrations/V1717160507__dropForeignKeysInOldOrganizationTables.sql @@ -0,0 +1 @@ +ALTER TABLE "old_organizationCacheLinks" DROP CONSTRAINT IF EXISTS "organizationCacheLinks_organizationId_fkey"; \ No newline at end of file diff --git a/backend/src/database/migrations/V1717482645__organization-identitiy-types.sql b/backend/src/database/migrations/V1717482645__organization-identitiy-types.sql new file mode 100644 index 0000000000..ac249a5071 --- /dev/null +++ b/backend/src/database/migrations/V1717482645__organization-identitiy-types.sql @@ -0,0 +1,750 @@ +-- region backup + +create table organization_identities_backup ( + "organizationId" uuid not null, + platform text, + name text, + "sourceId" text, + url text, + "tenantId" uuid not null, + "integrationId" uuid, + "createdAt" timestamptz, + "updatedAt" timestamptz, + + primary key ("organizationId", platform, name), + unique (platform, name, "tenantId") +); + +insert into organization_identities_backup("organizationId", platform, name, "sourceId", url, "tenantId", "integrationId", "createdAt", "updatedAt") +select "organizationId", + platform, + name, + "sourceId", + url, + "tenantId", + "integrationId", + "createdAt", + "updatedAt" +from "organizationIdentities"; + +-- endregion + +-- region first part of schema changes + +alter table organizations + add column names text[] not null default array []::text[]; + +alter table "organizationIdentities" + add column verified boolean not null default false; + +alter table "organizationIdentities" + add column type varchar(255) null; + +alter table "organizationIdentities" + add column value text null; + +-- create temp primary key +alter table "organizationIdentities" + add column temp_id uuid default uuid_generate_v4() not null; + +-- organizationId, platform, name +alter table "organizationIdentities" + drop constraint "organizationIdentities_pkey"; + +alter table "organizationIdentities" + add primary key (temp_id); + + +alter table "organizationIdentities" + alter column name drop not null; + +-- endregion + +-- region migrate organization.website +do +$$ + declare + org organizations%rowtype; + verified boolean; + platform text; + begin + for org in select * from organizations where website is not null and length(trim(website)) > 0 + loop + -- determine if it's verified or not + verified := false; + platform := 'integration'; + + if (select count(*) from "organizationIdentities" where "organizationId" = org.id) = 0 then + verified := true; + elseif (select count(*) + from "memberOrganizations" + where "organizationId" = org.id + and source in ('enrichment', 'ui')) > 0 then + verified := true; + end if; + + if 'website' = any (org."manuallyChangedFields") then + platform := 'custom'; + end if; + + insert into "organizationIdentities"("organizationId", "tenantId", platform, type, value, verified) + values (org.id, org."tenantId", platform, 'primary-domain', trim(org.website), verified); + + + if not verified and org."lastEnrichedAt" is not null and not org."manuallyCreated" then + update organizations + set "phoneNumbers" = null, + tags = null, + employees = null, + "revenueRange" = null, + location = null, + "employeeCountByCountry" = null, + type = null, + "geoLocation" = null, + size = null, + ticker = null, + headline = null, + profiles = null, + naics = null, + address = null, + industry = null, + founded = null, + "allSubsidiaries" = null, + "alternativeNames" = null, + "averageEmployeeTenure" = null, + "averageTenureByLevel" = null, + "averageTenureByRole" = null, + "directSubsidiaries" = null, + "employeeChurnRate" = null, + "employeeCountByMonth" = null, + "employeeGrowthRate" = null, + "employeeCountByMonthByLevel" = null, + "employeeCountByMonthByRole" = null, + "gicsSector" = null, + "grossAdditionsByMonth" = null, + "grossDeparturesByMonth" = null, + "ultimateParent" = null, + "immediateParent" = null + where id = org.id; + end if; + end loop; + end +$$; + +-- endregion + +-- region emails + +-- migrate email identities to primary-domain identities +update "organizationIdentities" +set value = name, + type = 'primary-domain', + name = null, + url = null, + verified = true +where platform = 'email'; + +-- endregion + +-- region github + +-- migrate the ones that have github url so we can extract usernames +update "organizationIdentities" +set value = split_part( + regexp_replace(url, '^(https?://)?', ''), + '/', + 2 + ), + type = 'username', + verified = true +where platform = 'github' + and url ilike 'https://github.com/%'; + +-- these username ones also had name set that we can use +with distinct_names as (select "organizationId", + array_agg(distinct name) as names + from "organizationIdentities" + where platform = 'github' + and name is not null + and url ilike 'https://github.com/%' + and length(trim(name)) > 0 + group by "organizationId") +update organizations o +set names = (select array( + select distinct unnest(array_cat(o.names, dn.names)) + )) +from distinct_names dn +where o.id = dn."organizationId"; + +update "organizationIdentities" +set name = null, + url = null +where platform = 'github' + and url ilike 'https://github.com/%'; + + +-- the rest that have url are not github usernames are websites +update "organizationIdentities" +set value = url, + type = 'primary-domain', + verified = false, + url = null +where platform = 'github' + and url is not null; + +-- these same ones also have names that we need +with distinct_names as (select "organizationId", + array_agg(distinct name) as names + from "organizationIdentities" + where platform = 'github' + and name is not null + and type = 'primary-domain' + and length(trim(name)) > 0 + group by "organizationId") +update organizations o +set names = (select array( + select distinct unnest(array_cat(o.names, dn.names)) + )) +from distinct_names dn +where o.id = dn."organizationId"; + +update "organizationIdentities" +set name = null +where platform = 'github' + and type = 'primary-domain'; + +-- there is but one left that has a name but no url +with distinct_names as (select "organizationId", + array_agg(distinct name) as names + from "organizationIdentities" + where platform = 'github' + and name is not null + and type is null + and url is null + and length(trim(name)) > 0 + group by "organizationId") +update organizations o +set names = (select array( + select distinct unnest(array_cat(o.names, dn.names)) + )) +from distinct_names dn +where o.id = dn."organizationId"; + +delete +from "organizationIdentities" +where platform = 'github' + and type is null + and name is not null; + +-- endregion + +-- region linkedin + +update "organizationIdentities" +set value = 'company:' || trim(name), + name = null, + verified = true, + type = 'username', + url = null +where platform = 'linkedin' + and name is not null + and length(trim(name)) > 0 + and ( + url ilike 'linkedin.com/company/%' or + url ilike 'https://linkedin.com/company/%' or + url ilike 'https://www.linkedin.com/company/%' or + url ilike 'www.linkedin.com/company/%' + ); + +update "organizationIdentities" +set value = 'school:' || trim(name), + name = null, + verified = true, + type = 'username', + url = null +where platform = 'linkedin' + and name is not null + and length(trim(name)) > 0 + and ( + url ilike 'linkedin.com/school/%' or + url ilike 'https://linkedin.com/school/%' or + url ilike 'https://www.linkedin.com/school/%' or + url ilike 'www.linkedin.com/school/%' + ); + +update "organizationIdentities" +set value = 'showcase:' || trim(name), + name = null, + verified = true, + type = 'username', + url = null +where platform = 'linkedin' + and name is not null + and length(trim(name)) > 0 + and ( + url ilike 'linkedin.com/showcase/%' or + url ilike 'https://linkedin.com/showcase/%' or + url ilike 'https://www.linkedin.com/showcase/%' or + url ilike 'www.linkedin.com/showcase/%' + ); + +-- endregion + +-- region enrichment + +-- migrate platform=enrichment identities as they are just names +with distinct_names as (select "organizationId", + array_agg(distinct name) as names + from "organizationIdentities" + where platform = 'enrichment' + and name is not null + and url is null + and length(trim(name)) > 0 + group by "organizationId") +update organizations o +set names = (select array( + select distinct unnest(array_cat(o.names, dn.names)) + )) +from distinct_names dn +where o.id = dn."organizationId"; + +delete +from "organizationIdentities" +where platform = 'enrichment' + and name is not null + and url is null; + +-- endregion + +-- region twitter + +-- migrate twitter +update "organizationIdentities" +set value = name, + type = 'username', + name = null, + url = null, + verified = false +where platform = 'twitter'; + +-- endregion + +-- region custom +-- migrate custom which are just names +with distinct_names as (select "organizationId", + array_agg(distinct name) as names + from "organizationIdentities" + where platform = 'custom' + and name is not null + and url is null + and length(trim(name)) > 0 + group by "organizationId") +update organizations o +set names = (select array( + select distinct unnest(array_cat(o.names, dn.names)) + )) +from distinct_names dn +where o.id = dn."organizationId"; + +delete +from "organizationIdentities" +where platform = 'custom' + and name is not null + and url is null; + +-- endregion + +-- region crunchbase + +update "organizationIdentities" +set type = 'username', + url = null, + value = name, + name = null, + verified = true +where platform = 'crunchbase'; + +-- endregion + +-- region just names left + +-- migrate organizationIdentities.name to organization.names + +with distinct_names as (select "organizationId", + array_agg(distinct name) as names + from "organizationIdentities" + where name is not null + and length(trim(name)) > 0 + group by "organizationId") +update organizations o +set names = (select array( + select distinct unnest(array_cat(o.names, dn.names)) + )) +from distinct_names dn +where o.id = dn."organizationId"; + +delete +from "organizationIdentities" +where name is not null; + +-- endregion + +-- region move organizations.github + +insert into "organizationIdentities"("organizationId", "tenantId", platform, type, value, verified) +select id, "tenantId", 'github', 'username', trim(github ->> 'handle'), false +from organizations o +where (github ->> 'handle') is not null + and length(trim((github ->> 'handle'))) > 0 + and not exists (select 1 + from "organizationIdentities" + where "organizationId" = o.id + and platform = 'github' + and type = 'username' + and value = (o.github ->> 'handle')); + +-- endregion + +-- region move organizations.twitter + +insert into "organizationIdentities"("organizationId", "tenantId", platform, type, value, verified) +select id, "tenantId", 'twitter', 'username', trim(twitter ->> 'handle'), false +from organizations o +where (twitter ->> 'handle') is not null + and length(trim((twitter ->> 'handle'))) > 0 + and not exists (select 1 + from "organizationIdentities" + where "organizationId" = o.id + and platform = 'twitter' + and type = 'username' + and value = (o.twitter ->> 'handle')); + +-- endregion + +-- region move organizations.crunchbase + +insert into "organizationIdentities"("organizationId", "tenantId", platform, type, value, verified) +select id, "tenantId", 'crunchbase', 'username', trim(crunchbase ->> 'handle'), false +from organizations o +where (crunchbase ->> 'handle') is not null + and length(trim((crunchbase ->> 'handle'))) > 0 + and not exists (select 1 + from "organizationIdentities" + where "organizationId" = o.id + and platform = 'crunchbase' + and type = 'username' + and value = (o.crunchbase ->> 'handle')); + +-- endregion + +-- region move organizations.linkedin +do +$$ + declare + org record; + begin + for org in select * + from organizations + where linkedin is not null + and (linkedin ->> 'url') is not null + and (linkedin ->> 'handle') is not null + and length(trim(linkedin ->> 'url')) > 0 + and length(trim(linkedin ->> 'handle')) > 0 + loop + if (org.linkedin ->> 'url') ilike '%linkedin.com/company/%' then + if (select count(*) + from "organizationIdentities" + where "organizationId" = org.id + and platform = 'linkedin' + and type = 'username' + and value = 'company' || trim((org.linkedin ->> 'handle'))) = 0 then + insert into "organizationIdentities"("organizationId", "tenantId", platform, type, value, verified) + values (org.id, org."tenantId", 'linkedin', 'username', 'company:' || trim((org.linkedin ->> 'handle')), false); + end if; + elseif (org.linkedin ->> 'url') ilike '%linkedin.com/school/%' then + if (select count(*) + from "organizationIdentities" + where "organizationId" = org.id + and platform = 'linkedin' + and type = 'username' + and value = 'school' || trim((org.linkedin ->> 'handle'))) = 0 then + insert into "organizationIdentities"("organizationId", "tenantId", platform, type, value, verified) + values (org.id, org."tenantId", 'linkedin', 'username', 'school:' || trim((org.linkedin ->> 'handle')), false); + end if; + elseif (org.linkedin ->> 'url') ilike '%linkedin.com/showcase/%' then + if (select count(*) + from "organizationIdentities" + where "organizationId" = org.id + and platform = 'linkedin' + and type = 'username' + and value = 'showcase' || trim((org.linkedin ->> 'handle'))) = 0 then + insert into "organizationIdentities"("organizationId", "tenantId", platform, type, value, verified) + values (org.id, org."tenantId", 'linkedin', 'username', 'showcase:' || trim((org.linkedin ->> 'handle')), false); + end if; + end if; + end loop; + end; +$$; + +-- endregion + +-- region move alternativeDomains + +do +$$ + declare + org organizations%rowtype; + domain text; + begin + for org in select * + from organizations + where "alternativeDomains" is not null + and cardinality("alternativeDomains") > 0 + loop + for domain in (select distinct unnest_domains + from (select unnest("alternativeDomains") as unnest_domains + from organizations + where id = org.id) as flattened_domains) + loop + if length(trim(domain)) > 0 then + insert into "organizationIdentities"("organizationId", "tenantId", platform, type, value, verified) + values (org.id, org."tenantId", 'enrichment', 'alternative-domain', trim(domain), false); + end if; + end loop; + end loop; + end; +$$; + +-- endregion + +-- region move affiliatedProfiles + +do +$$ + declare + org organizations%rowtype; + profile text; + begin + for org in select * + from organizations + where "affiliatedProfiles" is not null + and cardinality("affiliatedProfiles") > 0 + loop + for profile in (select distinct unnest_profiles + from (select unnest("affiliatedProfiles") as unnest_profiles + from organizations + where id = org.id) as flattened_profiles) + loop + if length(trim(profile)) > 0 then + insert into "organizationIdentities"("organizationId", "tenantId", platform, type, value, verified) + values (org.id, org."tenantId", 'enrichment', 'affiliated-profile', trim(profile), false); + end if; + end loop; + end loop; + end; +$$; + +-- endregion + +-- region move weakIdentities +update organizations +set "weakIdentities" = jsonb_build_array() +where "weakIdentities" is not null + and ("weakIdentities"::text) = '{}'; + +do +$$ + declare + org organizations%rowtype; + e jsonb; + clear boolean; + begin + for org in select * + from organizations + where "weakIdentities" is not null + and jsonb_array_length("weakIdentities") > 0 + loop + clear := false; + for e in select value from jsonb_array_elements(org."weakIdentities") as value + loop + if length(trim((e ->> 'name')::text)) > 0 then + if (e ->> 'platform') = 'twitter' then + insert into "organizationIdentities"("organizationId", "tenantId", platform, type, value, verified) + values (org.id, org."tenantId", 'enrichment', 'username', trim(e ->> 'name'), false); + + clear := true; + end if; + + if (e ->> 'platform') = 'linkedin' then + insert into "organizationIdentities"("organizationId", "tenantId", platform, type, value, verified) + values (org.id, org."tenantId", 'enrichment', 'username', 'company:' || trim((e ->> 'name')), false); + clear := true; + end if; + end if; + end loop; + if clear then + update organizations set "weakIdentities" = jsonb_build_array() where id = org.id; + end if; + end loop; + end; +$$; + +-- endregion + +-- region duplicate cleanup within the same organization so that we can setup primary key + +do +$$ + declare + i record; + count int; + begin + for i in select "organizationId", value, type, platform, count(*) as count + from "organizationIdentities" + group by "organizationId", value, type, platform + having count(*) > 1 + loop + -- try to delete the unverified duplicates first + with row_to_delete as (select ctid + from "organizationIdentities" + where "organizationId" = i."organizationId" + and platform = i.platform + and type = i.type + and value = i.value + and verified = false + limit i.count - 1) + delete + from "organizationIdentities" + where ctid in (select ctid from row_to_delete); + + -- if we have still duplicates left then delete verified ones until there is only one left + select count(*) + into count + from "organizationIdentities" + where "organizationId" = i."organizationId" + and platform = i.platform + and type = i.type + and value = i.value; + if count > 1 then + with row_to_delete as (select ctid + from "organizationIdentities" + where "organizationId" = i."organizationId" + and platform = i.platform + and type = i.type + and value = i.value + and verified = true + limit count - 1) + delete + from "organizationIdentities" + where ctid in (select ctid from row_to_delete); + end if; + end loop; + end; +$$; + +-- endregion + +-- region duplicate verified for the same tenant + +do +$$ + declare + row record; + orgid uuid; + duplicate record; + count int; + newvalue text; + begin + -- find all duplicates with verified=true for the same tenant + for row in select "tenantId", type, platform, value, count(*) as count + from "organizationIdentities" + where verified = true + group by "tenantId", type, platform, value + having count(*) > 1 + loop + -- we gonna rename all except one and add a merge suggestion for them + count := 1; + for duplicate in select ctid, * + from "organizationIdentities" + where "tenantId" = row."tenantId" + and platform = row.platform + and value = row.value + and verified = true + loop + if count = 1 then + orgid := duplicate."organizationId"; + else + newvalue := duplicate.value || ' ' || count; + update "organizationIdentities" + set value = newvalue + where ctid = duplicate.ctid; + + if (select count(*) + from "organizationToMerge" + where ("organizationId" = orgid and "toMergeId" = duplicate."organizationId") + or ("organizationId" = duplicate."organizationId" and "toMergeId" = orgid)) = 0 then + insert into "organizationToMerge"("organizationId", "toMergeId", status, "createdAt", "updatedAt") + values (orgid, duplicate."organizationId", 'ready', now(), now()); + end if; + end if; + + count := count + 1; + end loop; + end loop; + end; +$$; +-- endregion + +-- region final schema changes + +alter table "organizationIdentities" + drop column name; + +alter table "organizationIdentities" + drop column url; + +alter table "organizationIdentities" + alter column value set not null; + +alter table "organizationIdentities" + drop constraint "organizationIdentities_pkey"; + +alter table "organizationIdentities" + add primary key ("organizationId", platform, type, value); + +alter table "organizationIdentities" + drop column temp_id; + +create unique index "uix_organizationIdentities_plat_val_typ_tenantId_verified" + on "organizationIdentities" (platform, value, type, "tenantId", verified) + where (verified = true); + +alter table organizations + rename column website to old_website; + +alter table organizations + rename column "alternativeDomains" to "old_alternativeDomains"; + +alter table organizations + rename column "affiliatedProfiles" to "old_affiliatedProfiles"; + +alter table organizations + rename column "weakIdentities" to "old_weakIdentities"; + +alter table organizations + rename column github to old_github; + +alter table organizations + rename column twitter to old_twitter; + +alter table organizations + rename column crunchbase to old_crunchbase; + +alter table organizations + rename column linkedin to old_linkedin; + +-- endregion + +-- region delete organizations that don't have website and no identities + +-- endregion \ No newline at end of file diff --git a/backend/src/database/migrations/V1718008942__llmSuggestionVerdicts.sql b/backend/src/database/migrations/V1718008942__llmSuggestionVerdicts.sql new file mode 100644 index 0000000000..053158ebf9 --- /dev/null +++ b/backend/src/database/migrations/V1718008942__llmSuggestionVerdicts.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS "llmSuggestionVerdicts" ( + "id" UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + "type" TEXT NOT NULL, + "model" TEXT NOT NULL, + "primaryId" UUID NOT NULL, + "secondaryId" UUID NOT NULL, + "prompt" TEXT NOT NULL, + "verdict" TEXT NOT NULL, + "inputTokenCount" INTEGER NOT NULL, + "outputTokenCount" INTEGER NOT NULL, + "responseTimeSeconds" INTEGER NOT NULL, + "createdAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +create index if not exists "ix_llmSuggestionVerdicts_type_primaryId_secondaryId" on "llmSuggestionVerdicts" ("type", "primaryId", "secondaryId"); \ No newline at end of file diff --git a/backend/src/database/migrations/V1718289198__org-attributes.sql b/backend/src/database/migrations/V1718289198__org-attributes.sql new file mode 100644 index 0000000000..3ab9907f59 --- /dev/null +++ b/backend/src/database/migrations/V1718289198__org-attributes.sql @@ -0,0 +1,141 @@ +CREATE TABLE "orgAttributes" ( + "id" uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + "createdAt" TIMESTAMP NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMP NOT NULL DEFAULT NOW(), + "organizationId" uuid REFERENCES "organizations" ("id") NOT NULL, + "type" VARCHAR(255) NOT NULL, + "name" VARCHAR(255) NOT NULL, + "source" VARCHAR(255) NOT NULL, + "default" BOOLEAN NOT NULL DEFAULT FALSE, + "value" TEXT NOT NULL +); + +-- make sure there is only one default attribute +CREATE UNIQUE INDEX "orgAttributes_organizationId_name_default" ON "orgAttributes" ("organizationId", "name", "default") WHERE "default"; + +CREATE OR REPLACE FUNCTION add_org_attribute( + _org_id UUID, + _type TEXT, + _name TEXT, + _source TEXT, + _default BOOLEAN, + _value anyelement +) +RETURNS VOID AS $$ +BEGIN + IF _value IS NOT NULL THEN + INSERT INTO "orgAttributes" ("organizationId", "type", "name", "source", "default", "value") + VALUES (_org_id, _type, _name, _source, _default, _value::TEXT) + ON CONFLICT DO NOTHING; + END IF; +END; +$$ LANGUAGE plpgsql; + + +DO $$ +DECLARE + _org RECORD; + _value TEXT; + _source TEXT; +BEGIN + -- types: string | decimal | integer | boolean | object | array + FOR _org IN SELECT * FROM organizations LOOP + _source := CASE + WHEN _org."lastEnrichedAt" is null and NOT _org."manuallyCreated" THEN 'integration' + WHEN _org."lastEnrichedAt" is null and _org."manuallyCreated" THEN 'custom' + WHEN _org."lastEnrichedAt" is not null THEN 'peopledatalabs' + ELSE 'unknown' + END; + + PERFORM add_org_attribute(_org.id, 'string', 'description', _source, TRUE, _org.description); + + PERFORM add_org_attribute(_org.id, 'string', 'logo', _source, TRUE, _org.logo); + PERFORM add_org_attribute(_org.id, 'string', 'location', _source, TRUE, _org.location); + PERFORM add_org_attribute(_org.id, 'string', 'type', _source, TRUE, _org.type); + PERFORM add_org_attribute(_org.id, 'string', 'geoLocation', _source, TRUE, _org."geoLocation"); + PERFORM add_org_attribute(_org.id, 'string', 'size', _source, TRUE, _org.size); + PERFORM add_org_attribute(_org.id, 'string', 'ticker', _source, TRUE, _org.ticker); + PERFORM add_org_attribute(_org.id, 'string', 'headline', _source, TRUE, _org.headline); + PERFORM add_org_attribute(_org.id, 'string', 'industry', _source, TRUE, _org.industry); + PERFORM add_org_attribute(_org.id, 'string', 'gicsSector', _source, TRUE, _org."gicsSector"); + PERFORM add_org_attribute(_org.id, 'string', 'ultimateParent', _source, TRUE, _org."ultimateParent"); + PERFORM add_org_attribute(_org.id, 'string', 'immediateParent', _source, TRUE, _org."immediateParent"); + + PERFORM add_org_attribute(_org.id, 'integer', 'employees', _source, TRUE, _org.employees); + PERFORM add_org_attribute(_org.id, 'integer', 'founded', _source, TRUE, _org.founded); + + PERFORM add_org_attribute(_org.id, 'decimal', 'averageEmployeeTenure', _source, TRUE, _org."averageEmployeeTenure"); + + PERFORM add_org_attribute(_org.id, 'object', 'revenueRange', _source, TRUE, _org."revenueRange"); + PERFORM add_org_attribute(_org.id, 'object', 'employeeCountByCountry', _source, TRUE, _org."employeeCountByCountry"); + PERFORM add_org_attribute(_org.id, 'object', 'address', _source, TRUE, _org.address); + PERFORM add_org_attribute(_org.id, 'object', 'averageTenureByLevel', _source, TRUE, _org."averageTenureByLevel"); + PERFORM add_org_attribute(_org.id, 'object', 'averageTenureByRole', _source, TRUE, _org."averageTenureByRole"); + PERFORM add_org_attribute(_org.id, 'object', 'employeeChurnRate', _source, TRUE, _org."employeeChurnRate"); + PERFORM add_org_attribute(_org.id, 'object', 'employeeCountByMonth', _source, TRUE, _org."employeeCountByMonth"); + PERFORM add_org_attribute(_org.id, 'object', 'employeeGrowthRate', _source, TRUE, _org."employeeGrowthRate"); + PERFORM add_org_attribute(_org.id, 'object', 'employeeCountByMonthByLevel', _source, TRUE, _org."employeeCountByMonthByLevel"); + PERFORM add_org_attribute(_org.id, 'object', 'employeeCountByMonthByRole', _source, TRUE, _org."employeeCountByMonthByRole"); + PERFORM add_org_attribute(_org.id, 'object', 'grossAdditionsByMonth', _source, TRUE, _org."grossAdditionsByMonth"); + PERFORM add_org_attribute(_org.id, 'object', 'grossDeparturesByMonth', _source, TRUE, _org."grossDeparturesByMonth"); + PERFORM add_org_attribute(_org.id, 'object', 'naics', _source, TRUE, _org.naics); + + FOR _value IN SELECT UNNEST(_org."emails") AS value LOOP + PERFORM add_org_attribute(_org.id, 'string', 'email', _source, FALSE, _value); + END LOOP; + FOR _value IN SELECT UNNEST(_org."names") AS value LOOP + PERFORM add_org_attribute(_org.id, 'string', 'name', _source, FALSE, _value); + END LOOP; + PERFORM add_org_attribute(_org.id, 'string', 'name', _source, TRUE, _org."displayName"); + FOR _value IN SELECT UNNEST(_org."phoneNumbers") AS value LOOP + PERFORM add_org_attribute(_org.id, 'string', 'phoneNumber', _source, FALSE, _value); + END LOOP; + FOR _value IN SELECT UNNEST(_org."tags") AS value LOOP + PERFORM add_org_attribute(_org.id, 'string', 'tag', _source, FALSE, _value); + END LOOP; + FOR _value IN SELECT UNNEST(_org."profiles") AS value LOOP + PERFORM add_org_attribute(_org.id, 'string', 'profile', _source, FALSE, _value); + END LOOP; + FOR _value IN SELECT UNNEST(_org."allSubsidiaries") AS value LOOP + PERFORM add_org_attribute(_org.id, 'string', 'subsidiary', _source, FALSE, _value); + END LOOP; + FOR _value IN SELECT UNNEST(_org."alternativeNames") AS value LOOP + PERFORM add_org_attribute(_org.id, 'string', 'alternativeName', _source, FALSE, _value); + END LOOP; + FOR _value IN SELECT UNNEST(_org."directSubsidiaries") AS value LOOP + PERFORM add_org_attribute(_org.id, 'string', 'directSubsidiary', _source, FALSE, _value); + END LOOP; +-- EXCEPTION WHEN OTHERS THEN +-- RAISE EXCEPTION '_org: %', _org.id; +-- END; + END LOOP; +END; +$$; + +DROP FUNCTION add_org_attribute(UUID, TEXT, TEXT, TEXT, BOOLEAN, anyelement); + +ALTER TABLE organizations RENAME COLUMN "emails" TO "old_emails"; +ALTER TABLE organizations RENAME COLUMN "phoneNumbers" TO "old_phoneNumbers"; +ALTER TABLE organizations RENAME COLUMN "employeeCountByCountry" TO "old_employeeCountByCountry"; +ALTER TABLE organizations RENAME COLUMN "geoLocation" TO "old_geoLocation"; +ALTER TABLE organizations RENAME COLUMN "ticker" TO "old_ticker"; +ALTER TABLE organizations RENAME COLUMN "profiles" TO "old_profiles"; +ALTER TABLE organizations RENAME COLUMN "address" TO "old_address"; +ALTER TABLE organizations RENAME COLUMN "attributes" TO "old_attributes"; +ALTER TABLE organizations RENAME COLUMN "allSubsidiaries" TO "old_allSubsidiaries"; +ALTER TABLE organizations RENAME COLUMN "alternativeNames" TO "old_alternativeNames"; +ALTER TABLE organizations RENAME COLUMN "averageEmployeeTenure" TO "old_averageEmployeeTenure"; +ALTER TABLE organizations RENAME COLUMN "averageTenureByLevel" TO "old_averageTenureByLevel"; +ALTER TABLE organizations RENAME COLUMN "averageTenureByRole" TO "old_averageTenureByRole"; +ALTER TABLE organizations RENAME COLUMN "directSubsidiaries" TO "old_directSubsidiaries"; +ALTER TABLE organizations RENAME COLUMN "employeeCountByMonth" TO "old_employeeCountByMonth"; +ALTER TABLE organizations RENAME COLUMN "employeeCountByMonthByLevel" TO "old_employeeCountByMonthByLevel"; +ALTER TABLE organizations RENAME COLUMN "employeeCountByMonthByRole" TO "old_employeeCountByMonthByRole"; +ALTER TABLE organizations RENAME COLUMN "gicsSector" TO "old_gicsSector"; +ALTER TABLE organizations RENAME COLUMN "grossAdditionsByMonth" TO "old_grossAdditionsByMonth"; +ALTER TABLE organizations RENAME COLUMN "grossDeparturesByMonth" TO "old_grossDeparturesByMonth"; +ALTER TABLE organizations RENAME COLUMN "ultimateParent" TO "old_ultimateParent"; +ALTER TABLE organizations RENAME COLUMN "immediateParent" TO "old_immediateParent"; +ALTER TABLE organizations RENAME COLUMN "manuallyChangedFields" TO "old_manuallyChangedFields"; +ALTER TABLE organizations RENAME COLUMN "naics" TO "old_naics"; +ALTER TABLE organizations RENAME COLUMN "names" TO "old_names"; diff --git a/backend/src/database/migrations/V1718871280__missingIndexesForLLMQueries.sql b/backend/src/database/migrations/V1718871280__missingIndexesForLLMQueries.sql new file mode 100644 index 0000000000..ab06bbde1f --- /dev/null +++ b/backend/src/database/migrations/V1718871280__missingIndexesForLLMQueries.sql @@ -0,0 +1,3 @@ +CREATE INDEX IF NOT EXISTS "ix_organizationToMergeRaw_organizationId" ON "organizationToMergeRaw" ("organizationId"); + +CREATE INDEX IF NOT EXISTS "ix_lfxMemberships_organizationId" ON "lfxMemberships" ("organizationId"); \ No newline at end of file diff --git a/backend/src/database/migrations/V1719390926__orgAvgContributorEngagement.sql b/backend/src/database/migrations/V1719390926__orgAvgContributorEngagement.sql new file mode 100644 index 0000000000..29184a16e3 --- /dev/null +++ b/backend/src/database/migrations/V1719390926__orgAvgContributorEngagement.sql @@ -0,0 +1,21 @@ +ALTER TABLE "organizationSegmentsAgg" ADD COLUMN "avgContributorEngagement" INTEGER; + +-- compute avgContributorEngagement for existing organizations +WITH avg_engagement AS ( + SELECT + a."organizationId", + a."segmentId", + COALESCE(ROUND(AVG(a.score)), 0) AS "avgContributorEngagement" + FROM activities a + GROUP BY a."organizationId", a."segmentId", a."tenantId" +) +UPDATE "organizationSegmentsAgg" osa +SET "avgContributorEngagement" = ae."avgContributorEngagement" +FROM avg_engagement ae +WHERE osa."organizationId" = ae."organizationId" +AND osa."segmentId" = ae."segmentId"; + +-- set null to 0 for existings records +UPDATE "organizationSegmentsAgg" SET "avgContributorEngagement" = 0 WHERE "avgContributorEngagement" IS NULL; + +ALTER TABLE "organizationSegmentsAgg" ALTER COLUMN "avgContributorEngagement" SET NOT NULL; \ No newline at end of file diff --git a/backend/src/database/migrations/V1719995467__member-segment-aggregates.sql b/backend/src/database/migrations/V1719995467__member-segment-aggregates.sql new file mode 100644 index 0000000000..22a1214f96 --- /dev/null +++ b/backend/src/database/migrations/V1719995467__member-segment-aggregates.sql @@ -0,0 +1,61 @@ +CREATE TABLE "memberSegmentsAgg" ( + "id" UUID PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(), + "memberId" UUID NOT NULL, + "segmentId" UUID NOT NULL, + "tenantId" UUID NOT NULL, + "activityCount" BIGINT NOT NULL, + "lastActive" TIMESTAMP WITH TIME ZONE NOT NULL, + "activityTypes" TEXT[] NOT NULL, + "activeOn" TEXT[] NOT NULL, + "averageSentiment" NUMERIC(5, 2), -- 5 digits in total, 2 of them after the decimal point. To account for 100.00 + UNIQUE ("memberId", "segmentId") +); + + +INSERT INTO "memberSegmentsAgg" +WITH + segments_with_children AS ( + SELECT + pg.id AS segment_id, + 'project-group' AS segment_type, + sp.id AS subproject + FROM segments pg + JOIN segments p ON p."parentSlug" = pg.slug AND p."grandparentSlug" IS NULL + JOIN segments sp ON sp."parentSlug" = p.slug AND sp."grandparentSlug" = p."parentSlug" + WHERE pg."parentSlug" IS NULL + AND pg."grandparentSlug" IS NULL + + UNION ALL + + SELECT + p.id AS segment_id, + 'project' AS segment_type, + sp.id AS subproject + FROM segments p + JOIN segments sp ON sp."parentSlug" = p.slug AND sp."grandparentSlug" = p."parentSlug" + WHERE p."grandparentSlug" IS NULL + + UNION ALL + + SELECT + sp.id AS segment_id, + 'subproject' AS segment_type, + sp.id AS subproject + FROM segments sp + WHERE sp."parentSlug" IS NOT NULL AND sp."grandparentSlug" IS NOT NULL + ) +SELECT + gen_random_uuid(), + m."id", + s.segment_id, + m."tenantId", + COUNT(DISTINCT a.id) AS "activityCount", + MAX(a.timestamp) AS "lastActive", + ARRAY_AGG(DISTINCT CONCAT(a.platform, ':', a.type)) FILTER (WHERE a.platform IS NOT NULL) AS "activityTypes", + ARRAY_AGG(DISTINCT a.platform) FILTER (WHERE a.platform IS NOT NULL) AS "activeOn", + ROUND(AVG((a.sentiment ->> 'sentiment')::NUMERIC(5, 2)), 2) AS "averageSentiment" +FROM activities a +JOIN members m ON m."id" = a."memberId" +JOIN segments_with_children s ON s.subproject = a."segmentId" +GROUP BY m."id", s.segment_id, m."tenantId" +ON CONFLICT DO NOTHING; diff --git a/backend/src/database/migrations/V1720006515__org_attributes_org_index.sql b/backend/src/database/migrations/V1720006515__org_attributes_org_index.sql new file mode 100644 index 0000000000..89f7787d56 --- /dev/null +++ b/backend/src/database/migrations/V1720006515__org_attributes_org_index.sql @@ -0,0 +1 @@ +CREATE INDEX IF NOT EXISTS org_attributes_organization_id ON "orgAttributes" ("organizationId"); diff --git a/backend/src/database/migrations/V1720088742__org-attributes-dedup.sql b/backend/src/database/migrations/V1720088742__org-attributes-dedup.sql new file mode 100644 index 0000000000..6d7feaeb80 --- /dev/null +++ b/backend/src/database/migrations/V1720088742__org-attributes-dedup.sql @@ -0,0 +1,56 @@ +ALTER TABLE "orgAttributes" DROP COLUMN IF EXISTS "type"; + +DROP INDEX "orgAttributes_organizationId_name_default"; + +DO $$ +DECLARE + _org_id UUID; + _attr RECORD; + _count BIGINT; + _first_id UUID; + _deleted BIGINT; +BEGIN + FOR _org_id IN (SELECT DISTINCT "organizationId" FROM "orgAttributes") + LOOP + RAISE NOTICE 'Org: %', _org_id; + + FOR _attr IN (SELECT DISTINCT name, source, value FROM "orgAttributes" WHERE "organizationId" = _org_id) + LOOP + SELECT COUNT(*) INTO _count + FROM "orgAttributes" + WHERE "organizationId" = _org_id + AND name = _attr.name + AND source = _attr.source + AND value = _attr.value; + + IF _count = 1 THEN + CONTINUE; + ELSE + RAISE NOTICE 'Found % duplicates for attribute: name=%, source=%', _count - 1, _attr.name, _attr.source; + END IF; + + SELECT id INTO _first_id + FROM "orgAttributes" + WHERE "organizationId" = _org_id + AND name = _attr.name + AND source = _attr.source + AND value = _attr.value + LIMIT 1; + + DELETE FROM "orgAttributes" + WHERE "organizationId" = _org_id + AND name = _attr.name + AND source = _attr.source + AND value = _attr.value + AND id != _first_id; + GET DIAGNOSTICS _deleted = ROW_COUNT; + RAISE NOTICE 'Deleted % duplicates', _deleted; + + END LOOP; + END LOOP; + + RAISE NOTICE 'Creating index'; + + CREATE UNIQUE INDEX "orgAttributes_organizationId_name_source_value" ON "orgAttributes" ("organizationId", "name", "source", MD5(value)); +END; +$$; diff --git a/backend/src/database/migrations/V1720437208__drop-memberActivityAggregatesMVs.sql b/backend/src/database/migrations/V1720437208__drop-memberActivityAggregatesMVs.sql new file mode 100644 index 0000000000..4866515689 --- /dev/null +++ b/backend/src/database/migrations/V1720437208__drop-memberActivityAggregatesMVs.sql @@ -0,0 +1 @@ +DROP MATERIALIZED VIEW "memberActivityAggregatesMVs"; diff --git a/backend/src/database/migrations/V1720446425__segmentsWithParentIdsAndType.sql b/backend/src/database/migrations/V1720446425__segmentsWithParentIdsAndType.sql new file mode 100644 index 0000000000..9cfce4791b --- /dev/null +++ b/backend/src/database/migrations/V1720446425__segmentsWithParentIdsAndType.sql @@ -0,0 +1,64 @@ +ALTER TABLE "segments" ADD COLUMN IF NOT EXISTS "parentId" uuid; +ALTER TABLE "segments" ADD COLUMN IF NOT EXISTS "grandparentId" uuid; +ALTER TABLE "segments" ADD COLUMN "type" TEXT GENERATED ALWAYS AS ( + CASE + WHEN "parentSlug" IS NULL AND "grandparentSlug" IS NULL THEN 'projectGroup' + WHEN "grandparentSlug" IS NULL AND "parentSlug" IS NOT NULL THEN 'project' + ELSE 'subproject' + END + ) STORED; + + +DO +$$ + DECLARE + segment segments%ROWTYPE; + parent segments%ROWTYPE; + grandparent segments%ROWTYPE; + BEGIN + FOR segment IN SELECT * FROM segments + LOOP + IF segment."type" = 'project' THEN + SELECT * INTO parent + FROM segments + WHERE "slug" = segment."parentSlug" + AND "type" = 'projectGroup'; + + UPDATE segments + SET "parentId" = parent.id + WHERE id = segment.id; + + + ELSIF segment."type" = 'subproject' THEN + SELECT * INTO parent + FROM segments + WHERE "slug" = segment."parentSlug" + AND "type" = 'project'; + + SELECT * INTO grandparent + FROM segments + WHERE "slug" = segment."grandparentSlug" + AND "type" = 'projectGroup'; + + UPDATE segments + SET "parentId" = parent.id, + "grandparentId" = grandparent.id + WHERE id = segment.id; + + END IF; + END LOOP; + END ; +$$; + +COMMIT; + + +create index "segments_parent_id" + on public.segments ("parentId"); + + +create index "segments_grandparent_id" + on public.segments ("grandparentId"); + +create index "segments_type" + on public.segments ("type"); \ No newline at end of file diff --git a/backend/src/database/migrations/V1721115865__member-segments-agg-index.sql b/backend/src/database/migrations/V1721115865__member-segments-agg-index.sql new file mode 100644 index 0000000000..c635ed5656 --- /dev/null +++ b/backend/src/database/migrations/V1721115865__member-segments-agg-index.sql @@ -0,0 +1 @@ +CREATE INDEX IF NOT EXISTS member_segments_agg_segment_id ON "memberSegmentsAgg" ("segmentId"); diff --git a/backend/src/database/migrations/V1721311363__addStepToMergeActions.sql b/backend/src/database/migrations/V1721311363__addStepToMergeActions.sql new file mode 100644 index 0000000000..1bce0447f2 --- /dev/null +++ b/backend/src/database/migrations/V1721311363__addStepToMergeActions.sql @@ -0,0 +1,2 @@ +alter table "mergeActions" add column "step" text; + diff --git a/backend/src/database/migrations/V1721723311__improveMemberDisplayNameSearch.sql b/backend/src/database/migrations/V1721723311__improveMemberDisplayNameSearch.sql new file mode 100644 index 0000000000..07cd9816ee --- /dev/null +++ b/backend/src/database/migrations/V1721723311__improveMemberDisplayNameSearch.sql @@ -0,0 +1 @@ +create index if not exists idx_lower_displayName on members (lower("displayName")); \ No newline at end of file diff --git a/backend/src/database/migrations/V1722322929__addIdMemberIdentities.sql b/backend/src/database/migrations/V1722322929__addIdMemberIdentities.sql new file mode 100644 index 0000000000..26be5beb3f --- /dev/null +++ b/backend/src/database/migrations/V1722322929__addIdMemberIdentities.sql @@ -0,0 +1,3 @@ +ALTER TABLE "memberIdentities" ADD COLUMN "id" UUID NOT NULL DEFAULT uuid_generate_v4(); +ALTER TABLE "memberIdentities" DROP CONSTRAINT "memberIdentities_pkey"; +ALTER TABLE "memberIdentities" ADD PRIMARY KEY (id); diff --git a/backend/src/database/migrations/V1723102955__gitlabRepos.sql b/backend/src/database/migrations/V1723102955__gitlabRepos.sql new file mode 100644 index 0000000000..e4793a9df3 --- /dev/null +++ b/backend/src/database/migrations/V1723102955__gitlabRepos.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS "gitlabRepos" ( + id UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + "createdAt" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(), + "tenantId" UUID NOT NULL REFERENCES "tenants" (id), + "integrationId" UUID NOT NULL REFERENCES "integrations" (id), + "segmentId" UUID NOT NULL REFERENCES "segments" (id), + url VARCHAR(1024) NOT NULL, + + UNIQUE ("tenantId", url) +); + diff --git a/backend/src/database/migrations/V1723620708__makeSegmentNullableInOrgSegmentsAgg.sql b/backend/src/database/migrations/V1723620708__makeSegmentNullableInOrgSegmentsAgg.sql new file mode 100644 index 0000000000..9f1bcfbdfd --- /dev/null +++ b/backend/src/database/migrations/V1723620708__makeSegmentNullableInOrgSegmentsAgg.sql @@ -0,0 +1 @@ +alter table "organizationSegmentsAgg" alter column "segmentId" drop not null; \ No newline at end of file diff --git a/backend/src/database/migrations/V1725439490__installed-github-orgs.sql b/backend/src/database/migrations/V1725439490__installed-github-orgs.sql new file mode 100644 index 0000000000..c8d963932a --- /dev/null +++ b/backend/src/database/migrations/V1725439490__installed-github-orgs.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS "githubInstallations" ( + "id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + "installationId" VARCHAR(255) NOT NULL UNIQUE, + "type" VARCHAR(255) NOT NULL, + "numRepos" INTEGER NOT NULL, + "login" VARCHAR(255) NOT NULL, + "avatarUrl" TEXT, + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); diff --git a/backend/src/database/migrations/V1725465684__createRequestedForErasureMemberIdentities.sql b/backend/src/database/migrations/V1725465684__createRequestedForErasureMemberIdentities.sql new file mode 100644 index 0000000000..f4a00fbb3a --- /dev/null +++ b/backend/src/database/migrations/V1725465684__createRequestedForErasureMemberIdentities.sql @@ -0,0 +1,12 @@ +create table public."requestedForErasureMemberIdentities" ( + "id" uuid, + platform text not null, + value text not null, + "createdAt" timestamp with time zone default now() not null, + "updatedAt" timestamp with time zone default now() not null, + type varchar(255) not null, + primary key ("id") +); +create index ix_requested_for_erasure_memberidentities_platform_value_type on public."requestedForErasureMemberIdentities" (platform, value, type); +create index idx_requested_for_erasure_memberidentities_lower_value on public."requestedForErasureMemberIdentities" (lower(value)); +create index ix_requested_for_erasure_memberidentities_platform_lowervalue_type on public."requestedForErasureMemberIdentities" (platform, lower(value), type); \ No newline at end of file diff --git a/backend/src/database/migrations/V1726244596__maintainersInternal.sql b/backend/src/database/migrations/V1726244596__maintainersInternal.sql new file mode 100644 index 0000000000..1291a96604 --- /dev/null +++ b/backend/src/database/migrations/V1726244596__maintainersInternal.sql @@ -0,0 +1,18 @@ +create table "maintainersInternal" ( + id uuid default uuid_generate_v4() primary key, + role varchar(255) not null, + "repoUrl" varchar(1024) not null, + "repoId" uuid not null references "githubRepos"(id) on delete cascade, + "identityId" uuid not null references "memberIdentities"(id) on delete cascade, + "createdAt" timestamp with time zone not null default now(), + "updatedAt" timestamp with time zone not null default now() +); + +create index maintainers_internal_repo_id_idx on "maintainersInternal" ("repoId"); +create index maintainers_internal_identity_id_idx on "maintainersInternal" ("identityId"); + +create unique index maintainers_internal_repo_identity_unique_idx on "maintainersInternal" ("repoId", "identityId"); + +alter table "githubRepos" +add column "maintainerFile" text, +add column "lastMaintainerRunAt" timestamp with time zone; \ No newline at end of file diff --git a/backend/src/database/migrations/V1727097932__createDataIssuesTable.sql b/backend/src/database/migrations/V1727097932__createDataIssuesTable.sql new file mode 100644 index 0000000000..4e4e66f941 --- /dev/null +++ b/backend/src/database/migrations/V1727097932__createDataIssuesTable.sql @@ -0,0 +1,17 @@ +create table public."dataIssues" ( + "id" uuid, + entity text not null, + "profileUrl" text not null, + "dataIssue" text not null, + "dataType" text not null, + "githubIssueUrl" text not null, + "description" text not null, + "createdById" uuid not null, + "createdAt" timestamp with time zone default now() not null, + "updatedAt" timestamp with time zone default now() not null, + "resolutionEmailSentAt" timestamp with time zone default null, + "resolutionEmailSentTo" text null, + primary key ("id"), + foreign key ("createdById") references users (id) on delete cascade, + unique ("githubIssueUrl") +); \ No newline at end of file diff --git a/backend/src/database/migrations/V1727101659__trackErrorInAuditlogs.sql b/backend/src/database/migrations/V1727101659__trackErrorInAuditlogs.sql new file mode 100644 index 0000000000..4bd75f4ae4 --- /dev/null +++ b/backend/src/database/migrations/V1727101659__trackErrorInAuditlogs.sql @@ -0,0 +1 @@ +alter table "auditLogAction" add column "error" jsonb; \ No newline at end of file diff --git a/backend/src/database/migrations/V1727428673__maintainers-mv.sql b/backend/src/database/migrations/V1727428673__maintainers-mv.sql new file mode 100644 index 0000000000..8c422b58ff --- /dev/null +++ b/backend/src/database/migrations/V1727428673__maintainers-mv.sql @@ -0,0 +1,16 @@ +CREATE MATERIALIZED VIEW mv_maintainer_roles AS +SELECT + mai.id, + mei."memberId", + gr."segmentId", + mai."createdAt" AS "dateStart", + NULL as "dateEnd", + gr.url, + 'github' AS "repoType", + mai.role +FROM "maintainersInternal" mai +JOIN "memberIdentities" mei ON mai."identityId" = mei.id +JOIN "githubRepos" gr ON mai."repoId" = gr.id +; + +CREATE UNIQUE INDEX IF NOT EXISTS mv_maintainer_roles_id ON mv_maintainer_roles (id); diff --git a/backend/src/database/migrations/V1728381599__addSourceToMemberEnrichmentCache.sql b/backend/src/database/migrations/V1728381599__addSourceToMemberEnrichmentCache.sql new file mode 100644 index 0000000000..9c1f37e479 --- /dev/null +++ b/backend/src/database/migrations/V1728381599__addSourceToMemberEnrichmentCache.sql @@ -0,0 +1,11 @@ +ALTER TABLE "memberEnrichmentCache" ADD COLUMN "source" text DEFAULT null; + +UPDATE "memberEnrichmentCache" SET "source" = 'progai'; + +ALTER TABLE "memberEnrichmentCache" ALTER COLUMN source SET NOT NULL; + +ALTER TABLE public."memberEnrichmentCache" DROP CONSTRAINT "memberEnrichmentCache_pkey"; + +ALTER TABLE public."memberEnrichmentCache" ADD PRIMARY KEY ("memberId", "source"); + +ALTER TABLE "memberEnrichmentCache" ALTER COLUMN data DROP NOT NULL; diff --git a/backend/src/database/migrations/V1728473951__auditLogsFilterIndex.sql b/backend/src/database/migrations/V1728473951__auditLogsFilterIndex.sql new file mode 100644 index 0000000000..30252aa099 --- /dev/null +++ b/backend/src/database/migrations/V1728473951__auditLogsFilterIndex.sql @@ -0,0 +1,2 @@ +create index "auditLogAction_actionType" on "auditLogAction"("actionType"); + diff --git a/backend/src/database/migrations/V1730386050__activities-updated-at-index.sql b/backend/src/database/migrations/V1730386050__activities-updated-at-index.sql new file mode 100644 index 0000000000..c8ccafc20d --- /dev/null +++ b/backend/src/database/migrations/V1730386050__activities-updated-at-index.sql @@ -0,0 +1 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS activities_updated_at ON activities ("updatedAt"); diff --git a/backend/src/database/migrations/V1730898038__addDeletedAtColumnToGithubGitlabRepos.sql b/backend/src/database/migrations/V1730898038__addDeletedAtColumnToGithubGitlabRepos.sql new file mode 100644 index 0000000000..da13364331 --- /dev/null +++ b/backend/src/database/migrations/V1730898038__addDeletedAtColumnToGithubGitlabRepos.sql @@ -0,0 +1,5 @@ +alter table "githubRepos" +add column "deletedAt" timestamp without time zone; + +alter table "gitlabRepos" +add column "deletedAt" timestamp without time zone; \ No newline at end of file diff --git a/backend/src/database/migrations/V1731052735__llm-prompt-history.sql b/backend/src/database/migrations/V1731052735__llm-prompt-history.sql new file mode 100644 index 0000000000..0cb81df71c --- /dev/null +++ b/backend/src/database/migrations/V1731052735__llm-prompt-history.sql @@ -0,0 +1,62 @@ +create table "llmPromptHistory" ( + id bigserial primary key, + type varchar(255) not null, + model text not null, + "entityId" text null, + metadata jsonb null, + prompt text not null, + answer text not null, + "inputTokenCount" int not null, + "outputTokenCount" int not null, + "responseTimeSeconds" decimal not null, + "createdAt" timestamptz not null default now() +); + +create index "ix_llmPromptHistory_type_entityId" on "llmPromptHistory"("type", "entityId"); +create index "ix_llmPromptHistory_entityId" on "llmPromptHistory"("entityId"); +create index "ix_llmPromptHistory_type" on "llmPromptHistory"("type"); + +-- new table for tracking member enrichments +create table "memberEnrichments" ( + "memberId" uuid not null, + "lastTriedAt" timestamptz not null default now(), + "lastUpdatedAt" timestamptz null, + + primary key ("memberId") +); +-- we can also drop this column since we have a new table now +alter table members + drop column "lastEnriched"; + +-- backup members table +create table members_backup_14_11_2024 as +select * +from members + with no data; + +-- Copy all data +insert into members_backup_14_11_2024 +select * +from members; + +-- backup memberIdentities table +create table member_identities_backup_14_11_2024 as +select * +from "memberIdentities" + with no data; + +-- Copy all data +insert into member_identities_backup_14_11_2024 +select * +from "memberIdentities"; + +-- backup memberOrganizations table +create table member_organizations_backup_14_11_2024 as +select * +from "memberOrganizations" + with no data; + +-- Copy all data +insert into member_organizations_backup_14_11_2024 +select * +from "memberOrganizations"; \ No newline at end of file diff --git a/backend/src/database/migrations/V1731484783__drop-member-deprecated-columns.sql b/backend/src/database/migrations/V1731484783__drop-member-deprecated-columns.sql new file mode 100644 index 0000000000..257a90ace7 --- /dev/null +++ b/backend/src/database/migrations/V1731484783__drop-member-deprecated-columns.sql @@ -0,0 +1,5 @@ +alter table members + drop column "oldEmails"; + +alter table members + drop column "oldWeakIdentities"; \ No newline at end of file diff --git a/backend/src/database/migrations/V1732118484__members-segments-agg-created-at.sql b/backend/src/database/migrations/V1732118484__members-segments-agg-created-at.sql new file mode 100644 index 0000000000..5f416c9b33 --- /dev/null +++ b/backend/src/database/migrations/V1732118484__members-segments-agg-created-at.sql @@ -0,0 +1 @@ +ALTER TABLE "memberSegmentsAgg" ADD COLUMN IF NOT EXISTS "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(); diff --git a/backend/src/database/migrations/V1732613678__migrate-github-integration-settings.sql b/backend/src/database/migrations/V1732613678__migrate-github-integration-settings.sql new file mode 100644 index 0000000000..2da91eb4bd --- /dev/null +++ b/backend/src/database/migrations/V1732613678__migrate-github-integration-settings.sql @@ -0,0 +1,101 @@ +DO $$ +DECLARE + _record RECORD; + _new_settings JSONB; + _org JSONB; +BEGIN + FOR _record IN SELECT * FROM integrations WHERE platform = 'github' LOOP + /* + old settings: + { + "repos": [ + { + "url": "https://github.com/camaraproject/Governance", + "fork": false, + "name": "Governance", + "owner": "camaraproject", + "private": false, + "cloneUrl": "https://github.com/camaraproject/Governance.git", + "createdAt": "2021-06-28T04:15:55Z" + }, + { + "url": "https://github.com/camaraproject/QualityOnDemand", + "fork": false, + "name": "QualityOnDemand", + "owner": "camaraproject", + "private": false, + "cloneUrl": "https://github.com/camaraproject/QualityOnDemand.git", + "createdAt": "2022-02-22T10:05:50Z" + }, + ... + ], + "orgAvatar": "https://avatars.githubusercontent.com/u/91603532?v=4", + "unavailableRepos": [], + "updateMemberAttributes": false + } + */ + /* + new settings: + { + orgs: [ + { + name: "ASWF", + logo: "https://blah...png", + url: "https://github.com/ASWF", + fullSync: true|false, + updatedAt: "2024-11-18T12:03:09", + repos: [ + { + name: "foundation", + url: "https://github.com/ASWF/foundation", + updatedAt: "2024-11-18T12:03:09", + }, + { + name: "something-else", + url: "https://github.com/ASWF/something-else", + updatedAt: "2024-11-18T12:03:09", + } + ] + }, + ... + ] + } + */ + + -- Skip if settings already have orgs key + IF _record.settings ? 'orgs' THEN + RAISE NOTICE 'integration: %, skipping', _record.id; + CONTINUE; + END IF; + + -- Extract org name and avatar from first repo if available + IF jsonb_array_length(_record.settings->'repos') > 0 THEN + -- Create org object with repos + _org = jsonb_build_object( + 'name', (_record.settings->'repos'->0->>'owner'), + 'logo', _record.settings->>'orgAvatar', + 'url', 'https://github.com/' || (_record.settings->'repos'->0->>'owner'), + 'fullSync', true, + 'updatedAt', CURRENT_TIMESTAMP, + 'repos', ( + SELECT jsonb_agg( + jsonb_build_object( + 'name', repo->>'name', + 'url', repo->>'url', + 'updatedAt', repo->>'createdAt' + ) + ) + FROM jsonb_array_elements(_record.settings->'repos') repo + ) + ); + + -- Create new settings object with orgs array + _new_settings = jsonb_build_object('orgs', jsonb_build_array(_org)); + + UPDATE integrations + SET settings = _new_settings + WHERE id = _record.id; + END IF; + END LOOP; +END; +$$; diff --git a/backend/src/database/migrations/V1732695786__member-organization-unique-indexes-with-deletedAt.sql b/backend/src/database/migrations/V1732695786__member-organization-unique-indexes-with-deletedAt.sql new file mode 100644 index 0000000000..6165eb942f --- /dev/null +++ b/backend/src/database/migrations/V1732695786__member-organization-unique-indexes-with-deletedAt.sql @@ -0,0 +1,18 @@ +-- drop unique constraints and unique indexes without deletedAt +alter table "memberOrganizations" + drop constraint "memberOrganizations_memberId_organizationId_dateStart_dateE_key"; +drop index if exists ix_unique_member_org_no_date_nulls; +drop index if exists ix_unique_member_org_no_end_date_nulls; + +-- recreate with indexes where deletedAt is null +create unique index "ix_unique_member_org" + on "memberOrganizations" ("memberId", "organizationId", "dateStart", "dateEnd") + where "deletedAt" is null; + +create unique index ix_unique_member_org_no_date_nulls + on "memberOrganizations" ("memberId", "organizationId") + where ("dateStart" is null) and ("dateEnd" is null) and ("deletedAt" is null); + +create unique index ix_unique_member_org_no_end_date_nulls + on "memberOrganizations" ("memberId", "organizationId", "dateStart") + where ("dateEnd" is null) and ("deletedAt" is null); \ No newline at end of file diff --git a/backend/src/database/migrations/V1733930025__memberOrganizationAffiliationOverrides.sql b/backend/src/database/migrations/V1733930025__memberOrganizationAffiliationOverrides.sql new file mode 100644 index 0000000000..09d48b58ab --- /dev/null +++ b/backend/src/database/migrations/V1733930025__memberOrganizationAffiliationOverrides.sql @@ -0,0 +1,15 @@ +create table "memberOrganizationAffiliationOverrides" ( + id uuid not null, + "memberId" uuid not null, + "memberOrganizationId" uuid not null, + "allowAffiliation" boolean, + "isPrimaryWorkExperience" boolean, + + primary key ("id"), + foreign key ("memberOrganizationId") references "memberOrganizations" (id), + foreign key ("memberId") references members (id), + unique ("memberId", "memberOrganizationId") +); + + +create index "ix_memberOrganizationAffiliationOverrides_memberId" on "memberOrganizationAffiliationOverrides"("memberId"); \ No newline at end of file diff --git a/backend/src/database/migrations/V1734432927__changeGithubIssueUrlToIssueUrl.sql b/backend/src/database/migrations/V1734432927__changeGithubIssueUrlToIssueUrl.sql new file mode 100644 index 0000000000..c0453637da --- /dev/null +++ b/backend/src/database/migrations/V1734432927__changeGithubIssueUrlToIssueUrl.sql @@ -0,0 +1,8 @@ +alter table "dataIssues" +rename column "githubIssueUrl" to "issueUrl"; + +alter table "dataIssues" +drop column "resolutionEmailSentAt"; + +alter table "dataIssues" +drop column "resolutionEmailSentTo"; \ No newline at end of file diff --git a/backend/src/database/migrations/V1736414839__cleanupUnusedTablesAndColumns.sql b/backend/src/database/migrations/V1736414839__cleanupUnusedTablesAndColumns.sql new file mode 100644 index 0000000000..8c1e196e21 --- /dev/null +++ b/backend/src/database/migrations/V1736414839__cleanupUnusedTablesAndColumns.sql @@ -0,0 +1,61 @@ +drop table if exists "activityTasks", "memberTasks", "taskAssignees", tasks; +drop table if exists "memberNotes", notes; +drop table if exists "automationExecutions", automations; + +alter table integration.runs drop column if exists "microserviceId"; +alter table integration.results drop column if exists "microserviceId"; +alter table integration.streams drop column if exists "microserviceId"; + +drop table if exists public."integrationStreams"; +drop table if exists public."integrationRuns"; + +drop table if exists microservices; + +drop table if exists "recurringEmailsHistory"; + +drop table if exists member_organizations_backup_14_11_2024; +drop table if exists member_identities_backup_14_11_2024; +drop table if exists members_backup_14_11_2024; +drop table if exists "membersSyncRemote"; +drop table if exists "memberToMergeOld"; + +drop table if exists + "old_organizationCacheLinks", + "old_organizationCacheIdentities", + "old_organizationCaches", + "organizationToMergeOld", + organization_identities_backup, + "organizationsSyncRemote"; + +alter table organizations + drop column if exists "old_emails", + drop column if exists "old_phoneNumbers", + drop column if exists "old_twitter", + drop column if exists "old_linkedin", + drop column if exists "old_crunchbase", + drop column if exists "old_employeeCountByCountry", + drop column if exists "old_geoLocation", + drop column if exists "old_ticker", + drop column if exists "old_profiles", + drop column if exists "old_address", + drop column if exists "old_attributes", + drop column if exists "old_affiliatedProfiles", + drop column if exists "old_allSubsidiaries", + drop column if exists "old_alternativeDomains", + drop column if exists "old_alternativeNames", + drop column if exists "old_averageEmployeeTenure", + drop column if exists "old_averageTenureByLevel", + drop column if exists "old_averageTenureByRole", + drop column if exists "old_directSubsidiaries", + drop column if exists "old_employeeCountByMonth", + drop column if exists "old_employeeCountByMonthByLevel", + drop column if exists "old_employeeCountByMonthByRole", + drop column if exists "old_gicsSector", + drop column if exists "old_grossAdditionsByMonth", + drop column if exists "old_grossDeparturesByMonth", + drop column if exists "old_ultimateParent", + drop column if exists "old_immediateParent", + drop column if exists "old_weakIdentities", + drop column if exists "old_manuallyChangedFields", + drop column if exists "old_naics", + drop column if exists "old_names"; \ No newline at end of file diff --git a/backend/src/database/migrations/V1737705711__activityRelations.sql b/backend/src/database/migrations/V1737705711__activityRelations.sql new file mode 100644 index 0000000000..183eec8afc --- /dev/null +++ b/backend/src/database/migrations/V1737705711__activityRelations.sql @@ -0,0 +1,23 @@ +create table public."activityRelations" ( + "activityId" uuid not null primary key, + "memberId" uuid not null, + "objectMemberId" uuid null, + "organizationId" uuid null, + "conversationId" uuid null, + "parentId" uuid null, + "segmentId" uuid not null, + "platform" text not null, + "username" text not null, + "objectMemberUsername" text null, + "createdAt" timestamp with time zone default now() not null, + "updatedAt" timestamp with time zone default now() not null, + foreign key ("memberId") references members (id) on delete cascade, + foreign key ("organizationId") references organizations (id) on delete set null, + foreign key ("objectMemberId") references members (id) on delete set null, + foreign key ("conversationId") references conversations (id) on delete set null, + foreign key ("segmentId") references segments (id) on delete cascade, + unique ("activityId", "memberId") +); +create index "ix_activityRelations_memberId" on "activityRelations"("memberId"); +create index "ix_activityRelations_organizationId" on "activityRelations"("organizationId"); +create index "ix_activityRelations_platform_username" on "activityRelations"("platform", "username"); diff --git a/backend/src/database/migrations/V1738310688__collections.sql b/backend/src/database/migrations/V1738310688__collections.sql new file mode 100644 index 0000000000..c878d2c2fd --- /dev/null +++ b/backend/src/database/migrations/V1738310688__collections.sql @@ -0,0 +1,36 @@ +CREATE TABLE IF NOT EXISTS collections ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + description TEXT, + "isLF" BOOLEAN NOT NULL, + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS "insightsProjects" ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + description TEXT, + "segmentId" UUID REFERENCES segments(id), + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + + "logoUrl" TEXT, + "organizationId" UUID REFERENCES organizations(id), + "website" TEXT, + "github" TEXT, + "linkedin" TEXT, + "twitter" TEXT, + "widgets" TEXT[], + "repositories" JSONB +); + +CREATE TABLE IF NOT EXISTS "collectionsInsightsProjects" ( + "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "collectionId" UUID NOT NULL REFERENCES collections(id), + "insightsProjectId" UUID NOT NULL REFERENCES "insightsProjects"(id), + "starred" BOOLEAN NOT NULL, + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + UNIQUE ("collectionId", "insightsProjectId") +); diff --git a/backend/src/database/migrations/V1738589807__remove-tenantId-and-modify-indexes-on-indexed-entities.sql b/backend/src/database/migrations/V1738589807__remove-tenantId-and-modify-indexes-on-indexed-entities.sql new file mode 100644 index 0000000000..ebfd72d4c9 --- /dev/null +++ b/backend/src/database/migrations/V1738589807__remove-tenantId-and-modify-indexes-on-indexed-entities.sql @@ -0,0 +1,6 @@ +create index if not exists ix_indexed_entities_type_entity_id on indexed_entities (type, entity_id); + +drop index if exists ix_indexed_entities_tenant; + +alter table indexed_entities + drop column tenant_id; \ No newline at end of file diff --git a/backend/src/database/migrations/V1741006323__AddSlugToCollectionsAndInsightsProjects.sql b/backend/src/database/migrations/V1741006323__AddSlugToCollectionsAndInsightsProjects.sql new file mode 100644 index 0000000000..2984e52378 --- /dev/null +++ b/backend/src/database/migrations/V1741006323__AddSlugToCollectionsAndInsightsProjects.sql @@ -0,0 +1,37 @@ +ALTER TABLE collections ADD COLUMN slug TEXT; +ALTER TABLE "insightsProjects" ADD COLUMN slug TEXT; + +CREATE OR REPLACE FUNCTION generate_slug(table_name TEXT, input_text TEXT) RETURNS TEXT AS $$ +DECLARE + base_slug TEXT; + unique_slug TEXT; + counter INT := 1; + query TEXT; + slug_exists BOOLEAN; +BEGIN + base_slug := lower(regexp_replace(input_text, '[^a-zA-Z0-9]+', '-', 'g')); + base_slug := regexp_replace(base_slug, '-$', '', 'g'); + unique_slug := base_slug; + + -- Ensure uniqueness by appending a counter if needed + LOOP + query := format('SELECT EXISTS (SELECT 1 FROM %I WHERE slug = $1)', table_name); + EXECUTE query INTO slug_exists USING unique_slug; + + EXIT WHEN NOT slug_exists; + unique_slug := base_slug || '-' || counter; + counter := counter + 1; + END LOOP; + + RETURN unique_slug; +END; +$$ LANGUAGE plpgsql; + +UPDATE collections SET slug = generate_slug('collections', name) WHERE name IS NOT NULL; +UPDATE "insightsProjects" SET slug = generate_slug('insightsProjects', name) WHERE name IS NOT NULL; + +ALTER TABLE collections ALTER COLUMN slug SET NOT NULL; +ALTER TABLE collections ADD CONSTRAINT idx_collections_slug_unique UNIQUE (slug); + +ALTER TABLE "insightsProjects" ALTER COLUMN slug SET NOT NULL; +ALTER TABLE "insightsProjects" ADD CONSTRAINT "idx_insightsProjects_slug_unique" UNIQUE (slug); diff --git a/backend/src/database/migrations/V1741181466__cleanupExcludeList.sql b/backend/src/database/migrations/V1741181466__cleanupExcludeList.sql new file mode 100644 index 0000000000..7446c7081f --- /dev/null +++ b/backend/src/database/migrations/V1741181466__cleanupExcludeList.sql @@ -0,0 +1,5 @@ +create table if not exists "cleanupExcludeList" ( + "entityId" uuid not null, + "type" varchar(50) not null, + primary key ("entityId", "type") +); \ No newline at end of file diff --git a/backend/src/database/migrations/V1742210733__insights_projects_repositories_type.sql b/backend/src/database/migrations/V1742210733__insights_projects_repositories_type.sql new file mode 100644 index 0000000000..b9c7983ee1 --- /dev/null +++ b/backend/src/database/migrations/V1742210733__insights_projects_repositories_type.sql @@ -0,0 +1,9 @@ +CREATE OR REPLACE FUNCTION jsonb_to_text_array(jsonb) RETURNS text[] AS $$ +SELECT array_agg(value) FROM jsonb_array_elements_text($1) +$$ LANGUAGE SQL; + +ALTER TABLE "insightsProjects" + ALTER COLUMN "repositories" TYPE TEXT[] + USING jsonb_to_text_array("repositories"); + +DROP FUNCTION IF EXISTS jsonb_to_text_array(jsonb); diff --git a/backend/src/database/migrations/V1742295164__addIndexesForSequinBackfills.sql b/backend/src/database/migrations/V1742295164__addIndexesForSequinBackfills.sql new file mode 100644 index 0000000000..14630ea9b2 --- /dev/null +++ b/backend/src/database/migrations/V1742295164__addIndexesForSequinBackfills.sql @@ -0,0 +1,3 @@ +create index if not exists "ix_activityRelations_updatedAt_activityId" on public."activityRelations" ("updatedAt", "activityId"); +create index if not exists "ix_members_updatedAt_id" on public.members ("updatedAt", id); +create index if not exists "ix_organizations_updatedAt_id" on public.organizations ("updatedAt", id); \ No newline at end of file diff --git a/backend/src/database/migrations/V1742929045__createPublicationForSequin.sql b/backend/src/database/migrations/V1742929045__createPublicationForSequin.sql new file mode 100644 index 0000000000..ea576b2e15 --- /dev/null +++ b/backend/src/database/migrations/V1742929045__createPublicationForSequin.sql @@ -0,0 +1,18 @@ +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_publication WHERE pubname = 'sequin_pub' + ) THEN + CREATE PUBLICATION sequin_pub + FOR TABLE "activityRelations", segments, members, organizations, collections, "insightsProjects", "collectionsInsightsProjects"; + END IF; +END$$; + + +ALTER TABLE public.members REPLICA IDENTITY FULL; +ALTER TABLE public.organizations REPLICA IDENTITY FULL; +ALTER TABLE public.segments REPLICA IDENTITY FULL; +ALTER TABLE public."activityRelations" REPLICA IDENTITY FULL; +ALTER TABLE public.collections REPLICA IDENTITY FULL; +ALTER TABLE public."insightsProjects" REPLICA IDENTITY FULL; +ALTER TABLE public."collectionsInsightsProjects" REPLICA IDENTITY FULL; diff --git a/backend/src/database/migrations/V1742929293__createReplicationSlotForSequin.sql b/backend/src/database/migrations/V1742929293__createReplicationSlotForSequin.sql new file mode 100644 index 0000000000..c1f4501e6d --- /dev/null +++ b/backend/src/database/migrations/V1742929293__createReplicationSlotForSequin.sql @@ -0,0 +1,8 @@ +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_replication_slots WHERE slot_name = 'sequin_slot' + ) THEN + PERFORM pg_create_logical_replication_slot('sequin_slot', 'pgoutput'); + END IF; +END$$; \ No newline at end of file diff --git a/backend/src/database/migrations/V1743428456__addCollectionCategories.sql b/backend/src/database/migrations/V1743428456__addCollectionCategories.sql new file mode 100644 index 0000000000..c1eabeef36 --- /dev/null +++ b/backend/src/database/migrations/V1743428456__addCollectionCategories.sql @@ -0,0 +1,25 @@ +-- Create the categoryGroups table +CREATE TABLE IF NOT EXISTS "categoryGroups" +( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + slug VARCHAR(255) NOT NULL UNIQUE, + type TEXT CHECK (type IN ('vertical', 'horizontal')) NOT NULL, + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Create the categories table +CREATE TABLE IF NOT EXISTS "categories" +( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + slug VARCHAR(255) NOT NULL UNIQUE, + "categoryGroupId" UUID NOT NULL REFERENCES "categoryGroups"(id) ON UPDATE CASCADE ON DELETE CASCADE, + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Add the categoryId column to the collections table +ALTER TABLE collections + ADD COLUMN "categoryId" UUID REFERENCES "categories"(id) ON UPDATE CASCADE ON DELETE SET NULL; diff --git a/backend/src/database/migrations/V1743579581__categoryGroupNotMandatoryCategories.sql b/backend/src/database/migrations/V1743579581__categoryGroupNotMandatoryCategories.sql new file mode 100644 index 0000000000..6c104582ba --- /dev/null +++ b/backend/src/database/migrations/V1743579581__categoryGroupNotMandatoryCategories.sql @@ -0,0 +1,3 @@ +-- Migration to make "categoryGroupId" nullable in the categories table +ALTER TABLE "categories" + ALTER COLUMN "categoryGroupId" DROP NOT NULL; diff --git a/backend/src/database/migrations/V1743723469__collectionProjectUniqueNames.sql b/backend/src/database/migrations/V1743723469__collectionProjectUniqueNames.sql new file mode 100644 index 0000000000..7939a08a6a --- /dev/null +++ b/backend/src/database/migrations/V1743723469__collectionProjectUniqueNames.sql @@ -0,0 +1,5 @@ +ALTER TABLE "collections" + ADD CONSTRAINT "unique_collection_name" UNIQUE ("name"); + +ALTER TABLE "insightsProjects" + ADD CONSTRAINT "unique_insightsProjects_name" UNIQUE ("name"); diff --git a/backend/src/database/migrations/V1743723883__projectUniqueSegmentId.sql b/backend/src/database/migrations/V1743723883__projectUniqueSegmentId.sql new file mode 100644 index 0000000000..9362fc1da4 --- /dev/null +++ b/backend/src/database/migrations/V1743723883__projectUniqueSegmentId.sql @@ -0,0 +1,2 @@ +ALTER TABLE "insightsProjects" + ADD CONSTRAINT "unique_project_segmentId" UNIQUE ("segmentId"); diff --git a/backend/src/database/migrations/V1743724207__projectEnable.sql b/backend/src/database/migrations/V1743724207__projectEnable.sql new file mode 100644 index 0000000000..47f3edfbb6 --- /dev/null +++ b/backend/src/database/migrations/V1743724207__projectEnable.sql @@ -0,0 +1,2 @@ +ALTER TABLE "insightsProjects" + ADD COLUMN "enabled" BOOLEAN DEFAULT TRUE NOT NULL; diff --git a/backend/src/database/migrations/V1743724307__isProjectLf.sql b/backend/src/database/migrations/V1743724307__isProjectLf.sql new file mode 100644 index 0000000000..716158be0f --- /dev/null +++ b/backend/src/database/migrations/V1743724307__isProjectLf.sql @@ -0,0 +1,18 @@ +-- Remove "isLF" column from "collections" table +ALTER TABLE "collections" + DROP COLUMN IF EXISTS "isLF"; + +-- Add "isLF" column to "insightsProject" table +ALTER TABLE "insightsProjects" + ADD COLUMN "isLF" BOOLEAN; + +-- Update the "isLF" column based on the value of "segmentId" +UPDATE "insightsProjects" +SET "isLF" = CASE + WHEN "segmentId" IS NOT NULL THEN TRUE + ELSE FALSE + END; + +-- Ensure the "isLF" column does not allow NULL values (if appropriate) +ALTER TABLE "insightsProjects" + ALTER COLUMN "isLF" SET NOT NULL; diff --git a/backend/src/database/migrations/V1743724684__projectKeywords.sql b/backend/src/database/migrations/V1743724684__projectKeywords.sql new file mode 100644 index 0000000000..54e8d7fce5 --- /dev/null +++ b/backend/src/database/migrations/V1743724684__projectKeywords.sql @@ -0,0 +1,2 @@ +ALTER TABLE "insightsProjects" + ADD COLUMN "keywords" TEXT[] DEFAULT '{}'; diff --git a/backend/src/database/migrations/V1744813638__securityInsights.sql b/backend/src/database/migrations/V1744813638__securityInsights.sql new file mode 100644 index 0000000000..22847ef661 --- /dev/null +++ b/backend/src/database/migrations/V1744813638__securityInsights.sql @@ -0,0 +1,77 @@ +-- Security Insights Evaluation Suites +create table public."securityInsightsEvaluationSuites" ( + "id" uuid not null primary key, + "name" text not null, + "repo" text not null, + "catalogId" text not null, + "result" text not null, + "corruptedState" boolean not null, + "createdAt" timestamp with time zone default now() not null, + "updatedAt" timestamp with time zone default now() not null, + "insightsProjectId" uuid not null, + "insightsProjectSlug" text not null, + + foreign key ("insightsProjectId") references "insightsProjects" (id) on delete cascade, + unique ("repo", "catalogId") +); + +create index "ix_securityInsightsEvaluationSuites_repo" on "securityInsightsEvaluationSuites"("repo"); +create index "ix_securityInsightsEvaluationSuites_updatedAt_id" on "securityInsightsEvaluationSuites" ("updatedAt", id); + +-- Security Insights Evaluation Suites Control Evaluations +create table public."securityInsightsEvaluationSuiteControlEvaluations" ( + "id" uuid not null primary key, + "securityInsightsEvaluationSuiteId" uuid not null, + "name" text not null, + "repo" text not null, + "controlId" text not null, + "result" text not null, + "message" text not null, + "corruptedState" boolean not null, + "remediationGuide" text not null, + "createdAt" timestamp with time zone default now() not null, + "updatedAt" timestamp with time zone default now() not null, + "insightsProjectId" uuid not null, + "insightsProjectSlug" text not null, + + foreign key ("insightsProjectId") references "insightsProjects" (id) on delete cascade, + foreign key ("securityInsightsEvaluationSuiteId") references "securityInsightsEvaluationSuites" (id) on delete cascade, + unique ("securityInsightsEvaluationSuiteId", "repo", "controlId") +); + +create index "ix_securityInsightsControlEvaluations_repo" on "securityInsightsEvaluationSuiteControlEvaluations"("repo"); +create index "ix_securityInsightsControlEvaluations_updatedAt_id" on "securityInsightsEvaluationSuiteControlEvaluations" ("updatedAt", id); + + +-- Security Insights Evaluation Suites Control Evaluation Assessments +create table public."securityInsightsEvaluationSuiteControlEvaluationAssessments" ( + "id" uuid not null primary key, + "securityInsightsEvaluationSuiteControlEvaluationId" uuid not null, + "repo" text not null, + "requirementId" text not null, + "applicability" text[] not null, + "description" text not null, + "result" text not null, + "message" text not null, + "steps" text[] not null, + "stepsExecuted" integer not null, + "runDuration" text not null, + "createdAt" timestamp with time zone default now() not null, + "updatedAt" timestamp with time zone default now() not null, + "insightsProjectId" uuid not null, + "insightsProjectSlug" text not null, + + foreign key ("insightsProjectId") references "insightsProjects" (id) on delete cascade, + foreign key ("securityInsightsEvaluationSuiteControlEvaluationId") references "securityInsightsEvaluationSuiteControlEvaluations" (id) on delete cascade, + unique ("securityInsightsEvaluationSuiteControlEvaluationId", "repo", "requirementId") +); + +create index "ix_securityInsightsAssessments_repo" on "securityInsightsEvaluationSuiteControlEvaluationAssessments"("repo"); +create index "ix_securityInsightsAssessments_updatedAt_id" on "securityInsightsEvaluationSuiteControlEvaluationAssessments" ("updatedAt", id); + + +-- Sequin publication migrations +ALTER PUBLICATION sequin_pub ADD TABLE "securityInsightsEvaluationSuiteControlEvaluations"; +ALTER PUBLICATION sequin_pub ADD TABLE "securityInsightsEvaluationSuiteControlEvaluationAssessments"; +ALTER TABLE public."securityInsightsEvaluationSuiteControlEvaluations" REPLICA IDENTITY FULL; +ALTER TABLE public."securityInsightsEvaluationSuiteControlEvaluationAssessments" REPLICA IDENTITY FULL; \ No newline at end of file diff --git a/backend/src/database/migrations/V1746001914__projectMissingWidget.sql b/backend/src/database/migrations/V1746001914__projectMissingWidget.sql new file mode 100644 index 0000000000..1a9f48685f --- /dev/null +++ b/backend/src/database/migrations/V1746001914__projectMissingWidget.sql @@ -0,0 +1,16 @@ +UPDATE "insightsProjects" +SET widgets = ARRAY( + SELECT DISTINCT unnest( + widgets || ARRAY [ + 'socialMentions', + 'githubMentions', + 'pressMentions', + 'searchQueries', + 'packageDownloads', + 'mailingListMessages', + 'activeDays' + ] + ) + ), + "updatedAt" = CURRENT_TIMESTAMP +WHERE widgets IS NOT NULL; diff --git a/backend/src/database/migrations/V1746176553__dropcleanupExcludeListTable.sql b/backend/src/database/migrations/V1746176553__dropcleanupExcludeListTable.sql new file mode 100644 index 0000000000..d79f67a1be --- /dev/null +++ b/backend/src/database/migrations/V1746176553__dropcleanupExcludeListTable.sql @@ -0,0 +1 @@ +drop table if exists "cleanupExcludeList"; \ No newline at end of file diff --git a/backend/src/database/migrations/V1746535447__activityRelationsIndexesForCoreAggs.sql b/backend/src/database/migrations/V1746535447__activityRelationsIndexesForCoreAggs.sql new file mode 100644 index 0000000000..73e13dff11 --- /dev/null +++ b/backend/src/database/migrations/V1746535447__activityRelationsIndexesForCoreAggs.sql @@ -0,0 +1,8 @@ +-- Indexes to optimize queries for member and organization activity core aggregates +create index concurrently if not exists "ix_activityRelations_memberId_segmentId_include" +on "activityRelations" ("memberId", "segmentId") +include ("platform", "activityId"); + +create index concurrently if not exists "ix_activityRelations_organizationId_segmentId_include" +on "activityRelations" ("organizationId", "segmentId") +include ("platform", "activityId", "memberId"); diff --git a/backend/src/database/migrations/V1746542390__securityInsightsTableRename.sql b/backend/src/database/migrations/V1746542390__securityInsightsTableRename.sql new file mode 100644 index 0000000000..4fbd547553 --- /dev/null +++ b/backend/src/database/migrations/V1746542390__securityInsightsTableRename.sql @@ -0,0 +1,11 @@ +ALTER PUBLICATION sequin_pub DROP TABLE "securityInsightsEvaluationSuiteControlEvaluations"; +ALTER PUBLICATION sequin_pub DROP TABLE "securityInsightsEvaluationSuiteControlEvaluationAssessments"; + +ALTER TABLE "securityInsightsEvaluationSuiteControlEvaluations" RENAME TO "securityInsightsEvaluations"; +ALTER TABLE "securityInsightsEvaluationSuiteControlEvaluationAssessments" RENAME TO "securityInsightsEvaluationAssessments"; +ALTER TABLE "securityInsightsEvaluationAssessments" RENAME COLUMN "securityInsightsEvaluationSuiteControlEvaluationId" TO "securityInsightsEvaluationId"; + +ALTER PUBLICATION sequin_pub ADD TABLE "securityInsightsEvaluations"; +ALTER PUBLICATION sequin_pub ADD TABLE "securityInsightsEvaluationAssessments"; +ALTER TABLE public."securityInsightsEvaluations" REPLICA IDENTITY FULL; +ALTER TABLE public."securityInsightsEvaluationAssessments" REPLICA IDENTITY FULL; \ No newline at end of file diff --git a/backend/src/database/migrations/V1746546438__addStarredColumnToCollectionsTable.sql b/backend/src/database/migrations/V1746546438__addStarredColumnToCollectionsTable.sql new file mode 100644 index 0000000000..2d07b7dcb1 --- /dev/null +++ b/backend/src/database/migrations/V1746546438__addStarredColumnToCollectionsTable.sql @@ -0,0 +1,3 @@ +-- Add the starred column to the collections table +ALTER TABLE collections + ADD COLUMN "starred" boolean DEFAULT false NOT NULL; diff --git a/backend/src/database/migrations/V1747130236__addCategoriesToSequin.sql b/backend/src/database/migrations/V1747130236__addCategoriesToSequin.sql new file mode 100644 index 0000000000..261ab0a226 --- /dev/null +++ b/backend/src/database/migrations/V1747130236__addCategoriesToSequin.sql @@ -0,0 +1,7 @@ +create index "ix_categories_updatedAt_id" on "categories" ("updatedAt", id); +create index "ix_categoryGroups_updatedAt_id" on "categoryGroups" ("updatedAt", id); + +ALTER PUBLICATION sequin_pub ADD TABLE "categories"; +ALTER PUBLICATION sequin_pub ADD TABLE "categoryGroups"; +ALTER TABLE public."categories" REPLICA IDENTITY FULL; +ALTER TABLE public."categoryGroups" REPLICA IDENTITY FULL; diff --git a/backend/src/database/migrations/V1747131692__systemSettingsAndSegmentsAgg.sql b/backend/src/database/migrations/V1747131692__systemSettingsAndSegmentsAgg.sql new file mode 100644 index 0000000000..b2c379d16e --- /dev/null +++ b/backend/src/database/migrations/V1747131692__systemSettingsAndSegmentsAgg.sql @@ -0,0 +1,27 @@ +-- Add "updatedAt" column to track the async aggs updates +alter table "memberSegmentsAgg" + add column if not exists "updatedAt" timestamp with time zone not null default now(); + +-- For existing rows, set the initial value to the createdAt value +update "memberSegmentsAgg" set "updatedAt" = "createdAt"; + +-- Adding "createdAt" and "updatedAt" to make it consistent with the memberSegmentsAgg table. +alter table "organizationSegmentsAgg" + add column if not exists "createdAt" timestamp with time zone not null default now(); + +alter table "organizationSegmentsAgg" + add column if not exists "updatedAt" timestamp with time zone not null default now(); + +-- table store system wide settings since we are moving away from tenants +create table if not exists "systemSettings" ( + name varchar(255) not null primary key, + value jsonb not null, + description text, + "createdAt" timestamp with time zone not null default now(), + "updatedAt" timestamp with time zone not null default now() +); + +-- system settings for the display aggs last synced at +insert into "systemSettings" (name, value) values ('memberDisplayAggsLastSyncedAt', '{"timestamp": "2025-05-13T00:00:00Z"}'); + +insert into "systemSettings" (name, value) values ('organizationDisplayAggsLastSyncedAt', '{"timestamp": "2025-05-13T00:00:00Z"}'); diff --git a/backend/src/database/migrations/V1747133060__optimizeIndexedEntitiesIndexes.sql b/backend/src/database/migrations/V1747133060__optimizeIndexedEntitiesIndexes.sql new file mode 100644 index 0000000000..f5cda4c442 --- /dev/null +++ b/backend/src/database/migrations/V1747133060__optimizeIndexedEntitiesIndexes.sql @@ -0,0 +1,3 @@ +-- Index to optimize fetching recently indexed entities +create index concurrently if not exists "idx_indexed_entities_type_indexed_at_entity_id" +on "indexed_entities" ("type", "indexed_at", "entity_id"); diff --git a/backend/src/database/migrations/V1747210214__addMemberIdentitiesAndIntegrationsToTinybird.sql b/backend/src/database/migrations/V1747210214__addMemberIdentitiesAndIntegrationsToTinybird.sql new file mode 100644 index 0000000000..ddd0e15306 --- /dev/null +++ b/backend/src/database/migrations/V1747210214__addMemberIdentitiesAndIntegrationsToTinybird.sql @@ -0,0 +1,7 @@ +ALTER PUBLICATION sequin_pub ADD TABLE "memberIdentities"; +ALTER PUBLICATION sequin_pub ADD TABLE "integrations"; +ALTER TABLE public."memberIdentities" REPLICA IDENTITY FULL; +ALTER TABLE public."integrations" REPLICA IDENTITY FULL; + +create index "ix_memberIdentities_updatedAt_id" on "memberIdentities" ("updatedAt", id); +create index "ix_integrations_updatedAt_id" on "integrations" ("updatedAt", id); \ No newline at end of file diff --git a/backend/src/database/migrations/V1747250045__org-identities-indexes.sql b/backend/src/database/migrations/V1747250045__org-identities-indexes.sql new file mode 100644 index 0000000000..9ac399f4ec --- /dev/null +++ b/backend/src/database/migrations/V1747250045__org-identities-indexes.sql @@ -0,0 +1,2 @@ +create index if not exists "ix_organizationIdentities_plat_val_type_verified" on "organizationIdentities" (platform, lower(value), type, verified); +create index if not exists "ix_organizations_displayName" on organizations(trim(lower("displayName"))); \ No newline at end of file diff --git a/backend/src/database/migrations/V1747301290__addCommitActivitiesWidget.sql b/backend/src/database/migrations/V1747301290__addCommitActivitiesWidget.sql new file mode 100644 index 0000000000..fc19830a58 --- /dev/null +++ b/backend/src/database/migrations/V1747301290__addCommitActivitiesWidget.sql @@ -0,0 +1,10 @@ +UPDATE "insightsProjects" +SET widgets = ARRAY( + SELECT DISTINCT unnest( + widgets || ARRAY [ + 'commitActivities' + ] + ) + ), + "updatedAt" = CURRENT_TIMESTAMP +WHERE widgets IS NOT NULL; diff --git a/backend/src/database/migrations/V1747408625__addIsLfColumnToSegments.sql b/backend/src/database/migrations/V1747408625__addIsLfColumnToSegments.sql new file mode 100644 index 0000000000..7cd9c033f9 --- /dev/null +++ b/backend/src/database/migrations/V1747408625__addIsLfColumnToSegments.sql @@ -0,0 +1,31 @@ +ALTER TABLE public."segments" + ADD COLUMN "isLF" BOOLEAN NOT NULL DEFAULT TRUE; + +-- Create a trigger function to enforce isLF consistency +CREATE OR REPLACE FUNCTION check_segment_isLF_consistency() +RETURNS TRIGGER AS $$ +DECLARE + parent_isLF BOOLEAN; -- Grandparent's check (projectGroup) is not required because it will be performed upon project creation/update +BEGIN + -- Only check if there's a parent + IF NEW."parentId" IS NOT NULL THEN + SELECT "isLF" INTO parent_isLF + FROM public."segments" + WHERE id = NEW."parentId"; + + -- Verify segment's isLF matches parent + IF parent_isLF IS NOT NULL AND NEW."isLF" != parent_isLF THEN + RAISE EXCEPTION 'Segment (%) isLF value (%) must match its parent''s isLF value (%)', + NEW.id, NEW."isLF", parent_isLF; + END IF; + END IF; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +-- Create a constraint trigger that checks at transaction end because subprojects are updated in the same transaction as their parent segment +CREATE CONSTRAINT TRIGGER segment_isLF_consistency_check +AFTER INSERT OR UPDATE ON public."segments" +DEFERRABLE INITIALLY DEFERRED +FOR EACH ROW EXECUTE FUNCTION check_segment_isLF_consistency(); \ No newline at end of file diff --git a/backend/src/database/migrations/V1748332491__criticality-score.sql b/backend/src/database/migrations/V1748332491__criticality-score.sql new file mode 100644 index 0000000000..18a7b89887 --- /dev/null +++ b/backend/src/database/migrations/V1748332491__criticality-score.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS "criticalityScores" +( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + repoUrl VARCHAR(1024) NOT NULL, + score DOUBLE PRECISION NOT NULL, + rank INTEGER, + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +create index "ix_criticality_scores_updatedAt_id" on "criticalityScores" ("updatedAt", id); + +ALTER PUBLICATION sequin_pub ADD TABLE "criticalityScores"; +ALTER TABLE public."criticalityScores" REPLICA IDENTITY FULL; diff --git a/backend/src/database/migrations/V1748349736__criticalityScore-repoUrl.sql b/backend/src/database/migrations/V1748349736__criticalityScore-repoUrl.sql new file mode 100644 index 0000000000..2a63bd8ee2 --- /dev/null +++ b/backend/src/database/migrations/V1748349736__criticalityScore-repoUrl.sql @@ -0,0 +1,2 @@ +ALTER TABLE "criticalityScores" + RENAME COLUMN "repourl" TO "repoUrl"; diff --git a/backend/src/database/migrations/V1749153277__missing-member-indexes.sql b/backend/src/database/migrations/V1749153277__missing-member-indexes.sql new file mode 100644 index 0000000000..8b27eb6212 --- /dev/null +++ b/backend/src/database/migrations/V1749153277__missing-member-indexes.sql @@ -0,0 +1,5 @@ +create index concurrently if not exists "ix_activityRelations_objectMemberId" + on "activityRelations" ("objectMemberId") where "objectMemberId" is not null; + +create index concurrently if not exists "ix_memberOrganizations_memberId" + on "memberOrganizations" ("memberId"); \ No newline at end of file diff --git a/backend/src/database/migrations/V1749535600__addPackageDownloadsWidget.sql b/backend/src/database/migrations/V1749535600__addPackageDownloadsWidget.sql new file mode 100644 index 0000000000..145454be4e --- /dev/null +++ b/backend/src/database/migrations/V1749535600__addPackageDownloadsWidget.sql @@ -0,0 +1,11 @@ +UPDATE "insightsProjects" +SET widgets = ARRAY( + SELECT DISTINCT unnest( + widgets || ARRAY [ + 'packageDownloads', + 'packageDependency' + ] + ) + ), + "updatedAt" = CURRENT_TIMESTAMP +WHERE widgets IS NOT NULL; diff --git a/backend/src/database/migrations/V1749569644__addMaintainerDataToSequin.sql b/backend/src/database/migrations/V1749569644__addMaintainerDataToSequin.sql new file mode 100644 index 0000000000..aa6ab56abd --- /dev/null +++ b/backend/src/database/migrations/V1749569644__addMaintainerDataToSequin.sql @@ -0,0 +1,4 @@ +create index "ix_maintainersInternal_updatedAt_id" on "maintainersInternal" ("updatedAt", id); + +ALTER PUBLICATION sequin_pub ADD TABLE "maintainersInternal"; +ALTER TABLE public."maintainersInternal" REPLICA IDENTITY FULL; diff --git a/backend/src/database/migrations/V1750077155__safe_backup_members_before_enrichment.sql b/backend/src/database/migrations/V1750077155__safe_backup_members_before_enrichment.sql new file mode 100644 index 0000000000..26c2de1681 --- /dev/null +++ b/backend/src/database/migrations/V1750077155__safe_backup_members_before_enrichment.sql @@ -0,0 +1,32 @@ +-- backup members table +create table members_backup_16_06_2025 as +select * +from members + with no data; + +-- Copy all data +insert into members_backup_16_06_2025 +select * +from members; + +-- backup memberIdentities table +create table member_identities_backup_16_06_2025 as +select * +from "memberIdentities" + with no data; + +-- Copy all data +insert into member_identities_backup_16_06_2025 +select * +from "memberIdentities"; + +-- backup memberOrganizations table +create table member_organizations_backup_16_06_2025 as +select * +from "memberOrganizations" + with no data; + +-- Copy all data +insert into member_organizations_backup_16_06_2025 +select * +from "memberOrganizations"; \ No newline at end of file diff --git a/backend/src/database/migrations/V1750139894__addSearchQueriesWidget.sql b/backend/src/database/migrations/V1750139894__addSearchQueriesWidget.sql new file mode 100644 index 0000000000..b551657350 --- /dev/null +++ b/backend/src/database/migrations/V1750139894__addSearchQueriesWidget.sql @@ -0,0 +1,10 @@ +UPDATE "insightsProjects" +SET widgets = ARRAY( + SELECT DISTINCT unnest( + widgets || ARRAY [ + 'searchQueries' + ] + ) + ), + "updatedAt" = CURRENT_TIMESTAMP +WHERE widgets IS NOT NULL; diff --git a/backend/src/database/migrations/V1750680190__extend_activityRelations.sql b/backend/src/database/migrations/V1750680190__extend_activityRelations.sql new file mode 100644 index 0000000000..ef7fbcfdba --- /dev/null +++ b/backend/src/database/migrations/V1750680190__extend_activityRelations.sql @@ -0,0 +1,13 @@ +-- extend activityRelations table with additional columns +alter table "activityRelations" +add column "sourceId" varchar(150), +add column "type" varchar(50), +add column "timestamp" timestamp with time zone, +add column "sourceParentId" varchar(150), +add column "channel" varchar(150), +add column "sentimentScore" smallint, +add column "gitInsertions" integer, +add column "gitDeletions" integer, +add column "score" smallint, +add column "isContribution" boolean, +add column "pullRequestReviewState" varchar(20); diff --git a/backend/src/database/migrations/V1751017960__dedup_activity_relations_and_add_indexes.sql b/backend/src/database/migrations/V1751017960__dedup_activity_relations_and_add_indexes.sql new file mode 100644 index 0000000000..d5a5602bdd --- /dev/null +++ b/backend/src/database/migrations/V1751017960__dedup_activity_relations_and_add_indexes.sql @@ -0,0 +1,2 @@ +-- set the dedup keys as w/e is in questdb in relations table +create unique index concurrently if not exists ix_unique_activity_relations_dedup_key on "activityRelations" ("timestamp", "platform", "type", "sourceId", "channel", "segmentId"); diff --git a/backend/src/database/migrations/V1751298098__add_search_keywords.sql b/backend/src/database/migrations/V1751298098__add_search_keywords.sql new file mode 100644 index 0000000000..ac5bf8330a --- /dev/null +++ b/backend/src/database/migrations/V1751298098__add_search_keywords.sql @@ -0,0 +1,2 @@ +ALTER TABLE "insightsProjects" +ADD COLUMN "searchKeywords" TEXT[] DEFAULT ARRAY[]::TEXT[]; \ No newline at end of file diff --git a/backend/src/database/migrations/V1751459866__gitIntegration.sql b/backend/src/database/migrations/V1751459866__gitIntegration.sql new file mode 100644 index 0000000000..f9a0d72158 --- /dev/null +++ b/backend/src/database/migrations/V1751459866__gitIntegration.sql @@ -0,0 +1,80 @@ +-- Create the git schema +CREATE SCHEMA IF NOT EXISTS git; + +-- Main repositories table +CREATE TABLE git.repositories ( + id UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "deletedAt" TIMESTAMP WITH TIME ZONE, + + -- Repository identification + url VARCHAR(1024) NOT NULL, + + -- Processing state and priority + state VARCHAR(50) NOT NULL DEFAULT 'pending', + priority INTEGER NOT NULL DEFAULT 1, -- 0=urgent, 1=high, 2=normal + + -- Processing metadata + "lastProcessedAt" TIMESTAMP WITH TIME ZONE, + + -- Constraints + UNIQUE (url) +); + +-- Repository to Integration associations (many-to-many) +CREATE TABLE git."repositoryIntegrations" ( + id UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + + "repositoryId" UUID NOT NULL REFERENCES git.repositories (id) ON DELETE CASCADE, + "integrationId" UUID NOT NULL REFERENCES public."integrations" (id) ON DELETE CASCADE, + + -- Constraints + UNIQUE ("repositoryId", "integrationId") +); + +-- Function to clean up orphaned repositories +CREATE OR REPLACE FUNCTION git.cleanup_orphaned_repositories() +RETURNS TRIGGER AS $$ +BEGIN + -- Delete repositories that no longer have any associations + DELETE FROM git.repositories + WHERE id NOT IN ( + SELECT DISTINCT "repositoryId" + FROM git."repositoryIntegrations" + ); + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +-- Trigger to clean up orphaned repositories after association deletion +CREATE TRIGGER cleanup_orphaned_repositories_trigger + AFTER DELETE ON git."repositoryIntegrations" + FOR EACH ROW + EXECUTE FUNCTION git.cleanup_orphaned_repositories(); + + + +-- Create indexes for optimal query performance + +-- Repositories indexes +CREATE INDEX "ix_git_repositories_state" ON git.repositories (state); +CREATE INDEX "ix_git_repositories_priority" ON git.repositories (priority); +CREATE INDEX "ix_git_repositories_state_priority" ON git.repositories (state, priority); +CREATE INDEX "ix_git_repositories_lastProcessedAt" ON git.repositories ("lastProcessedAt"); + +-- Repository Integrations indexes +CREATE INDEX "ix_git_repositoryIntegrations_repositoryId" ON git."repositoryIntegrations" ("repositoryId"); +CREATE INDEX "ix_git_repositoryIntegrations_integrationId" ON git."repositoryIntegrations" ("integrationId"); + + + +-- Add comments for documentation +COMMENT ON SCHEMA git IS 'Schema for git integration system that manages repository processing and integration associations'; +COMMENT ON TABLE git.repositories IS 'Stores git repository metadata and processing state for the git integration system'; +COMMENT ON TABLE git."repositoryIntegrations" IS 'Many-to-many relationship between repositories and integrations'; + +COMMENT ON COLUMN git.repositories.priority IS 'Processing priority: 0=urgent, 1=high, 2=normal'; +COMMENT ON COLUMN git.repositories.state IS 'Current processing state of the repository'; \ No newline at end of file diff --git a/backend/src/database/migrations/V1751635377__addLockedAtToRepositories.sql b/backend/src/database/migrations/V1751635377__addLockedAtToRepositories.sql new file mode 100644 index 0000000000..5be1f8aac8 --- /dev/null +++ b/backend/src/database/migrations/V1751635377__addLockedAtToRepositories.sql @@ -0,0 +1,8 @@ +-- Add lockedAt column to git.repositories table +-- This column tracks when a repository was locked for processing + +ALTER TABLE git.repositories +ADD COLUMN "lockedAt" TIMESTAMP WITH TIME ZONE DEFAULT NULL; + +-- Add comment for documentation +COMMENT ON COLUMN git.repositories."lockedAt" IS 'Timestamp when the repository was locked for processing, NULL if not locked'; diff --git a/backend/src/database/migrations/V1752141258__add_affiliations_read_index.sql b/backend/src/database/migrations/V1752141258__add_affiliations_read_index.sql new file mode 100644 index 0000000000..b583b8cf3f --- /dev/null +++ b/backend/src/database/migrations/V1752141258__add_affiliations_read_index.sql @@ -0,0 +1,2 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS "ix_activityRelations_memberId_segmentId_timestamp" +ON "activityRelations" ("memberId", "segmentId", "timestamp"); \ No newline at end of file diff --git a/backend/src/database/migrations/V1752752559__addLastProcessedCommit.sql b/backend/src/database/migrations/V1752752559__addLastProcessedCommit.sql new file mode 100644 index 0000000000..2d1a42c70e --- /dev/null +++ b/backend/src/database/migrations/V1752752559__addLastProcessedCommit.sql @@ -0,0 +1,5 @@ +ALTER TABLE git.repositories +ADD COLUMN "lastProcessedCommit" VARCHAR(64) DEFAULT NULL; + +-- Add comment for documentation +COMMENT ON COLUMN git.repositories."lastProcessedCommit" IS 'The most recent commit hash that has been processed'; diff --git a/backend/src/database/migrations/V1752825515__projectsCategoriesSoftDeletion.sql b/backend/src/database/migrations/V1752825515__projectsCategoriesSoftDeletion.sql new file mode 100644 index 0000000000..3fffcebc0e --- /dev/null +++ b/backend/src/database/migrations/V1752825515__projectsCategoriesSoftDeletion.sql @@ -0,0 +1,64 @@ +-- 1. insightsProjects +alter table "insightsProjects" add column "deletedAt" timestamp without time zone; + +alter table public."insightsProjects" + drop constraint if exists "unique_insightsProjects_name", + drop constraint if exists "unique_project_segmentId"; + +drop index if exists "unique_insightsProjects_name"; +drop index if exists "unique_project_segmentId"; + +create unique index "unique_insightsProjects_name" + on public."insightsProjects" (name) + where "deletedAt" is null; + +create unique index "unique_project_segmentId" + on public."insightsProjects" ("segmentId") + where "deletedAt" is null; + + +-- 2. collections +alter table "collections" add column "deletedAt" timestamp without time zone; + +alter table public.collections + drop constraint if exists idx_collections_slug_unique, + drop constraint if exists unique_collection_name; + +drop index if exists idx_collections_slug_unique; +drop index if exists unique_collection_name; + +-- Recreate partial unique indexes (excluding soft-deleted rows) +create unique index idx_collections_slug_unique + on public.collections (slug) + where "deletedAt" is null; + +create unique index unique_collection_name + on public.collections (name) + where "deletedAt" is null; + + +-- 3. categories +alter table "categories" add column "deletedAt" timestamp without time zone; + +alter table public."categories" + drop constraint if exists "categories_slug_key"; + +drop index if exists "categories_slug_key"; + +create unique index "categories_slug_key" + on public."categories" (slug) + where "deletedAt" is null; + + +-- 4. categoryGroups +alter table "categoryGroups" add column "deletedAt" timestamp without time zone; + +alter table public."categoryGroups" + drop constraint if exists "categoryGroups_slug_key"; + +drop index if exists "categoryGroups_slug_key"; + +create unique index "categoryGroups_slug_key" + on public."categoryGroups" (slug) + where "deletedAt" is null; + diff --git a/backend/src/database/migrations/V1753110221__create_segment_repos_table.sql b/backend/src/database/migrations/V1753110221__create_segment_repos_table.sql new file mode 100644 index 0000000000..8f5a8ca63b --- /dev/null +++ b/backend/src/database/migrations/V1753110221__create_segment_repos_table.sql @@ -0,0 +1,11 @@ +-- 1. Create the table to map repositories to segments +CREATE TABLE IF NOT EXISTS "segmentRepositories" ( + "repository" TEXT NOT NULL UNIQUE, + "segmentId" UUID NOT NULL REFERENCES "segments"(id) ON DELETE CASCADE, + "insightsProjectId" UUID REFERENCES "insightsProjects"(id) ON DELETE CASCADE, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "excluded" BOOLEAN NOT NULL DEFAULT FALSE, + "archived" BOOLEAN NOT NULL DEFAULT FALSE, + + PRIMARY KEY ("repository", "segmentId") +); diff --git a/backend/src/database/migrations/V1753457861__add_unique_contraint_on_insights_slug.sql b/backend/src/database/migrations/V1753457861__add_unique_contraint_on_insights_slug.sql new file mode 100644 index 0000000000..50406e4a25 --- /dev/null +++ b/backend/src/database/migrations/V1753457861__add_unique_contraint_on_insights_slug.sql @@ -0,0 +1,28 @@ + +-- Add unique constraint on insightsProjects.slug +ALTER TABLE "insightsProjects" +ADD CONSTRAINT unique_insights_projects_slug UNIQUE (slug); + +-- FK on securityInsightsEvaluations +ALTER TABLE "securityInsightsEvaluations" +ADD CONSTRAINT fk_insights_projects_evaluations_project_slug +FOREIGN KEY ("insightsProjectSlug") +REFERENCES "insightsProjects"("slug") +ON UPDATE CASCADE +ON DELETE RESTRICT; + +-- FK on securityInsightsEvaluationSuites +ALTER TABLE "securityInsightsEvaluationSuites" +ADD CONSTRAINT fk_insights_projects_suites_project_slug +FOREIGN KEY ("insightsProjectSlug") +REFERENCES "insightsProjects"("slug") +ON UPDATE CASCADE +ON DELETE RESTRICT; + +-- FK on securityInsightsEvaluationAssessments +ALTER TABLE "securityInsightsEvaluationAssessments" +ADD CONSTRAINT fk_insights_projects_assessments_project_slug +FOREIGN KEY ("insightsProjectSlug") +REFERENCES "insightsProjects"("slug") +ON UPDATE CASCADE +ON DELETE RESTRICT; diff --git a/backend/src/database/migrations/V1753798343__revert_primary_key_segment_repositories.sql b/backend/src/database/migrations/V1753798343__revert_primary_key_segment_repositories.sql new file mode 100644 index 0000000000..3d2c128e40 --- /dev/null +++ b/backend/src/database/migrations/V1753798343__revert_primary_key_segment_repositories.sql @@ -0,0 +1,11 @@ +-- 1. Drop the current primary key +ALTER TABLE "segmentRepositories" +DROP CONSTRAINT "segmentRepositories_pkey"; + +-- 2. Add the new primary key +ALTER TABLE "segmentRepositories" +ADD CONSTRAINT "segmentRepositories_pkey" PRIMARY KEY ("repository", "insightsProjectId"); + +-- 3. Drop the NOT NULL constraint on segmentId +ALTER TABLE "segmentRepositories" +ALTER COLUMN "segmentId" DROP NOT NULL; diff --git a/backend/src/database/migrations/V1753798344__add_last_archived_check_o_segmentRepositories.sql b/backend/src/database/migrations/V1753798344__add_last_archived_check_o_segmentRepositories.sql new file mode 100644 index 0000000000..9ddf29169e --- /dev/null +++ b/backend/src/database/migrations/V1753798344__add_last_archived_check_o_segmentRepositories.sql @@ -0,0 +1,5 @@ +ALTER TABLE "segmentRepositories" +ADD COLUMN last_archived_check TIMESTAMP WITH TIME ZONE DEFAULT NULL; + +CREATE INDEX idx_segmentRepositories_last_archived_check + ON "segmentRepositories" (last_archived_check); diff --git a/backend/src/database/migrations/V1753798345__segmentRepositories_replication.sql b/backend/src/database/migrations/V1753798345__segmentRepositories_replication.sql new file mode 100644 index 0000000000..98f940db95 --- /dev/null +++ b/backend/src/database/migrations/V1753798345__segmentRepositories_replication.sql @@ -0,0 +1,3 @@ +ALTER PUBLICATION sequin_pub ADD TABLE "segmentRepositories"; +ALTER TABLE "segmentRepositories" REPLICA IDENTITY FULL; +GRANT SELECT ON "segmentRepositories" to sequin; diff --git a/backend/src/database/migrations/V1754382165__deprecate-notes-tags.sql b/backend/src/database/migrations/V1754382165__deprecate-notes-tags.sql new file mode 100644 index 0000000000..83b952c574 --- /dev/null +++ b/backend/src/database/migrations/V1754382165__deprecate-notes-tags.sql @@ -0,0 +1,11 @@ +alter table tags + drop constraint "tags_createdById_fkey", + drop constraint "tags_tenantId_fkey", + drop constraint "tags_updatedById_fkey"; + +alter table "memberTags" + drop constraint "memberTags_memberId_fkey", + drop constraint "memberTags_tagId_fkey"; + +alter table tags rename to old_tags; +alter table "memberTags" rename to "old_memberTags"; \ No newline at end of file diff --git a/backend/src/database/migrations/V1754395778__addSegmentAndIntegrationToRepositories.sql b/backend/src/database/migrations/V1754395778__addSegmentAndIntegrationToRepositories.sql new file mode 100644 index 0000000000..2844be54a5 --- /dev/null +++ b/backend/src/database/migrations/V1754395778__addSegmentAndIntegrationToRepositories.sql @@ -0,0 +1,12 @@ +-- Add integrationId and segmentId columns to git.repositories table +-- These columns reference segments and integrations from public schema +-- Both are nullable and set to null on delete + +ALTER TABLE git.repositories +ADD COLUMN "integrationId" UUID REFERENCES public.integrations (id) ON DELETE SET NULL, +ADD COLUMN "segmentId" UUID REFERENCES public.segments (id) ON DELETE SET NULL; + +-- Create indexes for better query performance +CREATE INDEX "ix_git_repositories_integrationId" ON git.repositories ("integrationId"); +CREATE INDEX "ix_git_repositories_segmentId" ON git.repositories ("segmentId"); +CREATE INDEX "ix_git_repositories_integrationId_segmentId" ON git.repositories ("integrationId", "segmentId"); \ No newline at end of file diff --git a/backend/src/database/migrations/V1754406504__deleteRepositoryIntegrations.sql b/backend/src/database/migrations/V1754406504__deleteRepositoryIntegrations.sql new file mode 100644 index 0000000000..e3abe8c4fe --- /dev/null +++ b/backend/src/database/migrations/V1754406504__deleteRepositoryIntegrations.sql @@ -0,0 +1,4 @@ +DROP TRIGGER IF EXISTS cleanup_orphaned_repositories_trigger ON git."repositoryIntegrations"; +DROP FUNCTION IF EXISTS git.cleanup_orphaned_repositories(); + +DROP TABLE IF EXISTS git."repositoryIntegrations"; diff --git a/backend/src/database/migrations/V1754924692__prevent-duplicate-unverified-member-identities.sql b/backend/src/database/migrations/V1754924692__prevent-duplicate-unverified-member-identities.sql new file mode 100644 index 0000000000..fd6e89de6a --- /dev/null +++ b/backend/src/database/migrations/V1754924692__prevent-duplicate-unverified-member-identities.sql @@ -0,0 +1,2 @@ +create unique index if not exists "uix_memberIdentities_memberId_platform_value_type" + on "memberIdentities" ("memberId", platform, value, type); \ No newline at end of file diff --git a/backend/src/database/migrations/V1755171546__addLfxMembershipsToTinybird.sql b/backend/src/database/migrations/V1755171546__addLfxMembershipsToTinybird.sql new file mode 100644 index 0000000000..bb8b61ee04 --- /dev/null +++ b/backend/src/database/migrations/V1755171546__addLfxMembershipsToTinybird.sql @@ -0,0 +1,4 @@ +ALTER PUBLICATION sequin_pub ADD TABLE "lfxMemberships"; +ALTER TABLE public."lfxMemberships" REPLICA IDENTITY FULL; + +create index "ix_lfxMemberships_updatedAt_id" on "lfxMemberships" ("updatedAt", id); \ No newline at end of file diff --git a/backend/src/database/migrations/V1755245912__addOrganizationIdentitiesToTinybird.sql b/backend/src/database/migrations/V1755245912__addOrganizationIdentitiesToTinybird.sql new file mode 100644 index 0000000000..cb3aadfd35 --- /dev/null +++ b/backend/src/database/migrations/V1755245912__addOrganizationIdentitiesToTinybird.sql @@ -0,0 +1,4 @@ +ALTER PUBLICATION sequin_pub ADD TABLE "organizationIdentities"; +ALTER TABLE public."organizationIdentities" REPLICA IDENTITY FULL; + +create index "ix_organizationIdentities_updatedAt_with_pk" on "organizationIdentities" ("updatedAt", "organizationId", platform, type, value); \ No newline at end of file diff --git a/backend/src/database/migrations/V1755343323__add_updatedAt_to_segmentRepositories.sql b/backend/src/database/migrations/V1755343323__add_updatedAt_to_segmentRepositories.sql new file mode 100644 index 0000000000..82dc2b06f1 --- /dev/null +++ b/backend/src/database/migrations/V1755343323__add_updatedAt_to_segmentRepositories.sql @@ -0,0 +1,2 @@ +ALTER TABLE "segmentRepositories" +ADD COLUMN updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; \ No newline at end of file diff --git a/backend/src/database/migrations/V1755867534__add_unique_constraint_on_repository.sql b/backend/src/database/migrations/V1755867534__add_unique_constraint_on_repository.sql new file mode 100644 index 0000000000..635881ed1d --- /dev/null +++ b/backend/src/database/migrations/V1755867534__add_unique_constraint_on_repository.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS segmentRepositories_repository_uq_idx ON "segmentRepositories" ("repository"); \ No newline at end of file diff --git a/backend/src/database/migrations/V1756112470__memberBotSuggestions.sql b/backend/src/database/migrations/V1756112470__memberBotSuggestions.sql new file mode 100644 index 0000000000..7fae7480d1 --- /dev/null +++ b/backend/src/database/migrations/V1756112470__memberBotSuggestions.sql @@ -0,0 +1,14 @@ +create table "memberBotSuggestions" ( + id uuid primary key not null default uuid_generate_v4(), + "memberId" uuid not null references members (id) on delete cascade, + confidence float not null, + "createdAt" timestamp with time zone not null, + constraint "memberBotSuggestions_memberId_key" unique("memberId") +); + +create table "memberNoBot" ( + id uuid primary key not null default uuid_generate_v4(), + "memberId" uuid not null references members (id) on delete cascade, + "createdAt" timestamp with time zone not null, + constraint "memberNoBot_memberId_key" unique("memberId") +); \ No newline at end of file diff --git a/backend/src/database/migrations/V1756301737__addLastMaintainerRunAt.sql b/backend/src/database/migrations/V1756301737__addLastMaintainerRunAt.sql new file mode 100644 index 0000000000..ffb5589c00 --- /dev/null +++ b/backend/src/database/migrations/V1756301737__addLastMaintainerRunAt.sql @@ -0,0 +1,5 @@ +ALTER TABLE git.repositories +ADD COLUMN "lastMaintainerRunAt" TIMESTAMP WITH TIME ZONE DEFAULT NULL; + +-- Add comment for documentation +COMMENT ON COLUMN git.repositories."lastMaintainerRunAt" IS 'Timestamp of when the repository maintainer processing was last executed'; diff --git a/backend/src/database/migrations/V1756301963__addMaintainerFile.sql b/backend/src/database/migrations/V1756301963__addMaintainerFile.sql new file mode 100644 index 0000000000..3a67b7fc9a --- /dev/null +++ b/backend/src/database/migrations/V1756301963__addMaintainerFile.sql @@ -0,0 +1,5 @@ +ALTER TABLE git.repositories +ADD COLUMN "maintainerFile" text DEFAULT NULL; + +-- Add comment for documentation +COMMENT ON COLUMN git.repositories."maintainerFile" IS 'Name of the file containing repository maintainer information and responsibilities (e.g., MAINTAINERS, CODEOWNERS)'; diff --git a/backend/src/database/migrations/V1756387524__addMaintainersFields.sql b/backend/src/database/migrations/V1756387524__addMaintainersFields.sql new file mode 100644 index 0000000000..59f53293ba --- /dev/null +++ b/backend/src/database/migrations/V1756387524__addMaintainersFields.sql @@ -0,0 +1,5 @@ +-- Add missing columns to maintainersInternal table (only if they don't exist) +ALTER TABLE "maintainersInternal" +ADD COLUMN IF NOT EXISTS "originalRole" VARCHAR(255), +ADD COLUMN IF NOT EXISTS "startDate" TIMESTAMP WITHOUT TIME ZONE, +ADD COLUMN IF NOT EXISTS "endDate" TIMESTAMP WITHOUT TIME ZONE; diff --git a/backend/src/database/migrations/V1756671476__createServiceExecutionsTable.sql b/backend/src/database/migrations/V1756671476__createServiceExecutionsTable.sql new file mode 100644 index 0000000000..478b33ee45 --- /dev/null +++ b/backend/src/database/migrations/V1756671476__createServiceExecutionsTable.sql @@ -0,0 +1,40 @@ +-- Create ENUM type for execution status +CREATE TYPE git.execution_status AS ENUM ('success', 'failure'); + +-- Create service executions table for tracking service execution metrics +CREATE TABLE IF NOT EXISTS git."serviceExecutions" ( + id UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + "repoId" UUID NOT NULL REFERENCES git.repositories(id) ON DELETE CASCADE, + "operationType" VARCHAR(50) NOT NULL, -- Service name (e.g., 'Clone', 'Commit', etc.) + "status" git.execution_status NOT NULL, + "errorCode" VARCHAR(50), -- Custom error codes + "errorMessage" TEXT, -- Detailed error message if status is error + "executionTimeSec" DECIMAL NOT NULL, -- Execution time in seconds + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Create indexes for efficient querying +CREATE INDEX IF NOT EXISTS "idx_serviceExecutions_repoId" ON git."serviceExecutions"("repoId"); +CREATE INDEX IF NOT EXISTS "idx_serviceExecutions_operationType" ON git."serviceExecutions"("operationType"); +CREATE INDEX IF NOT EXISTS "idx_serviceExecutions_status" ON git."serviceExecutions"("status"); +CREATE INDEX IF NOT EXISTS "idx_serviceExecutions_createdAt" ON git."serviceExecutions"("createdAt"); +CREATE INDEX IF NOT EXISTS "idx_serviceExecutions_composite" ON git."serviceExecutions"("repoId", "operationType", "status"); + +CREATE OR REPLACE FUNCTION git.trigger_cleanup_service_executions() +RETURNS trigger AS $$ +BEGIN + -- Only run cleanup 1% of the time (1 in 100 inserts) - due to high write load, keep cleanup minimal to avoid performance impact + IF RANDOM() < 0.01 THEN + DELETE FROM git."serviceExecutions" + WHERE "createdAt" < NOW() - INTERVAL '14 days'; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create trigger that fires on each insert +CREATE TRIGGER trigger_auto_cleanup_service_executions + AFTER INSERT ON git."serviceExecutions" + FOR EACH ROW + EXECUTE FUNCTION git.trigger_cleanup_service_executions(); \ No newline at end of file diff --git a/backend/src/database/migrations/V1757413130__repositoryGroups.sql b/backend/src/database/migrations/V1757413130__repositoryGroups.sql new file mode 100644 index 0000000000..02bc9d918d --- /dev/null +++ b/backend/src/database/migrations/V1757413130__repositoryGroups.sql @@ -0,0 +1,18 @@ +CREATE TABLE IF NOT EXISTS "repositoryGroups" +( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + slug VARCHAR(255) NOT NULL, + "repositories" VARCHAR[] DEFAULT ARRAY []::VARCHAR[], + "insightsProjectId" UUID NOT NULL, + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + "deletedAt" TIMESTAMP NULL DEFAULT NULL, + foreign key ("insightsProjectId") references "insightsProjects" (id) on delete cascade, + UNIQUE (slug, "insightsProjectId", "deletedAt") +); + +create index "ix_repositoryGroups_updatedAt_id" on "repositoryGroups" ("updatedAt", id); + +ALTER PUBLICATION sequin_pub ADD TABLE "repositoryGroups"; +ALTER TABLE public."repositoryGroups" REPLICA IDENTITY FULL; diff --git a/backend/src/database/migrations/V1757490935__create_activityTypes_table.sql b/backend/src/database/migrations/V1757490935__create_activityTypes_table.sql new file mode 100644 index 0000000000..f09a69f5bd --- /dev/null +++ b/backend/src/database/migrations/V1757490935__create_activityTypes_table.sql @@ -0,0 +1,119 @@ +CREATE TABLE "activityTypes" ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "activityType" VARCHAR(150), + platform VARCHAR(150), + "isCodeContribution" BOOLEAN, + "isCollaboration" BOOLEAN, + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX "idx_activityTypes_updatedAt" + ON "activityTypes" ("updatedAt"); + + +-- Insert Code Contributions activity types +-- Git platform (Code Contributions) +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('authored-commit', 'git', true, false), +('reviewed-commit', 'git', true, false), +('tested-commit', 'git', true, false), +('co-authored-commit', 'git', true, false), +('informed-commit', 'git', true, false), +('influenced-commit', 'git', true, false), +('approved-commit', 'git', true, false), +('committed-commit', 'git', true, false), +('reported-commit', 'git', true, false), +('resolved-commit', 'git', true, false), +('signed-off-commit', 'git', true, false); + +-- GitHub platform (Code Contributions) +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('pull_request-opened', 'github', true, false), +('pull_request-closed', 'github', true, false), +('pull_request-review-requested', 'github', true, false), +('pull_request-reviewed', 'github', true, false), +('pull_request-merged', 'github', true, false), +('pull_request-comment', 'github', true, false), +('authored-commit', 'github', true, false); + +-- Gerrit platform (Code Contributions) +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('changeset-new', 'gerrit', true, false), +('changeset-created', 'gerrit', true, false), +('changeset-merged', 'gerrit', true, false), +('changeset-closed', 'gerrit', true, false), +('changeset-abandoned', 'gerrit', true, false), +('changeset_comment-created', 'gerrit', true, false), +('patchset-created', 'gerrit', true, false), +('patchset_comment-created', 'gerrit', true, false), +('patchset_approval-created', 'gerrit', true, false); + +-- GitLab platform (Code Contributions) +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('merge_request-opened', 'gitlab', true, false), +('merge_request-closed', 'gitlab', true, false), +('merge_request-review-requested', 'gitlab', true, false), +('merge_request-review-approved', 'gitlab', true, false), +('merge_request-review-changes-requested', 'gitlab', true, false), +('merge_request-merged', 'gitlab', true, false), +('merge_request-comment', 'gitlab', true, false); + +-- Insert Collaboration activity types +-- GitHub platform (Collaboration) +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('discussion-started', 'github', false, true), +('issues-opened', 'github', false, true), +('issues-closed', 'github', false, true), +('issue-comment', 'github', false, true), +('discussion-comment', 'github', false, true); + +-- GitLab platform (Collaboration) +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('issues-opened', 'gitlab', false, true), +('issues-closed', 'gitlab', false, true), +('issue-comment', 'gitlab', false, true); + +-- Confluence platform (Collaboration) +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('page-created', 'confluence', false, true), +('page-updated', 'confluence', false, true), +('comment-created', 'confluence', false, true), +('attachment-created', 'confluence', false, true), +('blogpost-created', 'confluence', false, true), +('blogpost-updated', 'confluence', false, true), +('attachment', 'confluence', false, true), +('comment', 'confluence', false, true); + +-- Jira platform (Collaboration) +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('issue-created', 'jira', false, true), +('issue-closed', 'jira', false, true), +('issue-assigned', 'jira', false, true), +('issue-updated', 'jira', false, true), +('issue-comment-created', 'jira', false, true), +('issue-comment-updated', 'jira', false, true), +('issue-attachment-added', 'jira', false, true); + +-- Groups.io platform (Collaboration) +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('message', 'groups.io', false, true); + +-- Discord platform (Collaboration) +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('message', 'discord', false, true), +('thread_started', 'discord', false, true), +('thread_message', 'discord', false, true); + +-- Slack platform (Collaboration) +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('message', 'slack', false, true); + +-- Stack Overflow platform (Collaboration) +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('question', 'stackoverflow', false, true), +('answer', 'stackoverflow', false, true); + +ALTER PUBLICATION sequin_pub ADD TABLE "activityTypes"; +ALTER TABLE "activityTypes" REPLICA IDENTITY FULL; +GRANT SELECT ON "activityTypes" to sequin; diff --git a/backend/src/database/migrations/V1757500000__addRecommendationToSecurityInsightsAssessments.sql b/backend/src/database/migrations/V1757500000__addRecommendationToSecurityInsightsAssessments.sql new file mode 100644 index 0000000000..9d9455f5a8 --- /dev/null +++ b/backend/src/database/migrations/V1757500000__addRecommendationToSecurityInsightsAssessments.sql @@ -0,0 +1,7 @@ +-- Add additional columns to securityInsightsEvaluationAssessments table +ALTER TABLE public."securityInsightsEvaluationAssessments" +ADD COLUMN "recommendation" text, +ADD COLUMN "start" text, +ADD COLUMN "end" text, +ADD COLUMN "value" jsonb, +ADD COLUMN "changes" jsonb; \ No newline at end of file diff --git a/backend/src/database/migrations/V1758292471__organizationEnrichment.sql b/backend/src/database/migrations/V1758292471__organizationEnrichment.sql new file mode 100644 index 0000000000..2a175015bf --- /dev/null +++ b/backend/src/database/migrations/V1758292471__organizationEnrichment.sql @@ -0,0 +1,14 @@ +create table "organizationEnrichments"( + "organizationId" uuid primary key, + "lastTriedAt" timestamp with time zone, + "lastUpdatedAt" timestamp with time zone +); + +create table "organizationEnrichmentCache" ( + "organizationId" uuid not null references organizations(id) on delete no action on update no action, + "data" jsonb not null, + "source" text not null, + "createdAt" timestamp with time zone not null, + "updatedAt" timestamp with time zone not null, + primary key ("organizationId", source) +); \ No newline at end of file diff --git a/backend/src/database/migrations/V1759157837__add_activity_types.sql b/backend/src/database/migrations/V1759157837__add_activity_types.sql new file mode 100644 index 0000000000..e6f2cac41d --- /dev/null +++ b/backend/src/database/migrations/V1759157837__add_activity_types.sql @@ -0,0 +1,70 @@ +-- GitLab platform (Code Contributions) +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('authored-commit', 'gitlab', true, false); + +-- Fix Groups.io platform name +UPDATE "activityTypes" SET platform = 'groupsio' WHERE platform = 'groups.io'; + +-- Discourse platform (Collaboration) +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('create_topic', 'discourse', false, true), +('message_in_topic', 'discourse', false, true); + + +-- Insert other activity types + +-- Dev.to +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('comment', 'devto', false, false); + +-- Discord +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('joined_guild', 'discord', false, false); + +-- Discourse +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('join', 'discourse', false, false), +('like', 'discourse', false, false); + +-- Github +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('pull_request-assigned', 'github', false, false), +('fork', 'github', false, false), +('star', 'github', false, false), +('unstar', 'github', false, false); + +-- Gitlab +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('merge_request-assigned', 'gitlab', false, false), +('fork', 'gitlab', false, false), +('star', 'gitlab', false, false); + +-- Groups.io +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('member_join', 'groupsio', false, false), +('member_leave', 'groupsio', false, false); + +-- HackerNews +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('post', 'hackernews', false, false), +('comment', 'hackernews', false, false); + +-- LinkedIn +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('comment', 'linkedin', false, false), +('reaction', 'linkedin', false, false); + +-- Reddit +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('post', 'reddit', false, false), +('comment', 'reddit', false, false); + +-- Slack +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('channel_joined', 'slack', false, false); + +-- Twitter +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration") VALUES +('hashtag', 'twitter', false, false), +('mention', 'twitter', false, false), +('follow', 'twitter', false, false); diff --git a/backend/src/database/migrations/V1759325136__add_description_to_activity_types.sql b/backend/src/database/migrations/V1759325136__add_description_to_activity_types.sql new file mode 100644 index 0000000000..cb96de5729 --- /dev/null +++ b/backend/src/database/migrations/V1759325136__add_description_to_activity_types.sql @@ -0,0 +1,131 @@ +-- Add description column to activityTypes table +ALTER TABLE "activityTypes" ADD COLUMN description TEXT; + +-- Delete changeset-new activity type +DELETE FROM "activityTypes" WHERE "activityType" = 'changeset-new' AND platform = 'gerrit'; + +-- Update descriptions for existing activity types + +-- Confluence platform +UPDATE "activityTypes" SET description = 'Added file attachment to a Confluence page or blog post' WHERE "activityType" = 'attachment' AND platform = 'confluence'; +UPDATE "activityTypes" SET description = 'Created file attachment in a Confluence page or blog post' WHERE "activityType" = 'attachment-created' AND platform = 'confluence'; +UPDATE "activityTypes" SET description = 'Published a new Confluence blog post' WHERE "activityType" = 'blogpost-created' AND platform = 'confluence'; +UPDATE "activityTypes" SET description = 'Updated an existing Confluence blog post' WHERE "activityType" = 'blogpost-updated' AND platform = 'confluence'; +UPDATE "activityTypes" SET description = 'Posted a comment on a Confluence page or blog post' WHERE "activityType" = 'comment' AND platform = 'confluence'; +UPDATE "activityTypes" SET description = 'Created comment in a Confluence page or blog post' WHERE "activityType" = 'comment-created' AND platform = 'confluence'; +UPDATE "activityTypes" SET description = 'Created a new Confluence page' WHERE "activityType" = 'page-created' AND platform = 'confluence'; +UPDATE "activityTypes" SET description = 'Edited or updated a Confluence page' WHERE "activityType" = 'page-updated' AND platform = 'confluence'; + +-- Dev.to platform +UPDATE "activityTypes" SET description = 'Commented on a Dev.to article' WHERE "activityType" = 'comment' AND platform = 'devto'; + +-- Discord platform +UPDATE "activityTypes" SET description = 'Joined Discord server/guild' WHERE "activityType" = 'joined_guild' AND platform = 'discord'; +UPDATE "activityTypes" SET description = 'Sent a message in a Discord channel' WHERE "activityType" = 'message' AND platform = 'discord'; +UPDATE "activityTypes" SET description = 'Sent a message in a Discord thread' WHERE "activityType" = 'thread_message' AND platform = 'discord'; +UPDATE "activityTypes" SET description = 'Started thread in a Discord channel' WHERE "activityType" = 'thread_started' AND platform = 'discord'; + +-- Discourse platform +UPDATE "activityTypes" SET description = 'Liked post in a Discourse forum' WHERE "activityType" = 'like' AND platform = 'discourse'; +UPDATE "activityTypes" SET description = 'Joined Discourse forum' WHERE "activityType" = 'join' AND platform = 'discourse'; +UPDATE "activityTypes" SET description = 'Created topic in a Discourse forum' WHERE "activityType" = 'create_topic' AND platform = 'discourse'; +UPDATE "activityTypes" SET description = 'Replied to topic in a Discourse forum' WHERE "activityType" = 'message_in_topic' AND platform = 'discourse'; + +-- Gerrit platform +UPDATE "activityTypes" SET description = 'Abandoned a code change in Gerrit' WHERE "activityType" = 'changeset-abandoned' AND platform = 'gerrit'; +UPDATE "activityTypes" SET description = 'Closed a code review in Gerrit' WHERE "activityType" = 'changeset-closed' AND platform = 'gerrit'; +UPDATE "activityTypes" SET description = 'Submitted a new changeset in Gerrit' WHERE "activityType" = 'changeset-created' AND platform = 'gerrit'; +UPDATE "activityTypes" SET description = 'Merged a code change into the main branch in Gerrit' WHERE "activityType" = 'changeset-merged' AND platform = 'gerrit'; +UPDATE "activityTypes" SET description = 'Commented on a code changeset in Gerrit' WHERE "activityType" = 'changeset_comment-created' AND platform = 'gerrit'; +UPDATE "activityTypes" SET description = 'Uploaded a new patchset for review in Gerrit' WHERE "activityType" = 'patchset-created' AND platform = 'gerrit'; +UPDATE "activityTypes" SET description = 'Approved a code patchset for review in Gerrit' WHERE "activityType" = 'patchset_approval-created' AND platform = 'gerrit'; +UPDATE "activityTypes" SET description = 'Commented on a code patchset in Gerrit' WHERE "activityType" = 'patchset_comment-created' AND platform = 'gerrit'; + +-- Git platform +UPDATE "activityTypes" SET description = 'Approved a Git commit during code review in the default branch' WHERE "activityType" = 'approved-commit' AND platform = 'git'; +UPDATE "activityTypes" SET description = 'Authored a Git commit in the default branch' WHERE "activityType" = 'authored-commit' AND platform = 'git'; +UPDATE "activityTypes" SET description = 'Co-authored a Git commit with another user in the default branch' WHERE "activityType" = 'co-authored-commit' AND platform = 'git'; +UPDATE "activityTypes" SET description = 'Committed changes to a repository in the default branch' WHERE "activityType" = 'committed-commit' AND platform = 'git'; +UPDATE "activityTypes" SET description = 'Indirectly influenced a Git commit''s creation in the default branch' WHERE "activityType" = 'influenced-commit' AND platform = 'git'; +UPDATE "activityTypes" SET description = 'Contributed ideas or guidance that led to a Git commit in the default branch' WHERE "activityType" = 'informed-commit' AND platform = 'git'; +UPDATE "activityTypes" SET description = 'Reported an issue fixed by a Git commit in the default branch' WHERE "activityType" = 'reported-commit' AND platform = 'git'; +UPDATE "activityTypes" SET description = 'Resolved an issue with a Git commit in the default branch' WHERE "activityType" = 'resolved-commit' AND platform = 'git'; +UPDATE "activityTypes" SET description = 'Reviewed a Git commit in the default branch' WHERE "activityType" = 'reviewed-commit' AND platform = 'git'; +UPDATE "activityTypes" SET description = 'Signed off on a Git commit for compliance/review in the default branch' WHERE "activityType" = 'signed-off-commit' AND platform = 'git'; +UPDATE "activityTypes" SET description = 'Tested changes and marked them accordingly in the default branch' WHERE "activityType" = 'tested-commit' AND platform = 'git'; + +-- GitHub platform +UPDATE "activityTypes" SET description = 'Authored and pushed a commit in a pull request on GitHub' WHERE "activityType" = 'authored-commit' AND platform = 'github'; +UPDATE "activityTypes" SET description = 'Commented on a GitHub discussion' WHERE "activityType" = 'discussion-comment' AND platform = 'github'; +UPDATE "activityTypes" SET description = 'Started a GitHub discussion' WHERE "activityType" = 'discussion-started' AND platform = 'github'; +UPDATE "activityTypes" SET description = 'Forked a GitHub repository' WHERE "activityType" = 'fork' AND platform = 'github'; +UPDATE "activityTypes" SET description = 'Commented on a GitHub issue' WHERE "activityType" = 'issue-comment' AND platform = 'github'; +UPDATE "activityTypes" SET description = 'Closed a GitHub issue' WHERE "activityType" = 'issues-closed' AND platform = 'github'; +UPDATE "activityTypes" SET description = 'Opened a GitHub issue' WHERE "activityType" = 'issues-opened' AND platform = 'github'; +UPDATE "activityTypes" SET description = 'Assigned to a GitHub pull request' WHERE "activityType" = 'pull_request-assigned' AND platform = 'github'; +UPDATE "activityTypes" SET description = 'Closed a GitHub pull request' WHERE "activityType" = 'pull_request-closed' AND platform = 'github'; +UPDATE "activityTypes" SET description = 'Commented on a GitHub pull request' WHERE "activityType" = 'pull_request-comment' AND platform = 'github'; +UPDATE "activityTypes" SET description = 'Merged a GitHub pull request' WHERE "activityType" = 'pull_request-merged' AND platform = 'github'; +UPDATE "activityTypes" SET description = 'Opened a GitHub pull request' WHERE "activityType" = 'pull_request-opened' AND platform = 'github'; +UPDATE "activityTypes" SET description = 'Requested a review on a GitHub pull request' WHERE "activityType" = 'pull_request-review-requested' AND platform = 'github'; +UPDATE "activityTypes" SET description = 'Reviewed a GitHub pull request' WHERE "activityType" = 'pull_request-reviewed' AND platform = 'github'; +UPDATE "activityTypes" SET description = 'Starred a GitHub repository' WHERE "activityType" = 'star' AND platform = 'github'; +UPDATE "activityTypes" SET description = 'Unstarred a GitHub repository' WHERE "activityType" = 'unstar' AND platform = 'github'; + +-- GitLab platform +UPDATE "activityTypes" SET description = 'Authored and pushed a commit in a merge request on GitLab' WHERE "activityType" = 'authored-commit' AND platform = 'gitlab'; +UPDATE "activityTypes" SET description = 'Forked a GitLab repository' WHERE "activityType" = 'fork' AND platform = 'gitlab'; +UPDATE "activityTypes" SET description = 'Commented on a GitLab issue' WHERE "activityType" = 'issue-comment' AND platform = 'gitlab'; +UPDATE "activityTypes" SET description = 'Closed a GitLab issue' WHERE "activityType" = 'issues-closed' AND platform = 'gitlab'; +UPDATE "activityTypes" SET description = 'Opened a GitLab issue' WHERE "activityType" = 'issues-opened' AND platform = 'gitlab'; +UPDATE "activityTypes" SET description = 'Assigned to a GitLab merge request' WHERE "activityType" = 'merge_request-assigned' AND platform = 'gitlab'; +UPDATE "activityTypes" SET description = 'Closed a GitLab merge request' WHERE "activityType" = 'merge_request-closed' AND platform = 'gitlab'; +UPDATE "activityTypes" SET description = 'Commented on a GitLab merge request' WHERE "activityType" = 'merge_request-comment' AND platform = 'gitlab'; +UPDATE "activityTypes" SET description = 'Merged a GitLab merge request' WHERE "activityType" = 'merge_request-merged' AND platform = 'gitlab'; +UPDATE "activityTypes" SET description = 'Opened a GitLab merge request' WHERE "activityType" = 'merge_request-opened' AND platform = 'gitlab'; +UPDATE "activityTypes" SET description = 'Approved a GitLab merge request review' WHERE "activityType" = 'merge_request-review-approved' AND platform = 'gitlab'; +UPDATE "activityTypes" SET description = 'Requested changes on a GitLab merge request' WHERE "activityType" = 'merge_request-review-changes-requested' AND platform = 'gitlab'; +UPDATE "activityTypes" SET description = 'Requested review on a GitLab merge request' WHERE "activityType" = 'merge_request-review-requested' AND platform = 'gitlab'; +UPDATE "activityTypes" SET description = 'Starred a GitLab repository' WHERE "activityType" = 'star' AND platform = 'gitlab'; + +-- Groups.io platform +UPDATE "activityTypes" SET description = 'Sent message in a Groups.io mailing list' WHERE "activityType" = 'message' AND platform = 'groupsio'; +UPDATE "activityTypes" SET description = 'Joined Groups.io mailing list' WHERE "activityType" = 'member_join' AND platform = 'groupsio'; +UPDATE "activityTypes" SET description = 'Left Groups.io mailing list' WHERE "activityType" = 'member_leave' AND platform = 'groupsio'; + +-- Hackernews platform +UPDATE "activityTypes" SET description = 'Commented on a Hacker News story' WHERE "activityType" = 'comment' AND platform = 'hackernews'; +UPDATE "activityTypes" SET description = 'Posted a story on Hacker News' WHERE "activityType" = 'post' AND platform = 'hackernews'; + +-- Jira platform +UPDATE "activityTypes" SET description = 'Added an assignee to a Jira issue' WHERE "activityType" = 'issue-assigned' AND platform = 'jira'; +UPDATE "activityTypes" SET description = 'Added attachment to a Jira issue' WHERE "activityType" = 'issue-attachment-added' AND platform = 'jira'; +UPDATE "activityTypes" SET description = 'Closed a Jira issue' WHERE "activityType" = 'issue-closed' AND platform = 'jira'; +UPDATE "activityTypes" SET description = 'Created comment on a Jira issue' WHERE "activityType" = 'issue-comment-created' AND platform = 'jira'; +UPDATE "activityTypes" SET description = 'Updated comment on a Jira issue' WHERE "activityType" = 'issue-comment-updated' AND platform = 'jira'; +UPDATE "activityTypes" SET description = 'Created a Jira issue' WHERE "activityType" = 'issue-created' AND platform = 'jira'; +UPDATE "activityTypes" SET description = 'Updated a Jira issue' WHERE "activityType" = 'issue-updated' AND platform = 'jira'; + +-- LinkedIn platform +UPDATE "activityTypes" SET description = 'Commented on a LinkedIn post' WHERE "activityType" = 'comment' AND platform = 'linkedin'; +UPDATE "activityTypes" SET description = 'Reacted to a LinkedIn post' WHERE "activityType" = 'reaction' AND platform = 'linkedin'; + +-- Reddit platform +UPDATE "activityTypes" SET description = 'Commented on a Reddit post' WHERE "activityType" = 'comment' AND platform = 'reddit'; +UPDATE "activityTypes" SET description = 'Posted on Reddit' WHERE "activityType" = 'post' AND platform = 'reddit'; + +-- Slack platform +UPDATE "activityTypes" SET description = 'Joined a Slack channel' WHERE "activityType" = 'channel_joined' AND platform = 'slack'; +UPDATE "activityTypes" SET description = 'Sent a message in a Slack channel' WHERE "activityType" = 'message' AND platform = 'slack'; + +-- Stack Overflow platform +UPDATE "activityTypes" SET description = 'Answered a question on Stack Overflow' WHERE "activityType" = 'answer' AND platform = 'stackoverflow'; +UPDATE "activityTypes" SET description = 'Asked a question on Stack Overflow' WHERE "activityType" = 'question' AND platform = 'stackoverflow'; + +-- Twitter platform +UPDATE "activityTypes" SET description = 'Used hashtag in a Twitter post' WHERE "activityType" = 'hashtag' AND platform = 'twitter'; +UPDATE "activityTypes" SET description = 'Mentioned user in a Twitter post' WHERE "activityType" = 'mention' AND platform = 'twitter'; +UPDATE "activityTypes" SET description = 'Followed user on Twitter' WHERE "activityType" = 'follow' AND platform = 'twitter'; + +-- Add NOT NULL constraint to description column +ALTER TABLE "activityTypes" ALTER COLUMN description SET NOT NULL; \ No newline at end of file diff --git a/backend/src/database/migrations/V1759851185__addBranchColumnToRepositories.sql b/backend/src/database/migrations/V1759851185__addBranchColumnToRepositories.sql new file mode 100644 index 0000000000..097819fd32 --- /dev/null +++ b/backend/src/database/migrations/V1759851185__addBranchColumnToRepositories.sql @@ -0,0 +1,5 @@ +ALTER TABLE git.repositories +ADD COLUMN "branch" VARCHAR(255) DEFAULT NULL; + +-- Add comment for documentation +COMMENT ON COLUMN git.repositories."branch" IS 'The default branch being tracked for this repository (e.g., main, master, develop).'; diff --git a/backend/src/database/migrations/V1760018482__add_label_to_activityTypes.sql b/backend/src/database/migrations/V1760018482__add_label_to_activityTypes.sql new file mode 100644 index 0000000000..47fcff1d70 --- /dev/null +++ b/backend/src/database/migrations/V1760018482__add_label_to_activityTypes.sql @@ -0,0 +1,108 @@ +ALTER TABLE "activityTypes" ADD COLUMN "label" VARCHAR(300) DEFAULT NULL; + +-- SQL UPDATE statements for activityTypes table +-- Generated on: 2025-10-09T15:38:09.528Z + +-- Platform: GitHub +UPDATE "activityTypes" SET "label" = 'Authored a commit' WHERE "platform" = 'github' AND "activityType" = 'authored-commit'; +UPDATE "activityTypes" SET "label" = 'Closed a pull request' WHERE "platform" = 'github' AND "activityType" = 'pull_request-closed'; +UPDATE "activityTypes" SET "label" = 'Opened a pull request' WHERE "platform" = 'github' AND "activityType" = 'pull_request-opened'; +UPDATE "activityTypes" SET "label" = 'Commented on a pull request' WHERE "platform" = 'github' AND "activityType" = 'pull_request-comment'; +UPDATE "activityTypes" SET "label" = 'Merged a pull request' WHERE "platform" = 'github' AND "activityType" = 'pull_request-merged'; +UPDATE "activityTypes" SET "label" = 'Requested a review for a pull request' WHERE "platform" = 'github' AND "activityType" = 'pull_request-review-requested'; +UPDATE "activityTypes" SET "label" = 'Commented on a pull request review thread' WHERE "platform" = 'github' AND "activityType" = 'pull_request-review-thread-comment'; +UPDATE "activityTypes" SET "label" = 'Closed an issue' WHERE "platform" = 'github' AND "activityType" = 'issues-closed'; +UPDATE "activityTypes" SET "label" = 'Opened an issue' WHERE "platform" = 'github' AND "activityType" = 'issues-opened'; +UPDATE "activityTypes" SET "label" = 'Commented on an issue' WHERE "platform" = 'github' AND "activityType" = 'issue-comment'; +UPDATE "activityTypes" SET "label" = 'Started a discussion' WHERE "platform" = 'github' AND "activityType" = 'discussion-started'; +UPDATE "activityTypes" SET "label" = 'Commented on a discussion' WHERE "platform" = 'github' AND "activityType" = 'discussion-comment'; + +-- Platform: Git +UPDATE "activityTypes" SET "label" = 'Authored a commit' WHERE "platform" = 'git' AND "activityType" = 'authored-commit'; +UPDATE "activityTypes" SET "label" = 'Reviewed a commit' WHERE "platform" = 'git' AND "activityType" = 'reviewed-commit'; +UPDATE "activityTypes" SET "label" = 'Tested a commit' WHERE "platform" = 'git' AND "activityType" = 'tested-commit'; +UPDATE "activityTypes" SET "label" = 'Co-authored a commit' WHERE "platform" = 'git' AND "activityType" = 'co-authored-commit'; +UPDATE "activityTypes" SET "label" = 'Informed a commit' WHERE "platform" = 'git' AND "activityType" = 'informed-commit'; +UPDATE "activityTypes" SET "label" = 'Influenced a commit' WHERE "platform" = 'git' AND "activityType" = 'influenced-commit'; +UPDATE "activityTypes" SET "label" = 'Approved a commit' WHERE "platform" = 'git' AND "activityType" = 'approved-commit'; +UPDATE "activityTypes" SET "label" = 'Committed a commit' WHERE "platform" = 'git' AND "activityType" = 'committed-commit'; +UPDATE "activityTypes" SET "label" = 'Reported a commit' WHERE "platform" = 'git' AND "activityType" = 'reported-commit'; +UPDATE "activityTypes" SET "label" = 'Resolved a commit' WHERE "platform" = 'git' AND "activityType" = 'resolved-commit'; +UPDATE "activityTypes" SET "label" = 'Signed off a commit' WHERE "platform" = 'git' AND "activityType" = 'signed-off-commit'; + +-- Platform: Gerrit +UPDATE "activityTypes" SET "label" = 'Created a changeset' WHERE "platform" = 'gerrit' AND "activityType" = 'changeset-created'; +UPDATE "activityTypes" SET "label" = 'Merged a changeset' WHERE "platform" = 'gerrit' AND "activityType" = 'changeset-merged'; +UPDATE "activityTypes" SET "label" = 'Closed a changeset' WHERE "platform" = 'gerrit' AND "activityType" = 'changeset-closed'; +UPDATE "activityTypes" SET "label" = 'Abandoned a changeset' WHERE "platform" = 'gerrit' AND "activityType" = 'changeset-abandoned'; +UPDATE "activityTypes" SET "label" = 'Created a changeset comment' WHERE "platform" = 'gerrit' AND "activityType" = 'changeset_comment-created'; +UPDATE "activityTypes" SET "label" = 'Created a patchset' WHERE "platform" = 'gerrit' AND "activityType" = 'patchset-created'; +UPDATE "activityTypes" SET "label" = 'Created a patchset comment' WHERE "platform" = 'gerrit' AND "activityType" = 'patchset_comment-created'; +UPDATE "activityTypes" SET "label" = 'Created a patchset approval' WHERE "platform" = 'gerrit' AND "activityType" = 'patchset_approval-created'; + +-- Platform: GitLab +UPDATE "activityTypes" SET "label" = 'Opened an issue' WHERE "platform" = 'gitlab' AND "activityType" = 'issues-opened'; +UPDATE "activityTypes" SET "label" = 'Closed an issue' WHERE "platform" = 'gitlab' AND "activityType" = 'issues-closed'; +UPDATE "activityTypes" SET "label" = 'Closed a merge request' WHERE "platform" = 'gitlab' AND "activityType" = 'merge_request-closed'; +UPDATE "activityTypes" SET "label" = 'Opened a merge request' WHERE "platform" = 'gitlab' AND "activityType" = 'merge_request-opened'; +UPDATE "activityTypes" SET "label" = 'Commented on a merge request review thread' WHERE "platform" = 'gitlab' AND "activityType" = 'merge_request-review-thread-comment'; +UPDATE "activityTypes" SET "label" = 'Merged a merge request' WHERE "platform" = 'gitlab' AND "activityType" = 'merge_request-merged'; +UPDATE "activityTypes" SET "label" = 'Commented on a merge request' WHERE "platform" = 'gitlab' AND "activityType" = 'merge_request-comment'; +UPDATE "activityTypes" SET "label" = 'Commented on an issue' WHERE "platform" = 'gitlab' AND "activityType" = 'issue-comment'; +UPDATE "activityTypes" SET "label" = 'Authored a commit' WHERE "platform" = 'gitlab' AND "activityType" = 'authored-commit'; + +-- Platform: Groups.io +UPDATE "activityTypes" SET "label" = 'Sent a message' WHERE "platform" = 'groupsio' AND "activityType" = 'message'; + +-- Platform: Confluence +UPDATE "activityTypes" SET "label" = 'Created a page' WHERE "platform" = 'confluence' AND "activityType" = 'page-created'; +UPDATE "activityTypes" SET "label" = 'Updated a page' WHERE "platform" = 'confluence' AND "activityType" = 'page-updated'; +UPDATE "activityTypes" SET "label" = 'Created a comment' WHERE "platform" = 'confluence' AND "activityType" = 'comment-created'; +UPDATE "activityTypes" SET "label" = 'Created an attachment' WHERE "platform" = 'confluence' AND "activityType" = 'attachment-created'; +UPDATE "activityTypes" SET "label" = 'Created a blog post' WHERE "platform" = 'confluence' AND "activityType" = 'blogpost-created'; +UPDATE "activityTypes" SET "label" = 'Updated a blog post' WHERE "platform" = 'confluence' AND "activityType" = 'blogpost-updated'; +UPDATE "activityTypes" SET "label" = 'Attached a file' WHERE "platform" = 'confluence' AND "activityType" = 'attachment'; +UPDATE "activityTypes" SET "label" = 'Commented on a page' WHERE "platform" = 'confluence' AND "activityType" = 'comment'; + +-- Platform: Jira +UPDATE "activityTypes" SET "label" = 'Created an issue' WHERE "platform" = 'jira' AND "activityType" = 'issue-created'; +UPDATE "activityTypes" SET "label" = 'Closed an issue' WHERE "platform" = 'jira' AND "activityType" = 'issues-closed'; +UPDATE "activityTypes" SET "label" = 'Assigned an issue' WHERE "platform" = 'jira' AND "activityType" = 'issue-assigned'; +UPDATE "activityTypes" SET "label" = 'Updated an issue' WHERE "platform" = 'jira' AND "activityType" = 'issue-updated'; +UPDATE "activityTypes" SET "label" = 'Created an issue comment' WHERE "platform" = 'jira' AND "activityType" = 'issue-comment-created'; +UPDATE "activityTypes" SET "label" = 'Updated an issue comment' WHERE "platform" = 'jira' AND "activityType" = 'issue-comment-updated'; +UPDATE "activityTypes" SET "label" = 'Added an attachment to an issue' WHERE "platform" = 'jira' AND "activityType" = 'issue-attachment-added'; + +-- Platform: Dev.to +UPDATE "activityTypes" SET "label" = 'Commented on a post' WHERE "platform" = 'devto' AND "activityType" = 'comment'; + +-- Platform: Discord +UPDATE "activityTypes" SET "label" = 'Sent a message' WHERE "platform" = 'discord' AND "activityType" = 'message'; +UPDATE "activityTypes" SET "label" = 'Started a thread' WHERE "platform" = 'discord' AND "activityType" = 'thread-started'; +UPDATE "activityTypes" SET "label" = 'Sent a message in a thread' WHERE "platform" = 'discord' AND "activityType" = 'thread-message'; + +-- Platform: Discourse +UPDATE "activityTypes" SET "label" = 'Created a topic' WHERE "platform" = 'discourse' AND "activityType" = 'create-topic'; +UPDATE "activityTypes" SET "label" = 'Sent a message in a topic' WHERE "platform" = 'discourse' AND "activityType" = 'message-in-topic'; + +-- Platform: Hacker News +UPDATE "activityTypes" SET "label" = 'Posted a post' WHERE "platform" = 'hackernews' AND "activityType" = 'post'; +UPDATE "activityTypes" SET "label" = 'Commented on a post' WHERE "platform" = 'hackernews' AND "activityType" = 'comment'; + +-- Platform: LinkedIn +UPDATE "activityTypes" SET "label" = 'Commented on a post' WHERE "platform" = 'linkedin' AND "activityType" = 'comment'; + +-- Platform: Reddit +UPDATE "activityTypes" SET "label" = 'Posted a post' WHERE "platform" = 'reddit' AND "activityType" = 'post'; +UPDATE "activityTypes" SET "label" = 'Commented on a post' WHERE "platform" = 'reddit' AND "activityType" = 'comment'; + +-- Platform: Slack +UPDATE "activityTypes" SET "label" = 'Sent a message' WHERE "platform" = 'slack' AND "activityType" = 'message'; + +-- Platform: Stack Overflow +UPDATE "activityTypes" SET "label" = 'Asked a question' WHERE "platform" = 'stackoverflow' AND "activityType" = 'question'; +UPDATE "activityTypes" SET "label" = 'Answered a question' WHERE "platform" = 'stackoverflow' AND "activityType" = 'answer'; + +-- Platform: X/Twitter +UPDATE "activityTypes" SET "label" = 'Used a hashtag' WHERE "platform" = 'twitter' AND "activityType" = 'hashtag'; +UPDATE "activityTypes" SET "label" = 'Mentioned a user' WHERE "platform" = 'twitter' AND "activityType" = 'mention'; diff --git a/backend/src/database/migrations/V1760357098__add_missing_labels_in_activityTypes.sql b/backend/src/database/migrations/V1760357098__add_missing_labels_in_activityTypes.sql new file mode 100644 index 0000000000..209c437f1d --- /dev/null +++ b/backend/src/database/migrations/V1760357098__add_missing_labels_in_activityTypes.sql @@ -0,0 +1,32 @@ +UPDATE "activityTypes" SET label = 'Joined a server/guild' WHERE "activityType" = 'joined_guild' AND platform = 'discord'; +UPDATE "activityTypes" SET label = 'Sent a message in a thread' WHERE "activityType" = 'thread_message' AND platform = 'discord'; +UPDATE "activityTypes" SET label = 'Started a thread' WHERE "activityType" = 'thread_started' AND platform = 'discord'; + +UPDATE "activityTypes" SET label = 'Liked a post' WHERE "activityType" = 'like' AND platform = 'discourse'; +UPDATE "activityTypes" SET label = 'Joined a forum' WHERE "activityType" = 'join' AND platform = 'discourse'; +UPDATE "activityTypes" SET label = 'Created a topic' WHERE "activityType" = 'create_topic' AND platform = 'discourse'; +UPDATE "activityTypes" SET label = 'Sent a message in topic' WHERE "activityType" = 'message_in_topic' AND platform = 'discourse'; + +UPDATE "activityTypes" SET label = 'Forked a repository' WHERE "activityType" = 'fork' AND platform = 'github'; +UPDATE "activityTypes" SET label = 'Assigned to a pull request' WHERE "activityType" = 'pull_request-assigned' AND platform = 'github'; +UPDATE "activityTypes" SET label = 'Reviewed a pull request' WHERE "activityType" = 'pull_request-reviewed' AND platform = 'github'; +UPDATE "activityTypes" SET label = 'Starred a repository' WHERE "activityType" = 'star' AND platform = 'github'; +UPDATE "activityTypes" SET label = 'Unstarred a repository' WHERE "activityType" = 'unstar' AND platform = 'github'; + +UPDATE "activityTypes" SET label = 'Forked a repository' WHERE "activityType" = 'fork' AND platform = 'gitlab'; +UPDATE "activityTypes" SET label = 'Assigned to a merge request' WHERE "activityType" = 'merge_request-assigned' AND platform = 'gitlab'; +UPDATE "activityTypes" SET label = 'Approved a merge request review' WHERE "activityType" = 'merge_request-review-approved' AND platform = 'gitlab'; +UPDATE "activityTypes" SET label = 'Requested changes to a merge request' WHERE "activityType" = 'merge_request-review-changes-requested' AND platform = 'gitlab'; +UPDATE "activityTypes" SET label = 'Requested a review on a merge request' WHERE "activityType" = 'merge_request-review-requested' AND platform = 'gitlab'; +UPDATE "activityTypes" SET label = 'Starred a repository' WHERE "activityType" = 'star' AND platform = 'gitlab'; + +UPDATE "activityTypes" SET label = 'Joined a mailing list' WHERE "activityType" = 'member_join' AND platform = 'groupsio'; +UPDATE "activityTypes" SET label = 'Left a mailing list' WHERE "activityType" = 'member_leave' AND platform = 'groupsio'; + +UPDATE "activityTypes" SET label = 'Closed an issue' WHERE "activityType" = 'issue-closed' AND platform = 'jira'; + +UPDATE "activityTypes" SET label = 'Reacted to a post' WHERE "activityType" = 'reaction' AND platform = 'linkedin'; + +UPDATE "activityTypes" SET label = 'Joined a channel' WHERE "activityType" = 'channel_joined' AND platform = 'slack'; + +UPDATE "activityTypes" SET label = 'Followed a user' WHERE "activityType" = 'follow' AND platform = 'twitter'; diff --git a/backend/src/database/migrations/V1760530860__addMetricsToServiceExecution.sql b/backend/src/database/migrations/V1760530860__addMetricsToServiceExecution.sql new file mode 100644 index 0000000000..a174abf39a --- /dev/null +++ b/backend/src/database/migrations/V1760530860__addMetricsToServiceExecution.sql @@ -0,0 +1,10 @@ +-- Add metrics column to serviceExecutions table for storing service-specific execution metrics +-- Examples: ai_cost for maintainer service, total_commits/bad_commits/total_activities for commit service + +ALTER TABLE git."serviceExecutions" +ADD COLUMN metrics JSONB DEFAULT '{}'::jsonb; + +-- Create GIN index for efficient querying within JSONB data +CREATE INDEX IF NOT EXISTS "idx_serviceExecutions_metrics" +ON git."serviceExecutions" USING gin (metrics); + diff --git a/backend/src/database/migrations/V1760602430__alter_organizationEnrichmentCache_data_nullable.sql b/backend/src/database/migrations/V1760602430__alter_organizationEnrichmentCache_data_nullable.sql new file mode 100644 index 0000000000..2a219462bb --- /dev/null +++ b/backend/src/database/migrations/V1760602430__alter_organizationEnrichmentCache_data_nullable.sql @@ -0,0 +1,2 @@ +alter table "organizationEnrichmentCache" + alter column "data" drop not null; \ No newline at end of file diff --git a/backend/src/database/migrations/V1760611613__addFivetranTablesToSequin.sql b/backend/src/database/migrations/V1760611613__addFivetranTablesToSequin.sql new file mode 100644 index 0000000000..503c925300 --- /dev/null +++ b/backend/src/database/migrations/V1760611613__addFivetranTablesToSequin.sql @@ -0,0 +1,11 @@ +ALTER PUBLICATION sequin_pub ADD TABLE "githubRepos"; +ALTER TABLE public."githubRepos" REPLICA IDENTITY FULL; +GRANT SELECT ON "githubRepos" to sequin; + +ALTER PUBLICATION sequin_pub ADD TABLE "memberOrganizationAffiliationOverrides"; +ALTER TABLE public."memberOrganizationAffiliationOverrides" REPLICA IDENTITY FULL; +GRANT SELECT ON "memberOrganizationAffiliationOverrides" to sequin; + +ALTER PUBLICATION sequin_pub ADD TABLE "memberOrganizations"; +ALTER TABLE public."memberOrganizations" REPLICA IDENTITY FULL; +GRANT SELECT ON "memberOrganizations" to sequin; diff --git a/backend/src/database/migrations/V1760976799__addForkedFromToRepositories.sql b/backend/src/database/migrations/V1760976799__addForkedFromToRepositories.sql new file mode 100644 index 0000000000..ff3feed6fc --- /dev/null +++ b/backend/src/database/migrations/V1760976799__addForkedFromToRepositories.sql @@ -0,0 +1,6 @@ +ALTER TABLE git.repositories +ADD COLUMN "forkedFrom" VARCHAR(512) DEFAULT NULL; + +-- Add comment for documentation +COMMENT ON COLUMN git.repositories."forkedFrom" IS 'The source repository URL if this repository is a fork (e.g., https://github.com/original-owner/original-repo).'; + diff --git a/backend/src/database/migrations/V1762773409__integrations-backup.sql b/backend/src/database/migrations/V1762773409__integrations-backup.sql new file mode 100644 index 0000000000..7125accdbf --- /dev/null +++ b/backend/src/database/migrations/V1762773409__integrations-backup.sql @@ -0,0 +1,10 @@ +create table "integrationsBackup" as +select * +from "integrations" +where 1 = 2; + +alter table "integrationsBackup" + add column "backupCreatedAt" timestamptz not null default now(); + +create index if not exists ix_integration_history_integration_id on "integrationsBackup" ("id"); +create index if not exists ix_integration_history_history_created_at on "integrationsBackup" ("backupCreatedAt"); \ No newline at end of file diff --git a/backend/src/database/migrations/V1762789440__optimize_member_api.sql b/backend/src/database/migrations/V1762789440__optimize_member_api.sql new file mode 100644 index 0000000000..54ab5377dd --- /dev/null +++ b/backend/src/database/migrations/V1762789440__optimize_member_api.sql @@ -0,0 +1,12 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_msa_segment_activitycount_desc_member +ON public."memberSegmentsAgg" ("segmentId", "activityCount" DESC, "memberId"); + +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_members_displayname_trgm +ON public."members" +USING gin (LOWER("displayName") gin_trgm_ops); + +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_memberidentities_email_verified_trgm +ON public."memberIdentities" +USING gin (LOWER("value") gin_trgm_ops) +WHERE verified = true + AND type = 'email'; \ No newline at end of file diff --git a/backend/src/database/migrations/V1762938833__migrateMaintainersRepoReferenceToGitRepositories.sql b/backend/src/database/migrations/V1762938833__migrateMaintainersRepoReferenceToGitRepositories.sql new file mode 100644 index 0000000000..2beb1fa33b --- /dev/null +++ b/backend/src/database/migrations/V1762938833__migrateMaintainersRepoReferenceToGitRepositories.sql @@ -0,0 +1,11 @@ +-- Migration to remove the foreign key constraint from maintainersInternal.repoId to githubRepos.id +-- This removes the dependency on githubRepos table and creates the new reference to git.repositories table + +ALTER TABLE "maintainersInternal" +DROP CONSTRAINT IF EXISTS "maintainersInternal_repoId_fkey"; + +ALTER TABLE "maintainersInternal" +ADD CONSTRAINT "maintainersInternal_repoId_fkey" +FOREIGN KEY ("repoId") +REFERENCES git.repositories(id) +ON DELETE CASCADE; \ No newline at end of file diff --git a/backend/src/database/migrations/V1763654470__optimize_organization_api.sql b/backend/src/database/migrations/V1763654470__optimize_organization_api.sql new file mode 100644 index 0000000000..564869cf92 --- /dev/null +++ b/backend/src/database/migrations/V1763654470__optimize_organization_api.sql @@ -0,0 +1,9 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_osa_segment_coalesce_activity_org +ON public."organizationSegmentsAgg" ( + "segmentId", + (coalesce("activityCount", 0)::integer) DESC, + "organizationId" +); + +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_org_displayname_trgm +ON organizations USING gin ("displayName" gin_trgm_ops); \ No newline at end of file diff --git a/backend/src/database/migrations/V1764675824__addStuckReOnboardColumnToGitRepositories.sql b/backend/src/database/migrations/V1764675824__addStuckReOnboardColumnToGitRepositories.sql new file mode 100644 index 0000000000..fd2226a9bb --- /dev/null +++ b/backend/src/database/migrations/V1764675824__addStuckReOnboardColumnToGitRepositories.sql @@ -0,0 +1,9 @@ +-- Add stuckRequiresReOnboard column to git.repositories table +-- This column indicates if a repository is stuck and requires re-onboarding + +ALTER TABLE git.repositories +ADD COLUMN "stuckRequiresReOnboard" BOOLEAN NOT NULL DEFAULT FALSE; + +-- Add comment for documentation +COMMENT ON COLUMN git.repositories."stuckRequiresReOnboard" IS 'Indicates if the stuck repository is resolved by a re-onboarding'; + diff --git a/backend/src/database/migrations/V1764931554__create_dashboard_metrics_sink.sql b/backend/src/database/migrations/V1764931554__create_dashboard_metrics_sink.sql new file mode 100644 index 0000000000..e584021e92 --- /dev/null +++ b/backend/src/database/migrations/V1764931554__create_dashboard_metrics_sink.sql @@ -0,0 +1,11 @@ +CREATE TABLE public."dashboardMetricsTotalSnapshot" ( + id VARCHAR(50) PRIMARY KEY DEFAULT 'snapshot', + + "activitiesTotal" BIGINT, + "activitiesLast30Days" BIGINT, + "organizationsTotal" BIGINT, + "organizationsLast30Days" BIGINT, + "membersTotal" BIGINT, + "membersLast30Days" BIGINT, + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW() +); \ No newline at end of file diff --git a/backend/src/database/migrations/V1765185575__addReOnboardingCountToGitRepos.sql b/backend/src/database/migrations/V1765185575__addReOnboardingCountToGitRepos.sql new file mode 100644 index 0000000000..6fa85ddc33 --- /dev/null +++ b/backend/src/database/migrations/V1765185575__addReOnboardingCountToGitRepos.sql @@ -0,0 +1,7 @@ +-- Add reOnboardingCount column to git.repositories table +-- This column tracks the number of times a repository has been re-onboarded. +-- It is used to identify unreachable commits by matching against activity.attributes.cycle=onboarding-{reOnboardingCount} +ALTER TABLE git.repositories +ADD COLUMN "reOnboardingCount" INTEGER default 0 NOT NULL; + +COMMENT ON COLUMN git.repositories."reOnboardingCount" IS 'Tracks the number of times this repository has been re-onboarded. Used to identify unreachable commits via activity.attributes.cycle matching pattern onboarding-{reOnboardingCount}'; \ No newline at end of file diff --git a/backend/src/database/migrations/V1765279142__optimize-segments-agg-tables.sql b/backend/src/database/migrations/V1765279142__optimize-segments-agg-tables.sql new file mode 100644 index 0000000000..a5597dfe21 --- /dev/null +++ b/backend/src/database/migrations/V1765279142__optimize-segments-agg-tables.sql @@ -0,0 +1,46 @@ +-- Drop the existing primary key (id column) +ALTER TABLE "memberSegmentsAgg" DROP CONSTRAINT "memberSegmentsAgg_pkey"; + +-- Drop the id column +ALTER TABLE "memberSegmentsAgg" DROP COLUMN id; + +-- Drop the existing unique constraint (we'll make it the primary key) +ALTER TABLE "memberSegmentsAgg" DROP CONSTRAINT "memberSegmentsAgg_memberId_segmentId_key"; + +-- Add composite primary key (segmentId first for segment-based queries) +ALTER TABLE "memberSegmentsAgg" ADD PRIMARY KEY ("segmentId", "memberId"); + +-- Drop redundant indexes (covered by primary key which starts with segmentId) +DROP INDEX IF EXISTS member_segments_agg_segment_member; +DROP INDEX IF EXISTS member_segments_agg_segment_id; + +-- Add index on memberId for queries that filter by member only +CREATE INDEX idx_msa_member_id + ON "memberSegmentsAgg" ("memberId"); + +-- Drop the existing primary key (id column) +ALTER TABLE "organizationSegmentsAgg" DROP CONSTRAINT "organizationSegmentsAgg_pkey"; + +-- Drop the id column +ALTER TABLE "organizationSegmentsAgg" DROP COLUMN id; + +-- Drop the existing unique constraint (we'll make it the primary key) +ALTER TABLE "organizationSegmentsAgg" DROP CONSTRAINT "organizationSegmentsAgg_organizationId_segmentId_key"; + +-- Add composite primary key (segmentId first for segment-based queries) +ALTER TABLE "organizationSegmentsAgg" ADD PRIMARY KEY ("segmentId", "organizationId"); + +-- Drop redundant indexes (covered by primary key which starts with segmentId) +DROP INDEX IF EXISTS organization_segments_agg_segment_id; +DROP INDEX IF EXISTS idx_osa_segment_coalesce_activity_org; + +-- Add index on organizationId for queries that filter by organization only +CREATE INDEX idx_osa_organization_id + ON "organizationSegmentsAgg" ("organizationId"); + +-- Create index for segment + activity count ordering (commonly used in queries) +CREATE INDEX idx_org_segments_agg_segment_activity + ON "organizationSegmentsAgg" ("segmentId", "activityCount" DESC, "organizationId"); + +-- Drop indexed_entities table (no longer needed with new sync approach) +DROP TABLE IF EXISTS indexed_entities; diff --git a/backend/src/database/migrations/V1765381396__create_dashboard_metrics_per_segments_sink.sql b/backend/src/database/migrations/V1765381396__create_dashboard_metrics_per_segments_sink.sql new file mode 100644 index 0000000000..f94533ad5a --- /dev/null +++ b/backend/src/database/migrations/V1765381396__create_dashboard_metrics_per_segments_sink.sql @@ -0,0 +1,10 @@ +CREATE TABLE public."dashboardMetricsPerSegmentSnapshot" ( + "segmentId" UUID PRIMARY KEY, + "activitiesTotal" BIGINT, + "activitiesLast30Days" BIGINT, + "organizationsTotal" BIGINT, + "organizationsLast30Days" BIGINT, + "membersTotal" BIGINT, + "membersLast30Days" BIGINT, + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW() +); \ No newline at end of file diff --git a/backend/src/database/migrations/V1765984292__updateInsightsProjectsWidgets.sql b/backend/src/database/migrations/V1765984292__updateInsightsProjectsWidgets.sql new file mode 100644 index 0000000000..55bf36daa3 --- /dev/null +++ b/backend/src/database/migrations/V1765984292__updateInsightsProjectsWidgets.sql @@ -0,0 +1,80 @@ +-- Migration to update insights projects widgets +-- 1. Remove deprecated widgets: waitTimeFor1stReview and averageTimeToMerge +-- 2. Remove GitHub/GitLab-only widgets from projects without those platforms +-- 3. Add new widgets to projects with GitHub, GitLab, or Gerrit +-- 4. Add patchsetPerReview to projects with Gerrit + +BEGIN; + +-- Step 1: Remove deprecated widgets from all projects +UPDATE "insightsProjects" ip +SET + widgets = array_remove(array_remove(ip.widgets, 'waitTimeFor1stReview'), 'averageTimeToMerge'), + "updatedAt" = CURRENT_TIMESTAMP +WHERE + ip.widgets && ARRAY['waitTimeFor1stReview', 'averageTimeToMerge']; + +-- Step 2: Remove widgets from projects that don't have the required platforms +-- Remove pullRequests, mergeLeadTime, reviewTimeByPullRequestSize, codeReviewEngagement +-- from projects that are NOT connected to GitHub or GitLab +UPDATE "insightsProjects" ip +SET + widgets = ( + SELECT COALESCE(array_agg(w), ARRAY[]::TEXT[]) + FROM unnest(ip.widgets) AS w + WHERE w NOT IN ( + 'pullRequests', + 'mergeLeadTime', + 'reviewTimeByPullRequestSize', + 'codeReviewEngagement' + ) + ), + "updatedAt" = CURRENT_TIMESTAMP +WHERE NOT EXISTS ( + SELECT 1 + FROM integrations i + WHERE i."segmentId" = ip."segmentId" + AND i.platform IN ('github', 'github-nango', 'gitlab') + AND i."deletedAt" IS NULL +) +AND ip.widgets && ARRAY['pullRequests', 'mergeLeadTime', 'reviewTimeByPullRequestSize', 'codeReviewEngagement']; + +-- Step 3: Add new widgets to projects connected to GitHub, GitLab, or Gerrit +-- Adds: reviewEfficiency, medianTimeToClose, medianTimeToReview +UPDATE "insightsProjects" ip +SET widgets = ( + SELECT ARRAY( + SELECT DISTINCT unnest( + ip.widgets || + ARRAY['reviewEfficiency', 'medianTimeToClose', 'medianTimeToReview'] + ) + ) +), +"updatedAt" = CURRENT_TIMESTAMP +WHERE EXISTS ( + SELECT 1 + FROM integrations i + WHERE i."segmentId" = ip."segmentId" + AND i.platform IN ('github', 'github-nango', 'gitlab', 'gerrit') + AND i."deletedAt" IS NULL +) +AND NOT (ip.widgets @> ARRAY['reviewEfficiency', 'medianTimeToClose', 'medianTimeToReview']); + +-- Step 4: Add patchsetPerReview to projects connected to Gerrit +UPDATE "insightsProjects" ip +SET widgets = ( + SELECT ARRAY( + SELECT DISTINCT unnest(ip.widgets || ARRAY['patchsetPerReview']) + ) +), +"updatedAt" = CURRENT_TIMESTAMP +WHERE EXISTS ( + SELECT 1 + FROM integrations i + WHERE i."segmentId" = ip."segmentId" + AND i.platform = 'gerrit' + AND i."deletedAt" IS NULL +) +AND NOT (ip.widgets @> ARRAY['patchsetPerReview']); + +COMMIT; \ No newline at end of file diff --git a/backend/src/database/migrations/V1766051317__contributorWidgetEnable.sql b/backend/src/database/migrations/V1766051317__contributorWidgetEnable.sql new file mode 100644 index 0000000000..bf100f668b --- /dev/null +++ b/backend/src/database/migrations/V1766051317__contributorWidgetEnable.sql @@ -0,0 +1,38 @@ +-- Migration to enable contributor widgets for all projects +-- Adds: activeContributors, activeOrganization, contributorsLeaderboard, organizationsLeaderboard, +-- contributorDependency, organizationDependency, retention, geographicalDistribution + +BEGIN; + +-- Add all contributor widgets to all projects that don't already have them +UPDATE "insightsProjects" ip +SET widgets = ( + SELECT ARRAY( + SELECT DISTINCT unnest( + ip.widgets || + ARRAY[ + 'activeContributors', + 'activeOrganization', + 'contributorsLeaderboard', + 'organizationsLeaderboard', + 'contributorDependency', + 'organizationDependency', + 'retention', + 'geographicalDistribution' + ] + ) + ) +), +"updatedAt" = CURRENT_TIMESTAMP +WHERE NOT (ip.widgets @> ARRAY[ + 'activeContributors', + 'activeOrganization', + 'contributorsLeaderboard', + 'organizationsLeaderboard', + 'contributorDependency', + 'organizationDependency', + 'retention', + 'geographicalDistribution' +]); + +COMMIT; \ No newline at end of file diff --git a/backend/src/database/migrations/V1766661879__createRepositoriesTable.sql b/backend/src/database/migrations/V1766661879__createRepositoriesTable.sql new file mode 100644 index 0000000000..f6f60e46e5 --- /dev/null +++ b/backend/src/database/migrations/V1766661879__createRepositoriesTable.sql @@ -0,0 +1,27 @@ +CREATE TABLE public.repositories( + id UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + "url" VARCHAR(1024) NOT NULL UNIQUE, + "segmentId" UUID NOT NULL REFERENCES "segments"(id) ON DELETE CASCADE, + "gitIntegrationId" UUID NOT NULL REFERENCES public."integrations" (id) ON DELETE CASCADE, + "sourceIntegrationId" UUID NOT NULL REFERENCES public."integrations" (id) ON DELETE CASCADE, + "insightsProjectId" UUID NOT NULL REFERENCES "insightsProjects"(id) ON DELETE CASCADE, + "archived" BOOLEAN NOT NULL DEFAULT FALSE, + "forkedFrom" VARCHAR(1024) DEFAULT NULL, + "excluded" BOOLEAN NOT NULL DEFAULT FALSE, + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "deletedAt" TIMESTAMP WITH TIME ZONE, + "lastArchivedCheckAt" TIMESTAMP WITH TIME ZONE DEFAULT NULL +); + +CREATE INDEX ix_repositories_segmentId + ON repositories ("segmentId") + WHERE "deletedAt" IS NULL; + +CREATE INDEX ix_repositories_sourceIntegrationId + ON repositories ("sourceIntegrationId") + WHERE "deletedAt" IS NULL; + +CREATE INDEX ix_repositories_insightsProjectId + ON repositories ("insightsProjectId") + WHERE "deletedAt" IS NULL; diff --git a/backend/src/database/migrations/V1766669430__createGitRepositoryProcessingTable.sql b/backend/src/database/migrations/V1766669430__createGitRepositoryProcessingTable.sql new file mode 100644 index 0000000000..af2e052cb9 --- /dev/null +++ b/backend/src/database/migrations/V1766669430__createGitRepositoryProcessingTable.sql @@ -0,0 +1,29 @@ +CREATE TABLE git."repositoryProcessing" ( + "id" UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + + "repositoryId" UUID NOT NULL UNIQUE REFERENCES public.repositories ("id") ON DELETE CASCADE, + "branch" VARCHAR(255) DEFAULT NULL, + "state" VARCHAR(50) NOT NULL DEFAULT 'pending', + "priority" INTEGER NOT NULL DEFAULT 1, + "lockedAt" TIMESTAMP WITH TIME ZONE DEFAULT NULL, + "lastProcessedAt" TIMESTAMP WITH TIME ZONE DEFAULT NULL, + "lastProcessedCommit" VARCHAR(64) DEFAULT NULL, + "maintainerFile" VARCHAR(255) DEFAULT NULL, + "lastMaintainerRunAt" TIMESTAMP WITH TIME ZONE DEFAULT NULL, + "stuckRequiresReOnboard" BOOLEAN NOT NULL DEFAULT FALSE, + "reOnboardingCount" INTEGER NOT NULL DEFAULT 0 +); + +CREATE INDEX "ix_git_repositoryProcessing_onboarding_acquisition" + ON git."repositoryProcessing" ("state", "lockedAt", "priority", "createdAt") + WHERE "state" = 'pending' AND "lockedAt" IS NULL; + +CREATE INDEX "ix_git_repositoryProcessing_recurrent_acquisition" + ON git."repositoryProcessing" ("state", "lockedAt", "lastProcessedAt", "priority") + WHERE "lockedAt" IS NULL; + +CREATE INDEX "ix_git_repositoryProcessing_active_onboarding_count" + ON git."repositoryProcessing" ("state") + WHERE "state" = 'processing' AND "lastProcessedCommit" IS NULL; diff --git a/backend/src/database/migrations/V1768468017__add_fk_to_aggs_tables.sql b/backend/src/database/migrations/V1768468017__add_fk_to_aggs_tables.sql new file mode 100644 index 0000000000..1161e3d9d3 --- /dev/null +++ b/backend/src/database/migrations/V1768468017__add_fk_to_aggs_tables.sql @@ -0,0 +1,11 @@ +ALTER TABLE public."memberSegmentsAgg" +ADD CONSTRAINT "memberSegmentsAgg_memberId_fkey" +FOREIGN KEY ("memberId") +REFERENCES public."members"(id) +ON DELETE CASCADE; + +ALTER TABLE public."organizationSegmentsAgg" +ADD CONSTRAINT "organizationSegmentsAgg_organizationId_fkey" +FOREIGN KEY ("organizationId") +REFERENCES public."organizations"(id) +ON DELETE CASCADE; diff --git a/backend/src/database/migrations/V1768471840__addRepositoriesToSequin.sql b/backend/src/database/migrations/V1768471840__addRepositoriesToSequin.sql new file mode 100644 index 0000000000..0580d78a31 --- /dev/null +++ b/backend/src/database/migrations/V1768471840__addRepositoriesToSequin.sql @@ -0,0 +1,2 @@ +ALTER PUBLICATION sequin_pub ADD TABLE "repositories"; +ALTER TABLE public."repositories" REPLICA IDENTITY DEFAULT; \ No newline at end of file diff --git a/backend/src/database/migrations/V1768919787__add-deletedAt-collectionsInsightsProjects.sql b/backend/src/database/migrations/V1768919787__add-deletedAt-collectionsInsightsProjects.sql new file mode 100644 index 0000000000..6991597e82 --- /dev/null +++ b/backend/src/database/migrations/V1768919787__add-deletedAt-collectionsInsightsProjects.sql @@ -0,0 +1,4 @@ +-- Migration to add deletedAt column to collectionsInsightsProjects table for soft delete functionality + +ALTER TABLE public."collectionsInsightsProjects" +ADD COLUMN "deletedAt" timestamp with time zone NULL DEFAULT NULL; diff --git a/backend/src/database/migrations/V1768993579__add_is_affiliation_blocked_to_organizations.sql b/backend/src/database/migrations/V1768993579__add_is_affiliation_blocked_to_organizations.sql new file mode 100644 index 0000000000..fdde6e4f85 --- /dev/null +++ b/backend/src/database/migrations/V1768993579__add_is_affiliation_blocked_to_organizations.sql @@ -0,0 +1,3 @@ +alter table "organizations" + add column "isAffiliationBlocked" boolean not null default false; + \ No newline at end of file diff --git a/backend/src/database/migrations/V1769013737__add-deletedAt-in-constraint.sql b/backend/src/database/migrations/V1769013737__add-deletedAt-in-constraint.sql new file mode 100644 index 0000000000..c55541c9f2 --- /dev/null +++ b/backend/src/database/migrations/V1769013737__add-deletedAt-in-constraint.sql @@ -0,0 +1,10 @@ +-- Fix unique constraint for collectionsInsightsProjects to exclude soft-deleted rows + +-- Drop the existing unique constraint +ALTER TABLE public."collectionsInsightsProjects" +DROP CONSTRAINT IF EXISTS "collectionsInsightsProjects_collectionId_insightsProjectId_key"; + +-- Create a partial unique index that only applies to non-deleted rows +CREATE UNIQUE INDEX IF NOT EXISTS "collectionsInsightsProjects_unique_active" +ON public."collectionsInsightsProjects" ("collectionId", "insightsProjectId") +WHERE "deletedAt" IS NULL; \ No newline at end of file diff --git a/backend/src/database/migrations/V1769092368__changeMaintainersRepoReferenceToRepositories.sql b/backend/src/database/migrations/V1769092368__changeMaintainersRepoReferenceToRepositories.sql new file mode 100644 index 0000000000..c9c86fa3f5 --- /dev/null +++ b/backend/src/database/migrations/V1769092368__changeMaintainersRepoReferenceToRepositories.sql @@ -0,0 +1,9 @@ + +ALTER TABLE "maintainersInternal" +DROP CONSTRAINT IF EXISTS "maintainersInternal_repoId_fkey"; + +ALTER TABLE "maintainersInternal" +ADD CONSTRAINT "maintainersInternal_repoId_fkey" +FOREIGN KEY ("repoId") +REFERENCES repositories(id) +ON DELETE CASCADE; \ No newline at end of file diff --git a/backend/src/database/migrations/V1769097489__changeGitServiceExecutionRefreneceToRepositories.sql b/backend/src/database/migrations/V1769097489__changeGitServiceExecutionRefreneceToRepositories.sql new file mode 100644 index 0000000000..510b6fbd5a --- /dev/null +++ b/backend/src/database/migrations/V1769097489__changeGitServiceExecutionRefreneceToRepositories.sql @@ -0,0 +1,8 @@ +-- Drop the existing foreign key constraint to git.repositories +ALTER TABLE git."serviceExecutions" + DROP CONSTRAINT "serviceExecutions_repoId_fkey"; + +-- Add new foreign key constraint to public.repositories +ALTER TABLE git."serviceExecutions" + ADD CONSTRAINT "serviceExecutions_repoId_fkey" + FOREIGN KEY ("repoId") REFERENCES public.repositories(id) ON DELETE CASCADE; diff --git a/backend/src/database/migrations/V1769438813__addEnabledToRepositories.sql b/backend/src/database/migrations/V1769438813__addEnabledToRepositories.sql new file mode 100644 index 0000000000..a2d125586b --- /dev/null +++ b/backend/src/database/migrations/V1769438813__addEnabledToRepositories.sql @@ -0,0 +1,2 @@ +ALTER TABLE public.repositories ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT TRUE; +COMMENT ON COLUMN public.repositories.enabled IS 'Used to enable/disable repository on insights'; diff --git a/backend/src/database/migrations/V1769615410__remove-aggs-fk.sql b/backend/src/database/migrations/V1769615410__remove-aggs-fk.sql new file mode 100644 index 0000000000..ddccc0cdac --- /dev/null +++ b/backend/src/database/migrations/V1769615410__remove-aggs-fk.sql @@ -0,0 +1,25 @@ +-- Remove foreign key constraints from memberSegmentsAgg and organizationSegmentsAgg tables + +-- Remove foreign key constraint from memberSegmentsAgg table +ALTER TABLE public."memberSegmentsAgg" + DROP CONSTRAINT IF EXISTS "memberSegmentsAgg_memberId_fkey"; + +-- Remove foreign key constraint from organizationSegmentsAgg table +ALTER TABLE public."organizationSegmentsAgg" + DROP CONSTRAINT IF EXISTS "organizationSegmentsAgg_organizationId_fkey"; + +-- Create table to track orphan cleanup operations +CREATE TABLE IF NOT EXISTS public."orphanSegmentAggsCleanupRuns" ( + "id" uuid DEFAULT uuid_generate_v4() NOT NULL, + "tableName" VARCHAR(255) NOT NULL, + "startedAt" TIMESTAMPTZ NOT NULL, + "completedAt" TIMESTAMPTZ, + "status" VARCHAR(50) NOT NULL, -- 'running', 'completed', 'failed' + "orphansFound" INTEGER DEFAULT 0, + "orphansDeleted" INTEGER DEFAULT 0, + "executionTimeMs" INTEGER, + "errorMessage" TEXT, + "createdAt" TIMESTAMPTZ DEFAULT NOW() NOT NULL, + + CONSTRAINT "orphanSegmentAggsCleanupRuns_pkey" PRIMARY KEY ("id") +); diff --git a/backend/src/database/migrations/V1769675250__enable_soft_deletion_for_member_identities.sql b/backend/src/database/migrations/V1769675250__enable_soft_deletion_for_member_identities.sql new file mode 100644 index 0000000000..e0cfef4daf --- /dev/null +++ b/backend/src/database/migrations/V1769675250__enable_soft_deletion_for_member_identities.sql @@ -0,0 +1,70 @@ +-------------------------------------------------------------------------------- +-- add deletedAt column for soft deletion +-------------------------------------------------------------------------------- + +alter table "memberIdentities" + add column if not exists "deletedAt" timestamp with time zone; + +-------------------------------------------------------------------------------- +-- drop and reshape indexes that need tenantId removal or soft-delete support +-------------------------------------------------------------------------------- + +-- memberId lookup +drop index if exists "memberIdentities_memberId_index"; + +-- tenantId is being deprecated; drop tenant-scoped indexes so they can be +-- recreated without tenantId in the index key +drop index if exists "ix_memberIdentities_tenantId"; +drop index if exists ix_memberidentities_tenantid_platform_value_type; +drop index if exists ix_memberidentities_tenantid_platform_lowervalue_type; + +-- uniqueness indexes (will be recreated as partial) +drop index if exists "uix_memberIdentities_memberId_platform_value_type"; +drop index if exists "uix_memberIdentities_platform_value_type_tenantId_verified"; + +-- search/performance indexes to be recreated as partial +drop index if exists "ix_memberIdentities_platform_type_lowervalue_memberId"; +drop index if exists idx_member_identities_lower_value; +drop index if exists idx_memberidentities_email_verified_trgm; + +-------------------------------------------------------------------------------- +-- recreate unique indexes (partial) +-------------------------------------------------------------------------------- + +-- per-member uniqueness: active identities only +create unique index if not exists "uix_memberIdentities_memberId_platform_value_type" + on "memberIdentities" ("memberId", platform, value, type) + where "deletedAt" is null; + +-- verified uniqueness: active + verified only +create unique index if not exists "uix_memberIdentities_platform_value_type_verified" + on "memberIdentities" (platform, value, type) + where verified = true + and "deletedAt" is null; + +-------------------------------------------------------------------------------- +-- recreate performance indexes (partial) +-------------------------------------------------------------------------------- + +-- memberId lookups (hot path) +create index if not exists "idx_memberIdentities_memberId" + on "memberIdentities" ("memberId") + where "deletedAt" is null; + +-- platform / type / value lookups (formerly tenant-scoped) +create index if not exists "idx_memberIdentities_platform_type_lower_value_memberId" + on "memberIdentities" (platform, type, lower(value), "memberId") + where "deletedAt" is null; + +-- general value search +create index if not exists "idx_memberIdentities_lower_value" + on "memberIdentities" (lower(value)) + where "deletedAt" is null; + +-- email trigram search (verified only) +create index if not exists "idx_memberIdentities_email_verified_trgm" + on "memberIdentities" + using gin (lower(value) gin_trgm_ops) + where verified = true + and type = 'email' + and "deletedAt" is null; diff --git a/backend/src/database/migrations/V1770029588__dropGitlabReposTable.sql b/backend/src/database/migrations/V1770029588__dropGitlabReposTable.sql new file mode 100644 index 0000000000..3b16a289e4 --- /dev/null +++ b/backend/src/database/migrations/V1770029588__dropGitlabReposTable.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS "gitlabRepos"; \ No newline at end of file diff --git a/backend/src/database/migrations/V1770029960__dropGitRepositoriesTable.sql b/backend/src/database/migrations/V1770029960__dropGitRepositoriesTable.sql new file mode 100644 index 0000000000..4bd39d2447 --- /dev/null +++ b/backend/src/database/migrations/V1770029960__dropGitRepositoriesTable.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS git.repositories; \ No newline at end of file diff --git a/backend/src/database/migrations/V1770030360__dropSegmentRepositories.sql b/backend/src/database/migrations/V1770030360__dropSegmentRepositories.sql new file mode 100644 index 0000000000..d23a0c5bfe --- /dev/null +++ b/backend/src/database/migrations/V1770030360__dropSegmentRepositories.sql @@ -0,0 +1,2 @@ +ALTER PUBLICATION sequin_pub DROP TABLE "segmentRepositories"; +DROP TABLE IF EXISTS "segmentRepositories"; \ No newline at end of file diff --git a/backend/src/database/migrations/V1770033636__dropInsightsProjectsRepositoriesColumn.sql b/backend/src/database/migrations/V1770033636__dropInsightsProjectsRepositoriesColumn.sql new file mode 100644 index 0000000000..90a02527f2 --- /dev/null +++ b/backend/src/database/migrations/V1770033636__dropInsightsProjectsRepositoriesColumn.sql @@ -0,0 +1,5 @@ +-- Drop the deprecated repositories column from insightsProjects +-- Repositories are now managed via public.repositories table +-- The API still accepts repositories param which syncs public.repositories.enabled + +ALTER TABLE "insightsProjects" DROP COLUMN IF EXISTS "repositories"; diff --git a/backend/src/database/migrations/V1770038298__migrateMaintainerMVtoRepositories.sql b/backend/src/database/migrations/V1770038298__migrateMaintainerMVtoRepositories.sql new file mode 100644 index 0000000000..d8a400eba4 --- /dev/null +++ b/backend/src/database/migrations/V1770038298__migrateMaintainerMVtoRepositories.sql @@ -0,0 +1,22 @@ +-- Migrate mv_maintainer_roles from githubRepos to public.repositories +-- This must run before dropping the githubRepos table + +DROP MATERIALIZED VIEW IF EXISTS mv_maintainer_roles; + +CREATE MATERIALIZED VIEW mv_maintainer_roles AS +SELECT + mai.id, + mei."memberId", + r."segmentId", + mai."createdAt" AS "dateStart", + NULL as "dateEnd", + r.url, + CASE WHEN i.platform = 'github-nango' THEN 'github' ELSE i.platform END AS "repoType", + mai.role +FROM "maintainersInternal" mai +JOIN "memberIdentities" mei ON mai."identityId" = mei.id +JOIN public.repositories r ON mai."repoId" = r.id +JOIN integrations i ON r."sourceIntegrationId" = i.id +WHERE r."deletedAt" IS NULL; + +CREATE UNIQUE INDEX IF NOT EXISTS mv_maintainer_roles_id ON mv_maintainer_roles (id); diff --git a/backend/src/database/migrations/V1770038299__dropGithubRepos.sql b/backend/src/database/migrations/V1770038299__dropGithubRepos.sql new file mode 100644 index 0000000000..dc5fb3ad12 --- /dev/null +++ b/backend/src/database/migrations/V1770038299__dropGithubRepos.sql @@ -0,0 +1,2 @@ +ALTER PUBLICATION sequin_pub DROP TABLE "githubRepos"; +DROP TABLE IF EXISTS "githubRepos"; \ No newline at end of file diff --git a/backend/src/database/migrations/V1770653666__add-automatic_projects_discovery-tables.sql b/backend/src/database/migrations/V1770653666__add-automatic_projects_discovery-tables.sql new file mode 100644 index 0000000000..c2add79aae --- /dev/null +++ b/backend/src/database/migrations/V1770653666__add-automatic_projects_discovery-tables.sql @@ -0,0 +1,42 @@ +-- Project Catalog: candidate projects discovered from OSSF Criticality Score and other sources +CREATE TABLE IF NOT EXISTS "projectCatalog" ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "projectSlug" VARCHAR(255) NOT NULL, + "repoName" VARCHAR(255) NOT NULL, + "repoUrl" VARCHAR(1024) NOT NULL, + "ossfCriticalityScore" DOUBLE PRECISION, + "lfCriticalityScore" DOUBLE PRECISION, + "syncedAt" TIMESTAMP WITH TIME ZONE DEFAULT NULL, + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX "uix_projectCatalog_repoUrl" ON "projectCatalog" ("repoUrl"); +CREATE INDEX "ix_projectCatalog_ossfCriticalityScore" ON "projectCatalog" ("ossfCriticalityScore" DESC NULLS LAST); +CREATE INDEX "ix_projectCatalog_lfCriticalityScore" ON "projectCatalog" ("lfCriticalityScore" DESC NULLS LAST); +CREATE INDEX "ix_projectCatalog_syncedAt" ON "projectCatalog" ("syncedAt"); + +-- Evaluated Projects: AI evaluation results linked to catalog entries +CREATE TABLE IF NOT EXISTS "evaluatedProjects" ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "projectCatalogId" UUID NOT NULL REFERENCES "projectCatalog"(id) ON DELETE CASCADE, + "evaluationStatus" VARCHAR(50) NOT NULL DEFAULT 'pending', + "evaluationScore" DOUBLE PRECISION, + "evaluation" JSONB, + "evaluationReason" TEXT, + "evaluatedAt" TIMESTAMP WITH TIME ZONE, + "starsCount" INTEGER, + "forksCount" INTEGER, + "commitsCount" INTEGER, + "pullRequestsCount" INTEGER, + "issuesCount" INTEGER, + "onboarded" BOOLEAN NOT NULL DEFAULT FALSE, + "onboardedAt" TIMESTAMP WITH TIME ZONE, + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX "uix_evaluatedProjects_projectCatalogId" ON "evaluatedProjects" ("projectCatalogId"); +CREATE INDEX "ix_evaluatedProjects_evaluationStatus" ON "evaluatedProjects" ("evaluationStatus"); +CREATE INDEX "ix_evaluatedProjects_evaluationScore" ON "evaluatedProjects" ("evaluationScore" DESC NULLS LAST); +CREATE INDEX "ix_evaluatedProjects_onboarded" ON "evaluatedProjects" ("onboarded"); diff --git a/backend/src/database/migrations/V1770818540__createNangoCursorsTable.sql b/backend/src/database/migrations/V1770818540__createNangoCursorsTable.sql new file mode 100644 index 0000000000..8f010ecb3e --- /dev/null +++ b/backend/src/database/migrations/V1770818540__createNangoCursorsTable.sql @@ -0,0 +1,59 @@ +-- Backup settings before any modifications +CREATE TABLE integration.integrations_settings_backup_02_13_2026 AS +SELECT id, settings FROM integrations; + +CREATE TABLE integration.nango_cursors ( + "integrationId" UUID NOT NULL, + "connectionId" TEXT NOT NULL, + platform TEXT NOT NULL, + model TEXT NOT NULL, + cursor TEXT NOT NULL, + "lastCheckedAt" TIMESTAMPTZ, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY ("integrationId", "connectionId", model), + FOREIGN KEY ("integrationId") REFERENCES integrations(id) ON DELETE CASCADE +); + +CREATE INDEX ix_nango_cursors_lastCheckedAt ON integration.nango_cursors ("lastCheckedAt" NULLS FIRST); +CREATE INDEX ix_nango_cursors_connectionId ON integration.nango_cursors ("connectionId"); + +-- GitHub-nango: unnest nangoMapping keys x cursor models +INSERT INTO integration.nango_cursors ("integrationId", "connectionId", platform, model, cursor) +SELECT + i.id, + nm.key, + 'github', + cm.key, + cm.value #>> '{}' +FROM integrations i, + jsonb_each(i.settings->'nangoMapping') nm, + jsonb_each(COALESCE(i.settings->'cursors'->nm.key, '{}'::jsonb)) cm +WHERE i.platform = 'github-nango' + AND i."deletedAt" IS NULL + AND i.settings->'nangoMapping' IS NOT NULL +ON CONFLICT DO NOTHING; + +-- Non-GitHub nango: connectionId = integrationId, unnest cursor models +INSERT INTO integration.nango_cursors ("integrationId", "connectionId", platform, model, cursor) +SELECT + i.id, + i.id::text, + CASE + WHEN i.platform = 'gerrit' THEN 'gerrit' + WHEN i.platform = 'jira' THEN COALESCE(i.settings->>'nangoIntegrationName', 'jira-basic') + WHEN i.platform = 'confluence' THEN COALESCE(i.settings->>'nangoIntegrationName', 'confluence') + ELSE i.platform + END, + cm.key, + cm.value #>> '{}' +FROM integrations i, + jsonb_each(COALESCE(i.settings->'cursors'->i.id::text, '{}'::jsonb)) cm +WHERE i.platform IN ('gerrit', 'jira', 'confluence') + AND i."deletedAt" IS NULL +ON CONFLICT DO NOTHING; + +-- Clean up settings.cursors +UPDATE integrations +SET settings = settings - 'cursors' +WHERE settings->'cursors' IS NOT NULL; diff --git a/backend/src/database/migrations/V1771344764__addSnowflakeExportTable.sql b/backend/src/database/migrations/V1771344764__addSnowflakeExportTable.sql new file mode 100644 index 0000000000..347deae78e --- /dev/null +++ b/backend/src/database/migrations/V1771344764__addSnowflakeExportTable.sql @@ -0,0 +1,17 @@ +CREATE TABLE integration."snowflakeExportJobs" ( + id BIGSERIAL PRIMARY KEY, + platform VARCHAR(100) NOT NULL, + s3_path TEXT NOT NULL UNIQUE, + metrics JSONB, + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "processingStartedAt" TIMESTAMP WITH TIME ZONE, -- set when worker claims job (acts as lock) + "exportStartedAt" TIMESTAMP WITH TIME ZONE, + "completedAt" TIMESTAMP WITH TIME ZONE, + "cleanedAt" TIMESTAMP WITH TIME ZONE, + error TEXT +); + +CREATE INDEX "idx_snowflakeExportJobs_platform" ON integration."snowflakeExportJobs" (platform); +CREATE INDEX "idx_snowflakeExportJobs_pending" ON integration."snowflakeExportJobs" ("createdAt") + WHERE "processingStartedAt" IS NULL; diff --git a/backend/src/database/migrations/V1771403085__add_entity_verification_and_extend_audit_logs.sql b/backend/src/database/migrations/V1771403085__add_entity_verification_and_extend_audit_logs.sql new file mode 100644 index 0000000000..e6ee6c50e5 --- /dev/null +++ b/backend/src/database/migrations/V1771403085__add_entity_verification_and_extend_audit_logs.sql @@ -0,0 +1,52 @@ +-- --------------------------------------------------------------------------- +-- Add verification metadata for member identities +-- --------------------------------------------------------------------------- + +-- Track the original source of the identity and which source performed the latest verification +alter table "memberIdentities" add column if not exists "source" varchar(255); +alter table "memberIdentities" add column if not exists "verifiedBy" varchar(255); + +-- --------------------------------------------------------------------------- +-- Add verification metadata for organization identities +-- --------------------------------------------------------------------------- + +-- Track the original source of the identity +alter table "organizationIdentities" add column if not exists "source" varchar(255); + +-- --------------------------------------------------------------------------- +-- Add verification metadata for work experiences +-- --------------------------------------------------------------------------- + +-- Track if the work experience has been verified and by which source +alter table "memberOrganizations" add column if not exists "verified" boolean not null default false; +alter table "memberOrganizations" add column if not exists "verifiedBy" varchar(255); + +-- --------------------------------------------------------------------------- +-- Align audit logging with a generic actor model +-- --------------------------------------------------------------------------- + +-- Add actorId and actorType as nullable initially +alter table "auditLogAction" add column if not exists "actorId" varchar(255); +alter table "auditLogAction" add column if not exists "actorType" varchar(255); + +-- Backfill historical rows (map old userId into new columns) +update "auditLogAction" +set "actorId" = "userId"::text, + "actorType" = 'user' +where "actorId" is null; + +-- Enforce NOT NULL after backfill +alter table "auditLogAction" alter column "actorId" set not null; +alter table "auditLogAction" alter column "actorType" set not null; + +-- Add index on actorId to speed up queries +create index if not exists "auditLogAction_actorId" on "auditLogAction" ("actorId"); + +-- Remove old userId column +alter table "auditLogAction" drop column if exists "userId"; + +-- --------------------------------------------------------------------------- +-- Remove legacy auditLogs table +-- --------------------------------------------------------------------------- +-- This table is no longer used; all audit actions are tracked in auditLogAction +drop table if exists "auditLogs"; \ No newline at end of file diff --git a/backend/src/database/migrations/V1771497876__addCventActivityTypes.sql b/backend/src/database/migrations/V1771497876__addCventActivityTypes.sql new file mode 100644 index 0000000000..4b1de90fcb --- /dev/null +++ b/backend/src/database/migrations/V1771497876__addCventActivityTypes.sql @@ -0,0 +1,3 @@ +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration", description, "label") VALUES +('registered-event', 'cvent', false, false, 'User registers to an event', 'Registered for an event'), +('attended-event', 'cvent', false, false, 'User attends an event', 'Attended an event'); diff --git a/backend/src/database/migrations/V1771839722__refactorIntegrationsTable.sql b/backend/src/database/migrations/V1771839722__refactorIntegrationsTable.sql new file mode 100644 index 0000000000..96f99b3354 --- /dev/null +++ b/backend/src/database/migrations/V1771839722__refactorIntegrationsTable.sql @@ -0,0 +1,59 @@ +-- Backup full integration rows (including deprecated columns and nangoMapping in settings) before any modifications +CREATE TABLE integration.integrations_backup_02_24_2026 AS +SELECT id, settings, "limitCount", "limitLastResetAt", "emailSentAt", "importHash" FROM integrations; + +-- Part A: Drop deprecated columns from integrations table +ALTER TABLE public.integrations + DROP COLUMN IF EXISTS "limitCount", + DROP COLUMN IF EXISTS "limitLastResetAt", + DROP COLUMN IF EXISTS "emailSentAt", + DROP COLUMN IF EXISTS "importHash"; + +-- Drop same columns from integrationsBackup table (created as SELECT * FROM integrations WHERE 1=2) +ALTER TABLE public."integrationsBackup" + DROP COLUMN IF EXISTS "limitCount", + DROP COLUMN IF EXISTS "limitLastResetAt", + DROP COLUMN IF EXISTS "emailSentAt", + DROP COLUMN IF EXISTS "importHash"; + +-- Part B: Create nango_mapping table and migrate data from settings JSONB +CREATE TABLE integration.nango_mapping ( + "integrationId" UUID NOT NULL, + "connectionId" TEXT NOT NULL, + "repositoryId" UUID, + owner TEXT NOT NULL, + "repoName" TEXT NOT NULL, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY ("integrationId", "connectionId"), + FOREIGN KEY ("integrationId") REFERENCES integrations(id) ON DELETE CASCADE, + FOREIGN KEY ("repositoryId") REFERENCES repositories(id) ON DELETE SET NULL +); + +CREATE UNIQUE INDEX ix_nango_mapping_connectionId ON integration.nango_mapping ("connectionId"); +CREATE INDEX ix_nango_mapping_repositoryId ON integration.nango_mapping ("repositoryId"); + +-- Migrate existing data from settings.nangoMapping +INSERT INTO integration.nango_mapping ("integrationId", "connectionId", "repositoryId", owner, "repoName") +SELECT + i.id, + nm.key, + r.id, + nm.value->>'owner', + nm.value->>'repoName' +FROM integrations i +CROSS JOIN LATERAL jsonb_each(i.settings->'nangoMapping') nm +LEFT JOIN repositories r + ON lower(r.url) = lower('https://github.com/' || (nm.value->>'owner') || '/' || (nm.value->>'repoName')) + AND r."sourceIntegrationId" = i.id + AND r."deletedAt" IS NULL +WHERE i.platform = 'github-nango' + AND i."deletedAt" IS NULL + AND i.settings->'nangoMapping' IS NOT NULL +ON CONFLICT DO NOTHING; + +-- Remove nangoMapping from settings (only github-nango integrations ever stored this key) +UPDATE integrations +SET settings = settings - 'nangoMapping' +WHERE platform = 'github-nango' + AND settings->'nangoMapping' IS NOT NULL; diff --git a/backend/src/database/migrations/V1772043927__removeReposFromGithubSettings.sql b/backend/src/database/migrations/V1772043927__removeReposFromGithubSettings.sql new file mode 100644 index 0000000000..c1295f972d --- /dev/null +++ b/backend/src/database/migrations/V1772043927__removeReposFromGithubSettings.sql @@ -0,0 +1,30 @@ +-- Backup settings before any modifications +CREATE TABLE integration.integrations_settings_backup_02_28_2026 AS +SELECT id, settings FROM integrations; + +-- Strip repos from orgs in settings for github and github-nango integrations +-- Repos now live in public.repositories table and are populated into API responses +-- via the compatibility layer in integrationRepository._populateRelations +UPDATE integrations +SET settings = jsonb_set( + settings, + '{orgs}', + ( + SELECT coalesce(jsonb_agg( + org - 'repos' + ), '[]'::jsonb) + FROM jsonb_array_elements(settings->'orgs') org + ) +) +WHERE platform IN ('github', 'github-nango') + AND settings->'orgs' IS NOT NULL + AND "deletedAt" IS NULL + AND status != 'mapping'; + +-- Also clean up top-level repos/unavailableRepos if present +UPDATE integrations +SET settings = settings - 'repos' - 'unavailableRepos' +WHERE platform IN ('github', 'github-nango') + AND (settings ? 'repos' OR settings ? 'unavailableRepos') + AND "deletedAt" IS NULL + AND status != 'mapping'; diff --git a/backend/src/database/migrations/V1772438175__communityCollections.sql b/backend/src/database/migrations/V1772438175__communityCollections.sql new file mode 100644 index 0000000000..fec328c35d --- /dev/null +++ b/backend/src/database/migrations/V1772438175__communityCollections.sql @@ -0,0 +1,55 @@ +-- Add new columns to collections table +ALTER TABLE public.collections + ADD COLUMN "isPrivate" BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN "ssoUserId" VARCHAR(255), -- nullable: only set for community collections + ADD COLUMN "logoUrl" TEXT; -- nullable: only set for curated collections + +-- Create ssoUsers table (id = SSO subject ID, e.g. "auth0|abc123") +CREATE TABLE public."insightsSsoUsers" ( + id VARCHAR(255) PRIMARY KEY, + "displayName" VARCHAR(255), + "avatarUrl" TEXT, + "email" VARCHAR(255), + "username" VARCHAR(255), + "accountId" VARCHAR(255), + "accountName" VARCHAR(255), + "accountWebsite" TEXT, + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() +); + +-- FK: collections.ssoUserId -> ssoUsers.id +ALTER TABLE public.collections + ADD CONSTRAINT "fk_collections_ssoUserId" + FOREIGN KEY ("ssoUserId") REFERENCES public."insightsSsoUsers"(id) ON DELETE SET NULL; + +-- Create collectionsRepositories table +CREATE TABLE public."collectionsRepositories" ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "collectionId" UUID NOT NULL REFERENCES public.collections(id) ON DELETE CASCADE, + "repoId" UUID NOT NULL REFERENCES public.repositories(id) ON DELETE CASCADE, + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "deletedAt" TIMESTAMP WITH TIME ZONE +); + +CREATE INDEX "ix_collectionsRepositories_collectionId" + ON public."collectionsRepositories" ("collectionId") + WHERE "deletedAt" IS NULL; + +-- Create collectionLikes table +CREATE TABLE public."collectionLikes" ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "collectionId" UUID NOT NULL REFERENCES public.collections(id) ON DELETE CASCADE, + "ssoUserId" VARCHAR(255) NOT NULL REFERENCES public."insightsSsoUsers"(id) ON DELETE CASCADE, + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "deletedAt" TIMESTAMP WITH TIME ZONE +); + +-- Partial unique index so a user can re-like after unliking (soft delete) +CREATE UNIQUE INDEX "ix_collectionLikes_unique_active" + ON public."collectionLikes" ("collectionId", "ssoUserId") + WHERE "deletedAt" IS NULL; + +CREATE INDEX "ix_collectionLikes_collectionId" + ON public."collectionLikes" ("collectionId") + WHERE "deletedAt" IS NULL; diff --git a/backend/src/database/migrations/V1772548920__addSourceNameToSnowflakeExportJobs.sql b/backend/src/database/migrations/V1772548920__addSourceNameToSnowflakeExportJobs.sql new file mode 100644 index 0000000000..244965f1d4 --- /dev/null +++ b/backend/src/database/migrations/V1772548920__addSourceNameToSnowflakeExportJobs.sql @@ -0,0 +1,7 @@ +ALTER TABLE integration."snowflakeExportJobs" ADD COLUMN "sourceName" VARCHAR(100); + +UPDATE integration."snowflakeExportJobs" SET "sourceName" = 'event-registrations' WHERE platform = 'cvent'; + +ALTER TABLE integration."snowflakeExportJobs" ALTER COLUMN "sourceName" SET NOT NULL; + +CREATE INDEX "idx_snowflakeExportJobs_platform_source" ON integration."snowflakeExportJobs" (platform, "sourceName"); diff --git a/backend/src/database/migrations/V1772556158__addTncActivityTypes.sql b/backend/src/database/migrations/V1772556158__addTncActivityTypes.sql new file mode 100644 index 0000000000..790fa31fc3 --- /dev/null +++ b/backend/src/database/migrations/V1772556158__addTncActivityTypes.sql @@ -0,0 +1,6 @@ +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration", description, "label") VALUES +('enrolled-certification', 'tnc', false, false, 'Successful payment purchase of certification enrollment', 'Enrolled in certification'), +('enrolled-training', 'tnc', false, false, 'Successful payment purchase of training enrollment', 'Enrolled in training'), +('issued-certification', 'tnc', false, false, 'User is granted a certification', 'Issued certification'), +('attempted-course', 'tnc', false, false, 'Certification course is completed', 'Attempted course'), +('attempted-exam', 'tnc', false, false, 'Certification exam is completed', 'Attempted exam'); diff --git a/backend/src/database/migrations/V1772799041__add-missing-indexes-for-project-affiliations.sql b/backend/src/database/migrations/V1772799041__add-missing-indexes-for-project-affiliations.sql new file mode 100644 index 0000000000..abbe3fa344 --- /dev/null +++ b/backend/src/database/migrations/V1772799041__add-missing-indexes-for-project-affiliations.sql @@ -0,0 +1,7 @@ +-- Add missing index on memberSegmentAffiliations for memberId lookups +create index concurrently if not exists "ix_memberSegmentAffiliations_memberId" + on "memberSegmentAffiliations" ("memberId"); + +-- Add missing index on mv_maintainer_roles materialized view for memberId lookups +create index concurrently if not exists "ix_mv_maintainer_roles_memberId" + on mv_maintainer_roles ("memberId"); diff --git a/backend/src/database/migrations/V1773131945__collectionsImageColor.sql b/backend/src/database/migrations/V1773131945__collectionsImageColor.sql new file mode 100644 index 0000000000..11a05baba8 --- /dev/null +++ b/backend/src/database/migrations/V1773131945__collectionsImageColor.sql @@ -0,0 +1,2 @@ +ALTER TABLE collections ADD COLUMN IF NOT EXISTS "imageUrl" VARCHAR(1024); +ALTER TABLE collections ADD COLUMN IF NOT EXISTS "color" VARCHAR(30); diff --git a/backend/src/database/migrations/V1773139177__add-verified-to-member-segment-affiliations.sql b/backend/src/database/migrations/V1773139177__add-verified-to-member-segment-affiliations.sql new file mode 100644 index 0000000000..4b554f45ff --- /dev/null +++ b/backend/src/database/migrations/V1773139177__add-verified-to-member-segment-affiliations.sql @@ -0,0 +1,3 @@ +alter table "memberSegmentAffiliations" + add column if not exists "verified" boolean not null default false, + add column if not exists "verifiedBy" varchar(255) default null; diff --git a/backend/src/database/migrations/V1773938832__add-api-keys-tale.sql b/backend/src/database/migrations/V1773938832__add-api-keys-tale.sql new file mode 100644 index 0000000000..9248935984 --- /dev/null +++ b/backend/src/database/migrations/V1773938832__add-api-keys-tale.sql @@ -0,0 +1,13 @@ +CREATE TABLE "apiKeys" ( + "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "name" TEXT NOT NULL, + "keyHash" TEXT NOT NULL UNIQUE, + "keyPrefix" TEXT NOT NULL, + "scopes" TEXT[] NOT NULL DEFAULT '{}', + "expiresAt" TIMESTAMPTZ, + "lastUsedAt" TIMESTAMPTZ, + "createdById" TEXT, + "revokedAt" TIMESTAMPTZ, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now() +); diff --git a/backend/src/database/migrations/V1774363114__add-btree-index-for-member-identities-email-lookup.sql b/backend/src/database/migrations/V1774363114__add-btree-index-for-member-identities-email-lookup.sql new file mode 100644 index 0000000000..e7ee22dafa --- /dev/null +++ b/backend/src/database/migrations/V1774363114__add-btree-index-for-member-identities-email-lookup.sql @@ -0,0 +1,20 @@ +-- The existing idx_memberIdentities_email_verified_trgm covers the right partial +-- conditions (verified = true, type = 'email', deletedAt IS NULL) but is a GIN +-- trigram index -- PostgreSQL will not use it for equality joins on +-- lower(mi.value) against input emails, for example: +-- +-- JOIN ON lower(mi.value) = input_email +-- WHERE mi.verified = true AND mi.type = 'email' AND mi."deletedAt" IS NULL +-- +-- The only usable B-tree index (idx_memberIdentities_lower_value) only carries +-- the partial condition deletedAt IS NULL, so PG must heap-fetch every matched +-- row to re-check verified and type, which degrades as the table grows. +-- +-- This B-tree partial index lets the planner do a direct nested-loop index scan +-- (one lookup per input email) with no extra heap fetches for verified/type. + +CREATE INDEX CONCURRENTLY IF NOT EXISTS "idx_memberIdentities_verified_email_lower_value" + ON "memberIdentities" (lower(value)) + WHERE verified = true + AND type = 'email' + AND "deletedAt" IS NULL; diff --git a/backend/src/database/migrations/V1774430554__covering-indexes-for-member-identity-lookups.sql b/backend/src/database/migrations/V1774430554__covering-indexes-for-member-identity-lookups.sql new file mode 100644 index 0000000000..f044dacd22 --- /dev/null +++ b/backend/src/database/migrations/V1774430554__covering-indexes-for-member-identity-lookups.sql @@ -0,0 +1,36 @@ +-- Query: findMembersByVerifiedUsernames +-- +-- Joins memberIdentities on (platform, lower(value)) with WHERE: +-- verified = true AND type = 'username' AND deletedAt IS NULL +-- +-- The existing idx_memberIdentities_platform_type_lower_value_memberId is +-- missing verified = true in its partial condition, so PostgreSQL must +-- heap-fetch every row to recheck verified, which is expensive when there +-- are many unverified identities. +-- +-- This index adds verified = true to the partial condition and includes memberId +-- so the join to members can read memberId from the index without a heap fetch. +CREATE INDEX CONCURRENTLY IF NOT EXISTS "idx_memberIdentities_verified_username_platform_lower_value" + ON "memberIdentities" (platform, lower(value), "memberId") + WHERE verified = true + AND type = 'username' + AND "deletedAt" IS NULL; + +-- Query: findMembersByVerifiedEmails +-- +-- Joins memberIdentities on lower(value) with WHERE: +-- verified = true AND type = 'email' AND deletedAt IS NULL +-- +-- The existing idx_memberIdentities_verified_email_lower_value has the right +-- partial condition but only stores lower(value) — no memberId. Every matched +-- index entry requires a heap fetch to get memberId for the join to members. +-- Under concurrent insert/update load, those heap fetches queue behind buffer +-- pin locks, causing multi-second delays even for small inputs. +-- +-- This index adds memberId so the join to members can proceed without +-- touching the memberIdentities heap pages. +CREATE INDEX CONCURRENTLY IF NOT EXISTS "idx_memberIdentities_verified_email_lower_value_memberid" + ON "memberIdentities" (lower(value), "memberId") + WHERE verified = true + AND type = 'email' + AND "deletedAt" IS NULL; diff --git a/backend/src/database/migrations/V1774609007__data-sink-worker-optimizations.sql b/backend/src/database/migrations/V1774609007__data-sink-worker-optimizations.sql new file mode 100644 index 0000000000..b3be1b5e71 --- /dev/null +++ b/backend/src/database/migrations/V1774609007__data-sink-worker-optimizations.sql @@ -0,0 +1,11 @@ +-- Drop 4 unused activityRelations indexes (already dropped on prod 2026-03-27, +-- see ACTIVITYRELATIONS_INDEX_CLEANUP.md — IF EXISTS guards for idempotency) +alter table "activityRelations" drop constraint if exists "activityRelations_activityId_memberId_key"; + +drop index concurrently if exists "ix_activityRelations_memberId_segmentId_include"; +drop index concurrently if exists "ix_activityRelations_organizationId_segmentId_include"; +drop index concurrently if exists "ix_activityRelations_platform_username"; + +create index concurrently if not exists idx_osa_org_segment_membercount + on "organizationSegmentsAgg" ("organizationId", "segmentId") + include ("memberCount"); \ No newline at end of file diff --git a/backend/src/database/migrations/V1774627680__change_mergeActions_actionBy_to_text.sql b/backend/src/database/migrations/V1774627680__change_mergeActions_actionBy_to_text.sql new file mode 100644 index 0000000000..e7d5edc7d2 --- /dev/null +++ b/backend/src/database/migrations/V1774627680__change_mergeActions_actionBy_to_text.sql @@ -0,0 +1,2 @@ +alter table "mergeActions" drop constraint if exists "mergeActions_actionBy_fkey"; +alter table "mergeActions" alter column "actionBy" type text using "actionBy"::text; \ No newline at end of file diff --git a/backend/src/database/migrations/V1774628604__fixMaintainersInternalUniqueIndex.sql b/backend/src/database/migrations/V1774628604__fixMaintainersInternalUniqueIndex.sql new file mode 100644 index 0000000000..debd0a2167 --- /dev/null +++ b/backend/src/database/migrations/V1774628604__fixMaintainersInternalUniqueIndex.sql @@ -0,0 +1,32 @@ +-- Fix maintainersInternal unique index and clean up duplicates. +-- +-- Problem: +-- The unique index on ("repoId", "identityId", "startDate", "endDate") fails to +-- prevent duplicates because startDate and endDate are NULL on insert, and +-- PostgreSQL treats NULL != NULL in unique indexes — so the constraint never fires. +-- +-- Fix: +-- Unique index on ("repoId", "identityId", role). All three columns are NOT NULL, +-- so the uniqueness check always works. startDate/endDate become purely informational. + +-- Step 1: Remove duplicate rows, keeping the most recently updated row per group. +DELETE FROM "maintainersInternal" +WHERE id IN ( + SELECT id FROM ( + SELECT + id, + ROW_NUMBER() OVER ( + PARTITION BY "repoId", "identityId", role + ORDER BY "updatedAt" DESC NULLS LAST, "createdAt" DESC NULLS LAST + ) AS rn + FROM "maintainersInternal" + ) ranked + WHERE rn > 1 +); + +-- Step 2: Drop the existing unique index. +DROP INDEX IF EXISTS maintainers_internal_repo_identity_unique_idx; + +-- Step 3: Create the correct unique index on non-nullable columns. +CREATE UNIQUE INDEX maintainers_internal_repo_identity_role_unique_idx + ON "maintainersInternal" ("repoId", "identityId", role); diff --git a/backend/src/database/migrations/V1775064222__addCommitteesActivityTypes.sql b/backend/src/database/migrations/V1775064222__addCommitteesActivityTypes.sql new file mode 100644 index 0000000000..7e924d24c7 --- /dev/null +++ b/backend/src/database/migrations/V1775064222__addCommitteesActivityTypes.sql @@ -0,0 +1,3 @@ +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration", description, "label") VALUES +('added-to-committee', 'committees', false, false, 'Member is added to a committee', 'Added to committee'), +('removed-from-committee', 'committees', false, false, 'Member is removed from a committee', 'Removed from committee'); diff --git a/backend/src/database/migrations/V1775219382__addMeetingsActivityTypes.sql b/backend/src/database/migrations/V1775219382__addMeetingsActivityTypes.sql new file mode 100644 index 0000000000..5c4ec8ffc3 --- /dev/null +++ b/backend/src/database/migrations/V1775219382__addMeetingsActivityTypes.sql @@ -0,0 +1,3 @@ +INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration", description, "label") VALUES +('invited-meeting', 'meetings', false, false, 'User is invited to a meeting', 'Invited to a meeting'), +('attended-meeting', 'meetings', false, false, 'User attends a meeting', 'Attended a meeting'); diff --git a/backend/src/database/migrations/V1775312770__pcc-sync-worker-setup.sql b/backend/src/database/migrations/V1775312770__pcc-sync-worker-setup.sql new file mode 100644 index 0000000000..16cecdac82 --- /dev/null +++ b/backend/src/database/migrations/V1775312770__pcc-sync-worker-setup.sql @@ -0,0 +1,27 @@ +-- Add maturity field to segments for PCC project_maturity_level sync +ALTER TABLE segments ADD COLUMN IF NOT EXISTS maturity TEXT NULL; + +-- Catch-all table for PCC sync issues that require manual review +CREATE TABLE IF NOT EXISTS pcc_projects_sync_errors ( + id BIGSERIAL PRIMARY KEY, + run_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + external_project_id TEXT, + external_project_slug TEXT, + error_type TEXT NOT NULL, + details JSONB, + resolved BOOLEAN NOT NULL DEFAULT FALSE +); + +-- Deduplication index: one unresolved error per (project, error_type). +-- On repeated daily exports the same error upserts in place instead of accumulating rows. +-- Excludes rows where external_project_id IS NULL (e.g. SCHEMA_MISMATCH with no project id). +CREATE UNIQUE INDEX IF NOT EXISTS pcc_sync_errors_dedup_idx + ON pcc_projects_sync_errors (external_project_id, error_type) + WHERE NOT resolved AND external_project_id IS NOT NULL; + +-- Deduplication index for unidentifiable rows (no external_project_id). +-- Keyed on (error_type, reason) so repeated daily exports don't accumulate duplicate rows +-- for the same class of malformed input (e.g. rows missing PROJECT_ID/NAME/DEPTH). +CREATE UNIQUE INDEX IF NOT EXISTS pcc_sync_errors_dedup_unknown_idx + ON pcc_projects_sync_errors (error_type, (details->>'reason')) + WHERE NOT resolved AND external_project_id IS NULL; diff --git a/backend/src/database/migrations/V1776428661__dropStuckFlagFromRepositoryProcessing.sql b/backend/src/database/migrations/V1776428661__dropStuckFlagFromRepositoryProcessing.sql new file mode 100644 index 0000000000..a8dde98a93 --- /dev/null +++ b/backend/src/database/migrations/V1776428661__dropStuckFlagFromRepositoryProcessing.sql @@ -0,0 +1 @@ +ALTER TABLE git."repositoryProcessing" DROP COLUMN IF EXISTS "stuckRequiresReOnboard"; diff --git a/backend/src/database/migrations/V1776931245__member-organizations-email-domain-partial-index.sql b/backend/src/database/migrations/V1776931245__member-organizations-email-domain-partial-index.sql new file mode 100644 index 0000000000..404f5c18be --- /dev/null +++ b/backend/src/database/migrations/V1776931245__member-organizations-email-domain-partial-index.sql @@ -0,0 +1,3 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS "ix_memberOrganizations_memberId_emailDomain" + ON "memberOrganizations" ("memberId") + WHERE "source" = 'email-domain' AND "deletedAt" IS NULL; diff --git a/backend/src/database/migrations/V1776939912__collectionRepositoriesReplicaIdentityUpdates.sql b/backend/src/database/migrations/V1776939912__collectionRepositoriesReplicaIdentityUpdates.sql new file mode 100644 index 0000000000..92881bff1e --- /dev/null +++ b/backend/src/database/migrations/V1776939912__collectionRepositoriesReplicaIdentityUpdates.sql @@ -0,0 +1,14 @@ +ALTER TABLE "collectionsRepositories" REPLICA IDENTITY FULL; +GRANT SELECT ON "collectionsRepositories" TO sequin; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_publication_tables + WHERE pubname = 'sequin_pub' AND tablename = 'collectionsRepositories' + ) THEN + ALTER PUBLICATION sequin_pub ADD TABLE "collectionsRepositories"; + END IF; +END $$; + +CREATE INDEX IF NOT EXISTS "ix_collectionsRepositories_updatedAt_id" ON "collectionsRepositories" ("updatedAt", id); diff --git a/backend/src/database/migrations/V1778146717__drop-activities-and-activity-tasks.sql b/backend/src/database/migrations/V1778146717__drop-activities-and-activity-tasks.sql new file mode 100644 index 0000000000..71c5fa37ff --- /dev/null +++ b/backend/src/database/migrations/V1778146717__drop-activities-and-activity-tasks.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS "activityTasks"; +DROP TABLE IF EXISTS "activities"; diff --git a/backend/src/database/migrations/V1778154987__addLicenseToRepositories.sql b/backend/src/database/migrations/V1778154987__addLicenseToRepositories.sql new file mode 100644 index 0000000000..a0285f5fea --- /dev/null +++ b/backend/src/database/migrations/V1778154987__addLicenseToRepositories.sql @@ -0,0 +1 @@ +ALTER TABLE public.repositories ADD COLUMN license VARCHAR(255); diff --git a/backend/src/database/migrations/V1778600068__removeLicenseAddLicensesToRepositories.sql b/backend/src/database/migrations/V1778600068__removeLicenseAddLicensesToRepositories.sql new file mode 100644 index 0000000000..974c2c8a28 --- /dev/null +++ b/backend/src/database/migrations/V1778600068__removeLicenseAddLicensesToRepositories.sql @@ -0,0 +1,2 @@ +ALTER TABLE public.repositories DROP COLUMN license; +ALTER TABLE public.repositories ADD COLUMN licenses VARCHAR(255)[]; diff --git a/backend/src/database/migrations/V1778749030__refactor-projects-catalog.sql b/backend/src/database/migrations/V1778749030__refactor-projects-catalog.sql new file mode 100644 index 0000000000..f762a1596e --- /dev/null +++ b/backend/src/database/migrations/V1778749030__refactor-projects-catalog.sql @@ -0,0 +1,16 @@ +-- Drop evaluatedProjects table (data migrated into projectCatalog) +DROP TABLE IF EXISTS "evaluatedProjects"; + +-- Remove ossfCriticalityScore from projectCatalog +ALTER TABLE "projectCatalog" DROP COLUMN IF EXISTS "ossfCriticalityScore"; +DROP INDEX IF EXISTS "ix_projectCatalog_ossfCriticalityScore"; + +-- Add new columns to projectCatalog +ALTER TABLE "projectCatalog" + ADD COLUMN IF NOT EXISTS "source" VARCHAR(64), + ADD COLUMN IF NOT EXISTS "action" VARCHAR(16) NOT NULL DEFAULT 'auto', + ADD COLUMN IF NOT EXISTS "evaluatedAt" TIMESTAMP WITH TIME ZONE, + ADD COLUMN IF NOT EXISTS "onboardedAt" TIMESTAMP WITH TIME ZONE; + +CREATE INDEX "ix_projectCatalog_source" ON "projectCatalog" ("source"); +CREATE INDEX "ix_projectCatalog_action" ON "projectCatalog" ("action"); diff --git a/backend/src/database/migrations/V1779280838__exclusion-reason-to-project-catalog.sql b/backend/src/database/migrations/V1779280838__exclusion-reason-to-project-catalog.sql new file mode 100644 index 0000000000..b16801711c --- /dev/null +++ b/backend/src/database/migrations/V1779280838__exclusion-reason-to-project-catalog.sql @@ -0,0 +1,3 @@ +ALTER TABLE "projectCatalog" + ADD COLUMN IF NOT EXISTS "evaluationResult" TEXT, + ADD COLUMN IF NOT EXISTS "evaluationReason" TEXT; diff --git a/backend/src/database/migrations/V1780460201__add-deleted-at-to-member-segment-affiliations.sql b/backend/src/database/migrations/V1780460201__add-deleted-at-to-member-segment-affiliations.sql new file mode 100644 index 0000000000..017bcf3127 --- /dev/null +++ b/backend/src/database/migrations/V1780460201__add-deleted-at-to-member-segment-affiliations.sql @@ -0,0 +1,2 @@ +alter table "memberSegmentAffiliations" + add column if not exists "deletedAt" timestamp with time zone default null; diff --git a/backend/src/database/migrations/V1780555634__update-osps-catalog-id.sql b/backend/src/database/migrations/V1780555634__update-osps-catalog-id.sql new file mode 100644 index 0000000000..4213ae04ab --- /dev/null +++ b/backend/src/database/migrations/V1780555634__update-osps-catalog-id.sql @@ -0,0 +1,3 @@ +UPDATE "securityInsightsEvaluationSuites" +SET "catalogId" = 'osps-baseline-2026-02' +WHERE "catalogId" = 'OSPS_B'; diff --git a/backend/src/database/migrations/V1781455484__add-similarity-indexes-on-merge-raw-tables.sql b/backend/src/database/migrations/V1781455484__add-similarity-indexes-on-merge-raw-tables.sql new file mode 100644 index 0000000000..36293883b8 --- /dev/null +++ b/backend/src/database/migrations/V1781455484__add-similarity-indexes-on-merge-raw-tables.sql @@ -0,0 +1,7 @@ +-- Supports similarity range filtering used by +-- getRawMemberSuggestions / getRawOrganizationSuggestions. +create index concurrently if not exists "ix_memberToMergeRaw_similarity" + on "memberToMergeRaw" (similarity); + +create index concurrently if not exists "ix_organizationToMergeRaw_similarity" + on "organizationToMergeRaw" (similarity); diff --git a/backend/src/database/models/activity.ts b/backend/src/database/models/activity.ts deleted file mode 100644 index 07ba718e4b..0000000000 --- a/backend/src/database/models/activity.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { DataTypes } from 'sequelize' - -export default (sequelize) => { - const activity = sequelize.define( - 'activity', - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - type: { - type: DataTypes.TEXT, - allowNull: false, - validate: { - notEmpty: true, - }, - }, - timestamp: { - type: DataTypes.DATE, - allowNull: false, - }, - platform: { - type: DataTypes.TEXT, - allowNull: false, - validate: { - notEmpty: true, - }, - }, - isContribution: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - }, - score: { - type: DataTypes.INTEGER, - defaultValue: 2, - }, - sourceId: { - type: DataTypes.TEXT, - allowNull: false, - validate: { - notEmpty: true, - }, - }, - sourceParentId: { - type: DataTypes.STRING(255), - }, - username: { - type: DataTypes.TEXT, - }, - objectMemberUsername: { - type: DataTypes.TEXT, - }, - attributes: { - type: DataTypes.JSONB, - allowNull: false, - defaultValue: {}, - }, - channel: { - type: DataTypes.TEXT, - }, - body: { - type: DataTypes.TEXT, - }, - title: { - type: DataTypes.TEXT, - }, - url: { - type: DataTypes.TEXT, - }, - sentiment: { - type: DataTypes.JSONB, - defaultValue: {}, - }, - importHash: { - type: DataTypes.STRING(255), - allowNull: true, - validate: { - len: [0, 255], - }, - }, - organizationId: { - type: DataTypes.UUID, - allowNull: true, - }, - }, - { - indexes: [ - { - unique: true, - fields: ['importHash', 'tenantId'], - where: { - deletedAt: null, - }, - }, - { - fields: ['platform', 'tenantId', 'type', 'timestamp'], - where: { - deletedAt: null, - }, - }, - { - unique: false, - fields: ['timestamp', 'tenantId'], - where: { - deletedAt: null, - }, - }, - { - fields: ['sourceId', 'tenantId'], - where: { - deletedAt: null, - }, - }, - { - unique: false, - fields: ['memberId', 'tenantId'], - where: { - deletedAt: null, - }, - }, - { - unique: false, - fields: ['sourceParentId', 'tenantId'], - where: { - deletedAt: null, - }, - }, - { - unique: false, - fields: ['deletedAt'], - }, - { - unique: false, - fields: ['parentId', 'tenantId'], - where: { - deletedAt: null, - }, - }, - { - unique: false, - fields: ['conversationId', 'tenantId'], - where: { - deletedAt: null, - }, - }, - ], - timestamps: true, - paranoid: true, - }, - ) - - activity.associate = (models) => { - models.activity.belongsTo(models.member, { - as: 'member', - onDelete: 'cascade', - foreignKey: { - allowNull: false, - }, - }) - - models.activity.belongsTo(models.segment, { - as: 'segment', - foreignKey: { - allowNull: false, - }, - }) - - models.activity.belongsTo(models.member, { - as: 'objectMember', - }) - - models.activity.belongsTo(models.conversation, { - as: 'conversation', - }) - - models.activity.belongsTo(models.activity, { - as: 'parent', - // constraints: false, - }) - - models.activity.belongsToMany(models.task, { - as: 'tasks', - through: 'activityTasks', - }) - - models.activity.belongsTo(models.tenant, { - as: 'tenant', - foreignKey: { - allowNull: false, - }, - }) - - models.activity.belongsTo(models.user, { - as: 'createdBy', - }) - - models.activity.belongsTo(models.user, { - as: 'updatedBy', - }) - - models.activity.belongsTo(models.organization, { - as: 'organization', - }) - } - - return activity -} diff --git a/backend/src/database/models/auditLog.ts b/backend/src/database/models/auditLog.ts deleted file mode 100644 index 4ead7b3621..0000000000 --- a/backend/src/database/models/auditLog.ts +++ /dev/null @@ -1,57 +0,0 @@ -export default (sequelize, DataTypes) => { - const auditLog = sequelize.define( - 'auditLog', - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - entityName: { - type: DataTypes.STRING(255), - allowNull: false, - validate: { - len: [0, 255], - }, - }, - entityId: { - type: DataTypes.STRING(255), - allowNull: false, - validate: { - len: [0, 255], - }, - }, - tenantId: { - type: DataTypes.UUID, - allowNull: true, - }, - action: { - type: DataTypes.STRING(32), - allowNull: false, - validate: { - len: [0, 32], - }, - }, - createdById: { - type: DataTypes.UUID, - allowNull: true, - }, - createdByEmail: { - type: DataTypes.STRING(255), - validate: { - len: [0, 255], - }, - allowNull: true, - }, - timestamp: { type: DataTypes.DATE, allowNull: false }, - values: { type: DataTypes.JSON, allowNull: false }, - }, - { - timestamps: false, - }, - ) - - auditLog.associate = () => {} - - return auditLog -} diff --git a/backend/src/database/models/automation.ts b/backend/src/database/models/automation.ts deleted file mode 100644 index a2726ba29e..0000000000 --- a/backend/src/database/models/automation.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { DataTypes } from 'sequelize' - -export default (sequelize) => { - const automation = sequelize.define( - 'automation', - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - allowNull: false, - }, - type: { - type: DataTypes.STRING(80), - allowNull: false, - validate: { - notEmpty: true, - }, - }, - name: { - type: DataTypes.STRING(255), - }, - tenantId: { - type: DataTypes.UUID, - allowNull: false, - }, - trigger: { - type: DataTypes.STRING(80), - allowNull: false, - validate: { - notEmpty: true, - }, - }, - settings: { - type: DataTypes.JSONB, - allowNull: false, - }, - state: { - type: DataTypes.STRING(80), - allowNull: false, - validate: { - notEmpty: true, - }, - }, - }, - { - indexes: [ - { - fields: ['type', 'tenantId', 'trigger', 'state'], - }, - ], - timestamps: true, - }, - ) - - automation.associate = (models) => { - models.automation.belongsTo(models.tenant, { - as: 'tenant', - foreignKey: { - allowNull: false, - }, - }) - - models.automation.belongsTo(models.user, { - as: 'createdBy', - }) - - models.automation.belongsTo(models.user, { - as: 'updatedBy', - }) - } - - return automation -} diff --git a/backend/src/database/models/automationExecution.ts b/backend/src/database/models/automationExecution.ts deleted file mode 100644 index 72b59291ce..0000000000 --- a/backend/src/database/models/automationExecution.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { DataTypes } from 'sequelize' - -export default (sequelize) => { - const automationExecution = sequelize.define( - 'automationExecution', - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - allowNull: false, - }, - automationId: { - type: DataTypes.UUID, - allowNull: false, - }, - type: { - type: DataTypes.STRING(80), - allowNull: false, - validate: { - notEmpty: true, - }, - }, - tenantId: { - type: DataTypes.UUID, - allowNull: false, - }, - trigger: { - type: DataTypes.STRING(80), - allowNull: false, - validate: { - notEmpty: true, - }, - }, - state: { - type: DataTypes.STRING(80), - allowNull: false, - validate: { - notEmpty: true, - }, - }, - error: { - type: DataTypes.JSON, - allowNull: true, - }, - executedAt: { - type: DataTypes.DATE, - allowNull: false, - }, - eventId: { - type: DataTypes.STRING(255), - allowNull: false, - validate: { - notEmpty: true, - }, - }, - payload: { - type: DataTypes.JSON, - allowNull: false, - }, - }, - { - indexes: [ - { - fields: ['automationId'], - }, - ], - timestamps: false, - paranoid: false, - }, - ) - - automationExecution.associate = (models) => { - models.automationExecution.belongsTo(models.tenant, { - as: 'tenant', - foreignKey: { - allowNull: false, - }, - }) - - models.automationExecution.belongsTo(models.automation, { - as: 'automation', - foreignKey: { - allowNull: false, - }, - }) - } - - return automationExecution -} diff --git a/backend/src/database/models/conversation.ts b/backend/src/database/models/conversation.ts deleted file mode 100644 index 34cb7c6da2..0000000000 --- a/backend/src/database/models/conversation.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { DataTypes } from 'sequelize' - -export default (sequelize) => { - const conversation = sequelize.define( - 'conversation', - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - title: { - type: DataTypes.TEXT, - allowNull: false, - validate: { - notEmpty: true, - }, - }, - slug: { - type: DataTypes.TEXT, - allowNull: false, - validate: { - notEmpty: true, - }, - }, - published: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - }, - }, - { - indexes: [ - { - unique: true, - fields: ['slug', 'tenantId'], - }, - ], - timestamps: true, - }, - ) - - conversation.associate = (models) => { - models.conversation.hasMany(models.activity, { - as: 'activities', - }) - - models.conversation.belongsTo(models.tenant, { - as: 'tenant', - foreignKey: { - allowNull: false, - }, - }) - - models.conversation.belongsTo(models.segment, { - as: 'segment', - foreignKey: { - allowNull: false, - }, - }) - - models.conversation.belongsTo(models.user, { - as: 'createdBy', - }) - - models.conversation.belongsTo(models.user, { - as: 'updatedBy', - }) - } - - return conversation -} diff --git a/backend/src/database/models/conversationSettings.ts b/backend/src/database/models/conversationSettings.ts deleted file mode 100644 index 41f5b55b41..0000000000 --- a/backend/src/database/models/conversationSettings.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { DataTypes } from 'sequelize' - -export default (sequelize) => { - const conversationSettings = sequelize.define('conversationSettings', { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - enabled: { - type: DataTypes.BOOLEAN, - defaultValue: false, - }, - customUrl: { - type: DataTypes.TEXT, - }, - logoUrl: { - type: DataTypes.TEXT, - }, - faviconUrl: { - type: DataTypes.TEXT, - }, - theme: { - type: DataTypes.JSONB, - }, - autoPublish: { - type: DataTypes.JSONB, - }, - }) - - conversationSettings.associate = (models) => { - models.conversationSettings.belongsTo(models.tenant, { - as: 'tenant', - foreignKey: { - allowNull: false, - }, - }) - - models.conversationSettings.belongsTo(models.user, { - as: 'createdBy', - }) - - models.conversationSettings.belongsTo(models.user, { - as: 'updatedBy', - }) - } - - return conversationSettings -} diff --git a/backend/src/database/models/customView.ts b/backend/src/database/models/customView.ts index b8a50389c5..8c3902d16b 100644 --- a/backend/src/database/models/customView.ts +++ b/backend/src/database/models/customView.ts @@ -1,4 +1,5 @@ import { DataTypes } from 'sequelize' + import { CustomViewVisibility } from '@crowd/types' export default (sequelize) => { @@ -28,7 +29,7 @@ export default (sequelize) => { }, placement: { type: DataTypes.TEXT, - isIn: [['member', 'organization', 'activity', 'conversation']], + isIn: [['member', 'organization', 'activity']], allowNull: false, }, tenantId: { diff --git a/backend/src/database/models/index.ts b/backend/src/database/models/index.ts index 947f3de616..4b5df452ad 100644 --- a/backend/src/database/models/index.ts +++ b/backend/src/database/models/index.ts @@ -1,9 +1,14 @@ +import pg from 'pg' import Sequelize, { DataTypes } from 'sequelize' + +import { IS_CLOUD_ENV } from '@crowd/common' + /** * This module creates the Sequelize to the database and * exports all the models. */ import { getServiceChildLogger } from '@crowd/logging' + import { DB_CONFIG, SERVICE } from '../../conf' import * as configTypes from '../../conf/configTypes' @@ -11,6 +16,8 @@ const { highlight } = require('cli-highlight') const log = getServiceChildLogger('Database') +pg.usingSequelize = true + interface Credentials { username: string password: string @@ -35,46 +42,50 @@ function getCredentials(): Credentials { username: DB_CONFIG.jobGeneratorUsername, password: DB_CONFIG.jobGeneratorPassword, } - case configTypes.ServiceType.NODEJS_WORKER: - return { - username: DB_CONFIG.nodejsWorkerUsername, - password: DB_CONFIG.nodejsWorkerPassword, - } default: throw new Error('Incorrectly configured database connection settings!') } } -function models(queryTimeoutMilliseconds: number) { +async function models(queryTimeoutMilliseconds: number, databaseHostnameOverride = null) { + log.info('Initializing sequelize database connection!') const database = {} as any + let readHost = SERVICE === configTypes.ServiceType.API ? DB_CONFIG.readHost : DB_CONFIG.writeHost + let writeHost = DB_CONFIG.writeHost + + if (databaseHostnameOverride) { + readHost = databaseHostnameOverride + writeHost = databaseHostnameOverride + } + const credentials = getCredentials() - const sequelize = new (Sequelize)( + const sequelize = new (Sequelize as any)( DB_CONFIG.database, credentials.username, credentials.password, { dialect: DB_CONFIG.dialect, dialectOptions: { - application_name: SERVICE, - connectionTimeoutMillis: 5000, + application_name: SERVICE ? `${SERVICE}-seq` : 'unknown-app-seq', + connectionTimeoutMillis: 15000, query_timeout: queryTimeoutMilliseconds, - idle_in_transaction_session_timeout: 10000, + idle_in_transaction_session_timeout: 20000, + ssl: IS_CLOUD_ENV ? { rejectUnauthorized: false } : false, }, port: DB_CONFIG.port, replication: { read: [ { - host: - SERVICE === configTypes.ServiceType.API ? DB_CONFIG.readHost : DB_CONFIG.writeHost, + host: readHost, }, ], - write: { host: DB_CONFIG.writeHost }, + write: { host: writeHost }, }, pool: { max: SERVICE === configTypes.ServiceType.API ? 20 : 10, - min: 0, + min: 1, acquire: 50000, idle: 10000, }, @@ -91,33 +102,33 @@ function models(queryTimeoutMilliseconds: number) { }, ) + // if (profileQueries) { + // const oldQuery = sequelize.query + // sequelize.query = async (query, options) => { + // const { replacements } = options || {} + // const result = await logExecutionTimeV2( + // () => oldQuery.apply(sequelize, [query, options]), + // log, + // `DB Query:\n${query}\n${replacements ? `Params: ${JSON.stringify(replacements)}` : ''}`, + // ) + + // return result + // } + // } + const modelClasses = [ - require('./activity').default, - require('./auditLog').default, require('./member').default, require('./memberIdentity').default, require('./file').default, require('./integration').default, - require('./report').default, require('./settings').default, - require('./tag').default, require('./tenant').default, require('./tenantUser').default, require('./user').default, - require('./widget').default, - require('./microservice').default, - require('./conversation').default, - require('./conversationSettings').default, require('./eagleEyeContent').default, require('./eagleEyeAction').default, - require('./automation').default, - require('./automationExecution').default, require('./organization').default, - require('./organizationCache').default, require('./memberAttributeSettings').default, - require('./task').default, - require('./note').default, - require('./memberActivityAggregatesMV').default, require('./segment').default, require('./customView').default, require('./customViewOrder').default, @@ -137,6 +148,9 @@ function models(queryTimeoutMilliseconds: number) { database.sequelize = sequelize database.Sequelize = Sequelize + await sequelize.authenticate() + log.info('Sequelize database connection has been established successfully!') + return database } diff --git a/backend/src/database/models/integration.ts b/backend/src/database/models/integration.ts index 35a668b090..c541c471ec 100644 --- a/backend/src/database/models/integration.ts +++ b/backend/src/database/models/integration.ts @@ -15,12 +15,6 @@ export default (sequelize) => { status: { type: DataTypes.TEXT, }, - limitCount: { - type: DataTypes.INTEGER, - }, - limitLastResetAt: { - type: DataTypes.DATE, - }, token: { type: DataTypes.TEXT, }, @@ -34,26 +28,9 @@ export default (sequelize) => { integrationIdentifier: { type: DataTypes.TEXT, }, - importHash: { - type: DataTypes.STRING(255), - allowNull: true, - validate: { - len: [0, 255], - }, - }, - emailSentAt: { - type: DataTypes.DATE, - }, }, { indexes: [ - { - unique: true, - fields: ['importHash', 'tenantId'], - where: { - deletedAt: null, - }, - }, { unique: false, fields: ['integrationIdentifier'], diff --git a/backend/src/database/models/member.ts b/backend/src/database/models/member.ts index 469bdf0d8e..39a4036854 100644 --- a/backend/src/database/models/member.ts +++ b/backend/src/database/models/member.ts @@ -20,10 +20,6 @@ export default (sequelize) => { notEmpty: true, }, }, - emails: { - type: DataTypes.ARRAY(DataTypes.TEXT), - defaultValue: [], - }, score: { type: DataTypes.INTEGER, defaultValue: -1, @@ -51,9 +47,6 @@ export default (sequelize) => { contributions: { type: DataTypes.JSONB, }, - lastEnriched: { - type: DataTypes.DATE, - }, enrichedBy: { type: DataTypes.ARRAY(DataTypes.TEXT), }, @@ -62,6 +55,11 @@ export default (sequelize) => { allowNull: false, defaultValue: false, }, + manuallyChangedFields: { + type: DataTypes.ARRAY(DataTypes.TEXT), + allowNull: true, + default: [], + }, }, { indexes: [ @@ -112,30 +110,6 @@ export default (sequelize) => { timestamps: false, }) - models.member.hasOne(models.memberActivityAggregatesMV, { - as: 'memberActivityAggregatesMVs', - foreignKey: 'id', - }) - - models.member.hasMany(models.activity, { - as: 'activities', - }) - - models.member.belongsToMany(models.note, { - as: 'notes', - through: 'memberNotes', - }) - - models.member.belongsToMany(models.task, { - as: 'tasks', - through: 'memberTasks', - }) - - models.member.belongsToMany(models.tag, { - as: 'tags', - through: 'memberTags', - }) - models.member.belongsToMany(models.member, { as: 'noMerge', through: 'memberNoMerge', diff --git a/backend/src/database/models/memberActivityAggregatesMV.ts b/backend/src/database/models/memberActivityAggregatesMV.ts deleted file mode 100644 index 117365264c..0000000000 --- a/backend/src/database/models/memberActivityAggregatesMV.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { DataTypes } from 'sequelize' - -export default (sequelize) => { - // define your materialized view model - const memberActivityAggregatesMV = sequelize.define('memberActivityAggregatesMV', { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - lastActive: { - type: DataTypes.DATE, - allowNull: false, - }, - activeOn: { - type: DataTypes.ARRAY(DataTypes.STRING), - }, - averageSentiment: { - type: DataTypes.FLOAT, - }, - activityCount: { - type: DataTypes.INTEGER, - }, - activeDaysCount: { - type: DataTypes.INTEGER, - }, - identities: { - type: DataTypes.ARRAY(DataTypes.STRING), - }, - username: { - type: DataTypes.JSONB, - }, - }) - - return memberActivityAggregatesMV -} diff --git a/backend/src/database/models/memberAttributeSettings.ts b/backend/src/database/models/memberAttributeSettings.ts index 67640e19ae..721c795a2b 100644 --- a/backend/src/database/models/memberAttributeSettings.ts +++ b/backend/src/database/models/memberAttributeSettings.ts @@ -1,6 +1,7 @@ -import { MemberAttributeType } from '@crowd/types' import { DataTypes } from 'sequelize' +import { MemberAttributeType } from '@crowd/types' + export default (sequelize) => { const memberAttributeSettings = sequelize.define( 'memberAttributeSettings', diff --git a/backend/src/database/models/memberIdentity.ts b/backend/src/database/models/memberIdentity.ts index 9ecb4ca2e6..3528bc4d51 100644 --- a/backend/src/database/models/memberIdentity.ts +++ b/backend/src/database/models/memberIdentity.ts @@ -2,17 +2,21 @@ import { DataTypes } from 'sequelize' export default (sequelize) => { const memberIdentity = sequelize.define('memberIdentity', { - memberId: { + id: { type: DataTypes.UUID, primaryKey: true, }, + memberId: { + type: DataTypes.UUID, + }, platform: { type: DataTypes.TEXT, - primaryKey: true, }, - username: { + value: { + type: DataTypes.TEXT, + }, + type: { type: DataTypes.TEXT, - primaryKey: true, }, sourceId: { type: DataTypes.TEXT, diff --git a/backend/src/database/models/microservice.ts b/backend/src/database/models/microservice.ts deleted file mode 100644 index fe56e16024..0000000000 --- a/backend/src/database/models/microservice.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { DataTypes } from 'sequelize' - -export default (sequelize) => { - const microservice = sequelize.define( - 'microservice', - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - init: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - }, - running: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - }, - type: { - type: DataTypes.TEXT, - allowNull: false, - }, - variant: { - type: DataTypes.TEXT, - validate: { - isIn: [['default', 'premium']], - }, - defaultValue: 'default', - }, - settings: { - type: DataTypes.JSONB, - allowNull: false, - defaultValue: {}, - }, - importHash: { - type: DataTypes.STRING(255), - allowNull: true, - validate: { - len: [0, 255], - }, - }, - }, - { - indexes: [ - { - unique: true, - fields: ['importHash', 'tenantId'], - }, - { - unique: true, - fields: ['type', 'tenantId'], - }, - ], - timestamps: true, - }, - ) - - microservice.associate = (models) => { - models.microservice.belongsTo(models.tenant, { - as: 'tenant', - foreignKey: { - allowNull: false, - }, - }) - - models.microservice.belongsTo(models.user, { - as: 'createdBy', - }) - - models.microservice.belongsTo(models.user, { - as: 'updatedBy', - }) - } - - return microservice -} diff --git a/backend/src/database/models/note.ts b/backend/src/database/models/note.ts deleted file mode 100644 index e360f8e5a0..0000000000 --- a/backend/src/database/models/note.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { DataTypes } from 'sequelize' - -export default (sequelize) => { - const note = sequelize.define( - 'note', - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - body: { - type: DataTypes.TEXT, - allowNull: false, - }, - importHash: { - type: DataTypes.STRING(255), - allowNull: true, - validate: { - len: [0, 255], - }, - }, - }, - { - indexes: [], - timestamps: true, - paranoid: true, - }, - ) - - note.associate = (models) => { - models.note.belongsToMany(models.member, { - as: 'members', - through: 'memberNotes', - foreignKey: 'noteId', - }) - - models.note.belongsTo(models.tenant, { - as: 'tenant', - foreignKey: { - allowNull: false, - }, - }) - - models.note.belongsTo(models.user, { - as: 'createdBy', - }) - - models.note.belongsTo(models.user, { - as: 'updatedBy', - }) - } - - return note -} diff --git a/backend/src/database/models/organization.ts b/backend/src/database/models/organization.ts index f9002bcd3e..c2c04ef769 100644 --- a/backend/src/database/models/organization.ts +++ b/backend/src/database/models/organization.ts @@ -9,43 +9,46 @@ export default (sequelize) => { defaultValue: DataTypes.UUIDV4, primaryKey: true, }, - displayName: { - type: DataTypes.TEXT, - allowNull: false, + importHash: { + type: DataTypes.STRING(255), + allowNull: true, validate: { - notEmpty: true, + len: [0, 255], }, }, - website: { - type: DataTypes.TEXT, - allowNull: true, + isTeamOrganization: { + type: DataTypes.BOOLEAN, + defaultValue: false, + allowNull: false, }, - location: { - type: DataTypes.TEXT, - allowNull: true, + isAffiliationBlocked: { + type: DataTypes.BOOLEAN, + defaultValue: false, + allowNull: false, }, - description: { - type: DataTypes.TEXT, + + lastEnrichedAt: { + type: DataTypes.DATE, allowNull: true, - comment: 'A detailed description of the company', }, - immediateParent: { - type: DataTypes.TEXT, - allowNull: true, + manuallyCreated: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, }, - ultimateParent: { + + displayName: { type: DataTypes.TEXT, allowNull: true, }, - emails: { - type: DataTypes.ARRAY(DataTypes.TEXT), + location: { + type: DataTypes.TEXT, allowNull: true, - default: [], }, - phoneNumbers: { - type: DataTypes.ARRAY(DataTypes.TEXT), + description: { + type: DataTypes.TEXT, allowNull: true, - default: [], + comment: 'A detailed description of the company', }, logo: { type: DataTypes.TEXT, @@ -56,22 +59,6 @@ export default (sequelize) => { allowNull: true, default: [], }, - github: { - type: DataTypes.JSONB, - default: {}, - }, - twitter: { - type: DataTypes.JSONB, - default: {}, - }, - linkedin: { - type: DataTypes.JSONB, - default: {}, - }, - crunchbase: { - type: DataTypes.JSONB, - default: {}, - }, employees: { type: DataTypes.INTEGER, allowNull: true, @@ -82,18 +69,6 @@ export default (sequelize) => { allowNull: true, comment: 'inferred revenue range of the company', }, - importHash: { - type: DataTypes.STRING(255), - allowNull: true, - validate: { - len: [0, 255], - }, - }, - isTeamOrganization: { - type: DataTypes.BOOLEAN, - defaultValue: false, - allowNull: false, - }, founded: { type: DataTypes.INTEGER, allowNull: true, @@ -107,120 +82,24 @@ export default (sequelize) => { allowNull: true, comment: 'A range representing the size of the company.', }, - naics: { - type: DataTypes.ARRAY(DataTypes.JSONB), - allowNull: true, - comment: 'industry classifications for a company according to NAICS', - }, headline: { type: DataTypes.TEXT, allowNull: true, comment: 'A brief description of the company', }, - ticker: { - type: DataTypes.TEXT, - allowNull: true, - comment: "the company's stock symbol", - }, - geoLocation: { - type: DataTypes.STRING, - allowNull: true, - }, type: { type: DataTypes.TEXT, allowNull: true, comment: "The company's type. For example NGO", }, - employeeCountByCountry: { - type: DataTypes.JSONB, - allowNull: true, - }, - address: { - type: DataTypes.JSONB, - allowNull: true, - comment: "granular information about the location of the company's current headquarters.", - }, - profiles: { - type: DataTypes.ARRAY(DataTypes.TEXT), - allowNull: true, - }, - lastEnrichedAt: { - type: DataTypes.DATE, - allowNull: true, - }, - manuallyCreated: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - }, - attributes: { - type: DataTypes.JSONB, - defaultValue: {}, - }, - affiliatedProfiles: { - type: DataTypes.ARRAY(DataTypes.TEXT), - allowNull: true, - }, - allSubsidiaries: { - type: DataTypes.ARRAY(DataTypes.TEXT), - allowNull: true, - }, - alternativeDomains: { - type: DataTypes.ARRAY(DataTypes.TEXT), - allowNull: true, - }, - alternativeNames: { - type: DataTypes.ARRAY(DataTypes.TEXT), - allowNull: true, - }, - averageEmployeeTenure: { - type: DataTypes.FLOAT, - allowNull: true, - }, - averageTenureByLevel: { - type: DataTypes.JSONB, - allowNull: true, - }, - averageTenureByRole: { - type: DataTypes.JSONB, - allowNull: true, - }, - directSubsidiaries: { - type: DataTypes.ARRAY(DataTypes.TEXT), - allowNull: true, - }, employeeChurnRate: { type: DataTypes.JSONB, allowNull: true, }, - employeeCountByMonth: { - type: DataTypes.JSONB, - allowNull: true, - }, employeeGrowthRate: { type: DataTypes.JSONB, allowNull: true, }, - employeeCountByMonthByLevel: { - type: DataTypes.JSONB, - allowNull: true, - }, - employeeCountByMonthByRole: { - type: DataTypes.JSONB, - allowNull: true, - }, - gicsSector: { - type: DataTypes.TEXT, - allowNull: true, - }, - grossAdditionsByMonth: { - type: DataTypes.JSONB, - allowNull: true, - }, - grossDeparturesByMonth: { - type: DataTypes.JSONB, - allowNull: true, - }, }, { indexes: [ diff --git a/backend/src/database/models/organizationCache.ts b/backend/src/database/models/organizationCache.ts deleted file mode 100644 index 049400d403..0000000000 --- a/backend/src/database/models/organizationCache.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { DataTypes, Op } from 'sequelize' - -export default (sequelize) => { - const organizationCache = sequelize.define( - 'organizationCache', - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - name: { - type: DataTypes.TEXT, - allowNull: false, - }, - url: { - type: DataTypes.TEXT, - allowNull: true, - }, - description: { - type: DataTypes.TEXT, - allowNull: true, - }, - emails: { - type: DataTypes.ARRAY(DataTypes.TEXT), - allowNull: true, - default: [], - }, - phoneNumbers: { - type: DataTypes.ARRAY(DataTypes.TEXT), - allowNull: true, - default: [], - }, - logo: { - type: DataTypes.TEXT, - allowNull: true, - }, - tags: { - type: DataTypes.ARRAY(DataTypes.TEXT), - allowNull: true, - default: [], - }, - twitter: { - type: DataTypes.JSONB, - default: {}, - }, - linkedin: { - type: DataTypes.JSONB, - default: {}, - }, - github: { - type: DataTypes.JSONB, - default: {}, - }, - crunchbase: { - type: DataTypes.JSONB, - default: {}, - }, - employees: { - type: DataTypes.INTEGER, - allowNull: true, - }, - revenueRange: { - type: DataTypes.JSONB, - allowNull: true, - }, - importHash: { - type: DataTypes.STRING(255), - allowNull: true, - validate: { - len: [0, 255], - }, - }, - location: { - type: DataTypes.TEXT, - allowNull: true, - }, - website: { - type: DataTypes.TEXT, - allowNull: true, - }, - founded: { - type: DataTypes.INTEGER, - allowNull: true, - }, - industry: { - type: DataTypes.TEXT, - allowNull: true, - }, - size: { - type: DataTypes.TEXT, - allowNull: true, - comment: 'A range representing the size of the company.', - }, - naics: { - type: DataTypes.ARRAY(DataTypes.JSONB), - allowNull: true, - comment: 'industry classifications for a company according to NAICS', - }, - headline: { - type: DataTypes.TEXT, - allowNull: true, - comment: 'A brief description of the company', - }, - ticker: { - type: DataTypes.TEXT, - allowNull: true, - comment: "the company's stock symbol", - }, - geoLocation: { - type: DataTypes.STRING, - allowNull: true, - }, - type: { - type: DataTypes.TEXT, - allowNull: true, - comment: "The company's type. For example NGO", - }, - employeeCountByCountry: { - type: DataTypes.JSONB, - allowNull: true, - }, - address: { - type: DataTypes.JSONB, - allowNull: true, - comment: "granular information about the location of the company's current headquarters.", - }, - lastEnrichedAt: { - type: DataTypes.DATE, - allowNull: true, - }, - manuallyCreated: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - }, - }, - { - indexes: [ - { - fields: ['url'], - unique: true, - where: { - deletedAt: null, - url: { [Op.ne]: null }, - }, - }, - ], - timestamps: true, - paranoid: true, - }, - ) - - return organizationCache -} diff --git a/backend/src/database/models/report.ts b/backend/src/database/models/report.ts deleted file mode 100644 index 0ab7cafdc7..0000000000 --- a/backend/src/database/models/report.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { DataTypes } from 'sequelize' - -export default (sequelize) => { - const report = sequelize.define( - 'report', - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - public: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - }, - isTemplate: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - }, - name: { - type: DataTypes.TEXT, - allowNull: false, - }, - importHash: { - type: DataTypes.STRING(255), - allowNull: true, - validate: { - len: [0, 255], - }, - }, - viewedBy: { - type: DataTypes.ARRAY(DataTypes.TEXT), - }, - }, - { - indexes: [ - { - unique: true, - fields: ['importHash', 'tenantId'], - where: { - deletedAt: null, - }, - }, - ], - timestamps: true, - paranoid: true, - }, - ) - - report.associate = (models) => { - models.report.hasMany(models.widget, { - as: 'widgets', - constraints: false, - foreignKey: 'reportId', - onDelete: 'cascade', - }) - - models.report.belongsTo(models.tenant, { - as: 'tenant', - foreignKey: { - allowNull: false, - }, - }) - - models.report.belongsTo(models.segment, { - as: 'segment', - }) - - models.report.belongsTo(models.user, { - as: 'createdBy', - }) - - models.report.belongsTo(models.user, { - as: 'updatedBy', - }) - } - - return report -} diff --git a/backend/src/database/models/settings.ts b/backend/src/database/models/settings.ts index 739bfa20da..fe6793b640 100644 --- a/backend/src/database/models/settings.ts +++ b/backend/src/database/models/settings.ts @@ -21,6 +21,12 @@ export default (sequelize, DataTypes) => { slackWebHook: { type: DataTypes.STRING(1024), }, + organizationsViewed: { + type: DataTypes.BOOLEAN(), + }, + contactsViewed: { + type: DataTypes.BOOLEAN(), + }, attributeSettings: { type: DataTypes.JSONB, allowNull: false, diff --git a/backend/src/database/models/tag.ts b/backend/src/database/models/tag.ts deleted file mode 100644 index c0c79c13e4..0000000000 --- a/backend/src/database/models/tag.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { DataTypes } from 'sequelize' - -export default (sequelize) => { - const tag = sequelize.define( - 'tag', - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - name: { - type: DataTypes.TEXT, - allowNull: false, - }, - importHash: { - type: DataTypes.STRING(255), - allowNull: true, - validate: { - len: [0, 255], - }, - }, - }, - { - indexes: [ - { - unique: true, - fields: ['importHash', 'tenantId'], - where: { - deletedAt: null, - }, - }, - { - unique: true, - fields: ['name', 'tenantId'], - where: { - deletedAt: null, - }, - }, - ], - timestamps: true, - paranoid: true, - }, - ) - - tag.associate = (models) => { - models.tag.belongsToMany(models.member, { - as: 'members', - through: 'memberTags', - foreignKey: 'tagId', - }) - - models.tag.belongsTo(models.tenant, { - as: 'tenant', - foreignKey: { - allowNull: false, - }, - }) - - models.tag.belongsTo(models.user, { - as: 'createdBy', - }) - - models.tag.belongsTo(models.user, { - as: 'updatedBy', - }) - } - - return tag -} diff --git a/backend/src/database/models/task.ts b/backend/src/database/models/task.ts deleted file mode 100644 index 8165838d67..0000000000 --- a/backend/src/database/models/task.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { DataTypes } from 'sequelize' - -export default (sequelize) => { - const task = sequelize.define( - 'task', - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - name: { - type: DataTypes.TEXT, - allowNull: false, - }, - body: { - type: DataTypes.TEXT, - }, - type: { - type: DataTypes.STRING(255), - validate: { - isIn: [['regular', 'suggested']], - }, - defaultValue: 'regular', - }, - status: { - type: DataTypes.STRING(255), - validate: { - isIn: [['in-progress', 'done', 'archived']], - }, - defaultValue: 'in-progress', - }, - dueDate: { - type: DataTypes.DATE, - }, - importHash: { - type: DataTypes.STRING(255), - allowNull: true, - validate: { - len: [0, 255], - }, - }, - }, - { - indexes: [ - { - unique: true, - fields: ['importHash', 'tenantId'], - where: { - deletedAt: null, - }, - }, - { - fields: ['name', 'tenantId'], - where: { - deletedAt: null, - }, - }, - ], - timestamps: true, - paranoid: true, - }, - ) - - task.associate = (models) => { - models.task.belongsToMany(models.member, { - as: 'members', - through: 'memberTasks', - foreignKey: 'taskId', - }) - - models.task.belongsToMany(models.activity, { - as: 'activities', - through: 'activityTasks', - foreignKey: 'taskId', - }) - - models.task.belongsTo(models.tenant, { - as: 'tenant', - foreignKey: { - allowNull: false, - }, - }) - - models.task.belongsTo(models.segment, { - as: 'segment', - foreignKey: { - allowNull: false, - }, - }) - - models.task.belongsToMany(models.user, { - as: 'assignees', - through: 'taskAssignees', - foreignKey: 'taskId', - }) - - models.task.belongsTo(models.user, { - as: 'createdBy', - }) - - models.task.belongsTo(models.user, { - as: 'updatedBy', - }) - } - - return task -} diff --git a/backend/src/database/models/tenant.ts b/backend/src/database/models/tenant.ts index b9f27f41ee..caa4c458b0 100644 --- a/backend/src/database/models/tenant.ts +++ b/backend/src/database/models/tenant.ts @@ -1,7 +1,3 @@ -import Plans from '../../security/plans' - -const plans = Plans.values - export default (sequelize, DataTypes) => { const tenant = sequelize.define( 'tenant', @@ -39,15 +35,6 @@ export default (sequelize, DataTypes) => { reasonForUsingCrowd: { type: DataTypes.STRING(50), }, - plan: { - type: DataTypes.STRING(255), - allowNull: false, - validate: { - notEmpty: true, - isIn: [[plans.essential, plans.growth, plans.eagleEye, plans.enterprise, plans.scale]], - }, - defaultValue: plans.essential, - }, onboardedAt: { type: DataTypes.DATE, @@ -66,14 +53,6 @@ export default (sequelize, DataTypes) => { type: DataTypes.DATE, defaultValue: null, }, - stripeSubscriptionId: { - type: DataTypes.TEXT, - defaultValue: null, - }, - planSubscriptionEndsAt: { - type: DataTypes.DATE, - defaultValue: null, - }, }, { indexes: [ @@ -95,10 +74,6 @@ export default (sequelize, DataTypes) => { as: 'settings', }) - models.tenant.hasMany(models.conversationSettings, { - as: 'conversationSettings', - }) - models.tenant.hasMany(models.tenantUser, { as: 'users', }) diff --git a/backend/src/database/models/tenantUser.ts b/backend/src/database/models/tenantUser.ts index 0944307928..a3f2cf7efb 100644 --- a/backend/src/database/models/tenantUser.ts +++ b/backend/src/database/models/tenantUser.ts @@ -28,6 +28,9 @@ export default (sequelize, DataTypes) => { }, }, }, + adminSegments: { + type: SequelizeArrayUtils.DataType, + }, invitationToken: { type: DataTypes.STRING(255), allowNull: true, @@ -45,7 +48,6 @@ export default (sequelize, DataTypes) => { allowNull: false, defaultValue: { isEagleEyeGuideDismissed: false, - isQuickstartGuideDismissed: false, eagleEye: { onboarded: false, }, diff --git a/backend/src/database/models/widget.ts b/backend/src/database/models/widget.ts deleted file mode 100644 index 7b28510b12..0000000000 --- a/backend/src/database/models/widget.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { DataTypes } from 'sequelize' - -export default (sequelize) => { - const widget = sequelize.define( - 'widget', - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - type: { - type: DataTypes.TEXT, - allowNull: false, - validate: { - notEmpty: true, - }, - }, - title: { - type: DataTypes.TEXT, - }, - settings: { - type: DataTypes.JSONB, - }, - cache: { - type: DataTypes.JSONB, - }, - importHash: { - type: DataTypes.STRING(255), - allowNull: true, - validate: { - len: [0, 255], - }, - }, - }, - { - indexes: [ - { - unique: true, - fields: ['importHash', 'tenantId'], - where: { - deletedAt: null, - }, - }, - ], - timestamps: true, - paranoid: true, - }, - ) - - widget.associate = (models) => { - models.widget.belongsTo(models.report, { - as: 'report', - }) - - models.widget.belongsTo(models.tenant, { - as: 'tenant', - foreignKey: { - allowNull: false, - }, - }) - - models.widget.belongsTo(models.segment, { - as: 'segment', - foreignKey: { - allowNull: false, - }, - }) - - models.widget.belongsTo(models.user, { - as: 'createdBy', - }) - - models.widget.belongsTo(models.user, { - as: 'updatedBy', - }) - } - - return widget -} diff --git a/backend/src/database/repositories/IRepositoryOptions.ts b/backend/src/database/repositories/IRepositoryOptions.ts index 5eea2a2e15..069b1d248a 100644 --- a/backend/src/database/repositories/IRepositoryOptions.ts +++ b/backend/src/database/repositories/IRepositoryOptions.ts @@ -1,6 +1,8 @@ +import { DbConnection } from '@crowd/data-access-layer/src/database' import { Logger } from '@crowd/logging' import { RedisClient } from '@crowd/redis' -import { SegmentData } from '../../types/segmentTypes' +import { Client as TemporalClient } from '@crowd/temporal' +import { SegmentData } from '@crowd/types' export interface IRepositoryOptions { log: Logger @@ -13,4 +15,6 @@ export interface IRepositoryOptions { transaction?: any bypassPermissionValidation?: any opensearch?: any + temporal: TemporalClient + productDb: DbConnection } diff --git a/backend/src/database/repositories/__tests__/activityRepository.test.ts b/backend/src/database/repositories/__tests__/activityRepository.test.ts deleted file mode 100644 index 25612596b8..0000000000 --- a/backend/src/database/repositories/__tests__/activityRepository.test.ts +++ /dev/null @@ -1,1765 +0,0 @@ -import MemberRepository from '../memberRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import Error404 from '../../../errors/Error404' -import ActivityRepository from '../activityRepository' -import { MemberAttributeName, PlatformType } from '@crowd/types' -import TaskRepository from '../taskRepository' -import MemberAttributeSettingsRepository from '../memberAttributeSettingsRepository' -import MemberAttributeSettingsService from '../../../services/memberAttributeSettingsService' -import { DEFAULT_MEMBER_ATTRIBUTES, UNKNOWN_ACTIVITY_TYPE_DISPLAY } from '@crowd/integrations' -import OrganizationRepository from '../organizationRepository' - -const db = null - -describe('ActivityRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create the given activity succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - title: 'Title', - body: 'Here', - url: 'https://github.com', - channel: 'channel', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - const activityCreated = await ActivityRepository.create(activity, mockIRepositoryOptions) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - activityCreated.createdAt = activityCreated.createdAt.toISOString().split('T')[0] - activityCreated.updatedAt = activityCreated.updatedAt.toISOString().split('T')[0] - delete activityCreated.member - delete activityCreated.objectMember - - const expectedActivityCreated = { - id: activityCreated.id, - attributes: activity.attributes, - body: 'Here', - type: 'activity', - title: 'Title', - url: 'https://github.com', - channel: 'channel', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - timestamp: new Date('2020-05-27T15:13:30Z'), - platform: PlatformType.GITHUB, - isContribution: true, - score: 1, - username: 'test', - objectMemberUsername: null, - memberId: memberCreated.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - tasks: [], - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parent: null, - parentId: null, - sourceId: activity.sourceId, - sourceParentId: null, - conversationId: null, - display: UNKNOWN_ACTIVITY_TYPE_DISPLAY, - organizationId: null, - organization: null, - } - - expect(activityCreated).toStrictEqual(expectedActivityCreated) - }) - - it('Should create a bare-bones activity succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - member: memberCreated.id, - username: 'test', - sourceId: '#sourceId1', - } - - const activityCreated = await ActivityRepository.create(activity, mockIRepositoryOptions) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - activityCreated.createdAt = activityCreated.createdAt.toISOString().split('T')[0] - activityCreated.updatedAt = activityCreated.updatedAt.toISOString().split('T')[0] - delete activityCreated.member - delete activityCreated.objectMember - - const expectedActivityCreated = { - id: activityCreated.id, - attributes: {}, - body: null, - title: null, - url: null, - channel: null, - sentiment: {}, - type: 'activity', - timestamp: new Date('2020-05-27T15:13:30Z'), - platform: PlatformType.GITHUB, - isContribution: false, - score: 2, - username: 'test', - objectMemberUsername: null, - memberId: memberCreated.id, - objectMemberId: null, - tasks: [], - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parent: null, - parentId: null, - sourceId: activityCreated.sourceId, - sourceParentId: null, - conversationId: null, - display: UNKNOWN_ACTIVITY_TYPE_DISPLAY, - organizationId: null, - organization: null, - } - - expect(activityCreated).toStrictEqual(expectedActivityCreated) - }) - - it('Should throw error when no platform given', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - attributes: { - replies: 12, - }, - body: 'Here', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - } - - await expect(() => - ActivityRepository.create(activity, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should throw error when no type given', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity = { - platform: 'activity', - timestamp: '2020-05-27T15:13:30Z', - attributes: { - replies: 12, - }, - username: 'test', - body: 'Here', - isContribution: true, - member: memberCreated.id, - score: 1, - } - - await expect(() => - ActivityRepository.create(activity, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should throw error when no timestamp given', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity = { - platform: PlatformType.GITHUB, - type: 'activity', - attributes: { - replies: 12, - }, - username: 'test', - body: 'Here', - isContribution: true, - member: memberCreated.id, - score: 1, - } - - await expect(() => - ActivityRepository.create(activity, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should throw error when sentiment is incorrect', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - // Incomplete Object - await expect(() => - ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 1, - sentiment: 'positive', - score: 1, - }, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ), - ).rejects.toThrow() - - // No score - await expect(() => - ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 0.8, - negative: 0.2, - mixed: 0, - neutral: 0, - sentiment: 'positive', - }, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ), - ).rejects.toThrow() - - // Wrong Sentiment field - await expect(() => - ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 0.3, - negative: 0.2, - neutral: 0.5, - mixed: 0, - score: 0.1, - sentiment: 'smth', - }, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ), - ).rejects.toThrow() - - // Works with empty object - const created = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: {}, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - expect(created.sentiment).toStrictEqual({}) - }) - - it('Should leave allowed HTML tags in body and title', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: '

This is some HTML

', - title: '

This is some Title HTML

', - url: 'https://github.com', - channel: 'channel', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - const activityCreated = await ActivityRepository.create(activity, mockIRepositoryOptions) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - activityCreated.createdAt = activityCreated.createdAt.toISOString().split('T')[0] - activityCreated.updatedAt = activityCreated.updatedAt.toISOString().split('T')[0] - delete activityCreated.member - delete activityCreated.objectMember - - const expectedActivityCreated = { - id: activityCreated.id, - attributes: {}, - body: '

This is some HTML

', - type: 'activity', - title: '

This is some Title HTML

', - url: 'https://github.com', - channel: 'channel', - sentiment: {}, - timestamp: new Date('2020-05-27T15:13:30Z'), - platform: PlatformType.GITHUB, - isContribution: true, - score: 1, - tasks: [], - username: 'test', - objectMemberUsername: null, - memberId: memberCreated.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parent: null, - parentId: null, - sourceId: activity.sourceId, - sourceParentId: null, - conversationId: null, - display: UNKNOWN_ACTIVITY_TYPE_DISPLAY, - organizationId: null, - organization: null, - } - - expect(activityCreated).toStrictEqual(expectedActivityCreated) - }) - - it('Should remove script tags in body and title', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: "

Malicious

", - title: "

Malicious title

", - url: 'https://github.com', - channel: 'channel', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - const activityCreated = await ActivityRepository.create(activity, mockIRepositoryOptions) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - activityCreated.createdAt = activityCreated.createdAt.toISOString().split('T')[0] - activityCreated.updatedAt = activityCreated.updatedAt.toISOString().split('T')[0] - delete activityCreated.member - delete activityCreated.objectMember - - const expectedActivityCreated = { - id: activityCreated.id, - attributes: {}, - body: '

Malicious

', - type: 'activity', - title: '

Malicious title

', - url: 'https://github.com', - channel: 'channel', - sentiment: {}, - tasks: [], - timestamp: new Date('2020-05-27T15:13:30Z'), - platform: PlatformType.GITHUB, - isContribution: true, - score: 1, - username: 'test', - objectMemberUsername: null, - memberId: memberCreated.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parent: null, - parentId: null, - sourceId: activity.sourceId, - sourceParentId: null, - conversationId: null, - display: UNKNOWN_ACTIVITY_TYPE_DISPLAY, - organizationId: null, - organization: null, - } - - expect(activityCreated).toStrictEqual(expectedActivityCreated) - }) - - it('Should create an activity with tasks succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const tasks1 = await TaskRepository.create( - { - name: 'task1', - }, - mockIRepositoryOptions, - ) - - const task2 = await TaskRepository.create( - { - name: 'task2', - }, - mockIRepositoryOptions, - ) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - title: 'Title', - body: 'Here', - url: 'https://github.com', - channel: 'channel', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - tasks: [tasks1.id, task2.id], - sourceId: '#sourceId1', - } - - const activityCreated = await ActivityRepository.create(activity, mockIRepositoryOptions) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - expect(activityCreated.tasks.length).toBe(2) - }) - - it('Should create an activity with an organization succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const org1 = await OrganizationRepository.create( - { - displayName: 'crowd.dev', - }, - mockIRepositoryOptions, - ) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - title: 'Title', - body: 'Here', - url: 'https://github.com', - channel: 'channel', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - organizationId: org1.id, - sourceId: '#sourceId1', - } - - const activityCreated = await ActivityRepository.create(activity, mockIRepositoryOptions) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - expect(activityCreated.organizationId).toEqual(org1.id) - }) - }) - - describe('findById method', () => { - it('Should successfully find created activity by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - const activityCreated = await ActivityRepository.create(activity, mockIRepositoryOptions) - - const expectedActivityFound = { - id: activityCreated.id, - attributes: {}, - body: null, - title: null, - url: null, - channel: null, - sentiment: {}, - type: 'activity', - timestamp: new Date('2020-05-27T15:13:30Z'), - platform: PlatformType.GITHUB, - isContribution: true, - score: 1, - username: 'test', - objectMemberUsername: null, - memberId: memberCreated.id, - objectMemberId: null, - tasks: [], - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parent: null, - parentId: null, - sourceId: activity.sourceId, - sourceParentId: null, - conversationId: null, - display: UNKNOWN_ACTIVITY_TYPE_DISPLAY, - organizationId: null, - organization: null, - } - - const activityFound = await ActivityRepository.findById( - activityCreated.id, - mockIRepositoryOptions, - ) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - activityFound.createdAt = activityFound.createdAt.toISOString().split('T')[0] - activityFound.updatedAt = activityFound.updatedAt.toISOString().split('T')[0] - delete activityFound.member - delete activityFound.objectMember - - expect(activityFound).toStrictEqual(expectedActivityFound) - }) - - it('Should throw 404 error when no user found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - ActivityRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('filterIdsInTenant method', () => { - it('Should return the given ids of previously created activity entities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity1Returned = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - const activity2Returned = await ActivityRepository.create( - { - type: 'activity-2', - timestamp: '2020-06-27T15:13:30Z', - platform: PlatformType.GITHUB, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId2', - }, - mockIRepositoryOptions, - ) - - const filterIdsReturned = await ActivityRepository.filterIdsInTenant( - [activity1Returned.id, activity2Returned.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([activity1Returned.id, activity2Returned.id]) - }) - - it('Should only return the ids of previously created activities and filter random uuids out', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity3Returned = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - const { randomUUID } = require('crypto') - - const filterIdsReturned = await ActivityRepository.filterIdsInTenant( - [activity3Returned.id, randomUUID(), randomUUID()], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([activity3Returned.id]) - }) - - it('Should return an empty array for an irrelevant tenant', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity4Returned = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - // create a new tenant and bind options to it - const mockIRepositoryOptionsIr = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const filterIdsReturned = await ActivityRepository.filterIdsInTenant( - [activity4Returned.id], - mockIRepositoryOptionsIr, - ) - - expect(filterIdsReturned).toStrictEqual([]) - }) - }) - - describe('Activities findOne method', () => { - it('Should return the created activity for a simple query', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activityReturned = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - const found = await ActivityRepository.findOne({ type: 'activity' }, mockIRepositoryOptions) - - expect(found.id).toStrictEqual(activityReturned.id) - }) - - it('Should return the activity for a complex query', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activityReturned = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - thread: true, - }, - body: 'Here', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - const found = await ActivityRepository.findOne( - { 'attributes.thread': true }, - mockIRepositoryOptions, - ) - - expect(found.id).toStrictEqual(activityReturned.id) - }) - - it('Should return null when non-existent', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - body: 'Here', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - expect( - await ActivityRepository.findOne({ type: 'notype' }, mockIRepositoryOptions), - ).toBeNull() - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created activity - simple', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activityReturned = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - body: 'Here', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - const updateFields = { - type: 'activity-new', - platform: PlatformType.GITHUB, - } - - const updatedActivity = await ActivityRepository.update( - activityReturned.id, - updateFields, - mockIRepositoryOptions, - ) - - // check updatedAt field looks ok or not. Should be greater than createdAt - expect(updatedActivity.updatedAt.getTime()).toBeGreaterThan( - updatedActivity.createdAt.getTime(), - ) - - updatedActivity.createdAt = updatedActivity.createdAt.toISOString().split('T')[0] - updatedActivity.updatedAt = updatedActivity.updatedAt.toISOString().split('T')[0] - delete updatedActivity.member - delete updatedActivity.objectMember - - const expectedActivityUpdated = { - id: activityReturned.id, - body: activityReturned.body, - channel: null, - title: null, - sentiment: {}, - url: null, - attributes: activityReturned.attributes, - type: 'activity-new', - timestamp: new Date('2020-05-27T15:13:30Z'), - platform: PlatformType.GITHUB, - isContribution: true, - score: 1, - username: 'test', - objectMemberUsername: null, - memberId: memberCreated.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - tasks: [], - parent: null, - parentId: null, - sourceId: activityReturned.sourceId, - sourceParentId: null, - conversationId: null, - display: UNKNOWN_ACTIVITY_TYPE_DISPLAY, - organizationId: null, - organization: null, - } - - expect(updatedActivity).toStrictEqual(expectedActivityUpdated) - }) - - it('Should succesfully update previously created activity - with member relation', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const memberCreated2 = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test2', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activityReturned = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - body: 'Here', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - const updateFields = { - type: 'activity-new', - platform: PlatformType.GITHUB, - body: 'There', - title: 'Title', - channel: 'Channel', - url: 'https://www.google.com', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - username: 'test2', - member: memberCreated2.id, - } - - const updatedActivity = await ActivityRepository.update( - activityReturned.id, - updateFields, - mockIRepositoryOptions, - ) - - // check updatedAt field looks ok or not. Should be greater than createdAt - expect(updatedActivity.updatedAt.getTime()).toBeGreaterThan( - updatedActivity.createdAt.getTime(), - ) - - updatedActivity.createdAt = updatedActivity.createdAt.toISOString().split('T')[0] - updatedActivity.updatedAt = updatedActivity.updatedAt.toISOString().split('T')[0] - delete updatedActivity.member - delete updatedActivity.objectMember - - const expectedActivityUpdated = { - id: activityReturned.id, - attributes: activityReturned.attributes, - body: updateFields.body, - channel: updateFields.channel, - title: updateFields.title, - sentiment: updateFields.sentiment, - url: updateFields.url, - type: 'activity-new', - timestamp: new Date('2020-05-27T15:13:30Z'), - tasks: [], - platform: PlatformType.GITHUB, - isContribution: true, - score: 1, - username: 'test2', - objectMemberUsername: null, - memberId: memberCreated2.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parent: null, - parentId: null, - sourceId: activityReturned.sourceId, - sourceParentId: null, - conversationId: null, - display: UNKNOWN_ACTIVITY_TYPE_DISPLAY, - organizationId: null, - organization: null, - } - - expect(updatedActivity).toStrictEqual(expectedActivityUpdated) - }) - - it('Should succesfully update tasks of an activity', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activityReturned = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - body: 'Here', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - const tasks1 = await TaskRepository.create( - { - name: 'task1', - }, - mockIRepositoryOptions, - ) - - const task2 = await TaskRepository.create( - { - name: 'task2', - }, - mockIRepositoryOptions, - ) - - const updateFields = { - tasks: [tasks1.id, task2.id], - } - - const updatedActivity = await ActivityRepository.update( - activityReturned.id, - updateFields, - mockIRepositoryOptions, - ) - - expect(updatedActivity.tasks).toHaveLength(2) - expect(updatedActivity.tasks[0].id).toBe(tasks1.id) - expect(updatedActivity.tasks[1].id).toBe(task2.id) - }) - - it('Should update body and title with allowed HTML tags', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activityReturned = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - body: 'Here', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - const updateFields = { - body: '

This is some HTML

', - title: '

This is some Title HTML

', - } - - const updatedActivity = await ActivityRepository.update( - activityReturned.id, - updateFields, - mockIRepositoryOptions, - ) - - expect(updatedActivity.body).toBe('

This is some HTML

') - expect(updatedActivity.title).toBe('

This is some Title HTML

') - }) - - it('Should sanitize body and title from non-allowed HTML tags', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activityReturned = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - body: 'Here', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - const updateFields = { - body: "

Malicious

", - title: "

Malicious title

", - } - - const updatedActivity = await ActivityRepository.update( - activityReturned.id, - updateFields, - mockIRepositoryOptions, - ) - - expect(updatedActivity.body).toBe('

Malicious

') - expect(updatedActivity.title).toBe('

Malicious title

') - }) - - it('Should update an activity with an organization succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const org1 = await OrganizationRepository.create( - { - displayName: 'crowd.dev', - }, - mockIRepositoryOptions, - ) - - const org2 = await OrganizationRepository.create( - { - displayName: 'tesla', - }, - mockIRepositoryOptions, - ) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - title: 'Title', - body: 'Here', - url: 'https://github.com', - channel: 'channel', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - organizationId: org1.id, - sourceId: '#sourceId1', - } - - const activityCreated = await ActivityRepository.create(activity, mockIRepositoryOptions) - - const activityUpdated = await ActivityRepository.update( - activityCreated.id, - { organizationId: org2.id }, - mockIRepositoryOptions, - ) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - expect(activityUpdated.organizationId).toEqual(org2.id) - }) - }) - - describe('filter tests', () => { - it('Positive sentiment filter and sort', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity1 = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId1', - } - - const activity2 = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 0.55, - negative: 0.0, - neutral: 0.45, - mixed: 0.0, - label: 'neutral', - sentiment: 0.55, - }, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId2', - } - - const activityCreated1 = await ActivityRepository.create(activity1, mockIRepositoryOptions) - await ActivityRepository.create(activity2, mockIRepositoryOptions) - - // Control - expect( - (await ActivityRepository.findAndCountAll({ filter: {} }, mockIRepositoryOptions)).count, - ).toBe(2) - - // Filter by how positive activities are - const filteredActivities = await ActivityRepository.findAndCountAll( - { filter: { positiveSentimentRange: [0.6, 1] } }, - mockIRepositoryOptions, - ) - - expect(filteredActivities.count).toBe(1) - expect(filteredActivities.rows[0].id).toBe(activityCreated1.id) - - // Filter by whether activities are positive or not - const filteredActivities2 = await ActivityRepository.findAndCountAll( - { filter: { sentimentLabel: 'positive' } }, - mockIRepositoryOptions, - ) - - expect(filteredActivities2.count).toBe(1) - expect(filteredActivities2.rows[0].id).toBe(activityCreated1.id) - - // No filter, but sorting - const filteredActivities3 = await ActivityRepository.findAndCountAll( - { filter: {}, orderBy: 'sentiment.positive_DESC' }, - mockIRepositoryOptions, - ) - expect(filteredActivities3.count).toBe(2) - expect(filteredActivities3.rows[0].sentiment.positive).toBeGreaterThan( - filteredActivities3.rows[1].sentiment.positive, - ) - }) - it('Negative sentiment filter and sort', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity1 = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId1', - } - - const activity2 = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 0.01, - negative: 0.55, - neutral: 0.55, - mixed: 0.0, - label: 'negative', - sentiment: -0.54, - }, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId2', - } - - await ActivityRepository.create(activity1, mockIRepositoryOptions) - const activityCreated2 = await ActivityRepository.create(activity2, mockIRepositoryOptions) - - // Control - expect( - (await ActivityRepository.findAndCountAll({ filter: {} }, mockIRepositoryOptions)).count, - ).toBe(2) - - // Filter by how positive activities are - const filteredActivities = await ActivityRepository.findAndCountAll( - { filter: { negativeSentimentRange: [0.5, 1] } }, - mockIRepositoryOptions, - ) - - expect(filteredActivities.count).toBe(1) - expect(filteredActivities.rows[0].id).toBe(activityCreated2.id) - - // Filter by whether activities are positive or not - const filteredActivities2 = await ActivityRepository.findAndCountAll( - { filter: { sentimentLabel: 'negative' } }, - mockIRepositoryOptions, - ) - - expect(filteredActivities2.count).toBe(1) - expect(filteredActivities2.rows[0].id).toBe(activityCreated2.id) - - // No filter, but sorting - const filteredActivities3 = await ActivityRepository.findAndCountAll( - { filter: {}, orderBy: 'sentiment.negative_DESC' }, - mockIRepositoryOptions, - ) - expect(filteredActivities3.count).toBe(2) - expect(filteredActivities3.rows[0].sentiment.negative).toBeGreaterThan( - filteredActivities3.rows[1].sentiment.negative, - ) - }) - - it('Overall sentiment filter and sort', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - github: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity1 = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId1', - } - - const activity2 = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 0.55, - negative: 0.0, - neutral: 0.45, - mixed: 0.0, - label: 'neutral', - sentiment: 0.55, - }, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId2', - } - - const activityCreated1 = await ActivityRepository.create(activity1, mockIRepositoryOptions) - await ActivityRepository.create(activity2, mockIRepositoryOptions) - - // Control - expect( - (await ActivityRepository.findAndCountAll({ filter: {} }, mockIRepositoryOptions)).count, - ).toBe(2) - - // Filter by how positive activities are - const filteredActivities = await ActivityRepository.findAndCountAll( - { filter: { sentimentRange: [0.6, 1] } }, - mockIRepositoryOptions, - ) - - expect(filteredActivities.count).toBe(1) - expect(filteredActivities.rows[0].id).toBe(activityCreated1.id) - - // No filter, but sorting - const filteredActivities3 = await ActivityRepository.findAndCountAll( - { filter: {}, orderBy: 'sentiment_DESC' }, - mockIRepositoryOptions, - ) - expect(filteredActivities3.count).toBe(2) - expect(filteredActivities3.rows[0].sentiment.positive).toBeGreaterThan( - filteredActivities3.rows[1].sentiment.positive, - ) - }) - - it('Member related attributes filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mas = new MemberAttributeSettingsService(mockIRepositoryOptions) - - await mas.createPredefined(DEFAULT_MEMBER_ATTRIBUTES) - - const memberAttributeSettings = ( - await MemberAttributeSettingsRepository.findAndCountAll({}, mockIRepositoryOptions) - ).rows - - const memberCreated1 = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Anil', - attributes: { - [MemberAttributeName.IS_TEAM_MEMBER]: { - default: true, - [PlatformType.CROWD]: true, - }, - [MemberAttributeName.LOCATION]: { - default: 'Berlin', - [PlatformType.GITHUB]: 'Berlin', - [PlatformType.SLACK]: 'Turkey', - }, - }, - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const memberCreated2 = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'Michael', - }, - displayName: 'Michael', - attributes: { - [MemberAttributeName.IS_TEAM_MEMBER]: { - default: false, - [PlatformType.CROWD]: false, - }, - [MemberAttributeName.LOCATION]: { - default: 'Scranton', - [PlatformType.GITHUB]: 'Scranton', - [PlatformType.SLACK]: 'New York', - }, - }, - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity1 = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - username: 'test', - member: memberCreated1.id, - sourceId: '#sourceId1', - } - - const activity2 = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 0.55, - negative: 0.0, - neutral: 0.45, - mixed: 0.0, - label: 'neutral', - sentiment: 0.55, - }, - username: 'Michael', - member: memberCreated2.id, - sourceId: '#sourceId2', - } - - const activityCreated1 = await ActivityRepository.create(activity1, mockIRepositoryOptions) - const activityCreated2 = await ActivityRepository.create(activity2, mockIRepositoryOptions) - - // Control - expect( - (await ActivityRepository.findAndCountAll({ filter: {} }, mockIRepositoryOptions)).count, - ).toBe(2) - - // Filter by member.isTeamMember - let filteredActivities = await ActivityRepository.findAndCountAll( - { - advancedFilter: { - member: { - isTeamMember: { - not: false, - }, - }, - }, - attributesSettings: memberAttributeSettings, - }, - mockIRepositoryOptions, - ) - - expect(filteredActivities.count).toBe(1) - expect(filteredActivities.rows[0].id).toBe(activityCreated1.id) - - filteredActivities = await ActivityRepository.findAndCountAll( - { - advancedFilter: { - member: { - 'attributes.location.slack': 'New York', - }, - }, - attributesSettings: memberAttributeSettings, - }, - mockIRepositoryOptions, - ) - - expect(filteredActivities.count).toBe(1) - expect(filteredActivities.rows[0].id).toBe(activityCreated2.id) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/conversationRepository.test.ts b/backend/src/database/repositories/__tests__/conversationRepository.test.ts deleted file mode 100644 index bc5fa84a57..0000000000 --- a/backend/src/database/repositories/__tests__/conversationRepository.test.ts +++ /dev/null @@ -1,715 +0,0 @@ -import moment from 'moment' -import ConversationRepository from '../conversationRepository' -import ActivityRepository from '../activityRepository' -import MemberRepository from '../memberRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import Error404 from '../../../errors/Error404' -import { PlatformType } from '@crowd/types' -import { generateUUIDv1 } from '@crowd/common' -import { populateSegments } from '../../utils/segmentTestUtils' -import { UNKNOWN_ACTIVITY_TYPE_DISPLAY } from '@crowd/integrations' - -const db = null - -describe('ConversationRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create a conversation succesfully with default values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const conversation2Add = { title: 'some-title', slug: 'some-slug' } - - const conversationCreated = await ConversationRepository.create( - conversation2Add, - mockIRepositoryOptions, - ) - - conversationCreated.createdAt = conversationCreated.createdAt.toISOString().split('T')[0] - conversationCreated.updatedAt = conversationCreated.updatedAt.toISOString().split('T')[0] - - const conversationExpected = { - id: conversationCreated.id, - title: conversation2Add.title, - slug: conversation2Add.slug, - published: false, - activities: [], - activityCount: 0, - channel: null, - platform: null, - lastActive: null, - conversationStarter: null, - memberCount: 0, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(conversationCreated).toStrictEqual(conversationExpected) - }) - - it('Should create a conversation succesfully with given values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const conversation2Add = { title: 'some-title', slug: 'some-slug', published: true } - - const conversationCreated = await ConversationRepository.create( - conversation2Add, - mockIRepositoryOptions, - ) - - conversationCreated.createdAt = conversationCreated.createdAt.toISOString().split('T')[0] - conversationCreated.updatedAt = conversationCreated.updatedAt.toISOString().split('T')[0] - - const conversationExpected = { - id: conversationCreated.id, - title: conversation2Add.title, - slug: conversation2Add.slug, - published: conversation2Add.published, - activities: [], - activityCount: 0, - memberCount: 0, - conversationStarter: null, - platform: null, - channel: null, - lastActive: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(conversationCreated).toStrictEqual(conversationExpected) - }) - - it('Should throw not null constraint error if no slug is given', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await expect(() => - ConversationRepository.create({ title: 'some-title' }, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should throw not null constraint error if no title is given', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await expect(() => - ConversationRepository.create({ slug: 'some-slug' }, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should throw validation error if title is empty', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await expect(() => - ConversationRepository.create({ title: '' }, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should throw validation error if slug is empty', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await expect(() => - ConversationRepository.create({ slug: '' }, mockIRepositoryOptions), - ).rejects.toThrow() - }) - }) - - describe('findById method', () => { - it('Should successfully find created conversation by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const conversation2Add = { title: 'some-title', slug: 'some-slug' } - - const conversationCreated = await ConversationRepository.create( - conversation2Add, - mockIRepositoryOptions, - ) - - const conversationExpected = { - id: conversationCreated.id, - title: conversation2Add.title, - slug: conversation2Add.slug, - published: false, - activities: [], - activityCount: 0, - memberCount: 0, - conversationStarter: null, - platform: null, - channel: null, - lastActive: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - const conversationById = await ConversationRepository.findById( - conversationCreated.id, - mockIRepositoryOptions, - ) - - conversationById.createdAt = conversationById.createdAt.toISOString().split('T')[0] - conversationById.updatedAt = conversationById.updatedAt.toISOString().split('T')[0] - - expect(conversationById).toStrictEqual(conversationExpected) - }) - - it('Should throw 404 error when no conversation found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - ConversationRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('filterIdsInTenant method', () => { - it('Should return the given ids of previously created conversation entities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const conversation1Created = await ConversationRepository.create( - { title: 'some-title-1', slug: 'some-slug-1' }, - mockIRepositoryOptions, - ) - const conversation2Created = await ConversationRepository.create( - { title: 'some-title-2', slug: 'some-slug-2' }, - mockIRepositoryOptions, - ) - - const filterIdsReturned = await ConversationRepository.filterIdsInTenant( - [conversation1Created.id, conversation2Created.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([conversation1Created.id, conversation2Created.id]) - }) - - it('Should only return the ids of previously created conversations and filter random uuids out', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const conversationCreated = await ConversationRepository.create( - { title: 'some-title-1', slug: 'some-slug-1' }, - mockIRepositoryOptions, - ) - - const { randomUUID } = require('crypto') - - const filterIdsReturned = await ConversationRepository.filterIdsInTenant( - [conversationCreated.id, randomUUID(), randomUUID()], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([conversationCreated.id]) - }) - - it('Should return an empty array for an irrelevant tenant', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const conversationCreated = await ConversationRepository.create( - { title: 'some-title-1', slug: 'some-slug-1' }, - mockIRepositoryOptions, - ) - - // create a new tenant and bind options to it - mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const filterIdsReturned = await ConversationRepository.filterIdsInTenant( - [conversationCreated.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([]) - }) - }) - - describe('findAndCountAll method', () => { - it('Should find and count all conversations, with various filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - platform: PlatformType.SLACK, - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - let conversation1Created = await ConversationRepository.create( - { title: 'a cool title', slug: 'a-cool-title' }, - mockIRepositoryOptions, - ) - - const activity1Created = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T14:13:30Z', - platform: PlatformType.SLACK, - attributes: { - replies: 12, - }, - body: 'Some Parent Activity', - channel: 'general', - isContribution: true, - member: memberCreated.id, - username: 'test', - conversationId: conversation1Created.id, - score: 1, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - const activity2Created = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-28T15:13:30Z', - platform: PlatformType.SLACK, - attributes: { - replies: 12, - }, - body: 'Here', - channel: 'general', - isContribution: true, - member: memberCreated.id, - username: 'test', - score: 1, - parent: activity1Created.id, - conversationId: conversation1Created.id, - sourceId: '#sourceId2', - }, - mockIRepositoryOptions, - ) - - const activity3Created = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-29T16:13:30Z', - platform: PlatformType.SLACK, - attributes: { - replies: 12, - }, - body: 'Here', - channel: 'general', - isContribution: true, - member: memberCreated.id, - username: 'test', - score: 1, - parent: activity1Created.id, - conversationId: conversation1Created.id, - sourceId: '#sourceId3', - }, - mockIRepositoryOptions, - ) - - let conversation2Created = await ConversationRepository.create( - { title: 'a cool title 2', slug: 'a-cool-title-2' }, - mockIRepositoryOptions, - ) - - const activity4Created = await ActivityRepository.create( - { - type: 'message', - timestamp: '2020-06-02T15:13:30Z', - platform: PlatformType.DISCORD, - url: 'https://parent-id-url.com', - body: 'conversation activity 1', - channel: 'Some-Channel', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - conversationId: conversation2Created.id, - sourceId: '#sourceId4', - }, - mockIRepositoryOptions, - ) - - const activity5Created = await ActivityRepository.create( - { - type: 'message', - timestamp: '2020-06-03T15:13:30Z', - platform: PlatformType.DISCORD, - body: 'conversation activity 2', - channel: 'Some-Channel', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - conversationId: conversation2Created.id, - sourceId: '#sourceId5', - }, - mockIRepositoryOptions, - ) - let conversation3Created = await ConversationRepository.create( - { title: 'some other title', slug: 'some-other-title', published: true }, - mockIRepositoryOptions, - ) - - const activity6Created = await ActivityRepository.create( - { - type: 'message', - timestamp: '2020-06-05T15:13:30Z', - platform: PlatformType.SLACK, - url: 'https://parent-id-url.com', - body: 'conversation activity 1', - channel: 'Some-Channel', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - conversationId: conversation3Created.id, - sourceId: '#sourceId6', - }, - mockIRepositoryOptions, - ) - - const activity7Created = await ActivityRepository.create( - { - type: 'message', - timestamp: '2020-06-07T15:13:30Z', - platform: PlatformType.SLACK, - url: 'https://parent-id-url.com', - body: 'conversation activity 7', - channel: 'Some-Channel', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - conversationId: conversation3Created.id, - sourceId: '#sourceId7', - }, - mockIRepositoryOptions, - ) - - // activities are not included in findandcountall - conversation1Created = SequelizeTestUtils.objectWithoutKey( - await ConversationRepository.findById(conversation1Created.id, mockIRepositoryOptions), - 'activities', - ) - conversation2Created = SequelizeTestUtils.objectWithoutKey( - await ConversationRepository.findById(conversation2Created.id, mockIRepositoryOptions), - 'activities', - ) - conversation3Created = SequelizeTestUtils.objectWithoutKey( - await ConversationRepository.findById(conversation3Created.id, mockIRepositoryOptions), - 'activities', - ) - - // filter by id - let conversations = await ConversationRepository.findAndCountAll( - { filter: { id: conversation1Created.id }, lazyLoad: ['activities'] }, - mockIRepositoryOptions, - ) - - expect(conversations.count).toEqual(1) - - const memberReturnedWithinConversations = SequelizeTestUtils.objectWithoutKey(memberCreated, [ - 'activities', - 'activityCount', - 'averageSentiment', - 'lastActive', - 'lastActivity', - 'activityTypes', - 'noMerge', - 'notes', - 'organizations', - 'tags', - 'tasks', - 'toMerge', - 'activeOn', - 'identities', - 'activeDaysCount', - 'username', - 'numberOfOpenSourceContributions', - 'segments', - 'affiliations', - ]) - - const conversation1Expected = { - ...conversation1Created, - conversationStarter: { - ...SequelizeTestUtils.objectWithoutKey(activity1Created, ['tasks']), - member: memberReturnedWithinConversations, - }, - lastReplies: [ - { - ...SequelizeTestUtils.objectWithoutKey(activity2Created, ['tasks']), - parent: SequelizeTestUtils.objectWithoutKey(activity2Created.parent, ['display']), - member: memberReturnedWithinConversations, - }, - { - ...SequelizeTestUtils.objectWithoutKey(activity3Created, ['tasks']), - parent: SequelizeTestUtils.objectWithoutKey(activity3Created.parent, ['display']), - member: memberReturnedWithinConversations, - }, - ], - } - - const conversation2Expected = { - ...conversation2Created, - conversationStarter: { - ...SequelizeTestUtils.objectWithoutKey(activity4Created, ['tasks']), - member: memberReturnedWithinConversations, - }, - lastReplies: [ - { - ...SequelizeTestUtils.objectWithoutKey(activity5Created, ['tasks']), - member: memberReturnedWithinConversations, - }, - ], - } - - const conversation3Expected = { - ...conversation3Created, - conversationStarter: { - ...SequelizeTestUtils.objectWithoutKey(activity6Created, ['tasks']), - member: memberReturnedWithinConversations, - }, - lastReplies: [ - { - ...SequelizeTestUtils.objectWithoutKey(activity7Created, ['tasks']), - member: memberReturnedWithinConversations, - }, - ], - } - - expect(conversations.rows).toStrictEqual([conversation1Expected]) - - // filter by title - conversations = await ConversationRepository.findAndCountAll( - { filter: { title: 'a cool title' }, lazyLoad: ['activities'] }, - mockIRepositoryOptions, - ) - - expect(conversations.count).toEqual(2) - expect(conversations.rows).toStrictEqual([conversation2Expected, conversation1Expected]) - - // filter by slug - conversations = await ConversationRepository.findAndCountAll( - { filter: { slug: 'a-cool-title-2' }, lazyLoad: ['activities'] }, - mockIRepositoryOptions, - ) - - expect(conversations.count).toEqual(1) - expect(conversations.rows).toStrictEqual([conversation2Expected]) - - // filter by published - conversations = await ConversationRepository.findAndCountAll( - { filter: { published: true }, lazyLoad: ['activities'] }, - mockIRepositoryOptions, - ) - - expect(conversations.count).toEqual(1) - expect(conversations.rows).toStrictEqual([conversation3Expected]) - - // filter by activityCount only start input - conversations = await ConversationRepository.findAndCountAll( - { filter: { activityCountRange: [2] }, lazyLoad: ['activities'] }, - mockIRepositoryOptions, - ) - expect(conversations.count).toEqual(3) - expect(conversations.rows).toStrictEqual([ - conversation3Expected, - conversation2Expected, - conversation1Expected, - ]) - - // filter by activityCount start and end inputs - conversations = await ConversationRepository.findAndCountAll( - { filter: { activityCountRange: [0, 1] }, lazyLoad: ['activities'] }, - mockIRepositoryOptions, - ) - expect(conversations.count).toEqual(0) - expect(conversations.rows).toStrictEqual([]) - - // filter by platform - conversations = await ConversationRepository.findAndCountAll( - { filter: { platform: PlatformType.DISCORD }, lazyLoad: ['activities'] }, - mockIRepositoryOptions, - ) - - expect(conversations.count).toEqual(1) - expect(conversations.rows).toStrictEqual([conversation2Expected]) - - // filter by channel (channel) - conversations = await ConversationRepository.findAndCountAll( - { filter: { channel: 'Some-Channel' }, lazyLoad: ['activities'] }, - mockIRepositoryOptions, - ) - - expect(conversations.count).toEqual(2) - expect(conversations.rows).toStrictEqual([conversation3Expected, conversation2Expected]) - - // filter by channel (repo) - conversations = await ConversationRepository.findAndCountAll( - { filter: { channel: 'general' }, lazyLoad: ['activities'] }, - mockIRepositoryOptions, - ) - - expect(conversations.count).toEqual(1) - expect(conversations.rows).toStrictEqual([conversation1Expected]) - - // filter by lastActive only start - conversations = await ConversationRepository.findAndCountAll( - { filter: { lastActiveRange: ['2020-06-03T15:13:30Z'] }, lazyLoad: ['activities'] }, - mockIRepositoryOptions, - ) - - expect(conversations.count).toEqual(2) - expect(conversations.rows).toStrictEqual([conversation3Expected, conversation2Expected]) - - // filter by lastActive start and end - conversations = await ConversationRepository.findAndCountAll( - { - filter: { lastActiveRange: ['2020-06-03T15:13:30Z', '2020-06-04T15:13:30Z'] }, - lazyLoad: ['activities'], - }, - mockIRepositoryOptions, - ) - - expect(conversations.count).toEqual(1) - expect(conversations.rows).toStrictEqual([conversation2Expected]) - - // Test orderBy - conversations = await ConversationRepository.findAndCountAll( - { - filter: {}, - orderBy: 'lastActive_DESC', - lazyLoad: ['activities'], - }, - mockIRepositoryOptions, - ) - expect(moment(conversations.rows[0].lastActive).unix()).toBeGreaterThan( - moment(conversations.rows[1].lastActive).unix(), - ) - expect(moment(conversations.rows[1].lastActive).unix()).toBeGreaterThan( - moment(conversations.rows[2].lastActive).unix(), - ) - - // Test pagination - const conversationsP1 = await ConversationRepository.findAndCountAll( - { - filter: {}, - orderBy: 'lastActive_DESC', - limit: 2, - offset: 0, - }, - mockIRepositoryOptions, - ) - expect(conversationsP1.rows.length).toEqual(2) - expect(conversationsP1.count).toEqual(3) - - const conversationsP2 = await ConversationRepository.findAndCountAll( - { - filter: {}, - orderBy: 'lastActive_DESC', - limit: 2, - offset: 2, - }, - mockIRepositoryOptions, - ) - expect(conversationsP2.rows.length).toEqual(1) - expect(conversationsP2.count).toEqual(3) - expect(conversationsP2.rows[0].id).not.toBe(conversationsP1.rows[0].id) - expect(conversationsP2.rows[0].id).not.toBe(conversationsP1.rows[1].id) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created conversation', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const conversationCreated = await ConversationRepository.create( - { title: 'a cool title', slug: 'a-cool-title' }, - mockIRepositoryOptions, - ) - - const conversationUpdated = await ConversationRepository.update( - conversationCreated.id, - { - published: true, - slug: 'some-other-slug', - }, - mockIRepositoryOptions, - ) - - expect(conversationUpdated.updatedAt.getTime()).toBeGreaterThan( - conversationUpdated.createdAt.getTime(), - ) - - const conversationExpected = { - id: conversationCreated.id, - title: conversationCreated.title, - slug: conversationUpdated.slug, - published: conversationUpdated.published, - activities: [], - activityCount: 0, - memberCount: 0, - conversationStarter: null, - channel: null, - lastActive: null, - platform: null, - createdAt: conversationCreated.createdAt, - updatedAt: conversationUpdated.updatedAt, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(conversationUpdated).toStrictEqual(conversationExpected) - }) - it('Should throw 404 error when trying to update non existent conversation', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - ConversationRepository.update(randomUUID(), { slug: 'some-slug' }, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('destroy method', () => { - it('Should succesfully destroy previously created conversation', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const conversationCreated = await ConversationRepository.create( - { title: 'a cool title', slug: 'a-cool-title' }, - mockIRepositoryOptions, - ) - - await ConversationRepository.destroy(conversationCreated.id, mockIRepositoryOptions) - - // Try selecting it after destroy, should throw 404 - await expect(() => - ConversationRepository.findById(conversationCreated.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/eagleEyeActionRepository.test.ts b/backend/src/database/repositories/__tests__/eagleEyeActionRepository.test.ts deleted file mode 100644 index f3a64fa6ec..0000000000 --- a/backend/src/database/repositories/__tests__/eagleEyeActionRepository.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import EagleEyeContentRepository from '../eagleEyeContentRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import { EagleEyeAction, EagleEyeActionType, EagleEyeContent } from '../../../types/eagleEyeTypes' -import EagleEyeActionRepository from '../eagleEyeActionRepository' - -const db = null - -describe('eagleEyeActionRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('createActionForContent method', () => { - it('Should create a an action for a content succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const content = { - platform: 'reddit', - url: 'https://some-post-url', - post: { - title: 'post title', - body: 'post body', - }, - postedAt: '2020-05-27T15:13:30Z', - tenantId: mockIRepositoryOptions.currentTenant.id, - } as EagleEyeContent - - const contentCreated = await EagleEyeContentRepository.create(content, mockIRepositoryOptions) - - const action: EagleEyeAction = { - type: EagleEyeActionType.BOOKMARK, - timestamp: '2022-07-27T19:13:30Z', - } - - const actionCreated = await EagleEyeActionRepository.createActionForContent( - action, - contentCreated.id, - mockIRepositoryOptions, - ) - - actionCreated.createdAt = (actionCreated.createdAt as Date).toISOString().split('T')[0] - actionCreated.updatedAt = (actionCreated.updatedAt as Date).toISOString().split('T')[0] - - const expectedAction = { - id: actionCreated.id, - ...action, - timestamp: new Date(actionCreated.timestamp), - contentId: contentCreated.id, - actionById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - } - expect(expectedAction).toStrictEqual(actionCreated) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/eagleEyeContentRepository.test.ts b/backend/src/database/repositories/__tests__/eagleEyeContentRepository.test.ts deleted file mode 100644 index 96666a7148..0000000000 --- a/backend/src/database/repositories/__tests__/eagleEyeContentRepository.test.ts +++ /dev/null @@ -1,552 +0,0 @@ -import EagleEyeContentRepository from '../eagleEyeContentRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import { EagleEyeActionType, EagleEyeContent } from '../../../types/eagleEyeTypes' -import EagleEyeActionRepository from '../eagleEyeActionRepository' - -const db = null - -describe('eagleEyeContentRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create a content succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const content = { - platform: 'reddit', - url: 'https://some-post-url', - post: { - title: 'post title', - body: 'post body', - }, - postedAt: '2020-05-27T15:13:30Z', - tenantId: mockIRepositoryOptions.currentTenant.id, - } as EagleEyeContent - - const created = await EagleEyeContentRepository.create(content, mockIRepositoryOptions) - - created.createdAt = (created.createdAt as Date).toISOString().split('T')[0] - created.updatedAt = (created.updatedAt as Date).toISOString().split('T')[0] - - const expectedCreated = { - id: created.id, - ...content, - postedAt: new Date(content.postedAt), - actions: [], - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - } - expect(created).toStrictEqual(expectedCreated) - }) - - /* - - it('Should create a content with unix timestamp', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const withUnix = { - sourceId: 'sourceId', - vectorId: '123', - status: null, - platform: 'hacker_news', - title: 'title', - postAttributes: { - score: 10, - }, - userAttributes: { [PlatformType.GITHUB]: 'hey', [PlatformType.TWITTER]: 'ho' }, - text: 'text', - url: 'url', - timestamp: 1660712134, - - username: 'username', - keywords: ['keyword1', 'keyword2'], - similarityScore: 0.9, - } - - const created = await EagleEyeContentRepository.upsert(withUnix, mockIRepositoryOptions) - - created.createdAt = created.createdAt.toISOString().split('T')[0] - created.updatedAt = created.updatedAt.toISOString().split('T')[0] - - const expectedCreated = { - id: created.id, - ...toCreate, - timestamp: new Date(1660712134 * 1000), - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - expect(created).toStrictEqual(expectedCreated) - }) - - - - - }) - - describe('find by id method', () => { - it('Should find an existing record', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const created = await EagleEyeContentRepository.upsert(toCreate, mockIRepositoryOptions) - - const id = created.id - const found = await EagleEyeContentRepository.findById(id, mockIRepositoryOptions) - expect(found.id).toBe(id) - }) - - it('Should throw 404 error when no tag found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - EagleEyeContentRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('find and count all method', () => { - it('Should find all records without filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - - const found = await EagleEyeContentRepository.findAndCountAll( - { filter: {} }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(5) - }) - - it('Filter by date', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - - const found = await EagleEyeContentRepository.findAndCountAll( - { - filter: { - timestampRange: [moment().subtract(1, 'day').toISOString()], - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(3) - }) - - it('Filter by nDays', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - - const found = await EagleEyeContentRepository.findAndCountAll( - { - filter: { - nDays: 1, - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(3) - }) - - it('Filter by status NULL', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - - const found = await EagleEyeContentRepository.findAndCountAll( - { - filter: { - status: 'NULL', - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(3) - }) - - it('Filter by status NOT_NULL', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - - const found = await EagleEyeContentRepository.findAndCountAll( - { - filter: { - status: 'NOT_NULL', - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(2) - }) - - it('Filter by status engaged', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - - const found = await EagleEyeContentRepository.findAndCountAll( - { - filter: { - status: 'engaged', - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(1) - }) - - it('Filter by status rejected', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - - const found = await EagleEyeContentRepository.findAndCountAll( - { - filter: { - status: 'rejected', - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(1) - }) - - it('Filter by platform', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - - const found = await EagleEyeContentRepository.findAndCountAll( - { - filter: { - platforms: 'hacker_news', - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(2) - }) - - it('Filter by several platforms', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - await new EagleEyeContentService(mockIRepositoryOptions).upsert({ - sourceId: 't1', - vectorId: 't1', - url: 'url devto 3', - username: 'devtousername3', - status: null, - platform: 'twitter', - timestamp: moment().subtract(1, 'week').toDate(), - keywords: ['keyword3', 'keyword2'], - title: 'title devto 3', - }) - - const found = await EagleEyeContentRepository.findAndCountAll( - { - filter: { - platforms: 'hacker_news,twitter', - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(3) - }) - - it('Filter by timestamp and status', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - - expect( - ( - await EagleEyeContentRepository.findAndCountAll( - { - filter: { - timestampRange: [moment().subtract(1, 'day').toISOString()], - status: 'NULL', - }, - }, - mockIRepositoryOptions, - ) - ).count, - ).toBe(2) - }) - - it('Filter by keywords', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - - const k1 = { - sourceId: 'sourceIdk1', - vectorId: 'sourceIdk1', - status: null, - platform: 'hacker_news', - title: 'title', - userAttributes: { [PlatformType.GITHUB]: 'hey', [PlatformType.TWITTER]: 'ho' }, - text: 'text', - postAttributes: { - score: 10, - }, - url: 'url', - timestamp: new Date(), - username: 'username', - keywords: ['keyword1'], - similarityScore: 0.9, - } - - await new EagleEyeContentService(mockIRepositoryOptions).upsert(k1) - - const k2 = { - sourceId: 'sourceIdk2', - vectorId: 'sourceIdk2', - status: null, - platform: 'hacker_news', - title: 'title', - userAttributes: { [PlatformType.GITHUB]: 'hey', [PlatformType.TWITTER]: 'ho' }, - text: 'text', - postAttributes: { - score: 10, - }, - url: 'url', - timestamp: new Date(), - username: 'username', - keywords: ['keyword2'], - similarityScore: 0.9, - } - - try { - await EagleEyeContentRepository.findAndCountAll( - { - filter: { - keywords: 'keyword1,keyword2', - }, - }, - mockIRepositoryOptions, - ) - } catch (e) { - console.log(e) - } - - await new EagleEyeContentService(mockIRepositoryOptions).upsert(k2) - - expect( - ( - await EagleEyeContentRepository.findAndCountAll( - { - filter: { - keywords: 'keyword1,keyword2', - }, - }, - mockIRepositoryOptions, - ) - ).count, - ).toBe(5) - }) - }) - - describe('update method', () => { - it('Should update a record', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const created = await EagleEyeContentRepository.upsert(toCreate, mockIRepositoryOptions) - - const id = created.id - const updated = await EagleEyeContentRepository.update( - id, - { status: 'rejected', username: 'updated' }, - mockIRepositoryOptions, - ) - expect(updated.id).toBe(id) - expect(updated.status).toBe('rejected') - expect(updated.username).toBe('updated') - }) - - it('Should throw 404 error when no content found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - EagleEyeContentRepository.update(randomUUID(), {}, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw an error for an invalid status', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const created = await EagleEyeContentRepository.upsert(toCreate, mockIRepositoryOptions) - - const id = created.id - - await expect(() => - EagleEyeContentRepository.update(id, { status: 'smth' }, mockIRepositoryOptions), - ).rejects.toThrowError(new Error400('en', 'errors.invalidEagleEyeStatus.message')) - }) - - it('Keywords should not be updated', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const created = await EagleEyeContentRepository.upsert(toCreate, mockIRepositoryOptions) - - const id = created.id - const updated = await EagleEyeContentRepository.update( - id, - { keywords: ['1', '2'] }, - mockIRepositoryOptions, - ) - expect(updated.id).toBe(id) - expect(updated.keywords).toStrictEqual(created.keywords) - }) - }) - */ - }) - - describe('findAndCountAll method', () => { - it('Should find eagle eye contant, various cases', async () => { - // create random tenant with one user - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - // create additional users for same tenant to test out actionBy filtering - const randomUser = await SequelizeTestUtils.getRandomUser() - - console.log('random user: ') - console.log(randomUser) - - const user2 = await mockIRepositoryOptions.database.user.create(randomUser) - - await mockIRepositoryOptions.database.tenantUser.create({ - roles: ['admin'], - status: 'active', - tenantId: mockIRepositoryOptions.currentTenant.id, - userId: user2.id, - }) - - // create few content - // one without any actions - await EagleEyeContentRepository.create( - { - platform: 'reddit', - url: 'https://some-reddit-url', - post: { - title: 'post title', - body: 'post body', - }, - postedAt: '2020-05-27T15:13:30Z', - tenantId: mockIRepositoryOptions.currentTenant.id, - }, - mockIRepositoryOptions, - ) - - // one with a bookmark action - let c2 = await EagleEyeContentRepository.create( - { - platform: 'hackernews', - url: 'https://some-hackernews-url', - post: { - title: 'post title', - body: 'post body', - }, - postedAt: '2022-06-27T19:14:44Z', - tenantId: mockIRepositoryOptions.currentTenant.id, - }, - mockIRepositoryOptions, - ) - - // add bookmark action - await EagleEyeActionRepository.createActionForContent( - { - type: EagleEyeActionType.BOOKMARK, - timestamp: '2022-07-27T19:13:30Z', - }, - c2.id, - mockIRepositoryOptions, - ) - - c2 = await EagleEyeContentRepository.findById(c2.id, mockIRepositoryOptions) - - // another content with a thumbs-up(user1) and a bookmark(user2) action - let c3 = await EagleEyeContentRepository.create( - { - platform: 'devto', - url: 'https://some-devto-url', - post: { - title: 'post title', - body: 'post body', - }, - postedAt: '2022-06-27T19:14:44Z', - tenantId: mockIRepositoryOptions.currentTenant.id, - }, - mockIRepositoryOptions, - ) - - // add the thumbs up action - await EagleEyeActionRepository.createActionForContent( - { - type: EagleEyeActionType.THUMBS_UP, - timestamp: '2022-09-30T23:11:10Z', - }, - c3.id, - mockIRepositoryOptions, - ) - - // also add bookmark from user2 - await EagleEyeActionRepository.createActionForContent( - { - type: EagleEyeActionType.BOOKMARK, - timestamp: '2022-09-30T23:11:10Z', - }, - c3.id, - { ...mockIRepositoryOptions, currentUser: user2 }, - ) - - c3 = await EagleEyeContentRepository.findById(c3.id, mockIRepositoryOptions) - - // filter by action type - let res = await EagleEyeContentRepository.findAndCountAll( - { - advancedFilter: { - action: { - type: EagleEyeActionType.BOOKMARK, - }, - }, - }, - mockIRepositoryOptions, - ) - - expect(res.count).toBe(2) - expect(res.rows.sort((a, b) => (a.createdAt > b.createdAt ? 1 : -1))).toStrictEqual([c2, c3]) - - // filter by actionBy - res = await EagleEyeContentRepository.findAndCountAll( - { - advancedFilter: { - action: { - actionById: user2.id, - }, - }, - }, - mockIRepositoryOptions, - ) - - expect(res.count).toBe(1) - expect(res.rows).toStrictEqual([c3]) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/integrationRepository.test.ts b/backend/src/database/repositories/__tests__/integrationRepository.test.ts deleted file mode 100644 index c9e03e86d6..0000000000 --- a/backend/src/database/repositories/__tests__/integrationRepository.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import IntegrationRepository from '../integrationRepository' -import { PlatformType } from '@crowd/types' - -const db = null - -describe('Integration repository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('Find all active integrations', () => { - it('Should find a single active integration', async () => { - const int1 = { - status: 'done', - platform: PlatformType.TWITTER, - } - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await IntegrationRepository.create(int1, mockIRepositoryOptions) - - const found: any = await IntegrationRepository.findAllActive(PlatformType.TWITTER, 1, 100) - expect(found[0].tenantId).toBeDefined() - expect(found.length).toBe(1) - }) - - it('Should find all active integrations for a platform', async () => { - const int1 = { - status: 'done', - platform: PlatformType.TWITTER, - } - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await IntegrationRepository.create(int1, mockIRepositoryOptions) - - const mockIRepositoryOptions2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await IntegrationRepository.create(int1, mockIRepositoryOptions2) - - const found = await IntegrationRepository.findAllActive(PlatformType.TWITTER, 1, 100) - expect(found.length).toBe(2) - }) - - it('Should only find active integrations', async () => { - const int1 = { - status: 'done', - platform: PlatformType.TWITTER, - } - - const int2 = { - status: 'todo', - platform: PlatformType.TWITTER, - } - - const int3 = { - status: 'in-progress', - platform: PlatformType.TWITTER, - } - - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await IntegrationRepository.create(int1, mockIRepositoryOptions) - - const mockIRepositoryOptions2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await IntegrationRepository.create(int1, mockIRepositoryOptions2) - - const mockIRepositoryOptions3 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await IntegrationRepository.create(int2, mockIRepositoryOptions3) - - const mockIRepositoryOptions4 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await IntegrationRepository.create(int3, mockIRepositoryOptions4) - - const found = await IntegrationRepository.findAllActive(PlatformType.TWITTER, 1, 100) - expect(found.length).toBe(2) - }) - - it('Should only find integrations for the desired platform', async () => { - const int1 = { - status: 'done', - platform: PlatformType.TWITTER, - } - - const int2 = { - status: 'active', - platform: PlatformType.DISCORD, - } - - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await IntegrationRepository.create(int1, mockIRepositoryOptions) - - const mockIRepositoryOptions2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await IntegrationRepository.create(int1, mockIRepositoryOptions2) - - const mockIRepositoryOptions3 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await IntegrationRepository.create(int2, mockIRepositoryOptions3) - - const found = await IntegrationRepository.findAllActive(PlatformType.TWITTER, 1, 100) - expect(found.length).toBe(2) - }) - - it('Should return an empty list if no integrations are found', async () => { - const found = await IntegrationRepository.findAllActive(PlatformType.TWITTER, 1, 100) - expect(found.length).toBe(0) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/memberAttributeSettingsRepository.test.ts b/backend/src/database/repositories/__tests__/memberAttributeSettingsRepository.test.ts deleted file mode 100644 index 6cc754f101..0000000000 --- a/backend/src/database/repositories/__tests__/memberAttributeSettingsRepository.test.ts +++ /dev/null @@ -1,395 +0,0 @@ -import MemberAttributeSettingsRepository from '../memberAttributeSettingsRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import Error404 from '../../../errors/Error404' -import Error400 from '../../../errors/Error400' -import { MemberAttributeType } from '@crowd/types' - -const db = null - -describe('MemberAttributeSettings tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create settings for a member attribute succesfully with default values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const attribute = { - type: MemberAttributeType.BOOLEAN, - label: 'attribute 1', - name: 'attribute1', - } - - const attributeCreated = await MemberAttributeSettingsRepository.create( - attribute, - mockIRepositoryOptions, - ) - - attributeCreated.createdAt = (attributeCreated.createdAt as any).toISOString().split('T')[0] - attributeCreated.updatedAt = (attributeCreated.updatedAt as any).toISOString().split('T')[0] - - const attributeExpected = { - id: attributeCreated.id, - type: attribute.type, - label: attribute.label, - name: attribute.name, - options: [], - show: true, - canDelete: true, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(attributeCreated).toStrictEqual(attributeExpected) - }) - - it('Should create settings for a member attribute succesfully with given values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const attribute = { - type: MemberAttributeType.BOOLEAN, - label: 'attribute 1', - name: 'attribute1', - canDelete: false, - show: false, - } - - const attributeCreated = await MemberAttributeSettingsRepository.create( - attribute, - mockIRepositoryOptions, - ) - - attributeCreated.createdAt = (attributeCreated.createdAt as any).toISOString().split('T')[0] - attributeCreated.updatedAt = (attributeCreated.updatedAt as any).toISOString().split('T')[0] - - const attributeExpected = { - id: attributeCreated.id, - type: attribute.type, - label: attribute.label, - name: attribute.name, - show: false, - options: [], - canDelete: false, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(attributeCreated).toStrictEqual(attributeExpected) - }) - - it('Should throw unique constraint error for creation of already existing member attributes with same name in the same tenant', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const attribute = { - type: MemberAttributeType.BOOLEAN, - label: 'attribute 1', - name: 'attribute1', - } - - await MemberAttributeSettingsRepository.create(attribute, mockIRepositoryOptions) - - await expect(() => - MemberAttributeSettingsRepository.create( - { type: MemberAttributeType.STRING, label: 'some label', name: 'attribute1' }, - mockIRepositoryOptions, - ), - ).rejects.toThrow() - }) - - it('Should throw not null error if no name, label or type is given', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - // no type - await expect(() => - MemberAttributeSettingsRepository.create( - { type: undefined, label: 'attribute 1', name: 'attribute1' }, - mockIRepositoryOptions, - ), - ).rejects.toThrow() - - // no label - await expect(() => - MemberAttributeSettingsRepository.create( - { type: MemberAttributeType.BOOLEAN, name: 'attribute1', label: undefined }, - mockIRepositoryOptions, - ), - ).rejects.toThrow() - - // no name - await expect(() => - MemberAttributeSettingsRepository.create( - { type: MemberAttributeType.BOOLEAN, label: 'attribute 1' }, - mockIRepositoryOptions, - ), - ).rejects.toThrow() - }) - - it('Should throw 400 error if name exists in member fixed fields', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - // no type - await expect(() => - MemberAttributeSettingsRepository.create( - { type: MemberAttributeType.STRING, label: 'Some Email', name: 'emails' }, - mockIRepositoryOptions, - ), - ).rejects.toThrowError( - new Error400('en', 'settings.memberAttributes.errors.reservedField', 'emails'), - ) - }) - }) - - describe('findById method', () => { - it('Should successfully find created member attribute by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const attribute = { - type: MemberAttributeType.BOOLEAN, - label: 'attribute 1', - name: 'attribute1', - } - - const attributeCreated = await MemberAttributeSettingsRepository.create( - attribute, - mockIRepositoryOptions, - ) - - const attributeExpected = { - id: attributeCreated.id, - type: attribute.type, - label: attribute.label, - name: attribute.name, - show: true, - canDelete: true, - options: [], - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - const attributeById = await MemberAttributeSettingsRepository.findById( - attributeCreated.id, - mockIRepositoryOptions, - ) - - attributeById.createdAt = (attributeCreated.createdAt as any).toISOString().split('T')[0] - attributeById.updatedAt = (attributeCreated.updatedAt as any).toISOString().split('T')[0] - - expect(attributeById).toStrictEqual(attributeExpected) - }) - }) - - describe('findAndCountAll method', () => { - it('Should find and count all member attributes, with various filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const attribute1 = await MemberAttributeSettingsRepository.create( - { type: MemberAttributeType.BOOLEAN, label: 'a label', name: 'attribute1' }, - mockIRepositoryOptions, - ) - - const attribute2 = await MemberAttributeSettingsRepository.create( - { type: MemberAttributeType.STRING, label: 'a label', name: 'attribute2', show: false }, - mockIRepositoryOptions, - ) - - const attribute3 = await MemberAttributeSettingsRepository.create( - { - type: MemberAttributeType.STRING, - label: 'some other label', - name: 'attribute3', - show: false, - canDelete: false, - }, - mockIRepositoryOptions, - ) - - // filter by type - let attributes = await MemberAttributeSettingsRepository.findAndCountAll( - { filter: { type: MemberAttributeType.BOOLEAN } }, - mockIRepositoryOptions, - ) - - expect(attributes.count).toEqual(1) - expect(attributes.rows).toStrictEqual([attribute1]) - - // filter by id - attributes = await MemberAttributeSettingsRepository.findAndCountAll( - { filter: { id: attribute2.id } }, - mockIRepositoryOptions, - ) - - expect(attributes.count).toEqual(1) - expect(attributes.rows).toStrictEqual([attribute2]) - - // filter by label - attributes = await MemberAttributeSettingsRepository.findAndCountAll( - { filter: { label: 'a label' } }, - mockIRepositoryOptions, - ) - - expect(attributes.count).toEqual(2) - expect(attributes.rows).toStrictEqual([attribute2, attribute1]) - - // filter by name - attributes = await MemberAttributeSettingsRepository.findAndCountAll( - { filter: { name: 'attribute3' } }, - mockIRepositoryOptions, - ) - - expect(attributes.count).toEqual(1) - expect(attributes.rows).toStrictEqual([attribute3]) - - // filter by show - attributes = await MemberAttributeSettingsRepository.findAndCountAll( - { filter: { show: false } }, - mockIRepositoryOptions, - ) - - expect(attributes.count).toEqual(2) - expect(attributes.rows).toStrictEqual([attribute3, attribute2]) - - // filter by canDelete - attributes = await MemberAttributeSettingsRepository.findAndCountAll( - { filter: { canDelete: true } }, - mockIRepositoryOptions, - ) - - expect(attributes.count).toEqual(2) - expect(attributes.rows).toStrictEqual([attribute2, attribute1]) - - // filter by createdAt between createdAt a1 and a3 - attributes = await MemberAttributeSettingsRepository.findAndCountAll( - { - filter: { - createdAtRange: [attribute1.createdAt, attribute3.createdAt], - }, - }, - mockIRepositoryOptions, - ) - - expect(attributes.count).toEqual(3) - expect(attributes.rows).toStrictEqual([attribute3, attribute2, attribute1]) - - // filter by createdAt <= att2.createdAt - attributes = await MemberAttributeSettingsRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, attribute2.createdAt], - }, - }, - mockIRepositoryOptions, - ) - expect(attributes.count).toEqual(2) - expect(attributes.rows).toStrictEqual([attribute2, attribute1]) - - // filter by createdAt <= att1.createdAt - attributes = await MemberAttributeSettingsRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, attribute1.createdAt], - }, - }, - mockIRepositoryOptions, - ) - expect(attributes.count).toEqual(1) - expect(attributes.rows).toStrictEqual([attribute1]) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created attribute', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const attribute = await MemberAttributeSettingsRepository.create( - { type: MemberAttributeType.BOOLEAN, label: 'attribute 1', name: 'attribute1' }, - mockIRepositoryOptions, - ) - - const attributeUpdated = await MemberAttributeSettingsRepository.update( - attribute.id, - { - type: MemberAttributeType.STRING, - label: 'some other label', - name: 'some name', - show: false, - canDelete: false, - }, - mockIRepositoryOptions, - ) - - const attributeExpected = { - id: attribute.id, - type: attributeUpdated.type, - label: attributeUpdated.label, - name: attributeUpdated.name, - show: attributeUpdated.show, - canDelete: attributeUpdated.canDelete, - options: [], - createdAt: attribute.createdAt, - updatedAt: attributeUpdated.updatedAt, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(attributeUpdated).toStrictEqual(attributeExpected) - }) - - it('Should throw 404 error when trying to update non existent attribute', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - MemberAttributeSettingsRepository.update( - randomUUID(), - { type: 'some-type' } as any, - mockIRepositoryOptions, - ), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('destroy method', () => { - it('Should succesfully destroy previously created attribute', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const attribute = await MemberAttributeSettingsRepository.create( - { type: MemberAttributeType.BOOLEAN, label: 'attribute 1', name: 'attribute1' }, - mockIRepositoryOptions, - ) - - await MemberAttributeSettingsRepository.destroy(attribute.id, mockIRepositoryOptions) - - await expect(() => - MemberAttributeSettingsRepository.findById(attribute.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw 404 when trying to destroy a non existent microservice', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - MemberAttributeSettingsRepository.destroy(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/memberEnrichmentCacheRepository.test.ts b/backend/src/database/repositories/__tests__/memberEnrichmentCacheRepository.test.ts deleted file mode 100644 index 8b2438a340..0000000000 --- a/backend/src/database/repositories/__tests__/memberEnrichmentCacheRepository.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { randomUUID } from 'crypto' - -import MemberRepository from '../memberRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import { PlatformType } from '@crowd/types' -import MemberEnrichmentCacheRepository from '../memberEnrichmentCacheRepository' -import { generateUUIDv1 } from '@crowd/common' - -const db = null - -describe('MemberEnrichmentCacheRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('upsert method', () => { - it('Should create non existing item successfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member2add = { - username: { - [PlatformType.GITHUB]: { - username: 'michael_scott', - }, - }, - displayName: 'Member 1', - email: 'michael@dd.com', - score: 10, - attributes: {}, - joinedAt: '2020-05-27T15:13:30Z', - } - - const member = await MemberRepository.create(member2add, mockIRepositoryOptions) - - const enrichmentData = { - enrichmentField1: 'string', - enrichmentField2: 24, - arrayEnrichmentField: [1, 2, 3], - } - - const cache = await MemberEnrichmentCacheRepository.upsert( - member.id, - enrichmentData, - mockIRepositoryOptions, - ) - - expect(cache.memberId).toEqual(member.id) - expect(cache.data).toStrictEqual(enrichmentData) - }) - - it('Should update the data of existing cache item with incoming data', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member2add = { - username: { - [PlatformType.GITHUB]: { - username: 'michael_scott', - }, - }, - displayName: 'Member 1', - email: 'michael@dd.com', - score: 10, - attributes: {}, - joinedAt: '2020-05-27T15:13:30Z', - } - - const member = await MemberRepository.create(member2add, mockIRepositoryOptions) - - const enrichmentData = { - enrichmentField1: 'string', - enrichmentField2: 24, - arrayEnrichmentField: [1, 2, 3], - } - - let cache = await MemberEnrichmentCacheRepository.upsert( - member.id, - enrichmentData, - mockIRepositoryOptions, - ) - - const newerEnrichmentData = { - enrichmentField1: 'anotherString', - enrichmentField2: 99, - arrayEnrichmentField: ['a', 'b', 'c'], - } - - // should overwrite with new cache data - cache = await MemberEnrichmentCacheRepository.upsert( - member.id, - newerEnrichmentData, - mockIRepositoryOptions, - ) - - expect(cache.memberId).toEqual(member.id) - expect(cache.data).toStrictEqual(newerEnrichmentData) - - // when we send an empty object, it shouldn't overwrite - cache = await MemberEnrichmentCacheRepository.upsert(member.id, {}, mockIRepositoryOptions) - - expect(cache.memberId).toEqual(member.id) - expect(cache.data).toStrictEqual(newerEnrichmentData) - }) - }) - - describe('findByMemberId method', () => { - it('Should find enrichment cache by memberId', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member2add = { - username: { - [PlatformType.GITHUB]: { - username: 'michael_scott', - }, - }, - displayName: 'Member 1', - email: 'michael@dd.com', - score: 10, - attributes: {}, - joinedAt: '2020-05-27T15:13:30Z', - } - - const member = await MemberRepository.create(member2add, mockIRepositoryOptions) - - const enrichmentData = { - enrichmentField1: 'string', - enrichmentField2: 24, - arrayEnrichmentField: [1, 2, 3], - } - - await MemberEnrichmentCacheRepository.upsert( - member.id, - enrichmentData, - mockIRepositoryOptions, - ) - - const cache = await MemberEnrichmentCacheRepository.findByMemberId( - member.id, - mockIRepositoryOptions, - ) - - expect(cache.memberId).toEqual(member.id) - expect(cache.data).toEqual(enrichmentData) - }) - - it('Should return null for non-existing cache entry', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const cache = await MemberEnrichmentCacheRepository.findByMemberId( - randomUUID(), - mockIRepositoryOptions, - ) - expect(cache).toBeNull() - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/memberRepository.test.ts b/backend/src/database/repositories/__tests__/memberRepository.test.ts deleted file mode 100644 index 409381df2c..0000000000 --- a/backend/src/database/repositories/__tests__/memberRepository.test.ts +++ /dev/null @@ -1,3967 +0,0 @@ -import { Op } from 'sequelize' -import { v4 as uuid } from 'uuid' -import moment from 'moment' - -import Error404 from '../../../errors/Error404' -import { PlatformType } from '@crowd/types' -import { generateUUIDv1 } from '@crowd/common' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import MemberRepository from '../memberRepository' -import NoteRepository from '../noteRepository' -import OrganizationRepository from '../organizationRepository' -import TagRepository from '../tagRepository' -import TaskRepository from '../taskRepository' -import lodash from 'lodash' -import SegmentRepository from '../segmentRepository' -import { SegmentStatus } from '../../../types/segmentTypes' -import { populateSegments } from '../../utils/segmentTestUtils' -import MemberService from '../../../services/memberService' -import OrganizationService from '../../../services/organizationService' - -const db = null - -function mapUsername(data: any): any { - const username = {} - Object.keys(data).forEach((platform) => { - const usernameData = data[platform] - - if (Array.isArray(usernameData)) { - username[platform] = [] - if (usernameData.length > 0) { - for (const entry of usernameData) { - if (typeof entry === 'string') { - username[platform].push(entry) - } else if (typeof entry === 'object') { - username[platform].push((entry as any).username) - } else { - throw new Error('Invalid username type') - } - } - } - } else if (typeof usernameData === 'object') { - username[platform] = [usernameData.username] - } else if (typeof usernameData === 'string') { - username[platform] = [usernameData] - } else { - throw new Error('Invalid username type') - } - }) - return username -} - -describe('MemberRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create the given member succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member2add = { - username: { - [PlatformType.GITHUB]: { - username: 'anil_github', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - emails: ['lala@l.com'], - score: 10, - attributes: { - [PlatformType.GITHUB]: { - name: 'Quoc-Anh Nguyen', - isHireable: true, - url: 'https://github.com/imcvampire', - websiteUrl: 'https://imcvampire.js.org/', - bio: 'Lazy geek', - location: 'Helsinki, Finland', - actions: [ - { - score: 2, - timestamp: '2021-05-27T15:13:30Z', - }, - ], - }, - [PlatformType.TWITTER]: { - profile_url: 'https://twitter.com/imcvampire', - url: 'https://twitter.com/imcvampire', - }, - }, - joinedAt: '2020-05-27T15:13:30Z', - } - - const cloned = lodash.cloneDeep(member2add) - const memberCreated = await MemberRepository.create(cloned, mockIRepositoryOptions) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const expectedMemberCreated = { - id: memberCreated.id, - username: mapUsername(member2add.username), - attributes: member2add.attributes, - displayName: member2add.displayName, - emails: member2add.emails, - score: member2add.score, - identities: ['github'], - lastEnriched: null, - enrichedBy: [], - contributions: null, - organizations: [], - notes: [], - tasks: [], - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - activities: [], - activeOn: [], - activityTypes: [], - reach: { total: -1 }, - joinedAt: new Date('2020-05-27T15:13:30Z'), - tags: [], - noMerge: [], - toMerge: [], - activityCount: 0, - activeDaysCount: 0, - lastActive: null, - averageSentiment: 0, - numberOfOpenSourceContributions: 0, - lastActivity: null, - affiliations: [], - manuallyCreated: false, - } - expect(memberCreated).toStrictEqual(expectedMemberCreated) - }) - - it('Should create succesfully but return without relations when doPopulateRelations=false', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member2add = { - username: { - [PlatformType.GITHUB]: { - username: 'anil_github', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - emails: ['lala@l.com'], - score: 10, - attributes: { - [PlatformType.GITHUB]: { - name: 'Quoc-Anh Nguyen', - isHireable: true, - url: 'https://github.com/imcvampire', - websiteUrl: 'https://imcvampire.js.org/', - bio: 'Lazy geek', - location: 'Helsinki, Finland', - actions: [ - { - score: 2, - timestamp: '2021-05-27T15:13:30Z', - }, - ], - }, - [PlatformType.TWITTER]: { - profile_url: 'https://twitter.com/imcvampire', - url: 'https://twitter.com/imcvampire', - }, - }, - joinedAt: '2020-05-27T15:13:30Z', - } - - const cloned = lodash.cloneDeep(member2add) - const memberCreated = await MemberRepository.create(cloned, mockIRepositoryOptions, false) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const expectedMemberCreated = { - id: memberCreated.id, - username: mapUsername(member2add.username), - displayName: member2add.displayName, - attributes: member2add.attributes, - emails: member2add.emails, - lastEnriched: null, - enrichedBy: [], - contributions: null, - score: member2add.score, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - reach: { total: -1 }, - organizations: [], - joinedAt: new Date('2020-05-27T15:13:30Z'), - affiliations: [], - manuallyCreated: false, - } - expect(memberCreated).toStrictEqual(expectedMemberCreated) - }) - - it('Should succesfully create member with only mandatory username and joinedAt fields', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member2add = { - username: { - [PlatformType.GITHUB]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - } - - const cloned = lodash.cloneDeep(member2add) - const memberCreated = await MemberRepository.create(cloned, mockIRepositoryOptions) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const expectedMemberCreated = { - id: memberCreated.id, - username: mapUsername(member2add.username), - displayName: member2add.displayName, - organizations: [], - attributes: {}, - identities: ['github'], - emails: [], - lastEnriched: null, - enrichedBy: [], - contributions: null, - score: -1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - activities: [], - activeOn: [], - activityTypes: [], - reach: { total: -1 }, - joinedAt: new Date('2020-05-27T15:13:30Z'), - notes: [], - tasks: [], - tags: [], - noMerge: [], - toMerge: [], - activityCount: 0, - activeDaysCount: 0, - averageSentiment: 0, - numberOfOpenSourceContributions: 0, - lastActive: null, - lastActivity: null, - affiliations: [], - manuallyCreated: false, - } - - expect(memberCreated).toStrictEqual(expectedMemberCreated) - }) - - it('Should throw error when no username given', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - // no username field, should reject the promise with - // sequelize unique constraint - const member2add = { - joinedAt: '2020-05-27T15:13:30Z', - emails: ['test@crowd.dev'], - } - - await expect(() => - MemberRepository.create(member2add, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should throw error when no joinedAt given', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - // no username field, should reject the promise with - // sequelize unique constraint - const member2add = { - username: { - [PlatformType.GITHUB]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - emails: ['test@crowd.dev'], - } - - await expect(() => - MemberRepository.create(member2add, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should succesfully create member with notes', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const notes1 = await NoteRepository.create( - { - body: 'note1', - }, - mockIRepositoryOptions, - ) - - const notes2 = await NoteRepository.create( - { - body: 'note2', - }, - mockIRepositoryOptions, - ) - - const member2add = { - username: { - [PlatformType.SLACK]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - notes: [notes1.id, notes2.id], - } - - const memberCreated = await MemberRepository.create(member2add, mockIRepositoryOptions) - expect(memberCreated.notes).toHaveLength(2) - expect(memberCreated.notes[0].id).toEqual(notes1.id) - expect(memberCreated.notes[1].id).toEqual(notes2.id) - }) - - it('Should succesfully create member with tasks', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tasks1 = await TaskRepository.create( - { - name: 'task1', - }, - mockIRepositoryOptions, - ) - - const task2 = await TaskRepository.create( - { - name: 'task2', - }, - mockIRepositoryOptions, - ) - - const member2add = { - username: { - [PlatformType.DISCORD]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - tasks: [tasks1.id, task2.id], - } - - const memberCreated = await MemberRepository.create(member2add, mockIRepositoryOptions) - expect(memberCreated.tasks).toHaveLength(2) - expect(memberCreated.tasks.find((t) => t.id === tasks1.id)).not.toBeUndefined() - expect(memberCreated.tasks.find((t) => t.id === task2.id)).not.toBeUndefined() - }) - - it('Should succesfully create member with organization affiliations', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const segmentRepo = new SegmentRepository(mockIRepositoryOptions) - - const segment1 = await segmentRepo.create({ - name: 'Crowd.dev - Segment1', - url: '', - parentName: 'Crowd.dev - Segment1', - grandparentName: 'Crowd.dev - Segment1', - slug: 'crowd.dev-1', - parentSlug: 'crowd.dev-1', - grandparentSlug: 'crowd.dev-1', - status: SegmentStatus.ACTIVE, - sourceId: null, - sourceParentId: null, - }) - - const segment2 = await segmentRepo.create({ - name: 'Crowd.dev - Segment2', - url: '', - parentName: 'Crowd.dev - Segment2', - grandparentName: 'Crowd.dev - Segment2', - slug: 'crowd.dev-2', - parentSlug: 'crowd.dev-2', - grandparentSlug: 'crowd.dev-2', - status: SegmentStatus.ACTIVE, - sourceId: null, - sourceParentId: null, - }) - - const org1 = await OrganizationRepository.create( - { - displayName: 'crowd.dev', - }, - mockIRepositoryOptions, - ) - - const member2add = { - username: { - [PlatformType.DISCORD]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - affiliations: [ - { - segmentId: segment1.id, - organizationId: org1.id, - }, - { - segmentId: segment2.id, - organizationId: null, - }, - ], - } - - const memberCreated = await MemberRepository.create(member2add, mockIRepositoryOptions) - expect(memberCreated.affiliations).toHaveLength(2) - expect( - memberCreated.affiliations.filter((a) => a.segmentId === segment1.id)[0].organizationId, - ).toEqual(org1.id) - expect( - memberCreated.affiliations.filter((a) => a.segmentId === segment2.id)[0].organizationId, - ).toBeNull() - }) - }) - - describe('findById method', () => { - it('Should successfully find created member by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member2add = { - username: { - [PlatformType.GITHUB]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - } - - const cloned = lodash.cloneDeep(member2add) - const memberCreated = await MemberRepository.create(cloned, mockIRepositoryOptions) - - const expectedMemberFound = { - id: memberCreated.id, - username: mapUsername(member2add.username), - displayName: member2add.displayName, - identities: ['github'], - attributes: {}, - emails: [], - lastEnriched: null, - enrichedBy: [], - contributions: null, - score: -1, - importHash: null, - organizations: [], - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - activities: [], - activeOn: [], - activityTypes: [], - reach: { total: -1 }, - notes: [], - tasks: [], - joinedAt: new Date('2020-05-27T15:13:30Z'), - tags: [], - noMerge: [], - toMerge: [], - activityCount: 0, - activeDaysCount: 0, - averageSentiment: 0, - numberOfOpenSourceContributions: 0, - lastActive: null, - lastActivity: null, - affiliations: [], - manuallyCreated: false, - } - - const memberById = await MemberRepository.findById(memberCreated.id, mockIRepositoryOptions) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - memberById.createdAt = memberById.createdAt.toISOString().split('T')[0] - memberById.updatedAt = memberById.updatedAt.toISOString().split('T')[0] - - expect(memberById).toStrictEqual(expectedMemberFound) - }) - - it('Should return a plain object when called with doPopulateRelations false', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member2add = { - username: { - [PlatformType.GITHUB]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - } - - const cloned = lodash.cloneDeep(member2add) - const memberCreated = await MemberRepository.create(cloned, mockIRepositoryOptions) - - const expectedMemberFound = { - id: memberCreated.id, - username: mapUsername(member2add.username), - displayName: member2add.displayName, - lastEnriched: null, - enrichedBy: [], - contributions: null, - attributes: {}, - emails: [], - score: -1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - reach: { total: -1 }, - organizations: [], - joinedAt: new Date('2020-05-27T15:13:30Z'), - affiliations: [], - manuallyCreated: false, - } - - const memberById = await MemberRepository.findById( - memberCreated.id, - mockIRepositoryOptions, - true, - false, - ) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - memberById.createdAt = memberById.createdAt.toISOString().split('T')[0] - memberById.updatedAt = memberById.updatedAt.toISOString().split('T')[0] - - expect(memberById).toStrictEqual(expectedMemberFound) - }) - - it('Should throw 404 error when no member found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - MemberRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('filterIdsInTenant method', () => { - it('Should return the given ids of previously created member entities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const member1 = { - username: { - [PlatformType.GITHUB]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - } - const member2 = { - username: { - [PlatformType.GITHUB]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'some-other-name', - joinedAt: '2020-05-28T15:13:30Z', - } - - const member1Returned = await MemberRepository.create(member1, mockIRepositoryOptions) - const member2Returned = await MemberRepository.create(member2, mockIRepositoryOptions) - - const filterIdsReturned = await MemberRepository.filterIdsInTenant( - [member1Returned.id, member2Returned.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([member1Returned.id, member2Returned.id]) - }) - - it('Should only return the ids of previously created members and filter random uuids out', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = { - username: { - [PlatformType.GITHUB]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-29T15:14:30Z', - } - - const member1Returned = await MemberRepository.create(member1, mockIRepositoryOptions) - - const { randomUUID } = require('crypto') - - const filterIdsReturned = await MemberRepository.filterIdsInTenant( - [member1Returned.id, randomUUID(), randomUUID()], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([member1Returned.id]) - }) - - it('Should return an empty array for an irrelevant tenant', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = { - username: { - [PlatformType.GITHUB]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-04-29T15:14:30Z', - } - - const member1Returned = await MemberRepository.create(member1, mockIRepositoryOptions) - - // create a new tenant and bind options to it - mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const filterIdsReturned = await MemberRepository.filterIdsInTenant( - [member1Returned.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([]) - }) - }) - - describe('memberExists method', () => { - it('Should return the created member for a simple query', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const member1 = { - username: { - [PlatformType.TWITTER]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - emails: ['joan@crowd.dev'], - } - const member1Returned = await MemberRepository.create(member1, mockIRepositoryOptions) - - const found = await MemberRepository.memberExists( - 'test1', - PlatformType.TWITTER, - mockIRepositoryOptions, - ) - - expect(found).toStrictEqual(member1Returned) - }) - - it('Should a plain object when called with doPopulateRelations false', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const member1 = { - username: { - [PlatformType.TWITTER]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - emails: ['joan@crowd.dev'], - } - const member1Returned = await MemberRepository.create(member1, mockIRepositoryOptions) - delete member1Returned.toMerge - delete member1Returned.noMerge - delete member1Returned.tags - delete member1Returned.activities - delete member1Returned.notes - delete member1Returned.tasks - delete member1Returned.lastActive - delete member1Returned.activityCount - delete member1Returned.averageSentiment - delete member1Returned.lastActivity - delete member1Returned.activeOn - delete member1Returned.identities - delete member1Returned.activityTypes - delete member1Returned.activeDaysCount - delete member1Returned.numberOfOpenSourceContributions - delete member1Returned.affiliations - delete member1Returned.manuallyCreated - member1Returned.segments = member1Returned.segments.map((s) => s.id) - - const found = await MemberRepository.memberExists( - 'test1', - PlatformType.TWITTER, - mockIRepositoryOptions, - false, - ) - - expect(found).toStrictEqual(member1Returned) - }) - - it('Should return null when non-existent at platform level', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = { - username: { - [PlatformType.TWITTER]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - emails: ['joan@crowd.dev'], - } - await MemberRepository.create(member1, mockIRepositoryOptions) - - await expect(() => - MemberRepository.memberExists('test1', PlatformType.GITHUB, mockIRepositoryOptions), - ) - }) - - it('Should return null when non-existent at username level', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = { - username: { - [PlatformType.TWITTER]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - emails: ['joan@crowd.dev'], - } - await MemberRepository.create(member1, mockIRepositoryOptions) - - const memberExists = await MemberRepository.memberExists( - 'test2', - PlatformType.TWITTER, - mockIRepositoryOptions, - ) - - expect(memberExists).toBeNull() - }) - }) - - describe('findAndCountAll method', () => { - it('is successfully finding and counting all members, sortedBy activitiesCount DESC', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - const member2 = await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - const member3 = await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await mockIRepositoryOptions.database.activity.bulkCreate([ - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member1.id, - username: member1.username[PlatformType.SLACK], - sourceId: '#sourceId1', - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member2.id, - username: member2.username[PlatformType.SLACK], - sourceId: '#sourceId2', - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member2.id, - username: member2.username[PlatformType.SLACK], - sourceId: '#sourceId3', - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member3.id, - username: member3.username[PlatformType.SLACK], - sourceId: '#sourceId4', - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member3.id, - username: member3.username[PlatformType.SLACK], - sourceId: '#sourceId5', - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member3.id, - username: member3.username[PlatformType.SLACK], - sourceId: '#sourceId6', - }, - ]) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAll( - { filter: {}, orderBy: 'activityCount_DESC' }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(3) - expect(members.rows[0].activityCount).toEqual('3') - expect(members.rows[1].activityCount).toEqual('2') - expect(members.rows[2].activityCount).toEqual('1') - }) - - it('is successfully finding and counting all members, sortedBy numberOfOpenSourceContributions DESC', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test1' }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - contributions: [ - { - id: 112529472, - url: 'https://github.com/bachman/pied-piper', - topics: ['compression', 'data', 'middle-out', 'Java'], - summary: 'Pied Piper: 10 commits in 1 day', - numberCommits: 10, - lastCommitDate: '2023-03-10', - firstCommitDate: '2023-03-01', - }, - { - id: 112529473, - url: 'https://github.com/bachman/aviato', - topics: ['Python', 'Django'], - summary: 'Aviato: 5 commits in 1 day', - numberCommits: 5, - lastCommitDate: '2023-02-25', - firstCommitDate: '2023-02-20', - }, - { - id: 112529476, - url: 'https://github.com/bachman/erlichbot', - topics: ['Python', 'Slack API'], - summary: 'ErlichBot: 2 commits in 1 day', - numberCommits: 2, - lastCommitDate: '2023-01-25', - firstCommitDate: '2023-01-24', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test2' }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - contributions: [ - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - { - id: 112529474, - url: 'https://github.com/bighead/startup-ideas', - topics: ['Ideas', 'Startups'], - summary: 'Startup Ideas: 20 commits in 1 week', - numberCommits: 20, - lastCommitDate: '03/01/2023', - firstCommitDate: '02/22/2023', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test3' }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAll( - { filter: {}, orderBy: 'numberOfOpenSourceContributions_DESC' }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(3) - expect(members.rows[0].numberOfOpenSourceContributions).toEqual(3) - expect(members.rows[1].numberOfOpenSourceContributions).toEqual(2) - expect(members.rows[2].numberOfOpenSourceContributions).toEqual(0) - }) - - it('is successfully finding and counting all members, numberOfOpenSourceContributions range gte than 3 and less or equal to 6', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test1' }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - contributions: [ - { - id: 112529472, - url: 'https://github.com/bachman/pied-piper', - topics: ['compression', 'data', 'middle-out', 'Java'], - summary: 'Pied Piper: 10 commits in 1 day', - numberCommits: 10, - lastCommitDate: '2023-03-10', - firstCommitDate: '2023-03-01', - }, - { - id: 112529473, - url: 'https://github.com/bachman/aviato', - topics: ['Python', 'Django'], - summary: 'Aviato: 5 commits in 1 day', - numberCommits: 5, - lastCommitDate: '2023-02-25', - firstCommitDate: '2023-02-20', - }, - { - id: 112529476, - url: 'https://github.com/bachman/erlichbot', - topics: ['Python', 'Slack API'], - summary: 'ErlichBot: 2 commits in 1 day', - numberCommits: 2, - lastCommitDate: '2023-01-25', - firstCommitDate: '2023-01-24', - }, - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test2' }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - contributions: [ - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - { - id: 112529474, - url: 'https://github.com/bighead/startup-ideas', - topics: ['Ideas', 'Startups'], - summary: 'Startup Ideas: 20 commits in 1 week', - numberCommits: 20, - lastCommitDate: '03/01/2023', - firstCommitDate: '02/22/2023', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test3' }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAll( - { filter: { numberOfOpenSourceContributionsRange: [3, 6] } }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(1) - expect(members.rows[0].numberOfOpenSourceContributions).toEqual(4) - }) - - it('is successfully finding and counting all members, numberOfOpenSourceContributions range gte 2', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test1' }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - contributions: [ - { - id: 112529472, - url: 'https://github.com/bachman/pied-piper', - topics: ['compression', 'data', 'middle-out', 'Java'], - summary: 'Pied Piper: 10 commits in 1 day', - numberCommits: 10, - lastCommitDate: '2023-03-10', - firstCommitDate: '2023-03-01', - }, - { - id: 112529473, - url: 'https://github.com/bachman/aviato', - topics: ['Python', 'Django'], - summary: 'Aviato: 5 commits in 1 day', - numberCommits: 5, - lastCommitDate: '2023-02-25', - firstCommitDate: '2023-02-20', - }, - { - id: 112529476, - url: 'https://github.com/bachman/erlichbot', - topics: ['Python', 'Slack API'], - summary: 'ErlichBot: 2 commits in 1 day', - numberCommits: 2, - lastCommitDate: '2023-01-25', - firstCommitDate: '2023-01-24', - }, - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test2' }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - contributions: [ - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - { - id: 112529474, - url: 'https://github.com/bighead/startup-ideas', - topics: ['Ideas', 'Startups'], - summary: 'Startup Ideas: 20 commits in 1 week', - numberCommits: 20, - lastCommitDate: '03/01/2023', - firstCommitDate: '02/22/2023', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test3' }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAll( - { - filter: { numberOfOpenSourceContributionsRange: [2] }, - orderBy: 'numberOfOpenSourceContributions_DESC', - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows[0].numberOfOpenSourceContributions).toEqual(4) - expect(members.rows[1].numberOfOpenSourceContributions).toEqual(2) - }) - - it('is successfully finding and counting all members, and tags [nodejs, vuejs]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const nodeTag = await mockIRepositoryOptions.database.tag.create({ - name: 'nodejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - const vueTag = await mockIRepositoryOptions.database.tag.create({ - name: 'vuejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - - await MemberRepository.create( - { - username: { - [PlatformType.TWITTER]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.TWITTER]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - tags: [nodeTag.id, vueTag.id], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAll( - { filter: { tags: [nodeTag.id, vueTag.id] } }, - mockIRepositoryOptions, - ) - const member2 = members.rows.find((m) => m.username[PlatformType.TWITTER][0] === 'test2') - expect(members.rows.length).toEqual(1) - expect(member2.tags[0].name).toEqual('nodejs') - expect(member2.tags[1].name).toEqual('vuejs') - }) - - it('is successfully finding and counting all members, and tags [nodejs]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const nodeTag = await mockIRepositoryOptions.database.tag.create({ - name: 'nodejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - const vueTag = await mockIRepositoryOptions.database.tag.create({ - name: 'vuejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - tags: [nodeTag.id], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - tags: [nodeTag.id, vueTag.id], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAll( - { filter: { tags: [nodeTag.id] } }, - mockIRepositoryOptions, - ) - const member1 = members.rows.find((m) => m.username[PlatformType.GITHUB][0] === 'test1') - const member2 = members.rows.find((m) => m.username[PlatformType.GITHUB][0] === 'test2') - - expect(members.rows.length).toEqual(2) - expect(member1.tags[0].name).toEqual('nodejs') - expect(member1.tags[0].name).toEqual('nodejs') - expect(member2.tags[1].name).toEqual('vuejs') - }) - - it('is successfully finding and counting all members, and organisations [crowd.dev, pied piper]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const crowd = await mockIRepositoryOptions.database.organization.create({ - displayName: 'crowd.dev', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - await OrganizationRepository.addIdentity( - crowd.id, - { - name: 'crowd.dev', - url: 'https://crowd.dev', - platform: 'crowd', - }, - mockIRepositoryOptions, - ) - - const pp = await mockIRepositoryOptions.database.organization.create({ - displayName: 'pied piper', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - - await OrganizationRepository.addIdentity( - pp.id, - { - name: 'pied piper', - url: 'https://piedpiper.com', - platform: 'crowd', - }, - mockIRepositoryOptions, - ) - - await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - organizations: [crowd.id, pp.id], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAll( - { filter: { organizations: [crowd.id, pp.id] } }, - mockIRepositoryOptions, - ) - const member2 = members.rows.find((m) => m.username[PlatformType.SLACK][0] === 'test2') - expect(members.rows.length).toEqual(1) - expect(member2.organizations[0].displayName).toEqual('crowd.dev') - expect(member2.organizations[1].displayName).toEqual('pied piper') - }) - - it('is successfully finding and counting all members, and scoreRange is gte than 1 and less or equal to 6', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const user1 = { - username: { - [PlatformType.SLACK]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - } - const user2 = { - username: { - [PlatformType.SLACK]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - } - const user3 = { - username: { - [PlatformType.SLACK]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '7', - joinedAt: new Date(), - } - await MemberRepository.create(user1, mockIRepositoryOptions) - await MemberRepository.create(user2, mockIRepositoryOptions) - await MemberRepository.create(user3, mockIRepositoryOptions) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAll( - { filter: { scoreRange: [1, 6] } }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows.find((m) => m.username[PlatformType.SLACK][0] === 'test1').score).toEqual( - 1, - ) - expect(members.rows.find((m) => m.username[PlatformType.SLACK][0] === 'test2').score).toEqual( - 6, - ) - }) - - it('is successfully finding and counting all members, and scoreRange is gte than 7', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const user1 = { - username: { - [PlatformType.DISCORD]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - } - const user2 = { - username: { - [PlatformType.DISCORD]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - } - const user3 = { - username: { - [PlatformType.DISCORD]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - } - await MemberRepository.create(user1, mockIRepositoryOptions) - await MemberRepository.create(user2, mockIRepositoryOptions) - await MemberRepository.create(user3, mockIRepositoryOptions) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAll( - { filter: { scoreRange: [7] } }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(1) - for (const member of members.rows) { - expect(member.score).toBeGreaterThanOrEqual(7) - } - }) - - it('is successfully find and counting members with various filters, computed attributes, and full options (filter, limit, offset and orderBy)', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const nodeTag = await mockIRepositoryOptions.database.tag.create({ - name: 'nodejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - const vueTag = await mockIRepositoryOptions.database.tag.create({ - name: 'vuejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - - const member1 = await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - tags: [nodeTag.id], - reach: { - total: 15, - }, - }, - mockIRepositoryOptions, - ) - const member2 = await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - tags: [nodeTag.id, vueTag.id], - reach: { - total: 55, - }, - }, - mockIRepositoryOptions, - ) - const member3 = await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - tags: [vueTag.id], - reach: { - total: 124, - }, - }, - mockIRepositoryOptions, - ) - - await mockIRepositoryOptions.database.activity.bulkCreate([ - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-10'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member1.id, - username: member1.username[PlatformType.SLACK], - sourceId: '#sourceId1', - sentiment: { - positive: 0.55, - negative: 0.0, - neutral: 0.45, - mixed: 0.0, - label: 'positive', - sentiment: 0.1, - }, - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-11'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member2.id, - username: member2.username[PlatformType.SLACK], - sourceId: '#sourceId2', - sentiment: { - positive: 0.01, - negative: 0.55, - neutral: 0.55, - mixed: 0.0, - label: 'negative', - sentiment: -0.54, - }, - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-12'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member2.id, - username: member2.username[PlatformType.SLACK], - sourceId: '#sourceId3', - sentiment: { - positive: 0.94, - negative: 0.0, - neutral: 0.06, - mixed: 0.0, - label: 'positive', - sentiment: 0.94, - }, - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-13'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member3.id, - username: member3.username[PlatformType.SLACK], - sourceId: '#sourceId4', - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.42, - }, - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-14'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member3.id, - username: member3.username[PlatformType.SLACK], - sourceId: '#sourceId5', - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.41, - }, - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-15'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member3.id, - username: member3.username[PlatformType.SLACK], - sourceId: '#sourceId6', - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.18, - }, - }, - ]) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - let members = await MemberRepository.findAndCountAll( - { - filter: {}, - limit: 15, - offset: 0, - orderBy: 'activityCount_DESC', - }, - mockIRepositoryOptions, - ) - expect(members.rows.length).toEqual(3) - expect(members.rows[0].activityCount).toEqual('3') - expect(members.rows[0].lastActive.toISOString()).toEqual('2022-09-15T00:00:00.000Z') - - expect(members.rows[1].activityCount).toEqual('2') - expect(members.rows[1].lastActive.toISOString()).toEqual('2022-09-12T00:00:00.000Z') - - expect(members.rows[2].activityCount).toEqual('1') - expect(members.rows[2].tags[0].name).toEqual('nodejs') - expect(members.rows[2].lastActive.toISOString()).toEqual('2022-09-10T00:00:00.000Z') - - expect(members.rows[1].tags.map((i) => i.name).sort()).toEqual(['nodejs', 'vuejs']) - expect(members.rows[0].tags[0].name).toEqual('vuejs') - - // filter and order by reach - members = await MemberRepository.findAndCountAll( - { - filter: { - reachRange: [55], - }, - limit: 15, - offset: 0, - orderBy: 'reach_DESC', - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows[0].id).toEqual(member3.id) - expect(members.rows[1].id).toEqual(member2.id) - - // filter and sort by activity count - members = await MemberRepository.findAndCountAll( - { - filter: { - activityCountRange: [2], - }, - limit: 15, - offset: 0, - orderBy: 'activityCount_DESC', - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows.map((i) => i.id)).toEqual([member3.id, member2.id]) - - // filter and sort by lastActive - members = await MemberRepository.findAndCountAll( - { - filter: { - lastActiveRange: ['2022-09-11'], - }, - limit: 15, - offset: 0, - orderBy: 'lastActive_DESC', - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows.map((i) => i.id)).toEqual([member3.id, member2.id]) - - // filter and sort by averageSentiment (member1.avgSentiment = 0.1, member2.avgSentiment = 0.2, member3.avgSentiment = 0.34) - members = await MemberRepository.findAndCountAll( - { - filter: { - averageSentimentRange: [0.2], - }, - limit: 15, - offset: 0, - orderBy: 'averageSentiment_ASC', - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows.map((i) => i.id)).toEqual([member2.id, member3.id]) - }) - }) - - describe('findAndCountAllv2 method', () => { - it('is successfully finding and counting all members, sortedBy activitiesCount DESC', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test1' }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - const member2 = await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test2' }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - const member3 = await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test3' }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await mockIRepositoryOptions.database.activity.bulkCreate([ - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - username: 'test1', - memberId: member1.id, - sourceId: '#sourceId1', - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - username: 'test2', - memberId: member2.id, - sourceId: '#sourceId2', - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - username: 'test2', - memberId: member2.id, - sourceId: '#sourceId3', - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - username: 'test3', - memberId: member3.id, - sourceId: '#sourceId4', - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - username: 'test3', - memberId: member3.id, - sourceId: '#sourceId5', - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - username: 'test3', - memberId: member3.id, - sourceId: '#sourceId6', - }, - ]) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAllv2( - { filter: {}, orderBy: 'activityCount_DESC' }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(3) - expect(members.rows[0].activityCount).toEqual('3') - expect(members.rows[1].activityCount).toEqual('2') - expect(members.rows[2].activityCount).toEqual('1') - }) - - it('is successfully finding and counting all members, sortedBy numberOfOpenSourceContributions DESC', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test1' }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - contributions: [ - { - id: 112529472, - url: 'https://github.com/bachman/pied-piper', - topics: ['compression', 'data', 'middle-out', 'Java'], - summary: 'Pied Piper: 10 commits in 1 day', - numberCommits: 10, - lastCommitDate: '2023-03-10', - firstCommitDate: '2023-03-01', - }, - { - id: 112529473, - url: 'https://github.com/bachman/aviato', - topics: ['Python', 'Django'], - summary: 'Aviato: 5 commits in 1 day', - numberCommits: 5, - lastCommitDate: '2023-02-25', - firstCommitDate: '2023-02-20', - }, - { - id: 112529476, - url: 'https://github.com/bachman/erlichbot', - topics: ['Python', 'Slack API'], - summary: 'ErlichBot: 2 commits in 1 day', - numberCommits: 2, - lastCommitDate: '2023-01-25', - firstCommitDate: '2023-01-24', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test2' }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - contributions: [ - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - { - id: 112529474, - url: 'https://github.com/bighead/startup-ideas', - topics: ['Ideas', 'Startups'], - summary: 'Startup Ideas: 20 commits in 1 week', - numberCommits: 20, - lastCommitDate: '03/01/2023', - firstCommitDate: '02/22/2023', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test3' }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAllv2( - { filter: {}, orderBy: 'numberOfOpenSourceContributions_DESC' }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(3) - expect(members.rows[0].numberOfOpenSourceContributions).toEqual(3) - expect(members.rows[1].numberOfOpenSourceContributions).toEqual(2) - expect(members.rows[2].numberOfOpenSourceContributions).toEqual(0) - }) - - it('is successfully finding and counting all members, numberOfOpenSourceContributions range gte 3', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await MemberRepository.create( - { - username: { - [PlatformType.TWITTER]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - contributions: [ - { - id: 112529472, - url: 'https://github.com/bachman/pied-piper', - topics: ['compression', 'data', 'middle-out', 'Java'], - summary: 'Pied Piper: 10 commits in 1 day', - numberCommits: 10, - lastCommitDate: '2023-03-10', - firstCommitDate: '2023-03-01', - }, - { - id: 112529473, - url: 'https://github.com/bachman/aviato', - topics: ['Python', 'Django'], - summary: 'Aviato: 5 commits in 1 day', - numberCommits: 5, - lastCommitDate: '2023-02-25', - firstCommitDate: '2023-02-20', - }, - { - id: 112529476, - url: 'https://github.com/bachman/erlichbot', - topics: ['Python', 'Slack API'], - summary: 'ErlichBot: 2 commits in 1 day', - numberCommits: 2, - lastCommitDate: '2023-01-25', - firstCommitDate: '2023-01-24', - }, - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.TWITTER]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - contributions: [ - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - { - id: 112529474, - url: 'https://github.com/bighead/startup-ideas', - topics: ['Ideas', 'Startups'], - summary: 'Startup Ideas: 20 commits in 1 week', - numberCommits: 20, - lastCommitDate: '03/01/2023', - firstCommitDate: '02/22/2023', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.TWITTER]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAllv2( - { - filter: { - and: [ - { - and: [ - { - numberOfOpenSourceContributions: { - gte: 3, - }, - }, - ], - }, - ], - }, - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(1) - expect(members.rows[0].numberOfOpenSourceContributions).toEqual(4) - }) - - it('is successfully finding and counting all members, numberOfOpenSourceContributions range gte 2 and sort by asc', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test1' }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - contributions: [ - { - id: 112529472, - url: 'https://github.com/bachman/pied-piper', - topics: ['compression', 'data', 'middle-out', 'Java'], - summary: 'Pied Piper: 10 commits in 1 day', - numberCommits: 10, - lastCommitDate: '2023-03-10', - firstCommitDate: '2023-03-01', - }, - { - id: 112529473, - url: 'https://github.com/bachman/aviato', - topics: ['Python', 'Django'], - summary: 'Aviato: 5 commits in 1 day', - numberCommits: 5, - lastCommitDate: '2023-02-25', - firstCommitDate: '2023-02-20', - }, - { - id: 112529476, - url: 'https://github.com/bachman/erlichbot', - topics: ['Python', 'Slack API'], - summary: 'ErlichBot: 2 commits in 1 day', - numberCommits: 2, - lastCommitDate: '2023-01-25', - firstCommitDate: '2023-01-24', - }, - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test2' }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - contributions: [ - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - { - id: 112529474, - url: 'https://github.com/bighead/startup-ideas', - topics: ['Ideas', 'Startups'], - summary: 'Startup Ideas: 20 commits in 1 week', - numberCommits: 20, - lastCommitDate: '03/01/2023', - firstCommitDate: '02/22/2023', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test3' }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - const members = await MemberRepository.findAndCountAllv2( - { - filter: { - and: [ - { - and: [ - { - numberOfOpenSourceContributions: { - gte: 2, - }, - }, - ], - }, - ], - }, - orderBy: 'numberOfOpenSourceContributions_ASC', - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows[0].numberOfOpenSourceContributions).toEqual(2) - expect(members.rows[1].numberOfOpenSourceContributions).toEqual(4) - }) - - it('is successfully finding and counting all members, and tags [nodejs, vuejs]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const nodeTag = await mockIRepositoryOptions.database.tag.create({ - name: 'nodejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - const vueTag = await mockIRepositoryOptions.database.tag.create({ - name: 'vuejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - - await MemberRepository.create( - { - username: { - [PlatformType.TWITTER]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.TWITTER]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - tags: [nodeTag.id, vueTag.id], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAllv2( - { - filter: { - and: [ - { - tags: { - contains: [nodeTag.id, vueTag.id], - }, - }, - ], - }, - }, - mockIRepositoryOptions, - ) - const member2 = members.rows.find((m) => m.username[PlatformType.TWITTER].includes('test2')) - expect(members.rows.length).toEqual(1) - expect(member2.tags.map((t) => t.name)).toEqual(expect.arrayContaining(['nodejs', 'vuejs'])) - }) - - it('is successfully finding and counting all members, and tags [nodejs]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const nodeTag = await mockIRepositoryOptions.database.tag.create({ - name: 'nodejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - const vueTag = await mockIRepositoryOptions.database.tag.create({ - name: 'vuejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - tags: [nodeTag.id], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - tags: [nodeTag.id, vueTag.id], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAllv2( - { - filter: { - and: [ - { - tags: { - contains: [nodeTag.id], - }, - }, - ], - }, - }, - mockIRepositoryOptions, - ) - const member1 = members.rows.find((m) => m.username[PlatformType.GITHUB].includes('test1')) - const member2 = members.rows.find((m) => m.username[PlatformType.GITHUB].includes('test2')) - - expect(members.rows.length).toEqual(2) - expect(member1.tags[0].name).toEqual('nodejs') - expect(member2.tags.map((t) => t.name)).toEqual(expect.arrayContaining(['nodejs', 'vuejs'])) - }) - - it('is successfully finding and counting all members, and organisations [crowd.dev, pied piper]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const crowd = await OrganizationRepository.create( - { - identities: [ - { - name: 'crowd.dev', - url: 'https://crowd.dev', - platform: 'crowd', - }, - ], - displayName: 'crowd.dev', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }, - mockIRepositoryOptions, - ) - const pp = await OrganizationRepository.create( - { - identities: [ - { - name: 'pied piper', - url: 'https://piedpiper.com', - platform: 'crowd', - }, - ], - displayName: 'pied piper', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }, - mockIRepositoryOptions, - ) - - await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - organizations: [crowd.id, pp.id], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAllv2( - { - filter: { - and: [ - { - organizations: { - contains: [crowd.id, pp.id], - }, - }, - ], - }, - }, - mockIRepositoryOptions, - ) - const member2 = members.rows.find((m) => m.username[PlatformType.SLACK].includes('test2')) - expect(members.rows.length).toEqual(1) - expect(member2.organizations.map((o) => o.displayName)).toEqual( - expect.arrayContaining(['crowd.dev', 'pied piper']), - ) - }) - - it('is successfully finding and counting all members, and scoreRange is gte than 7', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const user1 = { - username: { - [PlatformType.DISCORD]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - } - const user2 = { - username: { - [PlatformType.DISCORD]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - } - const user3 = { - username: { - [PlatformType.DISCORD]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - } - await MemberRepository.create(user1, mockIRepositoryOptions) - await MemberRepository.create(user2, mockIRepositoryOptions) - await MemberRepository.create(user3, mockIRepositoryOptions) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAllv2( - { - filter: { - and: [ - { - and: [ - { - score: { - gte: 7, - }, - }, - ], - }, - ], - }, - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(1) - for (const member of members.rows) { - expect(member.score).toBeGreaterThanOrEqual(7) - } - }) - - it('is successfully find and counting members with various filters, computed attributes, and full options (filter, limit, offset and orderBy)', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const nodeTag = await mockIRepositoryOptions.database.tag.create({ - name: 'nodejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - const vueTag = await mockIRepositoryOptions.database.tag.create({ - name: 'vuejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - - const member1 = await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - tags: [nodeTag.id], - reach: { - total: 15, - }, - }, - mockIRepositoryOptions, - ) - const member2 = await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - tags: [nodeTag.id, vueTag.id], - reach: { - total: 55, - }, - }, - mockIRepositoryOptions, - ) - const member3 = await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - tags: [vueTag.id], - reach: { - total: 124, - }, - }, - mockIRepositoryOptions, - ) - - await mockIRepositoryOptions.database.activity.bulkCreate([ - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-10'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member1.id, - username: member1.username[PlatformType.SLACK], - sourceId: '#sourceId1', - sentiment: { - positive: 0.55, - negative: 0.0, - neutral: 0.45, - mixed: 0.0, - label: 'positive', - sentiment: 0.1, - }, - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-11'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member2.id, - username: member2.username[PlatformType.SLACK], - sourceId: '#sourceId2', - sentiment: { - positive: 0.01, - negative: 0.55, - neutral: 0.55, - mixed: 0.0, - label: 'negative', - sentiment: -0.54, - }, - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-12'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member2.id, - username: member2.username[PlatformType.SLACK], - sourceId: '#sourceId3', - sentiment: { - positive: 0.94, - negative: 0.0, - neutral: 0.06, - mixed: 0.0, - label: 'positive', - sentiment: 0.94, - }, - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-13'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member3.id, - username: member3.username[PlatformType.SLACK], - sourceId: '#sourceId4', - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.42, - }, - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-14'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member3.id, - username: member3.username[PlatformType.SLACK], - sourceId: '#sourceId5', - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.41, - }, - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-15'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member3.id, - username: member3.username[PlatformType.SLACK], - sourceId: '#sourceId6', - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.18, - }, - }, - ]) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - let members = await MemberRepository.findAndCountAllv2( - { - filter: {}, - limit: 15, - offset: 0, - orderBy: 'activityCount_DESC', - }, - mockIRepositoryOptions, - ) - expect(members.rows.length).toEqual(3) - expect(members.rows[0].activityCount).toEqual('3') - expect(members.rows[0].lastActive.toISOString()).toEqual('2022-09-15T00:00:00.000Z') - - expect(members.rows[1].activityCount).toEqual('2') - expect(members.rows[1].lastActive.toISOString()).toEqual('2022-09-12T00:00:00.000Z') - - expect(members.rows[2].activityCount).toEqual('1') - expect(members.rows[2].tags[0].name).toEqual('nodejs') - expect(members.rows[2].lastActive.toISOString()).toEqual('2022-09-10T00:00:00.000Z') - - expect(members.rows[1].tags.map((i) => i.name).sort()).toEqual(['nodejs', 'vuejs']) - expect(members.rows[0].tags[0].name).toEqual('vuejs') - - // filter and order by reach - members = await MemberRepository.findAndCountAllv2( - { - filter: { - and: [ - { - and: [ - { - reach: { - gte: 55, - }, - }, - ], - }, - ], - }, - limit: 15, - offset: 0, - orderBy: 'reach_DESC', - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows[0].id).toEqual(member3.id) - expect(members.rows[1].id).toEqual(member2.id) - - // filter and sort by activity count - members = await MemberRepository.findAndCountAllv2( - { - filter: { - and: [ - { - and: [ - { - activityCount: { - gte: 2, - }, - }, - ], - }, - ], - }, - limit: 15, - offset: 0, - orderBy: 'activityCount_DESC', - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows.map((i) => i.id)).toEqual([member3.id, member2.id]) - - // filter and sort by lastActive - members = await MemberRepository.findAndCountAllv2( - { - filter: { - and: [ - { - and: [ - { - lastActive: { - gte: '2022-09-11', - }, - }, - ], - }, - ], - }, - limit: 15, - offset: 0, - orderBy: 'lastActive_DESC', - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows.map((i) => i.id)).toEqual([member3.id, member2.id]) - - // filter and sort by averageSentiment (member1.avgSentiment = 0.1, member2.avgSentiment = 0.2, member3.avgSentiment = 0.34) - members = await MemberRepository.findAndCountAllv2( - { - filter: { - and: [ - { - and: [ - { - averageSentiment: { - gte: 0.2, - }, - }, - ], - }, - ], - }, - limit: 15, - offset: 0, - orderBy: 'averageSentiment_ASC', - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows.map((i) => i.id)).toEqual([member2.id, member3.id]) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created member', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = { - username: { - [PlatformType.DISCORD]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: '2021-05-27T15:14:30Z', - } - let cloned = lodash.cloneDeep(member1) - const returnedMember = await MemberRepository.create(cloned, mockIRepositoryOptions) - - const updateFields = { - username: { - [PlatformType.GITHUB]: { - username: 'anil_github', - integrationId: generateUUIDv1(), - }, - }, - emails: ['lala@l.com'], - score: 10, - attributes: { - [PlatformType.GITHUB]: { - name: 'Quoc-Anh Nguyen', - isHireable: true, - url: 'https://github.com/imcvampire', - websiteUrl: 'https://imcvampire.js.org/', - bio: 'Lazy geek', - location: 'Helsinki, Finland', - actions: [ - { - score: 2, - timestamp: '2021-05-27T15:13:30Z', - }, - ], - }, - [PlatformType.TWITTER]: { - profile_url: 'https://twitter.com/imcvampire', - url: 'https://twitter.com/imcvampire', - }, - }, - joinedAt: '2021-06-27T15:14:30Z', - location: 'Istanbul', - } - - cloned = lodash.cloneDeep(updateFields) - const updatedMember = await MemberRepository.update( - returnedMember.id, - cloned, - mockIRepositoryOptions, - ) - - // check updatedAt field looks ok or not. Should be greater than createdAt - expect(updatedMember.updatedAt.getTime()).toBeGreaterThan(updatedMember.createdAt.getTime()) - - updatedMember.createdAt = updatedMember.createdAt.toISOString().split('T')[0] - updatedMember.updatedAt = updatedMember.updatedAt.toISOString().split('T')[0] - - const expectedMemberCreated = { - id: returnedMember.id, - username: mapUsername({ - ...updateFields.username, - ...member1.username, - }), - identities: ['discord', 'github'], - displayName: returnedMember.displayName, - attributes: updateFields.attributes, - emails: updateFields.emails, - score: updateFields.score, - lastEnriched: null, - enrichedBy: [], - contributions: null, - organizations: [], - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - activities: [], - reach: { total: -1 }, - notes: [], - tasks: [], - activeOn: [], - activityTypes: [], - joinedAt: new Date(updateFields.joinedAt), - tags: [], - noMerge: [], - toMerge: [], - activityCount: 0, - activeDaysCount: 0, - averageSentiment: 0, - numberOfOpenSourceContributions: 0, - lastActive: null, - lastActivity: null, - affiliations: [], - manuallyCreated: false, - } - - expect(updatedMember).toStrictEqual(expectedMemberCreated) - }) - - it('Should update successfuly but return without relations when doPopulateRelations=false', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = { - username: { - [PlatformType.DISCORD]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: '2021-05-27T15:14:30Z', - } - const returnedMember = await MemberRepository.create(member1, mockIRepositoryOptions) - - const updateFields = { - username: { - [PlatformType.GITHUB]: { - username: 'anil_github', - integrationId: generateUUIDv1(), - }, - }, - emails: ['lala@l.com'], - score: 10, - attributes: { - [PlatformType.GITHUB]: { - name: 'Quoc-Anh Nguyen', - isHireable: true, - url: 'https://github.com/imcvampire', - websiteUrl: 'https://imcvampire.js.org/', - bio: 'Lazy geek', - location: 'Helsinki, Finland', - actions: [ - { - score: 2, - timestamp: '2021-05-27T15:13:30Z', - }, - ], - }, - [PlatformType.TWITTER]: { - profile_url: 'https://twitter.com/imcvampire', - url: 'https://twitter.com/imcvampire', - }, - }, - joinedAt: '2021-06-27T15:14:30Z', - location: 'Istanbul', - } - - const updatedMember = await MemberRepository.update( - returnedMember.id, - updateFields, - mockIRepositoryOptions, - false, - ) - - // check updatedAt field looks ok or not. Should be greater than createdAt - expect(updatedMember.updatedAt.getTime()).toBeGreaterThan(updatedMember.createdAt.getTime()) - - updatedMember.createdAt = updatedMember.createdAt.toISOString().split('T')[0] - updatedMember.updatedAt = updatedMember.updatedAt.toISOString().split('T')[0] - - const expectedMemberCreated = { - id: returnedMember.id, - username: mapUsername({ - [PlatformType.DISCORD]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - [PlatformType.GITHUB]: { - username: 'anil_github', - integrationId: generateUUIDv1(), - }, - }), - displayName: returnedMember.displayName, - attributes: updateFields.attributes, - lastEnriched: null, - enrichedBy: [], - organizations: [], - contributions: null, - emails: updateFields.emails, - score: updateFields.score, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - reach: { total: -1 }, - joinedAt: new Date(updateFields.joinedAt), - affiliations: [], - manuallyCreated: false, - } - - expect(updatedMember).toStrictEqual(expectedMemberCreated) - }) - - it('Should successfully update member with given tags', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tag1 = await TagRepository.create({ name: 'tag1' }, mockIRepositoryOptions) - const tag2 = await TagRepository.create({ name: 'tag2' }, mockIRepositoryOptions) - const tag3 = await TagRepository.create({ name: 'tag3' }, mockIRepositoryOptions) - - // Create member with tag3 - let member1 = await MemberRepository.create( - { - username: { - [PlatformType.DISCORD]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - tags: [tag3.id], - }, - mockIRepositoryOptions, - ) - - // When feeding tags attribute to update, update method will overwrite the member's tags with new given tags - // member1 is expected to have [tag1,tag2] after update - member1 = await MemberRepository.update( - member1.id, - { tags: [tag1.id, tag2.id] }, - mockIRepositoryOptions, - ) - - member1.createdAt = member1.createdAt.toISOString().split('T')[0] - member1.updatedAt = member1.updatedAt.toISOString().split('T')[0] - - member1.tags = member1.tags.map((i) => i.get({ plain: true })) - - // strip members field from tags created to expect. - // we won't be returning second level relationships. - const { members: _tag1Members, ...tag1Plain } = tag1 - const { members: _tag2Members, ...tag2Plain } = tag2 - - const expectedMemberCreated = { - id: member1.id, - username: member1.username, - displayName: member1.displayName, - identities: ['discord'], - attributes: {}, - emails: member1.emails, - score: member1.score, - organizations: [], - lastEnriched: null, - enrichedBy: [], - contributions: null, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - activities: [], - reach: { total: -1 }, - notes: [], - tasks: [], - activeOn: [], - activityTypes: [], - joinedAt: new Date(member1.joinedAt), - tags: [tag1Plain, tag2Plain], - noMerge: [], - toMerge: [], - activityCount: 0, - activeDaysCount: 0, - averageSentiment: 0, - numberOfOpenSourceContributions: 0, - lastActive: null, - lastActivity: null, - affiliations: [], - manuallyCreated: false, - } - - expect(member1).toStrictEqual(expectedMemberCreated) - }) - - it('Should successfully update member with given organizations', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const org1 = await OrganizationRepository.create( - { - displayName: 'crowd.dev', - identities: [{ name: 'crowd.dev', url: 'https://crowd.dev', platform: 'crowd' }], - }, - mockIRepositoryOptions, - ) - const org2 = await OrganizationRepository.create( - { - displayName: 'pied piper', - identities: [{ name: 'pied piper', url: 'https://piedpiper.com', platform: 'crowd' }], - }, - mockIRepositoryOptions, - ) - const org3 = await OrganizationRepository.create( - { - displayName: 'hooli', - identities: [{ name: 'hooli', url: 'https://hooli.com', platform: 'crowd' }], - }, - mockIRepositoryOptions, - ) - - // Create member with tag3 - let member1 = await MemberRepository.create( - { - username: { - [PlatformType.DISCORD]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: new Date(), - organizations: [org3.id], - }, - mockIRepositoryOptions, - ) - - // When feeding organizations attribute to update, update method will overwrite the member's organizations with new given orgs - // member1 is expected to have [org1,org2] after update - member1 = await MemberRepository.update( - member1.id, - { organizations: [org1.id, org2.id], organizationsReplace: true }, - mockIRepositoryOptions, - ) - - member1.createdAt = member1.createdAt.toISOString().split('T')[0] - member1.updatedAt = member1.updatedAt.toISOString().split('T')[0] - - member1.organizations = member1.organizations.map((i) => - SequelizeTestUtils.objectWithoutKey(i.get({ plain: true }), ['memberOrganizations']), - ) - - // // sort member organizations by createdAt - // member1.organizations.sort((a, b) => { - // return a.createdAt < b.createdAt ? -1 : 1 - // }) - - // strip members field from tags created to expect. - // we won't be returning second level relationships. - const { memberCount: _tag1Members, ...org1Plain } = org1 - const { memberCount: _tag2Members, ...org2Plain } = org2 - - const expectedMemberCreated = { - id: member1.id, - username: member1.username, - displayName: member1.displayName, - identities: ['discord'], - attributes: {}, - emails: member1.emails, - score: member1.score, - tags: [], - lastEnriched: null, - enrichedBy: [], - contributions: null, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - activeOn: [], - activityTypes: [], - activities: [], - reach: { total: -1 }, - joinedAt: new Date(member1.joinedAt), - organizations: [ - SequelizeTestUtils.objectWithoutKey(org1Plain, [ - 'lastActive', - 'identities', - 'activeOn', - 'joinedAt', - 'activityCount', - 'segments', - 'weakIdentities', - ]), - SequelizeTestUtils.objectWithoutKey(org2Plain, [ - 'lastActive', - 'identities', - 'activeOn', - 'joinedAt', - 'activityCount', - 'segments', - 'weakIdentities', - ]), - ], - noMerge: [], - toMerge: [], - notes: [], - tasks: [], - activityCount: 0, - activeDaysCount: 0, - averageSentiment: 0, - numberOfOpenSourceContributions: 0, - lastActive: null, - lastActivity: null, - affiliations: [], - manuallyCreated: false, - } - - member1.organizations = member1.organizations.sort((a, b) => { - if (a.displayName < b.displayName) { - return -1 - } - if (a.displayName > b.displayName) { - return 1 - } - return 0 - }) - - expect(member1).toStrictEqual(expectedMemberCreated) - }) - - it('Should succesfully update member with notes', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const notes1 = await NoteRepository.create( - { - body: 'note1', - }, - mockIRepositoryOptions, - ) - - const notes2 = await NoteRepository.create( - { - body: 'note2', - }, - mockIRepositoryOptions, - ) - - const member2add = { - username: { - [PlatformType.DISCORD]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - } - - const memberCreated = await MemberRepository.create(member2add, mockIRepositoryOptions) - const memberUpdated = await MemberRepository.update( - memberCreated.id, - { notes: [notes1.id, notes2.id] }, - mockIRepositoryOptions, - ) - expect(memberCreated.notes).toHaveLength(0) - expect(memberUpdated.notes).toHaveLength(2) - expect(memberUpdated.notes[0].id).toEqual(notes1.id) - expect(memberUpdated.notes[1].id).toEqual(notes2.id) - }) - - it('Should succesfully update member with tasks', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tasks1 = await TaskRepository.create( - { - name: 'task1', - }, - mockIRepositoryOptions, - ) - - const task2 = await TaskRepository.create( - { - name: 'task2', - }, - mockIRepositoryOptions, - ) - - const member2add = { - username: { - [PlatformType.DISCORD]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - } - - const memberCreated = await MemberRepository.create(member2add, mockIRepositoryOptions) - expect(memberCreated.tasks).toHaveLength(0) - - const memberUpdated = await MemberRepository.update( - memberCreated.id, - { tasks: [tasks1.id, task2.id] }, - mockIRepositoryOptions, - ) - expect(memberUpdated.tasks).toHaveLength(2) - expect(memberUpdated.tasks.find((t) => t.id === tasks1.id)).not.toBeUndefined() - expect(memberUpdated.tasks.find((t) => t.id === task2.id)).not.toBeUndefined() - }) - - it('Should throw 404 error when trying to update non existent member', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - MemberRepository.update(randomUUID(), { location: 'test' }, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw a sequelize foreign key error when trying to update a member with a non existing tag', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - const member1 = await MemberRepository.create( - { - username: { - [PlatformType.DISCORD]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await expect(() => - MemberRepository.update(member1.id, { tags: [randomUUID()] }, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should succesfully update member organization affiliations', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const segmentRepo = new SegmentRepository(mockIRepositoryOptions) - - const segment1 = await segmentRepo.create({ - name: 'Crowd.dev - Segment1', - url: '', - parentName: 'Crowd.dev - Segment1', - grandparentName: 'Crowd.dev - Segment1', - slug: 'crowd.dev-1', - parentSlug: 'crowd.dev-1', - grandparentSlug: 'crowd.dev-1', - status: SegmentStatus.ACTIVE, - sourceId: null, - sourceParentId: null, - }) - - const segment2 = await segmentRepo.create({ - name: 'Crowd.dev - Segment2', - url: '', - parentName: 'Crowd.dev - Segment2', - grandparentName: 'Crowd.dev - Segment2', - slug: 'crowd.dev-2', - parentSlug: 'crowd.dev-2', - grandparentSlug: 'crowd.dev-2', - status: SegmentStatus.ACTIVE, - sourceId: null, - sourceParentId: null, - }) - - const org1 = await OrganizationRepository.create( - { - displayName: 'crowd.dev', - }, - mockIRepositoryOptions, - ) - - const member2add = { - username: { - [PlatformType.DISCORD]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - affiliations: [ - { - segmentId: segment1.id, - organizationId: org1.id, - }, - { - segmentId: segment2.id, - organizationId: null, - }, - ], - } - - const memberCreated = await MemberRepository.create(member2add, mockIRepositoryOptions) - expect(memberCreated.affiliations).toHaveLength(2) - - // removes segment1 affiliation, and set segment2 affilition to org1 - const memberUpdated = await MemberRepository.update( - memberCreated.id, - { - affiliations: [ - { - segmentId: segment2.id, - organizationId: org1.id, - }, - ], - }, - mockIRepositoryOptions, - ) - - expect(memberUpdated.affiliations.filter((a) => a.segmentId === segment1.id)).toHaveLength(0) - expect( - memberUpdated.affiliations.filter((a) => a.segmentId === segment2.id)[0].organizationId, - ).toEqual(org1.id) - }) - }) - - describe('destroy method', () => { - it('Should succesfully destroy previously created member', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = { - username: { - [PlatformType.DISCORD]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: '2021-05-27T15:14:30Z', - } - const returnedMember = await MemberRepository.create(member1, mockIRepositoryOptions) - - await MemberRepository.destroy(returnedMember.id, mockIRepositoryOptions, true) - - // Try selecting it after destroy, should throw 404 - await expect(() => - MemberRepository.findById(returnedMember.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw 404 when trying to destroy a non existent member', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - MemberRepository.destroy(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('removeToMerge method', () => { - it('Should remove a member from other members toMerge list', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = { - username: { - [PlatformType.DISCORD]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - } - - const member2 = { - username: { - [PlatformType.DISCORD]: { - username: 'anil2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - joinedAt: '2020-05-27T15:13:30Z', - } - - const memberCreated1 = await MemberRepository.create(member1, mockIRepositoryOptions) - const memberCreated2 = await MemberRepository.create(member2, mockIRepositoryOptions) - - await MemberRepository.addToMerge( - [{ members: [memberCreated1.id, memberCreated2.id], similarity: null }], - mockIRepositoryOptions, - ) - await MemberRepository.addToMerge( - [{ members: [memberCreated2.id, memberCreated1.id], similarity: null }], - mockIRepositoryOptions, - ) - - let m1 = await MemberRepository.findById(memberCreated1.id, mockIRepositoryOptions) - const m2 = await MemberRepository.findById(memberCreated2.id, mockIRepositoryOptions) - m1 = await MemberRepository.removeToMerge( - memberCreated1.id, - memberCreated2.id, - mockIRepositoryOptions, - ) - - // Member2 should be removed from Member1.toMerge - expect(m1.toMerge.length).toBe(0) - - // Member1 is still in member2.toMerge list - expect(m2.toMerge[0]).toBe(m1.id) - }) - }) - - describe('addNoMerge method', () => { - it('Should add a member to other members noMerge list', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = { - username: { - [PlatformType.DISCORD]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - } - - const member2 = { - username: { - [PlatformType.DISCORD]: { - username: 'anil2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - joinedAt: '2020-05-27T15:13:30Z', - } - - const memberCreated1 = await MemberRepository.create(member1, mockIRepositoryOptions) - const memberCreated2 = await MemberRepository.create(member2, mockIRepositoryOptions) - - let memberUpdated1 = await MemberRepository.addNoMerge( - memberCreated1.id, - memberCreated2.id, - mockIRepositoryOptions, - ) - const memberUpdated2 = await MemberRepository.addNoMerge( - memberCreated2.id, - memberCreated1.id, - mockIRepositoryOptions, - ) - - memberUpdated1 = await MemberRepository.removeToMerge( - memberCreated1.id, - memberCreated2.id, - mockIRepositoryOptions, - ) - - expect(memberUpdated1.noMerge[0]).toBe(memberUpdated2.id) - expect(memberUpdated2.noMerge[0]).toBe(memberUpdated1.id) - }) - }) - - describe('removeNoMerge method', () => { - let options - let memberService - - let defaultMember - - beforeEach(async () => { - options = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(options) - - memberService = new MemberService(options) - - defaultMember = { - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - } - }) - it('Should remove a member from other members noMerge list', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = { - username: { - [PlatformType.DISCORD]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - } - - const member2 = { - username: { - [PlatformType.DISCORD]: { - username: 'anil2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - joinedAt: '2020-05-27T15:13:30Z', - } - - const memberCreated1 = await MemberRepository.create(member1, mockIRepositoryOptions) - const memberCreated2 = await MemberRepository.create(member2, mockIRepositoryOptions) - - let memberUpdated1 = await MemberRepository.addNoMerge( - memberCreated1.id, - memberCreated2.id, - mockIRepositoryOptions, - ) - const memberUpdated2 = await MemberRepository.addNoMerge( - memberCreated2.id, - memberCreated1.id, - mockIRepositoryOptions, - ) - - memberUpdated1 = await MemberRepository.removeNoMerge( - memberCreated1.id, - memberCreated2.id, - mockIRepositoryOptions, - ) - - // Member2 should be removed from Member1.noMerge - expect(memberUpdated1.noMerge.length).toBe(0) - - // Member1 is still in member2.noMerge list - expect(memberUpdated2.noMerge[0]).toBe(memberUpdated1.id) - }) - }) - - describe('work experiences', () => { - let options - let memberService - let organizationService - - let defaultMember - - beforeEach(async () => { - options = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(options) - - memberService = new MemberService(options) - organizationService = new OrganizationService(options) - - defaultMember = { - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - } - }) - - async function createMember(data = {}) { - return await memberService.upsert({ - ...defaultMember, - username: { - [PlatformType.GITHUB]: uuid(), - }, - ...data, - }) - } - - async function createOrg(name, data = {}) { - return await organizationService.createOrUpdate({ - identities: [ - { - name, - platform: 'crowd', - }, - ], - ...data, - }) - } - - async function addWorkExperience(memberId, orgId, data = {}) { - return await MemberRepository.createOrUpdateWorkExperience( - { - memberId, - organizationId: orgId, - source: 'test', - ...data, - }, - options, - ) - } - - async function findMember(id) { - return await memberService.findById(id) - } - - function formatDate(value) { - if (!value) { - return null - } - return moment(value).format('YYYY-MM-DD') - } - - it('Should not create multiple work experiences for same org without dates', async () => { - let member = await createMember() - - const org = await createOrg('org') - - await addWorkExperience(member.id, org.id) - await addWorkExperience(member.id, org.id) - - member = await findMember(member.id) - - expect(member.organizations.length).toBe(1) - }) - - it('Should not create multiple work experiences for same org with same start dates', async () => { - let member = await createMember() - - const org = await createOrg('org') - - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - }) - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - }) - - member = await findMember(member.id) - - expect(member.organizations.length).toBe(1) - }) - - it('Should not create multiple work experiences for same org with same dates', async () => { - let member = await createMember() - - const org = await createOrg('org') - - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - dateEnd: '2020-01-05', - }) - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - dateEnd: '2020-01-05', - }) - - member = await findMember(member.id) - - expect(member.organizations.length).toBe(1) - }) - - it('Should create multiple work experiences for same org with different dates', async () => { - let member = await createMember() - - const org = await createOrg('org') - - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - }) - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-08', - }) - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - dateEnd: '2020-01-05', - }) - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-06', - dateEnd: '2020-01-07', - }) - - member = await findMember(member.id) - - expect(member.organizations.length).toBe(4) - }) - - it('Should clean up work experiences without dates once we get start dates', async () => { - let member = await createMember() - - const org = await createOrg('org') - - await addWorkExperience(member.id, org.id) - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - }) - - member = await findMember(member.id) - - expect(member.organizations.length).toBe(1) - const dates = member.organizations[0].memberOrganizations.dataValues - expect(formatDate(dates.dateStart)).toBe('2020-01-01') - expect(formatDate(dates.dateEnd)).toBeNull() - }) - it('Should clean up work experiences without dates once we get both dates', async () => { - let member = await createMember() - - const org = await createOrg('org') - - await addWorkExperience(member.id, org.id) - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - dateEnd: '2020-07-01', - }) - - member = await findMember(member.id) - - expect(member.organizations.length).toBe(1) - const dates = member.organizations[0].memberOrganizations.dataValues - expect(formatDate(dates.dateStart)).toBe('2020-01-01') - expect(formatDate(dates.dateEnd)).toBe('2020-07-01') - }) - it('Should not add new work experiences without dates if we have start dates', async () => { - let member = await createMember() - - const org = await createOrg('org') - - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - }) - await addWorkExperience(member.id, org.id) - - member = await findMember(member.id) - - expect(member.organizations.length).toBe(1) - const dates = member.organizations[0].memberOrganizations.dataValues - expect(formatDate(dates.dateStart)).toBe('2020-01-01') - expect(formatDate(dates.dateEnd)).toBeNull() - }) - it('Should not add new work experiences without dates if we have both dates', async () => { - let member = await createMember() - - const org = await createOrg('org') - - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - dateEnd: '2020-07-01', - }) - await addWorkExperience(member.id, org.id) - - member = await findMember(member.id) - - expect(member.organizations.length).toBe(1) - const dates = member.organizations[0].memberOrganizations.dataValues - expect(formatDate(dates.dateStart)).toBe('2020-01-01') - expect(formatDate(dates.dateEnd)).toBe('2020-07-01') - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/microserviceRepository.test.ts b/backend/src/database/repositories/__tests__/microserviceRepository.test.ts deleted file mode 100644 index 2550e5290c..0000000000 --- a/backend/src/database/repositories/__tests__/microserviceRepository.test.ts +++ /dev/null @@ -1,463 +0,0 @@ -import MicroserviceRepository from '../microserviceRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import Error404 from '../../../errors/Error404' - -const db = null - -describe('MicroserviceRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create a microservice succesfully with default values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservice2Add = { type: 'members_score' } - - const microserviceCreated = await MicroserviceRepository.create( - microservice2Add, - mockIRepositoryOptions, - ) - - microserviceCreated.createdAt = microserviceCreated.createdAt.toISOString().split('T')[0] - microserviceCreated.updatedAt = microserviceCreated.updatedAt.toISOString().split('T')[0] - - const microserviceExpected = { - id: microserviceCreated.id, - init: false, - running: false, - type: microservice2Add.type, - variant: 'default', - settings: {}, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(microserviceCreated).toStrictEqual(microserviceExpected) - }) - - it('Should create a microservice succesfully with given values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservice2Add = { - init: true, - running: true, - type: 'members_score', - variant: 'premium', - settings: { testSettingsField: 'test' }, - } - - const microserviceCreated = await MicroserviceRepository.create( - microservice2Add, - mockIRepositoryOptions, - ) - - microserviceCreated.createdAt = microserviceCreated.createdAt.toISOString().split('T')[0] - microserviceCreated.updatedAt = microserviceCreated.updatedAt.toISOString().split('T')[0] - - const microserviceExpected = { - id: microserviceCreated.id, - init: microservice2Add.init, - running: microservice2Add.running, - type: microservice2Add.type, - variant: microservice2Add.variant, - settings: microservice2Add.settings, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(microserviceCreated).toStrictEqual(microserviceExpected) - }) - - it('Should throw unique constraint error for creation of already existing type microservice in the same tenant', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservice1 = { - init: true, - running: true, - type: 'members_score', - variant: 'premium', - settings: { testSettingsField: 'test' }, - } - - await MicroserviceRepository.create(microservice1, mockIRepositoryOptions) - - await expect(() => - MicroserviceRepository.create({ type: 'members_score' }, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should throw not null error if no type is given', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservice2Add = { - init: true, - running: true, - variant: 'premium', - settings: { testSettingsField: 'test' }, - } - - await expect(() => - MicroserviceRepository.create(microservice2Add, mockIRepositoryOptions), - ).rejects.toThrow() - }) - }) - - describe('findById method', () => { - it('Should successfully find created microservice by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservice2Add = { type: 'members_score' } - - const microserviceCreated = await MicroserviceRepository.create( - microservice2Add, - mockIRepositoryOptions, - ) - - const microserviceExpected = { - id: microserviceCreated.id, - init: false, - running: false, - type: microservice2Add.type, - variant: 'default', - settings: {}, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - const microserviceById = await MicroserviceRepository.findById( - microserviceCreated.id, - mockIRepositoryOptions, - ) - - microserviceById.createdAt = microserviceById.createdAt.toISOString().split('T')[0] - microserviceById.updatedAt = microserviceById.updatedAt.toISOString().split('T')[0] - - expect(microserviceById).toStrictEqual(microserviceExpected) - }) - - it('Should throw 404 error when no microservice found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - MicroserviceRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('filterIdsInTenant method', () => { - it('Should return the given ids of previously created microservice entities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservice1Created = await MicroserviceRepository.create( - { type: 'members_score' }, - mockIRepositoryOptions, - ) - const microservice2Created = await MicroserviceRepository.create( - { type: 'second' }, - mockIRepositoryOptions, - ) - - const filterIdsReturned = await MicroserviceRepository.filterIdsInTenant( - [microservice1Created.id, microservice2Created.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([microservice1Created.id, microservice2Created.id]) - }) - - it('Should only return the ids of previously created microservices and filter random uuids out', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microserviceCreated = await MicroserviceRepository.create( - { type: 'members_score' }, - mockIRepositoryOptions, - ) - - const { randomUUID } = require('crypto') - - const filterIdsReturned = await MicroserviceRepository.filterIdsInTenant( - [microserviceCreated.id, randomUUID(), randomUUID()], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([microserviceCreated.id]) - }) - - it('Should return an empty array for an irrelevant tenant', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microserviceCreated = await MicroserviceRepository.create( - { type: 'members_score' }, - mockIRepositoryOptions, - ) - - // create a new tenant and bind options to it - mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const filterIdsReturned = await MicroserviceRepository.filterIdsInTenant( - [microserviceCreated.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([]) - }) - }) - - describe('findAndCountAll method', () => { - it('Should find and count all microservices, with various filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservice1Created = await MicroserviceRepository.create( - { type: 'members_score', variant: 'premium' }, - mockIRepositoryOptions, - ) - - const microservice2Created = await MicroserviceRepository.create( - { type: 'second', variant: 'premium' }, - mockIRepositoryOptions, - ) - - // Filter by type - let microservices = await MicroserviceRepository.findAndCountAll( - { filter: { type: 'members_score' } }, - mockIRepositoryOptions, - ) - - expect(microservices.count).toEqual(1) - expect(microservices.rows).toStrictEqual([microservice1Created]) - - // Filter by id - microservices = await MicroserviceRepository.findAndCountAll( - { filter: { id: microservice1Created.id } }, - mockIRepositoryOptions, - ) - - expect(microservices.count).toEqual(1) - expect(microservices.rows).toStrictEqual([microservice1Created]) - - // Filter by variant - microservices = await MicroserviceRepository.findAndCountAll( - { filter: { variant: 'premium' } }, - mockIRepositoryOptions, - ) - - expect(microservices.count).toEqual(2) - expect(microservices.rows).toStrictEqual([microservice2Created, microservice1Created]) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created microservice', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microserviceCreated = await MicroserviceRepository.create( - { type: 'twitter_followers' }, - mockIRepositoryOptions, - ) - - const microserviceUpdated = await MicroserviceRepository.update( - microserviceCreated.id, - { - init: true, - running: true, - variant: 'premium', - settings: { - testSettingAttribute: { - someAtt: 'test', - someOtherAtt: true, - }, - }, - }, - mockIRepositoryOptions, - ) - - expect(microserviceUpdated.updatedAt.getTime()).toBeGreaterThan( - microserviceUpdated.createdAt.getTime(), - ) - - const microserviceExpected = { - id: microserviceCreated.id, - init: microserviceUpdated.init, - running: microserviceUpdated.running, - type: microserviceCreated.type, - variant: microserviceUpdated.variant, - settings: microserviceUpdated.settings, - importHash: null, - createdAt: microserviceCreated.createdAt, - updatedAt: microserviceUpdated.updatedAt, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(microserviceUpdated).toStrictEqual(microserviceExpected) - }) - - it('Should throw 404 error when trying to update non existent microservice', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - MicroserviceRepository.update(randomUUID(), { type: 'some-type' }, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - describe('destroy method', () => { - it('Should succesfully destroy previously created microservice', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microserviceCreated = await MicroserviceRepository.create( - { type: 'members_score', variant: 'premium' }, - mockIRepositoryOptions, - ) - - await MicroserviceRepository.destroy(microserviceCreated.id, mockIRepositoryOptions) - - // Try selecting it after destroy, should throw 404 - await expect(() => - MicroserviceRepository.findById(microserviceCreated.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw 404 when trying to destroy a non existent microservice', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - MicroserviceRepository.destroy(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('Find all available microservices', () => { - it('Should find a single available microservices for a type', async () => { - const ms1 = { - type: 'twitter-followers', - running: false, - init: false, - variant: 'default', - settings: {}, - } - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await MicroserviceRepository.create(ms1, mockIRepositoryOptions) - - const found: any = await MicroserviceRepository.findAllByType('twitter-followers', 1, 100) - expect(found[0].tenantId).toBeDefined() - expect(found.length).toBe(1) - }) - - it('Should find all available microservices for a type, multiple available', async () => { - const ms1 = { - type: 'twitter-followers', - running: false, - init: false, - variant: 'default', - settings: {}, - } - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await MicroserviceRepository.create(ms1, mockIRepositoryOptions) - - const mockIRepositoryOptions2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await MicroserviceRepository.create(ms1, mockIRepositoryOptions2) - - const found = await MicroserviceRepository.findAllByType('twitter-followers', 1, 100) - expect(found.length).toBe(2) - }) - - it('Should only find non-running microservices', async () => { - const ms1 = { - type: 'twitter-followers', - running: false, - init: false, - variant: 'default', - settings: {}, - } - - const ms2 = { - type: 'twitter-followers', - running: true, - init: false, - variant: 'default', - settings: {}, - } - - const ms3 = { - type: 'twitter-followers', - running: false, - init: false, - variant: 'default', - settings: {}, - } - - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await MicroserviceRepository.create(ms1, mockIRepositoryOptions) - - const mockIRepositoryOptions2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await MicroserviceRepository.create(ms2, mockIRepositoryOptions2) - - const mockIRepositoryOptions3 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await MicroserviceRepository.create(ms3, mockIRepositoryOptions3) - - const found = await MicroserviceRepository.findAllByType('twitter-followers', 1, 100) - expect(found.length).toBe(2) - }) - - it('Should only find microservices for the desired type', async () => { - const ms1 = { - type: 'twitter-followers', - running: false, - init: false, - variant: 'default', - settings: {}, - } - - const ms2 = { - type: 'members_score', - running: false, - init: false, - variant: 'default', - settings: {}, - } - - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await MicroserviceRepository.create(ms1, mockIRepositoryOptions) - - const mockIRepositoryOptions2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await MicroserviceRepository.create(ms1, mockIRepositoryOptions2) - - const mockIRepositoryOptions3 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await MicroserviceRepository.create(ms2, mockIRepositoryOptions3) - - const found = await MicroserviceRepository.findAllByType('twitter-followers', 1, 100) - expect(found.length).toBe(2) - }) - - it('Should return an empty list if no integrations are found', async () => { - const found = await MicroserviceRepository.findAllByType('twitter-followers', 1, 100) - expect(found.length).toBe(0) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/noteRepository.test.ts b/backend/src/database/repositories/__tests__/noteRepository.test.ts deleted file mode 100644 index 0a6d2f26f5..0000000000 --- a/backend/src/database/repositories/__tests__/noteRepository.test.ts +++ /dev/null @@ -1,331 +0,0 @@ -import NoteRepository from '../noteRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import Error404 from '../../../errors/Error404' -import MemberRepository from '../memberRepository' -import { PlatformType } from '@crowd/types' - -const db = null - -describe('NoteRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create the given note succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const note2add = { body: 'test-note' } - - const noteCreated = await NoteRepository.create(note2add, mockIRepositoryOptions) - - noteCreated.createdAt = noteCreated.createdAt.toISOString().split('T')[0] - noteCreated.updatedAt = noteCreated.updatedAt.toISOString().split('T')[0] - - const plainUser = mockIRepositoryOptions.currentUser.get({ plain: true }) - const expectedCreatedBy = { - id: plainUser.id, - fullName: plainUser.fullName, - avatarUrl: null, - } - - const expectedNoteCreated = { - id: noteCreated.id, - body: note2add.body, - members: [], - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdBy: expectedCreatedBy, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - expect(noteCreated).toStrictEqual(expectedNoteCreated) - }) - - it('Should throw sequelize not null error -- body field is required', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const note2add = {} - - await expect(() => NoteRepository.create(note2add, mockIRepositoryOptions)).rejects.toThrow() - }) - }) - - describe('findById method', () => { - it('Should successfully find created note by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const note2add = { body: 'test-note' } - - const noteCreated = await NoteRepository.create(note2add, mockIRepositoryOptions) - - noteCreated.createdAt = noteCreated.createdAt.toISOString().split('T')[0] - noteCreated.updatedAt = noteCreated.updatedAt.toISOString().split('T')[0] - - const plainUser = mockIRepositoryOptions.currentUser.get({ plain: true }) - const expectedCreatedBy = { - id: plainUser.id, - fullName: plainUser.fullName, - avatarUrl: null, - } - - const expectedNoteFound = { - id: noteCreated.id, - body: note2add.body, - members: [], - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdBy: expectedCreatedBy, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - const noteById = await NoteRepository.findById(noteCreated.id, mockIRepositoryOptions) - - noteById.createdAt = noteById.createdAt.toISOString().split('T')[0] - noteById.updatedAt = noteById.updatedAt.toISOString().split('T')[0] - - expect(noteById).toStrictEqual(expectedNoteFound) - }) - - it('Should throw 404 error when no note found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - NoteRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('filterIdsInTenant method', () => { - it('Should return the given ids of previously created note entities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const note1 = { body: 'test1' } - const note2 = { body: 'test2' } - - const note1Created = await NoteRepository.create(note1, mockIRepositoryOptions) - const note2Created = await NoteRepository.create(note2, mockIRepositoryOptions) - - const filterIdsReturned = await NoteRepository.filterIdsInTenant( - [note1Created.id, note2Created.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([note1Created.id, note2Created.id]) - }) - - it('Should only return the ids of previously created notes and filter random uuids out', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const note = { body: 'test1' } - - const noteCreated = await NoteRepository.create(note, mockIRepositoryOptions) - - const { randomUUID } = require('crypto') - - const filterIdsReturned = await NoteRepository.filterIdsInTenant( - [noteCreated.id, randomUUID(), randomUUID()], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([noteCreated.id]) - }) - - it('Should return an empty array for an irrelevant tenant', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const note = { body: 'test' } - - const noteCreated = await NoteRepository.create(note, mockIRepositoryOptions) - - // create a new tenant and bind options to it - mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const filterIdsReturned = await NoteRepository.filterIdsInTenant( - [noteCreated.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([]) - }) - }) - - describe('findAndCountAll method', () => { - it('Should find and count all notes, with various filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const member = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const note1 = { body: 'test-note', members: [member.id] } - const note2 = { body: 'test-note-2', members: [member.id] } - const note3 = { body: 'another-note', members: [member.id] } - - const note1Created = await NoteRepository.create(note1, mockIRepositoryOptions) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const note2Created = await NoteRepository.create(note2, mockIRepositoryOptions) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const note3Created = await NoteRepository.create(note3, mockIRepositoryOptions) - - // Test filter by body - // Current findAndCountAll uses wildcarded like statement so it matches both notes - let notes = await NoteRepository.findAndCountAll( - { filter: { body: 'test-note' } }, - mockIRepositoryOptions, - ) - - expect(notes.count).toEqual(2) - expect(notes.rows).toStrictEqual([note2Created, note1Created]) - - // Test filter by id - notes = await NoteRepository.findAndCountAll( - { filter: { id: note1Created.id } }, - mockIRepositoryOptions, - ) - - expect(notes.count).toEqual(1) - expect(notes.rows).toStrictEqual([note1Created]) - - // Test filter by createdAt - find all between note1.createdAt and note3.createdAt - notes = await NoteRepository.findAndCountAll( - { - filter: { - createdAtRange: [note1Created.createdAt, note3Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - - expect(notes.count).toEqual(3) - expect(notes.rows).toStrictEqual([note3Created, note2Created, note1Created]) - - // Test filter by createdAt - find all where createdAt < note2.createdAt - notes = await NoteRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, note2Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - expect(notes.count).toEqual(2) - expect(notes.rows).toStrictEqual([note2Created, note1Created]) - - // Test filter by createdAt - find all where createdAt < note1.createdAt - notes = await NoteRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, note1Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - expect(notes.count).toEqual(1) - expect(notes.rows).toStrictEqual([note1Created]) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created note', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const note1 = { body: 'test-note' } - - const noteCreated = await NoteRepository.create(note1, mockIRepositoryOptions) - - const noteUpdated = await NoteRepository.update( - noteCreated.id, - { body: 'updated-note-body' }, - mockIRepositoryOptions, - ) - - expect(noteUpdated.updatedAt.getTime()).toBeGreaterThan(noteUpdated.createdAt.getTime()) - - const plainUser = mockIRepositoryOptions.currentUser.get({ plain: true }) - const expectedCreatedBy = { - id: plainUser.id, - fullName: plainUser.fullName, - avatarUrl: null, - } - - const noteExpected = { - id: noteCreated.id, - body: noteUpdated.body, - importHash: null, - createdAt: noteCreated.createdAt, - updatedAt: noteUpdated.updatedAt, - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdBy: expectedCreatedBy, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - members: [], - } - - expect(noteUpdated).toStrictEqual(noteExpected) - }) - - it('Should throw 404 error when trying to update non existent note', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - NoteRepository.update(randomUUID(), { body: 'non-existent' }, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('destroy method', () => { - it('Should succesfully destroy previously created note', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const note = { body: 'test-note' } - - const returnedNote = await NoteRepository.create(note, mockIRepositoryOptions) - - await NoteRepository.destroy(returnedNote.id, mockIRepositoryOptions, true) - - // Try selecting it after destroy, should throw 404 - await expect(() => - NoteRepository.findById(returnedNote.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw 404 when trying to destroy a non existent note', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - NoteRepository.destroy(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/organizationCacheRepository.test.ts b/backend/src/database/repositories/__tests__/organizationCacheRepository.test.ts deleted file mode 100644 index 14f85df3d1..0000000000 --- a/backend/src/database/repositories/__tests__/organizationCacheRepository.test.ts +++ /dev/null @@ -1,300 +0,0 @@ -import organizationCacheRepository from '../organizationCacheRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import Error404 from '../../../errors/Error404' - -const db = null - -const toCreate = { - name: 'crowd.dev', - url: 'https://crowd.dev', - description: 'Community-led Growth for Developer-first Companies.\nJoin our private beta', - emails: ['hello@crowd.dev', 'jonathan@crow.dev'], - phoneNumbers: ['+42 424242424'], - logo: 'https://logo.clearbit.com/crowd.dev', - tags: ['community', 'growth', 'developer-first'], - website: 'https://crowd.dev', - location: 'Berlin', - github: { - handle: 'CrowdDotDev', - }, - twitter: { - handle: 'CrowdDotDev', - id: '1362101830923259908', - bio: 'Community-led Growth for Developer-first Companies.\nJoin our private beta. 👇', - followers: 107, - following: 0, - location: '🌍 remote', - site: 'https://t.co/GRLDhqFWk4', - avatar: 'https://pbs.twimg.com/profile_images/1419741008716251141/6exZe94-_normal.jpg', - }, - linkedin: { - handle: 'company/crowddevhq', - }, - crunchbase: { - handle: 'company/crowddevhq', - }, - employees: 42, - revenueRange: { - min: 10, - max: 50, - }, - type: null, - ticker: null, - size: null, - naics: null, - lastEnrichedAt: null, - industry: null, - headline: null, - geoLocation: null, - founded: null, - employeeCountByCountry: null, - address: null, - manuallyCreated: false, -} - -describe('OrganizationCacheCacheRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create the given organizationCache succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organizationCacheCreated = await organizationCacheRepository.create( - toCreate, - mockIRepositoryOptions, - ) - - organizationCacheCreated.createdAt = organizationCacheCreated.createdAt - .toISOString() - .split('T')[0] - organizationCacheCreated.updatedAt = organizationCacheCreated.updatedAt - .toISOString() - .split('T')[0] - - const expectedorganizationCacheCreated = { - id: organizationCacheCreated.id, - ...toCreate, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - } - expect(organizationCacheCreated).toStrictEqual(expectedorganizationCacheCreated) - }) - - it('Should throw sequelize not null error -- name field is required', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organizationCache2add = {} - - await expect(() => - organizationCacheRepository.create(organizationCache2add, mockIRepositoryOptions), - ).rejects.toThrow() - }) - }) - - describe('findById method', () => { - it('Should successfully find created organizationCache by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organizationCacheCreated = await organizationCacheRepository.create( - toCreate, - mockIRepositoryOptions, - ) - - organizationCacheCreated.createdAt = organizationCacheCreated.createdAt - .toISOString() - .split('T')[0] - organizationCacheCreated.updatedAt = organizationCacheCreated.updatedAt - .toISOString() - .split('T')[0] - - const expectedorganizationCacheFound = { - id: organizationCacheCreated.id, - ...toCreate, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - } - const organizationCacheById = await organizationCacheRepository.findById( - organizationCacheCreated.id, - mockIRepositoryOptions, - ) - - organizationCacheById.createdAt = organizationCacheById.createdAt.toISOString().split('T')[0] - organizationCacheById.updatedAt = organizationCacheById.updatedAt.toISOString().split('T')[0] - - expect(organizationCacheById).toStrictEqual(expectedorganizationCacheFound) - }) - - it('Should throw 404 error when no organizationCache found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - organizationCacheRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('findByUrl method', () => { - it('Should successfully find created organizationCache by URL', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organizationCacheCreated = await organizationCacheRepository.create( - toCreate, - mockIRepositoryOptions, - ) - - organizationCacheCreated.createdAt = organizationCacheCreated.createdAt - .toISOString() - .split('T')[0] - organizationCacheCreated.updatedAt = organizationCacheCreated.updatedAt - .toISOString() - .split('T')[0] - - const expectedorganizationCacheFound = { - id: organizationCacheCreated.id, - ...toCreate, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - } - const organizationCacheById = await organizationCacheRepository.findByUrl( - organizationCacheCreated.url, - mockIRepositoryOptions, - ) - - organizationCacheById.createdAt = organizationCacheById.createdAt.toISOString().split('T')[0] - organizationCacheById.updatedAt = organizationCacheById.updatedAt.toISOString().split('T')[0] - - expect(organizationCacheById).toStrictEqual(expectedorganizationCacheFound) - }) - - it('Should be independend of tenant', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mock2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organizationCacheCreated = await organizationCacheRepository.create( - toCreate, - mockIRepositoryOptions, - ) - - organizationCacheCreated.createdAt = organizationCacheCreated.createdAt - .toISOString() - .split('T')[0] - organizationCacheCreated.updatedAt = organizationCacheCreated.updatedAt - .toISOString() - .split('T')[0] - - const expectedorganizationCacheFound = { - id: organizationCacheCreated.id, - ...toCreate, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - } - const organizationCacheById = await organizationCacheRepository.findByUrl( - organizationCacheCreated.url, - mock2, - ) - - organizationCacheById.createdAt = organizationCacheById.createdAt.toISOString().split('T')[0] - organizationCacheById.updatedAt = organizationCacheById.updatedAt.toISOString().split('T')[0] - - expect(organizationCacheById).toStrictEqual(expectedorganizationCacheFound) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created organizationCache', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organizationCacheCreated = await organizationCacheRepository.create( - toCreate, - mockIRepositoryOptions, - ) - - const organizationCacheUpdated = await organizationCacheRepository.update( - organizationCacheCreated.id, - { name: 'updated-organizationCache-name' }, - mockIRepositoryOptions, - ) - - expect(organizationCacheUpdated.updatedAt.getTime()).toBeGreaterThan( - organizationCacheUpdated.createdAt.getTime(), - ) - - const organizationCacheExpected = { - id: organizationCacheCreated.id, - ...toCreate, - name: organizationCacheUpdated.name, - importHash: null, - createdAt: organizationCacheCreated.createdAt, - updatedAt: organizationCacheUpdated.updatedAt, - deletedAt: null, - } - - expect(organizationCacheUpdated).toStrictEqual(organizationCacheExpected) - }) - - it('Should throw 404 error when trying to update non existent organizationCache', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - organizationCacheRepository.update( - randomUUID(), - { name: 'non-existent' }, - mockIRepositoryOptions, - ), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('destroy method', () => { - it('Should succesfully destroy previously created organizationCache', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organizationCache = { name: 'test-organizationCache' } - - const returnedorganizationCache = await organizationCacheRepository.create( - organizationCache, - mockIRepositoryOptions, - ) - - await organizationCacheRepository.destroy( - returnedorganizationCache.id, - mockIRepositoryOptions, - true, - ) - - // Try selecting it after destroy, should throw 404 - await expect(() => - organizationCacheRepository.findById(returnedorganizationCache.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw 404 when trying to destroy a non existent organizationCache', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - organizationCacheRepository.destroy(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/organizationRepository.test.ts b/backend/src/database/repositories/__tests__/organizationRepository.test.ts deleted file mode 100644 index c939cecf97..0000000000 --- a/backend/src/database/repositories/__tests__/organizationRepository.test.ts +++ /dev/null @@ -1,1466 +0,0 @@ -import moment from 'moment' -import OrganizationRepository from '../organizationRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import Error404 from '../../../errors/Error404' -import MemberRepository from '../memberRepository' -import { PlatformType } from '@crowd/types' -import ActivityRepository from '../activityRepository' -import { generateUUIDv1 } from '@crowd/common' - -const db = null - -const toCreate = { - identities: [ - { - name: 'crowd.dev', - platform: 'crowd', - url: 'https://crowd.dev', - }, - ], - displayName: 'crowd.dev', - description: 'Community-led Growth for Developer-first Companies.\nJoin our private beta', - emails: ['hello@crowd.dev', 'jonathan@crow.dev'], - phoneNumbers: ['+42 424242424'], - logo: 'https://logo.clearbit.com/crowd.dev', - tags: ['community', 'growth', 'developer-first'], - twitter: { - handle: 'CrowdDotDev', - id: '1362101830923259908', - bio: 'Community-led Growth for Developer-first Companies.\nJoin our private beta. 👇', - followers: 107, - following: 0, - location: '🌍 remote', - site: 'https://t.co/GRLDhqFWk4', - avatar: 'https://pbs.twimg.com/profile_images/1419741008716251141/6exZe94-_normal.jpg', - }, - linkedin: { - handle: 'company/crowddevhq', - }, - crunchbase: { - handle: 'company/crowddevhq', - }, - employees: 42, - revenueRange: { - min: 10, - max: 50, - }, - type: null, - ticker: null, - size: null, - naics: null, - lastEnrichedAt: null, - industry: null, - headline: null, - geoLocation: null, - founded: null, - employeeCountByCountry: null, - address: null, - profiles: null, - manuallyCreated: false, - affiliatedProfiles: null, - allSubsidiaries: null, - alternativeDomains: null, - alternativeNames: null, - averageEmployeeTenure: null, - averageTenureByLevel: null, - averageTenureByRole: null, - directSubsidiaries: null, - employeeChurnRate: null, - employeeCountByMonth: null, - employeeGrowthRate: null, - employeeCountByMonthByLevel: null, - employeeCountByMonthByRole: null, - gicsSector: null, - grossAdditionsByMonth: null, - grossDeparturesByMonth: null, - ultimateParent: null, - immediateParent: null, -} - -async function createMembers(options) { - return await [ - ( - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'gilfoyle', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - options, - ) - ).id, - ( - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'dinesh', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - joinedAt: '2020-06-27T15:13:30Z', - }, - options, - ) - ).id, - ] -} - -async function createActivitiesForMembers(memberIds: string[], organizationId: string, options) { - for (const memberId of memberIds) { - await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - title: 'Title', - body: 'Here', - url: 'https://github.com', - channel: 'channel', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 98, - }, - isContribution: true, - username: 'test', - member: memberId, - organizationId, - score: 1, - sourceId: '#sourceId:' + memberId, - }, - options, - ) - } -} - -async function createOrganization(organization: any, options, members = []) { - const memberIds = [] - for (const member of members) { - const memberCreated = await MemberRepository.create( - SequelizeTestUtils.objectWithoutKey(member, 'activities'), - options, - ) - - if (member.activities) { - for (const activity of member.activities) { - await ActivityRepository.create({ ...activity, member: memberCreated.id }, options) - } - } - - memberIds.push(memberCreated.id) - } - organization.members = memberIds - const organizationCreated = await OrganizationRepository.create(organization, options) - return { ...organizationCreated, members: memberIds } -} - -describe('OrganizationRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create the given organization succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organizationCreated = await OrganizationRepository.create( - toCreate, - mockIRepositoryOptions, - ) - - organizationCreated.createdAt = organizationCreated.createdAt.toISOString().split('T')[0] - organizationCreated.updatedAt = organizationCreated.updatedAt.toISOString().split('T')[0] - - delete organizationCreated.identities[0].createdAt - delete organizationCreated.identities[0].updatedAt - - const primaryIdentity = toCreate.identities[0] - - const expectedOrganizationCreated = { - id: organizationCreated.id, - ...toCreate, - github: null, - location: null, - website: null, - memberCount: 0, - activityCount: 0, - activeOn: [], - identities: [ - { - integrationId: null, - name: primaryIdentity.name, - organizationId: organizationCreated.id, - platform: primaryIdentity.platform, - url: primaryIdentity.url, - sourceId: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - }, - ], - importHash: null, - lastActive: null, - joinedAt: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments.map((s) => s.id), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - isTeamOrganization: false, - attributes: {}, - weakIdentities: [], - } - expect(organizationCreated).toStrictEqual(expectedOrganizationCreated) - }) - - it('Should throw sequelize not null error -- name field is required', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organization2add = {} - - await expect(() => - OrganizationRepository.create(organization2add, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should create an organization with members succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberIds = await createMembers(mockIRepositoryOptions) - - const toCreateWithMember = { - ...toCreate, - members: memberIds, - } - let organizationCreated = await OrganizationRepository.create( - toCreateWithMember, - mockIRepositoryOptions, - ) - await createActivitiesForMembers(memberIds, organizationCreated.id, mockIRepositoryOptions) - await mockIRepositoryOptions.database.sequelize.query( - 'REFRESH MATERIALIZED VIEW mv_activities_cube', - ) - - organizationCreated = await OrganizationRepository.findById( - organizationCreated.id, - mockIRepositoryOptions, - ) - - organizationCreated.createdAt = organizationCreated.createdAt.toISOString().split('T')[0] - organizationCreated.updatedAt = organizationCreated.updatedAt.toISOString().split('T')[0] - organizationCreated.lastActive = organizationCreated.lastActive.toISOString().split('T')[0] - organizationCreated.joinedAt = organizationCreated.joinedAt.toISOString().split('T')[0] - - delete organizationCreated.identities[0].createdAt - delete organizationCreated.identities[0].updatedAt - - const primaryIdentity = toCreate.identities[0] - - const expectedOrganizationCreated = { - id: organizationCreated.id, - ...toCreate, - memberCount: 2, - identities: [ - { - integrationId: null, - name: primaryIdentity.name, - organizationId: organizationCreated.id, - platform: primaryIdentity.platform, - url: primaryIdentity.url, - sourceId: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - }, - ], - activityCount: 2, - github: null, - location: null, - website: null, - lastActive: '2020-05-27', - joinedAt: '2020-05-27', - activeOn: ['github'], - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments.map((s) => s.id), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - isTeamOrganization: false, - attributes: {}, - weakIdentities: [], - } - expect(organizationCreated).toStrictEqual(expectedOrganizationCreated) - - const member1 = await MemberRepository.findById(memberIds[0], mockIRepositoryOptions) - const member2 = await MemberRepository.findById(memberIds[1], mockIRepositoryOptions) - expect(member1.organizations.length).toEqual(1) - expect(member2.organizations.length).toEqual(1) - }) - }) - - describe('findById method', () => { - it('Should successfully find created organization by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organizationCreated = await OrganizationRepository.create( - toCreate, - mockIRepositoryOptions, - ) - - organizationCreated.createdAt = organizationCreated.createdAt.toISOString().split('T')[0] - organizationCreated.updatedAt = organizationCreated.updatedAt.toISOString().split('T')[0] - - const primaryIdentity = toCreate.identities[0] - - const expectedOrganizationFound = { - id: organizationCreated.id, - ...toCreate, - identities: [ - { - integrationId: null, - name: primaryIdentity.name, - organizationId: organizationCreated.id, - platform: primaryIdentity.platform, - url: primaryIdentity.url, - sourceId: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - }, - ], - github: null, - location: null, - website: null, - memberCount: 0, - activityCount: 0, - activeOn: [], - lastActive: null, - joinedAt: null, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments.map((s) => s.id), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - isTeamOrganization: false, - attributes: {}, - weakIdentities: [], - } - const organizationById = await OrganizationRepository.findById( - organizationCreated.id, - mockIRepositoryOptions, - ) - - organizationById.createdAt = organizationById.createdAt.toISOString().split('T')[0] - organizationById.updatedAt = organizationById.updatedAt.toISOString().split('T')[0] - - delete organizationById.identities[0].createdAt - delete organizationById.identities[0].updatedAt - - expect(organizationById).toStrictEqual(expectedOrganizationFound) - }) - - it('Should throw 404 error when no organization found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - OrganizationRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('filterIdsInTenant method', () => { - it('Should return the given ids of previously created organization entities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organization1 = { - identities: [{ name: 'test1', platform: 'crowd' }], - displayName: 'test1', - } - const organization2 = { - identities: [{ name: 'test2', platform: 'crowd' }], - displayName: 'test2', - } - - const organization1Created = await OrganizationRepository.create( - organization1, - mockIRepositoryOptions, - ) - const organization2Created = await OrganizationRepository.create( - organization2, - mockIRepositoryOptions, - ) - - const filterIdsReturned = await OrganizationRepository.filterIdsInTenant( - [organization1Created.id, organization2Created.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([organization1Created.id, organization2Created.id]) - }) - - it('Should only return the ids of previously created organizations and filter random uuids out', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organization = { - identities: [{ name: 'test1', platform: 'crowd' }], - displayName: 'test1', - } - - const organizationCreated = await OrganizationRepository.create( - organization, - mockIRepositoryOptions, - ) - - const { randomUUID } = require('crypto') - - const filterIdsReturned = await OrganizationRepository.filterIdsInTenant( - [organizationCreated.id, randomUUID(), randomUUID()], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([organizationCreated.id]) - }) - - it('Should return an empty array for an irrelevant tenant', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organization = { - identities: [{ name: 'test1', platform: 'crowd' }], - displayName: 'test1', - } - - const organizationCreated = await OrganizationRepository.create( - organization, - mockIRepositoryOptions, - ) - - // create a new tenant and bind options to it - mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const filterIdsReturned = await OrganizationRepository.filterIdsInTenant( - [organizationCreated.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([]) - }) - }) - - describe('findAndCountAll method', () => { - // we can skip this test - findAndCount is not used anymore - we use opensearch method findAndCountAllOpensearch instead - it.skip('Should find and count all organizations, with simple filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organization1 = { name: 'test-organization' } - const organization2 = { name: 'test-organization-2' } - const organization3 = { name: 'another-organization' } - - const organization1Created = await OrganizationRepository.create( - organization1, - mockIRepositoryOptions, - ) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const organization2Created = await OrganizationRepository.create( - organization2, - mockIRepositoryOptions, - ) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const organization3Created = await OrganizationRepository.create( - organization3, - mockIRepositoryOptions, - ) - - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'test-member', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: moment().toDate(), - organizations: [ - organization1Created.id, - organization2Created.id, - organization3Created.id, - ], - }, - mockIRepositoryOptions, - ) - - const foundOrganization1 = await OrganizationRepository.findById( - organization1Created.id, - mockIRepositoryOptions, - ) - - const foundOrganization2 = await OrganizationRepository.findById( - organization2Created.id, - mockIRepositoryOptions, - ) - - const foundOrganization3 = await OrganizationRepository.findById( - organization3Created.id, - mockIRepositoryOptions, - ) - - // Test filter by name - // Current findAndCountAll uses wildcarded like statement so it matches both organizations - let organizations - try { - organizations = await OrganizationRepository.findAndCountAll( - { filter: { name: 'test-organization' } }, - mockIRepositoryOptions, - ) - } catch (err) { - console.error(err) - throw err - } - - expect(organizations.count).toEqual(2) - expect(organizations.rows).toEqual([foundOrganization2, foundOrganization1]) - - // Test filter by id - organizations = await OrganizationRepository.findAndCountAll( - { filter: { id: organization1Created.id } }, - mockIRepositoryOptions, - ) - - expect(organizations.count).toEqual(1) - expect(organizations.rows).toStrictEqual([foundOrganization1]) - - // Test filter by createdAt - find all between organization1.createdAt and organization3.createdAt - organizations = await OrganizationRepository.findAndCountAll( - { - filter: { - createdAtRange: [organization1Created.createdAt, organization3Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - - expect(organizations.count).toEqual(3) - expect(organizations.rows).toStrictEqual([ - foundOrganization3, - foundOrganization2, - foundOrganization1, - ]) - - // Test filter by createdAt - find all where createdAt < organization2.createdAt - organizations = await OrganizationRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, organization2Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - expect(organizations.count).toEqual(2) - expect(organizations.rows).toStrictEqual([foundOrganization2, foundOrganization1]) - - // Test filter by createdAt - find all where createdAt < organization1.createdAt - organizations = await OrganizationRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, organization1Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - expect(organizations.count).toEqual(1) - expect(organizations.rows).toStrictEqual([foundOrganization1]) - }) - }) - - // we can skip these tests as well - we use opensearch method findAndCountAllOpensearch instead - describe.skip('filter method', () => { - const crowddev = { - identities: [ - { - name: 'crowd.dev', - platform: 'crowd', - url: 'https://crowd.dev', - }, - ], - description: 'Community-led Growth for Developer-first Companies.\nJoin our private beta', - emails: ['hello@crowd.dev', 'jonathan@crowd.dev'], - phoneNumbers: ['+42 424242424'], - logo: 'https://logo.clearbit.com/crowd.dev', - tags: ['community', 'growth', 'developer-first'], - twitter: { - handle: 'CrowdDotDev', - id: '1362101830923259908', - bio: 'Community-led Growth for Developer-first Companies.\nJoin our private beta. 👇', - followers: 107, - following: 0, - location: '🌍 remote', - site: 'https://t.co/GRLDhqFWk4', - avatar: 'https://pbs.twimg.com/profile_images/1419741008716251141/6exZe94-_normal.jpg', - }, - linkedin: { - handle: 'company/crowddevhq', - }, - crunchbase: { - handle: 'company/crowddevhq', - }, - employees: 42, - revenueRange: { - min: 10, - max: 50, - }, - } - - const piedpiper = { - identities: [ - { - name: 'Pied Piper', - platform: 'crowd', - url: 'https://piedpiper.io', - }, - ], - description: 'Pied Piper is a fictional technology company in the HBO television series', - emails: ['richard@piedpiper.io', 'jarded@pipedpiper.io'], - phoneNumbers: ['+42 54545454'], - logo: 'https://logo.clearbit.com/piedpiper', - tags: ['new-internet', 'compression'], - twitter: { - handle: 'piedPiper', - id: '1362101830923259908', - bio: 'Pied Piper is a making the new, decentralized internet', - followers: 1024, - following: 0, - location: 'silicon valley', - site: 'https://t.co/GRLDhqFWk4', - avatar: 'https://pbs.twimg.com/profile_images/1419741008716251141/6exZe94-_normal.jpg', - }, - linkedin: { - handle: 'company/piedpiper', - }, - crunchbase: { - handle: 'company/piedpiper', - }, - employees: 100, - revenueRange: { - min: 0, - max: 1, - }, - } - - const hooli = { - identities: [ - { - name: 'Hooli', - platform: 'crowd', - url: 'https://hooli.com', - }, - ], - description: 'Hooli is a fictional technology company in the HBO television series', - emails: ['gavin@hooli.com'], - phoneNumbers: ['+42 12121212'], - logo: 'https://logo.clearbit.com/hooli', - tags: ['not-google', 'elephant'], - twitter: { - handle: 'hooli', - id: '1362101830923259908', - bio: 'Hooli is making the world a better place', - followers: 1000000, - following: 0, - location: 'silicon valley', - site: 'https://t.co/GRLDhqFWk4', - avatar: 'https://pbs.twimg.com/profile_images/1419741008716251141/6exZe94-_normal.jpg', - }, - linkedin: { - handle: 'company/hooli', - }, - crunchbase: { - handle: 'company/hooli', - }, - employees: 10000, - revenueRange: { - min: 200, - max: 500, - }, - } - - it('Should filter by name', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - const found = await OrganizationRepository.findAndCountAll( - { - filter: { - name: 'Pied Piper', - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found.count).toEqual(1) - expect(found.rows[0].name).toEqual('Pied Piper') - }) - - it('Should filter by url', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - const found = await OrganizationRepository.findAndCountAll( - { - filter: { - url: 'crowd.dev', - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found.count).toEqual(1) - expect(found.rows[0].name).toBe('crowd.dev') - }) - - it('Should filter by description', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - const found = await OrganizationRepository.findAndCountAll( - { - filter: { - description: 'community', - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found.count).toEqual(1) - expect(found.rows[0].name).toBe('crowd.dev') - }) - - it('Should filter by emails', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - const found = await OrganizationRepository.findAndCountAll( - { - filter: { - emails: 'richard@piedpiper.io,jonathan@crowd.dev', - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found.count).toEqual(2) - - const found2 = await OrganizationRepository.findAndCountAll( - { - filter: { - emails: ['richard@piedpiper.io', 'jonathan@crowd.dev'], - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found2.count).toEqual(2) - }) - - it('Should filter by tags', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - const found = await OrganizationRepository.findAndCountAll( - { - filter: { - tags: 'new-internet,not-google,new', - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found.count).toEqual(2) - - const found2 = await OrganizationRepository.findAndCountAll( - { - filter: { - tags: ['new-internet', 'not-google', 'new'], - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found2.count).toEqual(2) - }) - - it('Should filter by twitter handle', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - const found = await OrganizationRepository.findAndCountAll( - { - filter: { - twitter: 'crowdDotDev', - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found.count).toEqual(1) - expect(found.rows[0].name).toBe('crowd.dev') - }) - - it('Should filter by linkedin handle', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - const found = await OrganizationRepository.findAndCountAll( - { - filter: { - linkedin: 'crowddevhq', - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found.count).toEqual(1) - expect(found.rows[0].name).toBe('crowd.dev') - }) - - it('Should filter by employee range', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - const found = await OrganizationRepository.findAndCountAll( - { - filter: { - employeesRange: [90, 120], - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found.count).toEqual(1) - expect(found.rows[0].name).toBe('Pied Piper') - }) - - it('Should filter by revenue range', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - const found = await OrganizationRepository.findAndCountAll( - { - filter: { - revenueMin: 0, - revenueMax: 1, - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found.count).toEqual(1) - expect(found.rows[0].name).toBe('Pied Piper') - - const found2 = await OrganizationRepository.findAndCountAll( - { - filter: { - revenueMin: 9, - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found2.count).toEqual(2) - }) - - it('Should filter by members', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions, [ - { - username: { - github: { - username: 'joan', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Joan', - joinedAt: moment().toDate(), - activities: [ - { - username: 'joan', - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sourceId: '#sourceId1', - }, - ], - }, - ]) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const memberId = await ( - await MemberRepository.findAndCountAll({}, mockIRepositoryOptions) - ).rows[0].id - - const found = await OrganizationRepository.findAndCountAll( - { - filter: { - members: [memberId], - }, - }, - mockIRepositoryOptions, - ) - - expect(found.count).toEqual(1) - expect(found.rows[0].name).toBe('crowd.dev') - }) - - // we can skip this test - findAndCount is not used anymore - we use opensearch method findAndCountAllOpensearch instead - it.skip('Should filter by memberCount', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const org1 = await createOrganization(crowddev, mockIRepositoryOptions, [ - { - username: { - github: { - username: 'joan', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Joan', - joinedAt: moment().toDate(), - }, - { - username: { - github: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'anil', - joinedAt: moment().toDate(), - }, - { - username: { - github: { - username: 'uros', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'uros', - joinedAt: moment().toDate(), - }, - ]) - const org2 = await createOrganization(piedpiper, mockIRepositoryOptions, [ - { - username: { - github: { - username: 'mario', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'mario', - joinedAt: moment().toDate(), - }, - { - username: { - github: { - username: 'igor', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'igor', - joinedAt: moment().toDate(), - }, - ]) - await createOrganization(hooli, mockIRepositoryOptions) - - const found = await OrganizationRepository.findAndCountAll( - { - advancedFilter: { - memberCount: { - gte: 2, - }, - }, - }, - mockIRepositoryOptions, - ) - - delete org1.members - delete org2.members - - expect(found.count).toBe(2) - expect(found.rows.sort((a, b) => (a.createdAt > b.createdAt ? 1 : -1))).toStrictEqual([ - org1, - org2, - ]) - }) - - it('Should work with advanced filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions, [ - { - username: { - github: { - username: 'joan', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Joan', - joinedAt: moment().toDate(), - }, - ]) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const memberId = await ( - await MemberRepository.findAndCountAll({}, mockIRepositoryOptions) - ).rows[0].id - - // Revenue nested field - expect( - ( - await OrganizationRepository.findAndCountAll( - { - advancedFilter: { - revenue: { - gte: 9, - }, - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - ).count, - ).toEqual(2) - - // Twitter bio - expect( - ( - await OrganizationRepository.findAndCountAll( - { - advancedFilter: { - 'twitter.bio': { - textContains: 'world a better place', - }, - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - ).count, - ).toEqual(1) - - expect( - ( - await OrganizationRepository.findAndCountAll( - { - advancedFilter: { - or: [ - { - and: [ - { - revenue: { - gte: 9, - }, - }, - { - revenue: { - lte: 100, - }, - }, - ], - }, - { - 'twitter.bio': { - textContains: 'world a better place', - }, - }, - ], - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - ).count, - ).toEqual(2) - - expect( - ( - await OrganizationRepository.findAndCountAll( - { - advancedFilter: { - or: [ - { - and: [ - { - tags: { - overlap: ['not-google'], - }, - }, - { - 'twitter.location': { - textContains: 'silicon valley', - }, - }, - ], - }, - { - members: [memberId], - }, - ], - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - ).count, - ).toEqual(2) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created organization', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organizationCreated = await OrganizationRepository.create( - toCreate, - mockIRepositoryOptions, - ) - - const organizationUpdated = await OrganizationRepository.update( - organizationCreated.id, - { displayName: 'updated-organization-name' }, - mockIRepositoryOptions, - ) - - expect(organizationUpdated.updatedAt.getTime()).toBeGreaterThan( - organizationUpdated.createdAt.getTime(), - ) - - const primaryIdentity = organizationCreated.identities[0] - - delete organizationUpdated.identities[0].createdAt - delete organizationUpdated.identities[0].updatedAt - - const organizationExpected = { - id: organizationCreated.id, - ...toCreate, - github: null, - location: null, - website: null, - memberCount: 0, - activityCount: 0, - activeOn: [], - identities: [ - { - integrationId: null, - name: primaryIdentity.name, - organizationId: organizationCreated.id, - platform: primaryIdentity.platform, - url: primaryIdentity.url, - sourceId: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - }, - ], - lastActive: null, - joinedAt: null, - displayName: organizationUpdated.displayName, - importHash: null, - createdAt: organizationCreated.createdAt, - updatedAt: organizationUpdated.updatedAt, - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments.map((s) => s.id), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - isTeamOrganization: false, - attributes: {}, - weakIdentities: [], - } - - expect(organizationUpdated).toStrictEqual(organizationExpected) - }) - - it('Should throw 404 error when trying to update non existent organization', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - OrganizationRepository.update( - randomUUID(), - { name: 'non-existent' }, - mockIRepositoryOptions, - ), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('setOrganizationIsTeam method', () => { - const member1 = { - username: { - devto: { - username: 'iambarker', - integrationId: generateUUIDv1(), - }, - github: { - username: 'barker', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Jack Barker', - attributes: { - bio: { - github: 'Head of development at Hooli', - twitter: 'Head of development at Hooli | ex CEO at Pied Piper', - }, - sample: { crowd: true, default: true }, - jobTitle: { custom: 'Head of development', default: 'Head of development' }, - location: { github: 'Silicon Valley', default: 'Silicon Valley' }, - avatarUrl: { - custom: - 'https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/jack-barker-best.jpg', - default: - 'https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/jack-barker-best.jpg', - }, - }, - joinedAt: moment().toDate(), - activities: [ - { - type: 'star', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - username: 'barker', - sourceId: '#sourceId1', - }, - ], - } - - const member2 = { - username: { - devto: { - username: 'thebelson', - integrationId: generateUUIDv1(), - }, - github: { - username: 'gavinbelson', - integrationId: generateUUIDv1(), - }, - discord: { - username: 'gavinbelson', - integrationId: generateUUIDv1(), - }, - twitter: { - username: 'gavin', - integrationId: generateUUIDv1(), - }, - linkedin: { - username: 'gavinbelson', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Gavin Belson', - attributes: { - bio: { - custom: 'CEO at Hooli', - github: 'CEO at Hooli', - default: 'CEO at Hooli', - twitter: 'CEO at Hooli', - }, - sample: { crowd: true, default: true }, - jobTitle: { custom: 'CEO', default: 'CEO' }, - location: { github: 'Silicon Valley', default: 'Silicon Valley' }, - avatarUrl: { - custom: 'https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/gavin.jpg', - default: 'https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/gavin.jpg', - }, - }, - joinedAt: moment().toDate(), - activities: [ - { - type: 'star', - timestamp: '2020-05-28T15:13:30Z', - username: 'gavinbelson', - platform: PlatformType.GITHUB, - sourceId: '#sourceId2', - }, - ], - } - - const member3 = { - username: { - devto: { - username: 'bigheader', - integrationId: generateUUIDv1(), - }, - github: { - username: 'bighead', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Big Head', - attributes: { - bio: { - custom: 'Executive at the Hooli XYZ project', - github: 'Co-head Dreamer of the Hooli XYZ project', - default: 'Executive at the Hooli XYZ project', - twitter: 'Co-head Dreamer of the Hooli XYZ project', - }, - sample: { crowd: true, default: true }, - jobTitle: { custom: 'Co-head Dreamer', default: 'Co-head Dreamer' }, - location: { github: 'Silicon Valley', default: 'Silicon Valley' }, - avatarUrl: { - custom: 'https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/big-head-small.jpg', - default: 'https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/big-head-small.jpg', - }, - }, - joinedAt: moment().toDate(), - activities: [ - { - type: 'star', - timestamp: '2020-05-29T15:13:30Z', - platform: PlatformType.GITHUB, - username: 'bighead', - sourceId: '#sourceId3', - }, - ], - } - it('Should succesfully set/unset organization members as team members', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const org = await createOrganization(toCreate, mockIRepositoryOptions, [ - member1, - member2, - member3, - ]) - - // mark organization members as team members - await OrganizationRepository.setOrganizationIsTeam(org.id, true, mockIRepositoryOptions) - - let m1 = await MemberRepository.findById(org.members[0], mockIRepositoryOptions) - let m2 = await MemberRepository.findById(org.members[1], mockIRepositoryOptions) - let m3 = await MemberRepository.findById(org.members[2], mockIRepositoryOptions) - - expect(m1.attributes.isTeamMember.default).toEqual(true) - expect(m2.attributes.isTeamMember.default).toEqual(true) - expect(m3.attributes.isTeamMember.default).toEqual(true) - - // expect other attributes intact - delete m1.attributes.isTeamMember - expect(m1.attributes).toStrictEqual(member1.attributes) - - delete m2.attributes.isTeamMember - expect(m2.attributes).toStrictEqual(member2.attributes) - - delete m3.attributes.isTeamMember - expect(m3.attributes).toStrictEqual(member3.attributes) - - // now unmark - await OrganizationRepository.setOrganizationIsTeam(org.id, false, mockIRepositoryOptions) - - m1 = await MemberRepository.findById(org.members[0], mockIRepositoryOptions) - m2 = await MemberRepository.findById(org.members[1], mockIRepositoryOptions) - m3 = await MemberRepository.findById(org.members[2], mockIRepositoryOptions) - - expect(m1.attributes.isTeamMember.default).toEqual(false) - expect(m2.attributes.isTeamMember.default).toEqual(false) - expect(m3.attributes.isTeamMember.default).toEqual(false) - - // expect other attributes intact - delete m1.attributes.isTeamMember - expect(m1.attributes).toStrictEqual(member1.attributes) - - delete m2.attributes.isTeamMember - expect(m2.attributes).toStrictEqual(member2.attributes) - - delete m3.attributes.isTeamMember - expect(m3.attributes).toStrictEqual(member3.attributes) - }) - }) - - describe('destroy method', () => { - it('Should succesfully destroy previously created organization', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organization = { displayName: 'test-organization' } - - const returnedOrganization = await OrganizationRepository.create( - organization, - mockIRepositoryOptions, - ) - - await OrganizationRepository.destroy(returnedOrganization.id, mockIRepositoryOptions, true) - - // Try selecting it after destroy, should throw 404 - await expect(() => - OrganizationRepository.findById(returnedOrganization.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw 404 when trying to destroy a non existent organization', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - OrganizationRepository.destroy(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/recurringEmailsHistoryRepository.test.ts b/backend/src/database/repositories/__tests__/recurringEmailsHistoryRepository.test.ts deleted file mode 100644 index 8eaf8b98b8..0000000000 --- a/backend/src/database/repositories/__tests__/recurringEmailsHistoryRepository.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { randomUUID } from 'crypto' - -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import { - RecurringEmailsHistoryData, - RecurringEmailType, -} from '../../../types/recurringEmailsHistoryTypes' -import RecurringEmailsHistoryRepository from '../recurringEmailsHistoryRepository' - -const db = null - -describe('RecurringEmailsHistory tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create recurring email history with given values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const historyData: RecurringEmailsHistoryData = { - emailSentAt: '2023-01-02T00:00:00Z', - type: RecurringEmailType.WEEKLY_ANALYTICS, - emailSentTo: ['anil@crowd.dev', 'uros@crowd.dev'], - tenantId: mockIRepositoryOptions.currentTenant.id, - weekOfYear: '1', - } - - const rehRepository = new RecurringEmailsHistoryRepository(mockIRepositoryOptions) - const history = await rehRepository.create(historyData) - - expect(new Date(historyData.emailSentAt)).toStrictEqual(history.emailSentAt) - expect(historyData.emailSentTo).toStrictEqual(history.emailSentTo) - expect(historyData.tenantId).toStrictEqual(history.tenantId) - expect(historyData.weekOfYear).toStrictEqual(history.weekOfYear) - }) - - it('Should throw an error when mandatory fields are missing', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const rehRepository = new RecurringEmailsHistoryRepository(mockIRepositoryOptions) - await expect(() => - rehRepository.create({ - emailSentAt: '2023-01-02T00:00:00Z', - emailSentTo: ['anil@crowd.dev', 'uros@crowd.dev'], - tenantId: mockIRepositoryOptions.currentTenant.id, - type: undefined, - }), - ).rejects.toThrowError() - - await expect(() => - rehRepository.create({ - emailSentAt: undefined, - emailSentTo: ['anil@crowd.dev', 'uros@crowd.dev'], - tenantId: mockIRepositoryOptions.currentTenant.id, - weekOfYear: '1', - type: RecurringEmailType.WEEKLY_ANALYTICS, - }), - ).rejects.toThrowError() - - await expect(() => - rehRepository.create({ - emailSentAt: '2023-01-02T00:00:00Z', - emailSentTo: undefined, - tenantId: mockIRepositoryOptions.currentTenant.id, - weekOfYear: '1', - type: RecurringEmailType.WEEKLY_ANALYTICS, - }), - ).rejects.toThrowError() - - await expect(() => - rehRepository.create({ - emailSentAt: '2023-01-02T00:00:00Z', - emailSentTo: ['anil@crowd.dev', 'uros@crowd.dev'], - tenantId: undefined, - weekOfYear: '1', - type: RecurringEmailType.WEEKLY_ANALYTICS, - }), - ).rejects.toThrowError() - }) - }) - - describe('findById method', () => { - it('Should find historical receipt by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const historyData: RecurringEmailsHistoryData = { - emailSentAt: '2023-01-02T00:00:00Z', - emailSentTo: ['anil@crowd.dev', 'uros@crowd.dev'], - tenantId: mockIRepositoryOptions.currentTenant.id, - weekOfYear: '1', - type: RecurringEmailType.WEEKLY_ANALYTICS, - } - - const rehRepository = new RecurringEmailsHistoryRepository(mockIRepositoryOptions) - - const receiptCreated = await rehRepository.create(historyData) - const receiptFoundById = await rehRepository.findById(receiptCreated.id) - - expect(receiptFoundById).toStrictEqual(receiptCreated) - }) - - it('Should return null for non-existing receipt entry', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const rehRepository = new RecurringEmailsHistoryRepository(mockIRepositoryOptions) - - const cache = await rehRepository.findById(randomUUID()) - expect(cache).toBeNull() - }) - }) - - describe('findByWeekOfYear method', () => { - it('Should find historical receipt by week of year', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const historyData: RecurringEmailsHistoryData = { - emailSentAt: '2023-01-02T00:00:00Z', - emailSentTo: ['anil@crowd.dev', 'uros@crowd.dev'], - tenantId: mockIRepositoryOptions.currentTenant.id, - weekOfYear: '1', - type: RecurringEmailType.EAGLE_EYE_DIGEST, - } - - const rehRepository = new RecurringEmailsHistoryRepository(mockIRepositoryOptions) - - const receiptCreated = await rehRepository.create(historyData) - - // should find recently created receipt - let receiptFound = await rehRepository.findByWeekOfYear( - mockIRepositoryOptions.currentTenant.id, - '1', - RecurringEmailType.EAGLE_EYE_DIGEST, - ) - - expect(receiptCreated).toStrictEqual(receiptFound) - - // shouldn't find any receipts - receiptFound = await rehRepository.findByWeekOfYear( - mockIRepositoryOptions.currentTenant.id, - '2', - RecurringEmailType.EAGLE_EYE_DIGEST, - ) - - expect(receiptFound).toBeNull() - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/reportRepository.test.ts b/backend/src/database/repositories/__tests__/reportRepository.test.ts deleted file mode 100644 index 614bed8db9..0000000000 --- a/backend/src/database/repositories/__tests__/reportRepository.test.ts +++ /dev/null @@ -1,450 +0,0 @@ -import ReportRepository from '../reportRepository' -import WidgetRepository from '../widgetRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import Error404 from '../../../errors/Error404' - -const db = null - -describe('ReportRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create a report succesfully with default values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report2Add = { name: 'test-report' } - - const reportCreated = await ReportRepository.create(report2Add, mockIRepositoryOptions) - - reportCreated.createdAt = reportCreated.createdAt.toISOString().split('T')[0] - reportCreated.updatedAt = reportCreated.updatedAt.toISOString().split('T')[0] - - const reportExpected = { - id: reportCreated.id, - public: false, - isTemplate: false, - name: report2Add.name, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - widgets: [], - viewedBy: [], - } - - expect(reportCreated).toStrictEqual(reportExpected) - }) - - it('Should create a report succesfully with given values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report2Add = { - name: 'test-report', - public: true, - } - - const reportCreated = await ReportRepository.create(report2Add, mockIRepositoryOptions) - - reportCreated.createdAt = reportCreated.createdAt.toISOString().split('T')[0] - reportCreated.updatedAt = reportCreated.updatedAt.toISOString().split('T')[0] - - const reportExpected = { - id: reportCreated.id, - public: report2Add.public, - name: report2Add.name, - importHash: null, - isTemplate: false, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - widgets: [], - viewedBy: [], - } - - expect(reportCreated).toStrictEqual(reportExpected) - }) - - it('Should create a report succesfully with given values and widgets', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - let widget1 = await WidgetRepository.create( - { type: 'test-type', title: 'Some line graph' }, - mockIRepositoryOptions, - ) - let widget2 = await WidgetRepository.create( - { type: 'test-type-2', title: 'Some bar graph' }, - mockIRepositoryOptions, - ) - let widget3 = await WidgetRepository.create( - { type: 'test-type-3', title: 'Some area graph' }, - mockIRepositoryOptions, - ) - - const report2Add = { - name: 'test-report', - public: true, - widgets: [widget1.id, widget2.id, widget3.id], - } - - const reportCreated = await ReportRepository.create(report2Add, mockIRepositoryOptions) - - reportCreated.widgets = reportCreated.widgets.map((i) => i.get({ plain: true })) - - widget1 = await WidgetRepository.findById(widget1.id, mockIRepositoryOptions) - widget2 = await WidgetRepository.findById(widget2.id, mockIRepositoryOptions) - widget3 = await WidgetRepository.findById(widget3.id, mockIRepositoryOptions) - - // strip report object from widgets (only first layer associations are returned) - const { report: _widget1Report, ...widget1Raw } = widget1 - const { report: _widget2Report, ...widget2Raw } = widget2 - const { report: _widget3Report, ...widget3Raw } = widget3 - - reportCreated.createdAt = reportCreated.createdAt.toISOString().split('T')[0] - reportCreated.updatedAt = reportCreated.updatedAt.toISOString().split('T')[0] - - const reportExpected = { - id: reportCreated.id, - public: report2Add.public, - name: report2Add.name, - importHash: null, - isTemplate: false, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - widgets: [widget1Raw, widget2Raw, widget3Raw], - viewedBy: [], - } - - expect(reportCreated).toStrictEqual(reportExpected) - }) - - it('Should throw sequelize not null error -- name field is required', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report2Add = {} - - await expect(() => - ReportRepository.create(report2Add, mockIRepositoryOptions), - ).rejects.toThrow() - }) - }) - - describe('findById method', () => { - it('Should successfully find created report by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report2Add = { name: 'test-report' } - - const reportCreated = await ReportRepository.create(report2Add, mockIRepositoryOptions) - - reportCreated.createdAt = reportCreated.createdAt.toISOString().split('T')[0] - reportCreated.updatedAt = reportCreated.updatedAt.toISOString().split('T')[0] - - const expectedReport = { - id: reportCreated.id, - public: false, - name: report2Add.name, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - isTemplate: false, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - widgets: [], - viewedBy: [], - } - const reportById = await ReportRepository.findById(reportCreated.id, mockIRepositoryOptions) - - reportById.createdAt = reportById.createdAt.toISOString().split('T')[0] - reportById.updatedAt = reportById.updatedAt.toISOString().split('T')[0] - - expect(reportById).toStrictEqual(expectedReport) - }) - - it('Should throw 404 error when no report found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - ReportRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('filterIdsInTenant method', () => { - it('Should return the given ids of previously created report entities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report1 = { - name: 'report-test-1', - public: true, - } - const report2 = { name: 'report-test-2' } - - const report1Created = await ReportRepository.create(report1, mockIRepositoryOptions) - const report2Created = await ReportRepository.create(report2, mockIRepositoryOptions) - - const filterIdsReturned = await ReportRepository.filterIdsInTenant( - [report1Created.id, report2Created.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([report1Created.id, report2Created.id]) - }) - - it('Should only return the ids of previously created reports and filter random uuids out', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report = { name: 'report-test' } - - const reportCreated = await ReportRepository.create(report, mockIRepositoryOptions) - - const { randomUUID } = require('crypto') - - const filterIdsReturned = await ReportRepository.filterIdsInTenant( - [reportCreated.id, randomUUID(), randomUUID()], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([reportCreated.id]) - }) - - it('Should return an empty array for an irrelevant tenant', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report = { name: 'report-test' } - - const reportCreated = await ReportRepository.create(report, mockIRepositoryOptions) - - // create a new tenant and bind options to it - mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const filterIdsReturned = await ReportRepository.filterIdsInTenant( - [reportCreated.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([]) - }) - }) - - describe('findAndCountAll method', () => { - it('Should find and count all reports, with various filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report1 = await ReportRepository.create( - { name: 'test-report-1', public: true, isTemplate: false }, - mockIRepositoryOptions, - ) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const report2 = await ReportRepository.create( - { name: 'test-report-2', public: false, isTemplate: true }, - mockIRepositoryOptions, - ) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const report3 = await ReportRepository.create( - { name: 'another-report', public: false, isTemplate: true }, - mockIRepositoryOptions, - ) - - // Filter by name - let reports = await ReportRepository.findAndCountAll( - { filter: { name: 'test-report' } }, - mockIRepositoryOptions, - ) - - expect(reports.count).toEqual(2) - expect(reports.rows).toStrictEqual([report2, report1]) - - // Filter by id - reports = await ReportRepository.findAndCountAll( - { filter: { id: report3.id } }, - mockIRepositoryOptions, - ) - - expect(reports.count).toEqual(1) - expect(reports.rows).toStrictEqual([report3]) - - // filter by public - reports = await ReportRepository.findAndCountAll( - { filter: { public: false } }, - mockIRepositoryOptions, - ) - - expect(reports.count).toEqual(2) - expect(reports.rows).toStrictEqual([report3, report2]) - - // Filter by createdAt - find all between report1.createdAt and report3.createdAt - reports = await ReportRepository.findAndCountAll( - { - filter: { - createdAtRange: [report1.createdAt, report3.createdAt], - }, - }, - mockIRepositoryOptions, - ) - - expect(reports.count).toEqual(3) - expect(reports.rows).toStrictEqual([report3, report2, report1]) - - // Filter by createdAt - find all where createdAt < report2.createdAt - reports = await ReportRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, report2.createdAt], - }, - }, - mockIRepositoryOptions, - ) - - expect(reports.count).toEqual(2) - expect(reports.rows).toStrictEqual([report2, report1]) - - // Filter by createdAt - find all where createdAt < report1.createdAt - reports = await ReportRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, report1.createdAt], - }, - }, - mockIRepositoryOptions, - ) - - expect(reports.count).toEqual(1) - expect(reports.rows).toStrictEqual([report1]) - - // filter by isTemplate - reports = await ReportRepository.findAndCountAll( - { filter: { isTemplate: false } }, - mockIRepositoryOptions, - ) - - expect(reports.count).toEqual(1) - expect(reports.rows).toStrictEqual([report1]) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created report', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report = await ReportRepository.create({ name: 'test-report' }, mockIRepositoryOptions) - - let widget = await WidgetRepository.create({ type: 'widget-test' }, mockIRepositoryOptions) - - const reportUpdated = await ReportRepository.update( - report.id, - { - name: 'updated-report-name', - public: true, - widgets: [widget.id], - }, - mockIRepositoryOptions, - ) - - // Check updatedat is updated correctly - expect(reportUpdated.updatedAt.getTime()).toBeGreaterThan(reportUpdated.createdAt.getTime()) - - reportUpdated.widgets = reportUpdated.widgets.map((i) => i.get({ plain: true })) - - widget = await WidgetRepository.findById(widget.id, mockIRepositoryOptions) - - const { report: _widget1Report, ...widgetRaw } = widget - - const reportExpected = { - id: report.id, - public: reportUpdated.public, - name: reportUpdated.name, - importHash: null, - createdAt: report.createdAt, - updatedAt: reportUpdated.updatedAt, - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - widgets: [widgetRaw], - isTemplate: false, - viewedBy: [], - } - - expect(reportUpdated).toStrictEqual(reportExpected) - }) - - it('Should throw 404 error when trying to update non existent report', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - ReportRepository.update(randomUUID(), { type: 'non-existent' }, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('destroy method', () => { - it('Should succesfully destroy previously created report and its widgets', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widget = await WidgetRepository.create({ type: 'widget-test' }, mockIRepositoryOptions) - - const report = await ReportRepository.create( - { - name: 'test-report', - public: true, - widgets: [widget.id], - }, - mockIRepositoryOptions, - ) - - await ReportRepository.destroy(report.id, mockIRepositoryOptions, true) - - // Try selecting both report and its widget after destroy, should throw 404 - await expect(() => - ReportRepository.findById(report.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - await expect(() => - WidgetRepository.findById(widget.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw 404 when trying to destroy a non existent report', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - ReportRepository.destroy(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/tagRepository.test.ts b/backend/src/database/repositories/__tests__/tagRepository.test.ts deleted file mode 100644 index 5aaf2274f4..0000000000 --- a/backend/src/database/repositories/__tests__/tagRepository.test.ts +++ /dev/null @@ -1,295 +0,0 @@ -import TagRepository from '../tagRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import Error404 from '../../../errors/Error404' - -const db = null - -describe('TagRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create the given tag succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tag2add = { name: 'test-tag' } - - const tagCreated = await TagRepository.create(tag2add, mockIRepositoryOptions) - - tagCreated.createdAt = tagCreated.createdAt.toISOString().split('T')[0] - tagCreated.updatedAt = tagCreated.updatedAt.toISOString().split('T')[0] - - const expectedTagCreated = { - id: tagCreated.id, - name: tag2add.name, - members: [], - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - expect(tagCreated).toStrictEqual(expectedTagCreated) - }) - - it('Should throw sequelize not null error -- name field is required', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tag2add = {} - - await expect(() => TagRepository.create(tag2add, mockIRepositoryOptions)).rejects.toThrow() - }) - }) - - describe('findById method', () => { - it('Should successfully find created tag by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tag2add = { name: 'test-tag' } - - const tagCreated = await TagRepository.create(tag2add, mockIRepositoryOptions) - - tagCreated.createdAt = tagCreated.createdAt.toISOString().split('T')[0] - tagCreated.updatedAt = tagCreated.updatedAt.toISOString().split('T')[0] - - const expectedTagFound = { - id: tagCreated.id, - name: tag2add.name, - members: [], - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - const tagById = await TagRepository.findById(tagCreated.id, mockIRepositoryOptions) - - tagById.createdAt = tagById.createdAt.toISOString().split('T')[0] - tagById.updatedAt = tagById.updatedAt.toISOString().split('T')[0] - - expect(tagById).toStrictEqual(expectedTagFound) - }) - - it('Should throw 404 error when no tag found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - TagRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('filterIdsInTenant method', () => { - it('Should return the given ids of previously created tag entities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tag1 = { name: 'test1' } - const tag2 = { name: 'test2' } - - const tag1Created = await TagRepository.create(tag1, mockIRepositoryOptions) - const tag2Created = await TagRepository.create(tag2, mockIRepositoryOptions) - - const filterIdsReturned = await TagRepository.filterIdsInTenant( - [tag1Created.id, tag2Created.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([tag1Created.id, tag2Created.id]) - }) - - it('Should only return the ids of previously created tags and filter random uuids out', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tag = { name: 'test1' } - - const tagCreated = await TagRepository.create(tag, mockIRepositoryOptions) - - const { randomUUID } = require('crypto') - - const filterIdsReturned = await TagRepository.filterIdsInTenant( - [tagCreated.id, randomUUID(), randomUUID()], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([tagCreated.id]) - }) - - it('Should return an empty array for an irrelevant tenant', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tag = { name: 'test' } - - const tagCreated = await TagRepository.create(tag, mockIRepositoryOptions) - - // create a new tenant and bind options to it - mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const filterIdsReturned = await TagRepository.filterIdsInTenant( - [tagCreated.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([]) - }) - }) - - describe('findAndCountAll method', () => { - it('Should find and count all tags, with various filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tag1 = { name: 'test-tag' } - const tag2 = { name: 'test-tag-2' } - const tag3 = { name: 'another-tag' } - - const tag1Created = await TagRepository.create(tag1, mockIRepositoryOptions) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const tag2Created = await TagRepository.create(tag2, mockIRepositoryOptions) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const tag3Created = await TagRepository.create(tag3, mockIRepositoryOptions) - - // Test filter by name - // Current findAndCountAll uses wildcarded like statement so it matches both tags - let tags = await TagRepository.findAndCountAll( - { filter: { name: 'test-tag' } }, - mockIRepositoryOptions, - ) - - expect(tags.count).toEqual(2) - expect(tags.rows).toStrictEqual([tag2Created, tag1Created]) - - // Test filter by id - tags = await TagRepository.findAndCountAll( - { filter: { id: tag1Created.id } }, - mockIRepositoryOptions, - ) - - expect(tags.count).toEqual(1) - expect(tags.rows).toStrictEqual([tag1Created]) - - // Test filter by createdAt - find all between tag1.createdAt and tag3.createdAt - tags = await TagRepository.findAndCountAll( - { - filter: { - createdAtRange: [tag1Created.createdAt, tag3Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - - expect(tags.count).toEqual(3) - expect(tags.rows).toStrictEqual([tag3Created, tag2Created, tag1Created]) - - // Test filter by createdAt - find all where createdAt < tag2.createdAt - tags = await TagRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, tag2Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - expect(tags.count).toEqual(2) - expect(tags.rows).toStrictEqual([tag2Created, tag1Created]) - - // Test filter by createdAt - find all where createdAt < tag1.createdAt - tags = await TagRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, tag1Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - expect(tags.count).toEqual(1) - expect(tags.rows).toStrictEqual([tag1Created]) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created tag', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tag1 = { name: 'test-tag' } - - const tagCreated = await TagRepository.create(tag1, mockIRepositoryOptions) - - const tagUpdated = await TagRepository.update( - tagCreated.id, - { name: 'updated-tag-name' }, - mockIRepositoryOptions, - ) - - expect(tagUpdated.updatedAt.getTime()).toBeGreaterThan(tagUpdated.createdAt.getTime()) - - const tagExpected = { - id: tagCreated.id, - name: tagUpdated.name, - importHash: null, - createdAt: tagCreated.createdAt, - updatedAt: tagUpdated.updatedAt, - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - members: [], - } - - expect(tagUpdated).toStrictEqual(tagExpected) - }) - - it('Should throw 404 error when trying to update non existent tag', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - TagRepository.update(randomUUID(), { name: 'non-existent' }, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('destroy method', () => { - it('Should succesfully destroy previously created tag', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tag = { name: 'test-tag' } - - const returnedTag = await TagRepository.create(tag, mockIRepositoryOptions) - - await TagRepository.destroy(returnedTag.id, mockIRepositoryOptions, true) - - // Try selecting it after destroy, should throw 404 - await expect(() => - TagRepository.findById(returnedTag.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw 404 when trying to destroy a non existent tag', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - TagRepository.destroy(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/taskRepository.test.ts b/backend/src/database/repositories/__tests__/taskRepository.test.ts deleted file mode 100644 index f5027a9c1c..0000000000 --- a/backend/src/database/repositories/__tests__/taskRepository.test.ts +++ /dev/null @@ -1,1270 +0,0 @@ -import moment from 'moment' -import TaskRepository from '../taskRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import Error404 from '../../../errors/Error404' -import MemberRepository from '../memberRepository' -import ActivityRepository from '../activityRepository' -import { PlatformType } from '@crowd/types' -import { generateUUIDv1 } from '@crowd/common' -import lodash from 'lodash' - -const db = null - -const toCreate = { - name: 'name', - body: 'body', - type: 'regular', - status: 'done', - dueDate: new Date(), -} - -const sampleMembers = [ - { - username: { - [PlatformType.GITHUB]: { - username: 'harry_potter', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Harry Potter', - joinedAt: new Date(), - }, - { - username: { - [PlatformType.GITHUB]: { - username: 'hermione', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Hermione Granger', - joinedAt: new Date(), - }, - { - username: { - [PlatformType.GITHUB]: { - username: 'ron_weasley', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Ron Weasley', - joinedAt: new Date(), - }, -] - -const sampleActivities = [ - { - type: 'type', - timestamp: new Date(), - platform: 'daily_prophet', - sourceId: 'sourceId1', - }, - { - type: 'type', - timestamp: new Date(), - platform: 'daily_prophet', - sourceId: 'sourceId2', - }, - { - type: 'type', - timestamp: new Date(), - platform: 'daily_prophet', - sourceId: 'sourceId3', - }, -] - -async function getToCreate(task, options, from = { fromMembers: [], fromActivities: [] }) { - const { fromMembers, fromActivities } = from - task.members = [] - task.activities = [] - task.assignees = [] - - for (const sampleMember of fromMembers) { - const cloned = lodash.cloneDeep(sampleMember) - task.members.push((await MemberRepository.create(cloned, options)).id) - } - - for (const sampleActivity of fromActivities) { - const existing = await MemberRepository.memberExists( - sampleMembers[0].username[PlatformType.GITHUB].username, - PlatformType.GITHUB, - options, - ) - - if (existing) { - sampleActivity.member = existing.id - sampleActivity.username = sampleMembers[0].username[PlatformType.GITHUB].username - } else { - const cloned = lodash.cloneDeep(sampleMembers[0]) - const member = await MemberRepository.create(cloned, options) - sampleActivity.member = member.id - sampleActivity.username = sampleMembers[0].username[PlatformType.GITHUB].username - } - - task.activities.push((await ActivityRepository.create(sampleActivity, options)).id) - } - task.assignees.push(options.currentUser.id) - return task -} - -describe('TaskRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create the given task succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - createdTask.createdAt = createdTask.createdAt.toISOString().split('T')[0] - createdTask.updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - createdTask.assignees = createdTask.assignees.map((assignee) => assignee.id) - - const expectedTaskCreated = { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - expect(createdTask).toStrictEqual(expectedTaskCreated) - }) - - it('Should create a task with members', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: sampleMembers, - fromActivities: [], - }) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - createdTask.createdAt = createdTask.createdAt.toISOString().split('T')[0] - createdTask.updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - - createdTask.members = createdTask.members.map((member) => member.id) - createdTask.activities = createdTask.activities.map((activity) => activity.id) - createdTask.assignees = createdTask.assignees.map((assignee) => assignee.id) - - const expectedTaskCreated = { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - const clone1 = { ...createdTask } - const clone2 = { ...expectedTaskCreated } - delete clone1.members - delete clone2.members - expect(clone1).toStrictEqual(clone2) - expect(createdTask.members.sort()).toEqual(expectedTaskCreated.members.sort()) - - // Make sure the task exists in the member - for (const memberId of createdTask.members) { - const found = await MemberRepository.findById(memberId, mockIRepositoryOptions) - expect(found.tasks[0].id).toBe(createdTask.id) - } - }) - - it('Should create a task with activities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: sampleActivities, - }) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - createdTask.createdAt = createdTask.createdAt.toISOString().split('T')[0] - createdTask.updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - - createdTask.members = createdTask.members.map((member) => member.id) - createdTask.activities = createdTask.activities.map((activity) => activity.id) - createdTask.assignees = createdTask.assignees.map((assignee) => assignee.id) - - const expectedTaskCreated = { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - expect(createdTask).toStrictEqual(expectedTaskCreated) - expect(createdTask.activities.length).toBe(sampleActivities.length) - - // Make sure the task exists in the member - for (const activityId of createdTask.activities) { - const found = await ActivityRepository.findById(activityId, mockIRepositoryOptions) - expect(found.tasks[0].id).toBe(createdTask.id) - } - }) - - it('Should create a task with members and activities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: sampleMembers, - fromActivities: sampleActivities, - }) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - createdTask.createdAt = createdTask.createdAt.toISOString().split('T')[0] - createdTask.updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - - createdTask.members = createdTask.members.map((member) => member.id) - createdTask.activities = createdTask.activities.map((activity) => activity.id) - createdTask.assignees = createdTask.assignees.map((assignee) => assignee.id) - - const expectedTaskCreated = { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - const clone1 = { ...createdTask } - const clone2 = { ...expectedTaskCreated } - delete clone1.members - delete clone2.members - expect(clone1).toStrictEqual(clone2) - expect(createdTask.members.sort()).toEqual(expectedTaskCreated.members.sort()) - expect(createdTask.activities.length).toBe(sampleActivities.length) - expect(createdTask.members.length).toBe(sampleMembers.length) - }) - - it('Should create a task with a different assignee as the user creating it', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mockAssignee = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockAssignee, { - fromMembers: [], - fromActivities: [], - }) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - createdTask.createdAt = createdTask.createdAt.toISOString().split('T')[0] - createdTask.updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - - createdTask.members = createdTask.members.map((member) => member.id) - createdTask.activities = createdTask.activities.map((activity) => activity.id) - createdTask.assignees = createdTask.assignees.map((assignee) => assignee.id) - - const expectedTaskCreated = { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - expect(createdTask).toStrictEqual(expectedTaskCreated) - expect(createdTask.assignees[0]).toBe(mockAssignee.currentUser.id) - expect(createdTask.assignees[0]).not.toBe(mockIRepositoryOptions.currentUser.id) - }) - - it('Should throw an error when status is something not allowed', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const task2add = { - name: 'Task 2', - status: 'something', - } - - await expect(() => TaskRepository.create(task2add, mockIRepositoryOptions)).rejects.toThrow() - }) - - it('Should throw sequelize not null error -- name field is required', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const task2add = {} - - await expect(() => TaskRepository.create(task2add, mockIRepositoryOptions)).rejects.toThrow() - }) - }) - - describe('createSuggestedTasks method', () => { - it('Should create the static suggested tasks succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await TaskRepository.createSuggestedTasks(mockIRepositoryOptions) - - const tasks = await TaskRepository.findAndCountAll({ filter: {} }, mockIRepositoryOptions) - - expect(tasks.count).toBe(6) - - expect(tasks.rows.map((i) => i.name).sort()).toStrictEqual([ - 'Check for negative reactions', - 'Engage with relevant content', - 'Reach out to influential contacts', - 'Reach out to poorly engaged contacts', - 'Setup your team', - 'Setup your workpace integrations', - ]) - }) - - it('Should create a task with members', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: sampleMembers, - fromActivities: [], - }) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - createdTask.createdAt = createdTask.createdAt.toISOString().split('T')[0] - createdTask.updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - - createdTask.members = createdTask.members.map((member) => member.id) - createdTask.activities = createdTask.activities.map((activity) => activity.id) - createdTask.assignees = createdTask.assignees.map((assignee) => assignee.id) - - const expectedTaskCreated = { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - const clone1 = { ...createdTask } - const clone2 = { ...expectedTaskCreated } - delete clone1.members - delete clone2.members - expect(clone1).toStrictEqual(clone2) - expect(createdTask.members.sort()).toEqual(expectedTaskCreated.members.sort()) - expect(createdTask.members.length).toBe(sampleMembers.length) - - // Make sure the task exists in the member - for (const memberId of createdTask.members) { - const found = await MemberRepository.findById(memberId, mockIRepositoryOptions) - expect(found.tasks[0].id).toBe(createdTask.id) - } - }) - - it('Should create a task with activities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: sampleActivities, - }) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - createdTask.createdAt = createdTask.createdAt.toISOString().split('T')[0] - createdTask.updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - - createdTask.members = createdTask.members.map((member) => member.id) - createdTask.activities = createdTask.activities.map((activity) => activity.id) - createdTask.assignees = createdTask.assignees.map((assignee) => assignee.id) - - const expectedTaskCreated = { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - expect(createdTask).toStrictEqual(expectedTaskCreated) - expect(createdTask.activities.length).toBe(sampleActivities.length) - - // Make sure the task exists in the member - for (const activityId of createdTask.activities) { - const found = await ActivityRepository.findById(activityId, mockIRepositoryOptions) - expect(found.tasks[0].id).toBe(createdTask.id) - } - }) - - it('Should create a task with members and activities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: sampleMembers, - fromActivities: sampleActivities, - }) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - createdTask.createdAt = createdTask.createdAt.toISOString().split('T')[0] - createdTask.updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - - createdTask.members = createdTask.members.map((member) => member.id) - createdTask.activities = createdTask.activities.map((activity) => activity.id) - createdTask.assignees = createdTask.assignees.map((assignee) => assignee.id) - - const expectedTaskCreated = { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - const clone1 = { ...createdTask } - const clone2 = { ...expectedTaskCreated } - delete clone1.members - delete clone2.members - expect(clone1).toStrictEqual(clone2) - expect(createdTask.members.sort()).toEqual(expectedTaskCreated.members.sort()) - expect(createdTask.activities.length).toBe(sampleActivities.length) - expect(createdTask.members.length).toBe(sampleMembers.length) - }) - - it('Should create a task with a different assignee as the user creating it', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mockAssignee = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockAssignee, { - fromMembers: [], - fromActivities: [], - }) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - createdTask.createdAt = createdTask.createdAt.toISOString().split('T')[0] - createdTask.updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - - createdTask.members = createdTask.members.map((member) => member.id) - createdTask.activities = createdTask.activities.map((activity) => activity.id) - createdTask.assignees = createdTask.assignees.map((assignee) => assignee.id) - - const expectedTaskCreated = { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - expect(createdTask).toStrictEqual(expectedTaskCreated) - expect(createdTask.assignees[0]).toBe(mockAssignee.currentUser.id) - expect(createdTask.assignees[0]).not.toBe(mockIRepositoryOptions.currentUser.id) - }) - - it('Should throw an error when status is something not allowed', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const task2add = { - name: 'Task 2', - status: 'something', - } - - await expect(() => TaskRepository.create(task2add, mockIRepositoryOptions)).rejects.toThrow() - }) - - it('Should throw sequelize not null error -- name field is required', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const task2add = {} - - await expect(() => TaskRepository.create(task2add, mockIRepositoryOptions)).rejects.toThrow() - }) - }) - - describe('findById method', () => { - it('Should successfully find created task by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [], - }) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - createdTask.createdAt = createdTask.createdAt.toISOString().split('T')[0] - createdTask.updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - - createdTask.members = createdTask.members.map((member) => member.id) - createdTask.activities = createdTask.activities.map((activity) => activity.id) - createdTask.assignees = createdTask.assignees.map((assignee) => assignee.id) - - const expectedTaskFound = { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - const taskById = await TaskRepository.findById(createdTask.id, mockIRepositoryOptions) - - taskById.createdAt = taskById.createdAt.toISOString().split('T')[0] - taskById.updatedAt = taskById.updatedAt.toISOString().split('T')[0] - taskById.assignees = taskById.assignees.map((assignee) => assignee.id) - - expect(taskById).toStrictEqual(expectedTaskFound) - }) - - it('Should throw 404 error when no task found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - TaskRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('filterIdsInTenant method', () => { - it('Should return the given ids of previously created task entities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const task1 = { name: 'test1' } - const task2 = { name: 'test2' } - - const task1Created = await TaskRepository.create(task1, mockIRepositoryOptions) - const task2Created = await TaskRepository.create(task2, mockIRepositoryOptions) - - const filterIdsReturned = await TaskRepository.filterIdsInTenant( - [task1Created.id, task2Created.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([task1Created.id, task2Created.id]) - }) - - it('Should only return the ids of previously created tasks and filter random uuids out', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const task = { name: 'test1' } - - const taskCreated = await TaskRepository.create(task, mockIRepositoryOptions) - - const { randomUUID } = require('crypto') - - const filterIdsReturned = await TaskRepository.filterIdsInTenant( - [taskCreated.id, randomUUID(), randomUUID()], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([taskCreated.id]) - }) - - it('Should return an empty array for an irrelevant tenant', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const task = { name: 'test' } - - const taskCreated = await TaskRepository.create(task, mockIRepositoryOptions) - - // create a new tenant and bind options to it - mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const filterIdsReturned = await TaskRepository.filterIdsInTenant( - [taskCreated.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([]) - }) - }) - - describe('findAndCountAll method', () => { - it('Should find and count all tasks', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [], - }) - const toCreate2 = await getToCreate( - { - name: 'Task 2', - type: 'regular', - status: 'done', - }, - mockIRepositoryOptions, - ) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - const createdTask2 = await TaskRepository.create(toCreate2, mockIRepositoryOptions) - - const found = await TaskRepository.findAndCountAll({ filter: {} }, mockIRepositoryOptions) - - found.rows[1].createdAt = createdTask.createdAt.toISOString().split('T')[0] - found.rows[1].updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - - found.rows[1].members = createdTask.members.map((member) => member.id) - found.rows[1].activities = createdTask.activities.map((activity) => activity.id) - found.rows[1].assignees = createdTask.assignees.map((assignee) => assignee.id) - - found.rows[0].createdAt = createdTask2.createdAt.toISOString().split('T')[0] - found.rows[0].updatedAt = createdTask2.updatedAt.toISOString().split('T')[0] - - found.rows[0].members = createdTask2.members.map((member) => member.id) - found.rows[0].activities = createdTask2.activities.map((activity) => activity.id) - found.rows[0].assignees = createdTask2.assignees.map((assignee) => assignee.id) - - expect(found).toStrictEqual({ - rows: [ - { - id: createdTask2.id, - ...toCreate2, - body: null, - dueDate: null, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - }, - { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - }, - ], - count: 2, - limit: 10, - offset: 0, - }) - }) - - describe('filter', () => { - it('by name', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [], - }) - const toCreate2 = await getToCreate( - { - name: 'Task', - status: 'done', - }, - mockIRepositoryOptions, - ) - await TaskRepository.create(toCreate1, mockIRepositoryOptions) - await TaskRepository.create(toCreate2, mockIRepositoryOptions) - - const found = await TaskRepository.findAndCountAll( - { filter: { name: 'Task' } }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(1) - expect(found.rows[0].name).toBe('Task') - }) - - it('by type', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [], - }) - const toCreate2 = await getToCreate( - { - name: 'Suggested task', - type: 'suggested', - }, - mockIRepositoryOptions, - ) - await TaskRepository.create(toCreate1, mockIRepositoryOptions) - await TaskRepository.create(toCreate2, mockIRepositoryOptions) - - const found = await TaskRepository.findAndCountAll( - { filter: { type: 'suggested' } }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(1) - expect(found.rows[0].name).toBe('Suggested task') - }) - - it('by status', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [], - }) - const toCreate2 = await getToCreate( - { - name: 'Task', - status: 'in-progress', - }, - mockIRepositoryOptions, - ) - await TaskRepository.create(toCreate1, mockIRepositoryOptions) - await TaskRepository.create(toCreate2, mockIRepositoryOptions) - - const found = await TaskRepository.findAndCountAll( - { filter: { status: 'done' } }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(1) - expect(found.rows[0].status).toBe('done') - }) - - it('by assignees', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const options2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [], - }) - const toCreate2 = await getToCreate( - { - name: 'Task', - status: 'in-progress', - }, - options2, - ) - await TaskRepository.create(toCreate1, mockIRepositoryOptions) - await TaskRepository.create(toCreate2, mockIRepositoryOptions) - - const toFilter = options2.currentUser.id.toString() - - const found = await TaskRepository.findAndCountAll( - { filter: { assignees: [toFilter] } }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(1) - expect(found.rows[0].assignees[0].id).toBe(options2.currentUser.id) - }) - - it('by dueDate', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const options2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [], - }) - const toCreate2 = await getToCreate( - { - name: 'Task', - status: 'in-progress', - dueDate: moment().add(1, 'day').toDate(), - }, - options2, - ) - await TaskRepository.create(toCreate1, mockIRepositoryOptions) - await TaskRepository.create(toCreate2, mockIRepositoryOptions) - - const found = await TaskRepository.findAndCountAll( - { - filter: { - dueDateRange: [moment().startOf('day').toDate(), moment().endOf('day').toDate()], - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(1) - expect(found.rows[0].name).toBe(toCreate1.name) - }) - - it('by members', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [sampleMembers[0]], - fromActivities: [], - }) - const toCreate2 = await getToCreate( - { - name: 'Task', - status: 'in-progress', - dueDate: moment().add(1, 'day').toDate(), - }, - mockIRepositoryOptions, - { - fromMembers: [sampleMembers[1], sampleMembers[2]], - fromActivities: [], - }, - ) - await TaskRepository.create(toCreate1, mockIRepositoryOptions) - await TaskRepository.create(toCreate2, mockIRepositoryOptions) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const member = ( - await MemberRepository.findAndCountAll( - { - filter: {}, - }, - mockIRepositoryOptions, - ) - ).rows[0] - - const toFilter = [member.id.toString()] - - const found = await TaskRepository.findAndCountAll( - { - filter: { - members: toFilter, - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(1) - - const members = ( - await MemberRepository.findAndCountAll( - { - filter: {}, - }, - mockIRepositoryOptions, - ) - ).rows - - const m0Id = members.filter((m) => m.displayName === sampleMembers[0].displayName)[0].id - - const m1Id = members.filter((m) => m.displayName === sampleMembers[1].displayName)[0].id - const found2 = await TaskRepository.findAndCountAll( - { - filter: { - members: [m0Id, m1Id], - }, - }, - mockIRepositoryOptions, - ) - expect(found2.count).toBe(2) - }) - - it('by activity', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [sampleActivities[0]], - }) - const toCreate2 = await getToCreate( - { - name: 'Task', - status: 'in-progress', - dueDate: moment().add(1, 'day').toDate(), - }, - mockIRepositoryOptions, - { - fromMembers: [], - fromActivities: [sampleActivities[1], sampleActivities[2]], - }, - ) - await TaskRepository.create(toCreate1, mockIRepositoryOptions) - await TaskRepository.create(toCreate2, mockIRepositoryOptions) - - const act = ( - await ActivityRepository.findAndCountAll( - { - filter: {}, - }, - mockIRepositoryOptions, - ) - ).rows[0] - - const toFilter = [act.id.toString()] - - const found = await TaskRepository.findAndCountAll( - { - filter: { - activities: toFilter, - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(1) - - const activities = ( - await ActivityRepository.findAndCountAll( - { - filter: {}, - }, - mockIRepositoryOptions, - ) - ).rows - - const a0Id = activities.filter((a) => a.sourceId === sampleActivities[0].sourceId)[0].id - - const a1Id = activities.filter((a) => a.sourceId === sampleActivities[1].sourceId)[0].id - - const found2 = await TaskRepository.findAndCountAll( - { - filter: { - activities: [a0Id, a1Id], - }, - }, - mockIRepositoryOptions, - ) - expect(found2.count).toBe(2) - }) - }) - - it('by activities and members', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [sampleMembers[0]], - fromActivities: [sampleActivities[0]], - }) - const toCreate2 = await getToCreate( - { - name: 'Task', - status: 'in-progress', - dueDate: moment().add(1, 'day').toDate(), - }, - mockIRepositoryOptions, - { - fromMembers: [sampleMembers[1]], - fromActivities: [sampleActivities[1]], - }, - ) - const toCreate3 = await getToCreate( - { - name: 'Task 3', - status: 'in-progress', - dueDate: moment().add(1, 'day').toDate(), - }, - mockIRepositoryOptions, - { - fromMembers: [], - fromActivities: [sampleActivities[2]], - }, - ) - await TaskRepository.create(toCreate1, mockIRepositoryOptions) - await TaskRepository.create(toCreate2, mockIRepositoryOptions) - await TaskRepository.create(toCreate3, mockIRepositoryOptions) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = ( - await MemberRepository.findAndCountAll( - { - filter: {}, - }, - mockIRepositoryOptions, - ) - ).rows - - const m1Id = members.filter((m) => m.displayName === sampleMembers[1].displayName)[0].id - - const activities = ( - await ActivityRepository.findAndCountAll( - { - filter: {}, - }, - mockIRepositoryOptions, - ) - ).rows - - const a1Id = activities.filter((a) => a.sourceId === sampleActivities[1].sourceId)[0].id - const a2Id = activities.filter((a) => a.sourceId === sampleActivities[2].sourceId)[0].id - - const found = await TaskRepository.findAndCountAll( - { - filter: { - activities: [a1Id], - members: [m1Id], - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(1) - - const found2 = await TaskRepository.findAndCountAll( - { - advancedFilter: { - or: [ - { - activities: [a2Id], - }, - { - members: [m1Id], - }, - ], - }, - }, - mockIRepositoryOptions, - ) - expect(found2.count).toBe(2) - - const found3 = await TaskRepository.findAndCountAll( - { - advancedFilter: { - activities: [a1Id], - members: [m1Id], - }, - }, - mockIRepositoryOptions, - ) - expect(found3.count).toBe(1) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created task', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [], - }) - - const taskCreated = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - const taskUpdated = await TaskRepository.update( - taskCreated.id, - { name: 'updated-task-name' }, - mockIRepositoryOptions, - ) - - expect(taskUpdated.updatedAt.getTime()).toBeGreaterThan(taskCreated.createdAt.getTime()) - - taskUpdated.createdAt = taskUpdated.createdAt.toISOString().split('T')[0] - taskUpdated.updatedAt = taskUpdated.updatedAt.toISOString().split('T')[0] - - const taskExpected = { - id: taskCreated.id, - ...taskUpdated, - name: taskUpdated.name, - createdAt: taskUpdated.createdAt, - updatedAt: taskUpdated.updatedAt, - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - members: [], - } - - expect(taskUpdated).toStrictEqual(taskExpected) - }) - - it('Should succesfully update members related to the task', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [sampleMembers[0]], - fromActivities: [], - }) - - const newMembers = ( - await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [sampleMembers[1]], - fromActivities: [], - }) - ).members - - const taskCreated = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - const taskUpdated = await TaskRepository.update( - taskCreated.id, - { members: newMembers }, - mockIRepositoryOptions, - ) - - taskUpdated.createdAt = taskUpdated.createdAt.toISOString().split('T')[0] - taskUpdated.updatedAt = taskUpdated.updatedAt.toISOString().split('T')[0] - - expect(taskUpdated.members.length).toBe(1) - expect(taskUpdated.members[0].id).toStrictEqual(newMembers[0]) - }) - - it('Should succesfully update activities related to the task', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [sampleActivities[0]], - }) - - const newActivities = ( - await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [sampleActivities[1]], - }) - ).activities - - const taskCreated = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - const taskUpdated = await TaskRepository.update( - taskCreated.id, - { activities: newActivities }, - mockIRepositoryOptions, - ) - - taskUpdated.createdAt = taskUpdated.createdAt.toISOString().split('T')[0] - taskUpdated.updatedAt = taskUpdated.updatedAt.toISOString().split('T')[0] - - expect(taskUpdated.activities.length).toBe(1) - expect(taskUpdated.activities[0].id).toStrictEqual(newActivities[0]) - }) - - it('Should succesfully update assignees', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [], - }) - - const options2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const taskCreated = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - const toUpdate = options2.currentUser.id.toString() - - const taskUpdated = await TaskRepository.update( - taskCreated.id, - { assignees: [toUpdate] }, - mockIRepositoryOptions, - ) - - taskUpdated.createdAt = taskUpdated.createdAt.toISOString().split('T')[0] - taskUpdated.updatedAt = taskUpdated.updatedAt.toISOString().split('T')[0] - - expect(taskUpdated.assignees.map((i) => i.id)).toStrictEqual([toUpdate]) - }) - - it('Should throw 404 error when trying to update non existent task', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - TaskRepository.update(randomUUID(), { name: 'non-existent' }, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('destroy method', () => { - it('Should succesfully destroy previously created task', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const task = { name: 'test-task' } - - const returnedTask = await TaskRepository.create(task, mockIRepositoryOptions) - - await TaskRepository.destroy(returnedTask.id, mockIRepositoryOptions, true) - - // Try selecting it after destroy, should throw 404 - await expect(() => - TaskRepository.findById(returnedTask.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw 404 when trying to destroy a non existent task', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - TaskRepository.destroy(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('updateBulk method', () => { - it('Should succesfully bulk update given tasks', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - let task1 = await TaskRepository.create( - { name: 'test-task', status: 'in-progress' }, - mockIRepositoryOptions, - ) - let task2 = await TaskRepository.create( - { name: 'test-task-2', status: 'in-progress' }, - mockIRepositoryOptions, - ) - let task3 = await TaskRepository.create( - { name: 'test-task-3', status: 'archived' }, - mockIRepositoryOptions, - ) - - let result = await TaskRepository.updateBulk( - [task1.id, task2.id], - { status: 'done' }, - mockIRepositoryOptions, - ) - - expect(result.rowsUpdated).toBe(2) - - task1 = await TaskRepository.findById(task1.id, mockIRepositoryOptions) - task2 = await TaskRepository.findById(task2.id, mockIRepositoryOptions) - task3 = await TaskRepository.findById(task3.id, mockIRepositoryOptions) - - expect(task1.status).toStrictEqual('done') - expect(task2.status).toStrictEqual('done') - expect(task3.status).toStrictEqual('archived') - - result = await TaskRepository.updateBulk( - [task1.id, task2.id, task3.id], - { status: 'in-progress' }, - mockIRepositoryOptions, - ) - - task1 = await TaskRepository.findById(task1.id, mockIRepositoryOptions) - task2 = await TaskRepository.findById(task2.id, mockIRepositoryOptions) - task3 = await TaskRepository.findById(task3.id, mockIRepositoryOptions) - - expect(task1.status).toStrictEqual('in-progress') - expect(task2.status).toStrictEqual('in-progress') - expect(task3.status).toStrictEqual('in-progress') - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/tenantRepository.test.ts b/backend/src/database/repositories/__tests__/tenantRepository.test.ts deleted file mode 100644 index 7e29fa2497..0000000000 --- a/backend/src/database/repositories/__tests__/tenantRepository.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import TenantRepository from '../tenantRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import Plans from '../../../security/plans' - -const db = null - -describe('TenantRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('getPayingTenantIds method', () => { - it('should return tenants not using the essential plan', async () => { - const ToCreatePLanForEssentialPlanTenantOnTrial = { - name: 'essential tenant name', - url: 'an-essential-tenant-name', - plan: Plans.values.essential, - } - const ToCreatPlanForGrowthTenantOnTrial = { - name: 'growth tenant name', - url: 'a-growth-tenant-name', - plan: Plans.values.growth, - } - const options = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await options.database.tenant.create(ToCreatePLanForEssentialPlanTenantOnTrial) - const growthTenant = await options.database.tenant.create(ToCreatPlanForGrowthTenantOnTrial) - const tenantIds = await TenantRepository.getPayingTenantIds(options) - - expect(tenantIds).toHaveLength(1) - expect(growthTenant.id).toStrictEqual(tenantIds[0].id) - }) - }) - - describe('generateTenantUrl method', () => { - it('Should generate a url from name - 0 existing tenants with same url', async () => { - const options = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const tenantName = 'some tenant Name with !@#_% non-alphanumeric characters' - - const generatedUrl = await TenantRepository.generateTenantUrl(tenantName, options) - const expectedGeneratedUrl = 'some-tenant-name-with-non-alphanumeric-characters' - - expect(generatedUrl).toStrictEqual(expectedGeneratedUrl) - }) - - it('Should generate a url from name - with existing tenant that has the same url', async () => { - const options = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const tenantName = 'a tenant name' - - // create a tenant with url 'a-tenant-name' - await options.database.tenant.create({ - name: tenantName, - url: 'a-tenant-name', - plan: Plans.values.essential, - }) - - // now generate function should return 'a-tenant-name-1' because it already exists - const generatedUrl = await TenantRepository.generateTenantUrl(tenantName, options) - - const expectedGeneratedUrl = 'a-tenant-name-1' - - expect(generatedUrl).toStrictEqual(expectedGeneratedUrl) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/userRepository.test.ts b/backend/src/database/repositories/__tests__/userRepository.test.ts deleted file mode 100644 index a18315d05a..0000000000 --- a/backend/src/database/repositories/__tests__/userRepository.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import UserRepository from '../userRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import Error404 from '../../../errors/Error404' -import Roles from '../../../security/roles' - -const db = null - -describe('UserRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('findAllUsersOfTenant method', () => { - it('Should find all related users of a tenant successfully', async () => { - // Getting options already creates one random user - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - let allUsersOfTenant = ( - await UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id) - ).map((u) => SequelizeTestUtils.objectWithoutKey(u, 'tenants')) - - expect(allUsersOfTenant).toStrictEqual([ - mockIRepositoryOptions.currentUser.get({ plain: true }), - ]) - - // add more users to the test tenant - const randomUser2 = await SequelizeTestUtils.getRandomUser() - const user2 = await mockIRepositoryOptions.database.user.create(randomUser2) - - await mockIRepositoryOptions.database.tenantUser.create({ - roles: [Roles.values.admin], - status: 'active', - tenantId: mockIRepositoryOptions.currentTenant.id, - userId: user2.id, - }) - - allUsersOfTenant = ( - await UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id) - ).map((u) => SequelizeTestUtils.objectWithoutKey(u, 'tenants')) - - expect(allUsersOfTenant).toStrictEqual([ - mockIRepositoryOptions.currentUser.get({ plain: true }), - user2.get({ plain: true }), - ]) - - const randomUser3 = await SequelizeTestUtils.getRandomUser() - const user3 = await mockIRepositoryOptions.database.user.create(randomUser3) - - await mockIRepositoryOptions.database.tenantUser.create({ - roles: [Roles.values.admin], - status: 'active', - tenantId: mockIRepositoryOptions.currentTenant.id, - userId: user3.id, - }) - - allUsersOfTenant = ( - await UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id) - ).map((u) => SequelizeTestUtils.objectWithoutKey(u, 'tenants')) - - expect(allUsersOfTenant).toStrictEqual([ - mockIRepositoryOptions.currentUser.get({ plain: true }), - user2.get({ plain: true }), - user3.get({ plain: true }), - ]) - - // add other users and tenants that are non related to previous couples - await SequelizeTestUtils.getTestIRepositoryOptions(db) - - // users of the previous tenant should be the same - allUsersOfTenant = ( - await UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id) - ).map((u) => SequelizeTestUtils.objectWithoutKey(u, 'tenants')) - - expect(allUsersOfTenant).toStrictEqual([ - mockIRepositoryOptions.currentUser.get({ plain: true }), - user2.get({ plain: true }), - user3.get({ plain: true }), - ]) - - const tenantUsers = await mockIRepositoryOptions.database.tenantUser.findAll({ - tenantId: mockIRepositoryOptions.currentTenant.id, - }) - - // remove last user added to the tenant - await tenantUsers[2].destroy({ force: true }) - - allUsersOfTenant = ( - await UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id) - ).map((u) => SequelizeTestUtils.objectWithoutKey(u, 'tenants')) - - expect(allUsersOfTenant).toStrictEqual([ - mockIRepositoryOptions.currentUser.get({ plain: true }), - user2.get({ plain: true }), - ]) - - // remove first user added to the tenant - await tenantUsers[0].destroy({ force: true }) - - allUsersOfTenant = ( - await UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id) - ).map((u) => SequelizeTestUtils.objectWithoutKey(u, 'tenants')) - - expect(allUsersOfTenant).toStrictEqual([user2.get({ plain: true })]) - - // remove the last remaining user from the tenant - await tenantUsers[1].destroy({ force: true }) - - // function now should be throwing Error404 - await expect(() => - UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id), - ).rejects.toThrowError(new Error404()) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/widgetRepository.test.ts b/backend/src/database/repositories/__tests__/widgetRepository.test.ts deleted file mode 100644 index 21d9169972..0000000000 --- a/backend/src/database/repositories/__tests__/widgetRepository.test.ts +++ /dev/null @@ -1,573 +0,0 @@ -import WidgetRepository from '../widgetRepository' -import ReportRepository from '../reportRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import Error404 from '../../../errors/Error404' - -const db = null - -describe('WidgetRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create a widget succesfully with default values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widget2Add = { type: 'test-widget' } - - const widgetCreated = await WidgetRepository.create(widget2Add, mockIRepositoryOptions) - - widgetCreated.createdAt = widgetCreated.createdAt.toISOString().split('T')[0] - widgetCreated.updatedAt = widgetCreated.updatedAt.toISOString().split('T')[0] - - const widgetExpected = { - id: widgetCreated.id, - type: widget2Add.type, - title: null, - settings: null, - cache: null, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - reportId: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - report: null, - } - - expect(widgetCreated).toStrictEqual(widgetExpected) - }) - - it('Should create a widget succesfully with given values -- without report', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widget2Add = { - type: 'test-widget', - title: 'Activities by Date', - settings: { - chartType: 'line', - query: { - measures: ['Activities.activityCount'], - timeDimensions: [ - { - dimension: 'Activities.date', - granularity: 'week', - dateRange: 'Last 30 days', - }, - ], - limit: 10000, - }, - layout: { - x: 6, - y: 0, - w: 6, - h: 18, - i: '620d303b0895bb8bee0a7e24', - moved: false, - }, - }, - } - - const widgetCreated = await WidgetRepository.create(widget2Add, mockIRepositoryOptions) - - widgetCreated.createdAt = widgetCreated.createdAt.toISOString().split('T')[0] - widgetCreated.updatedAt = widgetCreated.updatedAt.toISOString().split('T')[0] - - // Trim the report object, we'll only expect the reportId - const { report: _reportObj, ...widgetWithoutReport } = widgetCreated - - const widgetExpected = { - id: widgetCreated.id, - type: widget2Add.type, - title: widget2Add.title, - settings: widget2Add.settings, - cache: null, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - reportId: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(widgetWithoutReport).toStrictEqual(widgetExpected) - }) - - it('Should create a widget succesfully with given values -- with report', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report2Add = { - name: 'test-report', - public: true, - } - - const reportCreated = await ReportRepository.create(report2Add, mockIRepositoryOptions) - - const widget2Add = { - type: 'test-widget', - title: 'Activities by Date', - report: reportCreated.id, - settings: { - chartType: 'line', - query: { - measures: ['Activities.activityCount'], - timeDimensions: [ - { - dimension: 'Activities.date', - granularity: 'week', - dateRange: 'Last 30 days', - }, - ], - limit: 10000, - }, - layout: { - x: 6, - y: 0, - w: 6, - h: 18, - i: '620d303b0895bb8bee0a7e24', - moved: false, - }, - }, - } - - const widgetCreated = await WidgetRepository.create(widget2Add, mockIRepositoryOptions) - - widgetCreated.createdAt = widgetCreated.createdAt.toISOString().split('T')[0] - widgetCreated.updatedAt = widgetCreated.updatedAt.toISOString().split('T')[0] - - // Trim the report object, we'll only expect the reportId - const { report: _reportObj, ...widgetWithoutReport } = widgetCreated - - const widgetExpected = { - id: widgetCreated.id, - type: widget2Add.type, - title: widget2Add.title, - settings: widget2Add.settings, - cache: null, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - reportId: reportCreated.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(widgetWithoutReport).toStrictEqual(widgetExpected) - }) - - it('Should throw sequelize not null error -- type field is required', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widget2Add = {} - - await expect(() => - WidgetRepository.create(widget2Add, mockIRepositoryOptions), - ).rejects.toThrow() - }) - }) - - describe('findById method', () => { - it('Should successfully find created widget by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widget2add = { type: 'test-widget' } - - const widgetCreated = await WidgetRepository.create(widget2add, mockIRepositoryOptions) - - widgetCreated.createdAt = widgetCreated.createdAt.toISOString().split('T')[0] - widgetCreated.updatedAt = widgetCreated.updatedAt.toISOString().split('T')[0] - - const widgetExpected = { - id: widgetCreated.id, - type: widget2add.type, - title: null, - settings: null, - cache: null, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - reportId: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - const widgetById = await WidgetRepository.findById(widgetCreated.id, mockIRepositoryOptions) - - widgetById.createdAt = widgetById.createdAt.toISOString().split('T')[0] - widgetById.updatedAt = widgetById.updatedAt.toISOString().split('T')[0] - - const { report: _reportObj, ...widgetWithoutReport } = widgetById - - expect(widgetWithoutReport).toStrictEqual(widgetExpected) - }) - - it('Should throw 404 error when no widget found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - WidgetRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('findByType method', () => { - it('Should successfully find one widget by type', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widgetCreated = await WidgetRepository.create( - { type: 'test-widget' }, - mockIRepositoryOptions, - ) - - widgetCreated.createdAt = widgetCreated.createdAt.toISOString().split('T')[0] - widgetCreated.updatedAt = widgetCreated.updatedAt.toISOString().split('T')[0] - - const widgetExpected = { - id: widgetCreated.id, - type: widgetCreated.type, - title: null, - settings: null, - cache: null, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - reportId: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - const widgetByType = await WidgetRepository.findByType( - widgetCreated.type, - mockIRepositoryOptions, - ) - - widgetByType.createdAt = widgetByType.createdAt.toISOString().split('T')[0] - widgetByType.updatedAt = widgetByType.updatedAt.toISOString().split('T')[0] - - const { report: _reportObj, ...widgetWithoutReport } = widgetByType - - expect(widgetWithoutReport).toStrictEqual(widgetExpected) - }) - - it('Should throw 404 error when no widget found with given type', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await WidgetRepository.create({ type: 'some-type' }, mockIRepositoryOptions) - - await expect(() => - WidgetRepository.findByType('some-other-type', mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('filterIdsInTenant method', () => { - it('Should return the given ids of previously created widget entities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widget1 = { type: 'widget-test1' } - const widget2 = { type: 'widget-test2' } - - const widget1Created = await WidgetRepository.create(widget1, mockIRepositoryOptions) - const widget2Created = await WidgetRepository.create(widget2, mockIRepositoryOptions) - - const filterIdsReturned = await WidgetRepository.filterIdsInTenant( - [widget1Created.id, widget2Created.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([widget1Created.id, widget2Created.id]) - }) - - it('Should only return the ids of previously created widgets and filter random uuids out', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widget = { type: 'widget-test' } - - const widgetCreated = await WidgetRepository.create(widget, mockIRepositoryOptions) - - const { randomUUID } = require('crypto') - - const filterIdsReturned = await WidgetRepository.filterIdsInTenant( - [widgetCreated.id, randomUUID(), randomUUID()], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([widgetCreated.id]) - }) - - it('Should return an empty array for an irrelevant tenant', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widget = { type: 'widget-test' } - - const widgetCreated = await WidgetRepository.create(widget, mockIRepositoryOptions) - - // create a new tenant and bind options to it - mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const filterIdsReturned = await WidgetRepository.filterIdsInTenant( - [widgetCreated.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([]) - }) - }) - - describe('findAndCountAll method', () => { - it('Should find and count all widgets, with various filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report1 = { name: 'test-report', public: true } - const report2 = { name: 'test-report', public: true } - - const report1Created = await ReportRepository.create(report1, mockIRepositoryOptions) - const report2Created = await ReportRepository.create(report2, mockIRepositoryOptions) - - const widget1 = { - title: 'Number of activities - graph', - type: 'number-activities-graph', - report: report1Created.id, - settings: { - l1_settings: { - l2_settings: { - values: ['test2'], - }, - values: ['test1'], - }, - }, - } - - const widget2 = { - title: 'Time to first interaction - graph', - type: 'time-to-first-interaction-graph', - report: report1Created.id, - settings: { - l1_settings: { - l2_settings: { - values: ['test2'], - }, - values: ['test1'], - }, - }, - } - - const widget3 = { - title: 'Some cubejs widget', - type: 'cubejs', - report: report2Created.id, - } - const widget4 = { type: 'number-activities' } - - const widget1Created = await WidgetRepository.create(widget1, mockIRepositoryOptions) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const widget2Created = await WidgetRepository.create(widget2, mockIRepositoryOptions) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const widget3Created = await WidgetRepository.create(widget3, mockIRepositoryOptions) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const widget4Created = await WidgetRepository.create(widget4, mockIRepositoryOptions) - - // Filter by type - // Current findAndCountAll uses wildcarded like statement so it matches both widget1 and widget4 - let widgets = await WidgetRepository.findAndCountAll( - { filter: { type: 'number-activities' } }, - mockIRepositoryOptions, - ) - - expect(widgets.count).toEqual(2) - expect(widgets.rows).toStrictEqual([widget4Created, widget1Created]) - - // Filter by id - widgets = await WidgetRepository.findAndCountAll( - { filter: { id: widget1Created.id } }, - mockIRepositoryOptions, - ) - - expect(widgets.count).toEqual(1) - expect(widgets.rows).toStrictEqual([widget1Created]) - - // Filter by createdAt - find all between widget1.createdAt and widget3.createdAt - widgets = await WidgetRepository.findAndCountAll( - { - filter: { - createdAtRange: [widget1Created.createdAt, widget3Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - - expect(widgets.count).toEqual(3) - expect(widgets.rows).toStrictEqual([widget3Created, widget2Created, widget1Created]) - - // Filter by createdAt - find all where createdAt < widget2.createdAt - widgets = await WidgetRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, widget2Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - expect(widgets.count).toEqual(2) - expect(widgets.rows).toStrictEqual([widget2Created, widget1Created]) - - // Filter by createdAt - find all where createdAt < widget1.createdAt - widgets = await WidgetRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, widget1Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - expect(widgets.count).toEqual(1) - expect(widgets.rows).toStrictEqual([widget1Created]) - - // Filter by title - widgets = await WidgetRepository.findAndCountAll( - { filter: { title: 'graph' } }, - mockIRepositoryOptions, - ) - - expect(widgets.count).toEqual(2) - expect(widgets.rows).toStrictEqual([widget2Created, widget1Created]) - - // Filter by report1 - widgets = await WidgetRepository.findAndCountAll( - { filter: { report: report1Created.id } }, - mockIRepositoryOptions, - ) - - expect(widgets.count).toEqual(2) - expect(widgets.rows).toStrictEqual([widget2Created, widget1Created]) - - // Filter by report2 - widgets = await WidgetRepository.findAndCountAll( - { filter: { report: report2Created.id } }, - mockIRepositoryOptions, - ) - - expect(widgets.count).toEqual(1) - expect(widgets.rows).toStrictEqual([widget3Created]) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created widget', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widget1 = { type: 'widget-test' } - - const widgetCreated = await WidgetRepository.create(widget1, mockIRepositoryOptions) - - // Test adding report to widget on update as well - const report = { name: 'test-report', public: true } - - const reportCreated = await ReportRepository.create(report, mockIRepositoryOptions) - - const widgetUpdated = await WidgetRepository.update( - widgetCreated.id, - { - type: 'updated-widget-type', - title: 'new-title', - report: reportCreated.id, - }, - mockIRepositoryOptions, - ) - - expect(widgetUpdated.updatedAt.getTime()).toBeGreaterThan(widgetUpdated.createdAt.getTime()) - - const widgetExcpected = { - id: widgetCreated.id, - type: widgetUpdated.type, - title: widgetUpdated.title, - settings: null, - cache: null, - importHash: null, - createdAt: widgetCreated.createdAt, - updatedAt: widgetUpdated.updatedAt, - deletedAt: null, - reportId: reportCreated.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - const { report: _reportObj, ...widgetWithoutReport } = widgetUpdated - - expect(widgetWithoutReport).toStrictEqual(widgetExcpected) - }) - - it('Should throw 404 error when trying to update non existent widget', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - WidgetRepository.update(randomUUID(), { type: 'non-existent' }, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('destroy method', () => { - it('Should succesfully destroy previously created widget', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widget = { - type: 'integrations', - title: 'Metric graph', - } - - const returnedWidget = await WidgetRepository.create(widget, mockIRepositoryOptions) - - await WidgetRepository.destroy(returnedWidget.id, mockIRepositoryOptions, true) - - // Try selecting it after destroy, should throw 404 - await expect(() => - WidgetRepository.findById(returnedWidget.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw 404 when trying to destroy a non existent widget', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - WidgetRepository.destroy(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) -}) diff --git a/backend/src/database/repositories/activityRepository.ts b/backend/src/database/repositories/activityRepository.ts index 129cca2939..01881416ff 100644 --- a/backend/src/database/repositories/activityRepository.ts +++ b/backend/src/database/repositories/activityRepository.ts @@ -1,790 +1,37 @@ -import sanitizeHtml from 'sanitize-html' -import lodash from 'lodash' -import Sequelize from 'sequelize' -import SequelizeRepository from './sequelizeRepository' -import AuditLogRepository from './auditLogRepository' -import SequelizeFilterUtils from '../utils/sequelizeFilterUtils' -import Error400 from '../../errors/Error400' -import Error404 from '../../errors/Error404' -import { IRepositoryOptions } from './IRepositoryOptions' -import QueryParser from './filters/queryParser' -import { QueryOutput } from './filters/queryTypes' -import { AttributeData } from '../attributes/attribute' -import MemberRepository from './memberRepository' -import ActivityDisplayService from '../../services/activityDisplayService' -import SegmentRepository from './segmentRepository' +import { QueryTypes } from 'sequelize' -const { Op } = Sequelize +import { IIntegrationResult, IntegrationResultState } from '@crowd/types' -const log: boolean = false +import { IRepositoryOptions } from './IRepositoryOptions' +import SequelizeRepository from './sequelizeRepository' class ActivityRepository { - static async create(data, options: IRepositoryOptions) { - const currentUser = SequelizeRepository.getCurrentUser(options) - + static async createResults(result: IIntegrationResult, options: IRepositoryOptions) { const tenant = SequelizeRepository.getCurrentTenant(options) - const transaction = SequelizeRepository.getTransaction(options) - const segment = SequelizeRepository.getStrictlySingleActiveSegment(options) - // Data and body will be displayed as HTML. We need to sanitize them. - if (data.body) { - data.body = sanitizeHtml(data.body).trim() - } - - if (data.title) { - data.title = sanitizeHtml(data.title).trim() - } - - if (data.sentiment) { - this._validateSentiment(data.sentiment) - } - - // type and platform to lowercase - if (data.type) { - data.type = data.type.toLowerCase() - } - if (data.platform) { - data.platform = data.platform.toLowerCase() - } - - const record = await options.database.activity.create( - { - ...lodash.pick(data, [ - 'type', - 'timestamp', - 'platform', - 'isContribution', - 'score', - 'attributes', - 'channel', - 'body', - 'title', - 'url', - 'sentiment', - 'sourceId', - 'importHash', - 'username', - 'objectMemberUsername', - ]), - memberId: data.member || null, - objectMemberId: data.objectMember || undefined, - organizationId: data.organizationId || undefined, - parentId: data.parent || null, - sourceParentId: data.sourceParentId || null, - conversationId: data.conversationId || null, - segmentId: segment.id, - tenantId: tenant.id, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { - transaction, - }, - ) - - await record.setTasks(data.tasks || [], { - transaction, - }) - - await this._createAuditLog(AuditLogRepository.CREATE, record, data, options) - - return this.findById(record.id, options) - } - - /** - * Check whether sentiment data is valid - * @param sentimentData Object: {positive: number, negative: number, mixed: number, neutral: number, sentiment: 'positive' | 'negative' | 'mixed' | 'neutral'} - */ - static _validateSentiment(sentimentData) { - if (!lodash.isEmpty(sentimentData)) { - const moods = ['positive', 'negative', 'mixed', 'neutral'] - for (const prop of moods) { - if (typeof sentimentData[prop] !== 'number') { - throw new Error400('en', 'activity.error.sentiment.mood') - } - } - if (!moods.includes(sentimentData.label)) { - throw new Error400('en', 'activity.error.sentiment.label') - } - if (typeof sentimentData.sentiment !== 'number') { - throw new Error('activity.error.sentiment.sentiment') - } - } - } - - static async update(id, data, options: IRepositoryOptions) { - const currentUser = SequelizeRepository.getCurrentUser(options) - - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const segment = SequelizeRepository.getStrictlySingleActiveSegment(options) - - let record = await options.database.activity.findOne({ - where: { - id, - tenantId: currentTenant.id, - segmentId: segment.id, - }, - transaction, - }) - - await record.setTasks(data.tasks || [], { - transaction, - }) - - if (!record) { - throw new Error404() - } - - // Data and body will be displayed as HTML. We need to sanitize them. - if (data.body) { - data.body = sanitizeHtml(data.body).trim() - } - if (data.title) { - data.title = sanitizeHtml(data.title).trim() - } - - if (data.sentiment) { - this._validateSentiment(data.sentiment) - } - - record = await record.update( - { - ...lodash.pick(data, [ - 'type', - 'timestamp', - 'platform', - 'isContribution', - 'attributes', - 'channel', - 'body', - 'title', - 'url', - 'sentiment', - 'score', - 'sourceId', - 'importHash', - 'username', - 'objectMemberUsername', - ]), - memberId: data.member || undefined, - objectMemberId: data.objectMember || undefined, - organizationId: data.organizationId, - parentId: data.parent || undefined, - sourceParentId: data.sourceParentId || undefined, - conversationId: data.conversationId || undefined, - updatedById: currentUser.id, - }, - { - transaction, - }, - ) - - await this._createAuditLog(AuditLogRepository.UPDATE, record, data, options) - - return this.findById(record.id, options) - } - - static async destroy(id, options: IRepositoryOptions, force = false) { - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const record = await options.database.activity.findOne({ - where: { - id, - tenantId: currentTenant.id, - segmentId: SequelizeRepository.getSegmentIds(options), - }, - transaction, - }) - - if (!record) { - throw new Error404() - } - - await record.destroy({ - transaction, - force, - }) - - await this._createAuditLog(AuditLogRepository.DELETE, record, record, options) - } - - static async findById(id, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const include = [ - { - model: options.database.member, - as: 'member', - }, - { - model: options.database.member, - as: 'objectMember', - }, - { - model: options.database.activity, - as: 'parent', - }, - { - model: options.database.organization, - as: 'organization', - }, - ] - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const record = await options.database.activity.findOne({ - where: { - id, - tenantId: currentTenant.id, - segmentId: SequelizeRepository.getSegmentIds(options), - }, - include, - transaction, - }) - - if (!record) { - throw new Error404() - } - - return this._populateRelations(record, options) - } - - /** - * Find a record in the database given a query. - * @param query Query to find by - * @param options Repository options - * @returns The found record. Null if none is found. - */ - static async findOne(query, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const record = await options.database.activity.findOne({ - where: { - tenantId: currentTenant.id, - segmentId: SequelizeRepository.getSegmentIds(options), - ...query, - }, - transaction, - }) - - return this._populateRelations(record, options) - } - - static async filterIdInTenant(id, options: IRepositoryOptions) { - return lodash.get(await this.filterIdsInTenant([id], options), '[0]', null) - } - - static async filterIdsInTenant(ids, options: IRepositoryOptions) { - if (!ids || !ids.length) { - return [] - } - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const transaction = SequelizeRepository.getTransaction(options) - - const where = { - id: { - [Op.in]: ids, - }, - tenantId: currentTenant.id, - } - - const records = await options.database.activity.findAll({ - attributes: ['id'], - where, - transaction, - }) - - return records.map((record) => record.id) - } - - static async count(filter, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - return options.database.activity.count({ - where: { - ...filter, - tenantId: tenant.id, - segmentId: SequelizeRepository.getSegmentIds(options), - }, - transaction, - }) - } - - static async findAndCountAll( - { - filter = {} as any, - advancedFilter = null as any, - limit = 0, - offset = 0, - orderBy = '', - attributesSettings = [] as AttributeData[], - }, - options: IRepositoryOptions, - ) { - // If the advanced filter is empty, we construct it from the query parameter filter - if (!advancedFilter) { - advancedFilter = { and: [] } - - if (filter.id) { - advancedFilter.and.push({ - id: filter.id, - }) - } - - if (filter.type) { - advancedFilter.and.push({ - type: { - textContains: filter.type, - }, - }) - } - - if (filter.timestampRange) { - const [start, end] = filter.timestampRange - - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - timestamp: { - gte: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - timestamp: { - lte: end, - }, - }) - } - } - - if (filter.platform) { - advancedFilter.and.push({ - platform: { - textContains: filter.platform, - }, - }) - } - - if (filter.member) { - advancedFilter.and.push({ - memberId: filter.member, - }) - } - - if (filter.objectMember) { - advancedFilter.and.push({ - objectMemberId: filter.objectMember, - }) - } - - if ( - filter.isContribution === true || - filter.isContribution === 'true' || - filter.isContribution === false || - filter.isContribution === 'false' - ) { - advancedFilter.and.push({ - isContribution: filter.isContribution === true || filter.isContribution === 'true', - }) - } - - if (filter.scoreRange) { - const [start, end] = filter.scoreRange - - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - score: { - gte: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - score: { - lte: end, - }, - }) - } - } - - if (filter.channel) { - advancedFilter.and.push({ - channel: { - textContains: filter.channel, - }, - }) - } - - if (filter.body) { - advancedFilter.and.push({ - body: { - textContains: filter.body, - }, - }) - } + const seq = SequelizeRepository.getSequelize(options) - if (filter.title) { - advancedFilter.and.push({ - title: { - textContains: filter.title, - }, - }) - } - - if (filter.url) { - advancedFilter.and.push({ - textContains: filter.channel, - }) - } - - if (filter.sentimentRange) { - const [start, end] = filter.sentimentRange - - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - sentiment: { - gte: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - sentiment: { - lte: end, - }, - }) - } - } - - if (filter.sentimentLabel) { - advancedFilter.and.push({ - 'sentiment.label': filter.sentimentLabel, - }) - } - - for (const mood of ['positive', 'negative', 'neutral', 'mixed']) { - if (filter[`${mood}SentimentRange`]) { - const [start, end] = filter[`${mood}SentimentRange`] - - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - [`sentiment.${mood}`]: { - gte: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - [`sentiment.${mood}`]: { - lte: end, - }, - }) - } - } - } - - if (filter.parent) { - advancedFilter.and.push({ - parentId: filter.parent, - }) - } - - if (filter.sourceParentId) { - advancedFilter.and.push({ - sourceParentId: filter.sourceParentId, - }) - } - - if (filter.sourceId) { - advancedFilter.and.push({ - sourceId: filter.sourceId, - }) - } - - if (filter.conversationId) { - advancedFilter.and.push({ - conversationId: filter.conversationId, - }) - } - - if (filter.organizations) { - advancedFilter.and.push({ - organizationId: filter.organizations, - }) - } - - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange - - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - createdAt: { - gte: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - createdAt: { - gte: end, - }, - }) - } - } - } - - const memberSequelizeInclude = { - model: options.database.member, - as: 'member', - where: {}, - } - - if (advancedFilter.member) { - const { dynamicAttributesDefaultNestedFields, dynamicAttributesPlatformNestedFields } = - await MemberRepository.getDynamicAttributesLiterals(attributesSettings, options) - - const memberQueryParser = new QueryParser( - { - nestedFields: { - ...dynamicAttributesDefaultNestedFields, - ...dynamicAttributesPlatformNestedFields, - reach: 'reach.total', - }, - manyToMany: { - tags: { - table: 'members', - model: 'member', - relationTable: { - name: 'memberTags', - from: 'memberId', - to: 'tagId', - }, - }, - segments: { - table: 'members', - model: 'member', - relationTable: { - name: 'memberSegments', - from: 'memberId', - to: 'segmentId', - }, - }, - organizations: { - table: 'members', - model: 'member', - relationTable: { - name: 'memberOrganizations', - from: 'memberId', - to: 'organizationId', - }, - }, - }, - customOperators: { - username: { - model: 'member', - column: 'username', - }, - platform: { - model: 'member', - column: 'username', - }, - }, - }, - options, - ) + result.segmentId = segment.id - const parsedMemberQuery: QueryOutput = memberQueryParser.parse({ - filter: advancedFilter.member, - orderBy: orderBy || ['joinedAt_DESC'], - limit, - offset, - }) - - memberSequelizeInclude.where = parsedMemberQuery.where ?? {} - delete advancedFilter.member - } - - if (advancedFilter.organizations) { - advancedFilter.organizationId = advancedFilter.organizations - delete advancedFilter.organizations - } - - const include = [ - memberSequelizeInclude, - { - model: options.database.activity, - as: 'parent', - include: [ - { - model: options.database.member, - as: 'member', - }, - ], - }, - { - model: options.database.member, - as: 'objectMember', - }, - { - model: options.database.organization, - as: 'organization', - }, - ] - - const parser = new QueryParser( + const results = await seq.query( + ` + insert into integration.results(state, data, "tenantId") + values(:state, :data, :tenantId) + returning id; + `, { - nestedFields: { - sentiment: 'sentiment.sentiment', + replacements: { + tenantId: tenant.id, + state: IntegrationResultState.PENDING, + data: JSON.stringify(result), }, - manyToMany: { - organizations: { - table: 'activities', - model: 'activity', - overrideJoinField: 'memberId', - relationTable: { - name: 'memberOrganizations', - from: 'memberId', - to: 'organizationId', - }, - }, - }, - }, - options, - ) - - const parsed: QueryOutput = parser.parse({ - filter: advancedFilter, - orderBy: orderBy || ['timestamp_DESC'], - limit, - offset, - }) - - let { - rows, - count, // eslint-disable-line prefer-const - } = await options.database.activity.findAndCountAll({ - include, - attributes: [ - ...SequelizeFilterUtils.getLiteralProjectionsOfModel('activity', options.database), - ], - ...(parsed.where ? { where: parsed.where } : {}), - ...(parsed.having ? { having: parsed.having } : {}), - order: parsed.order, - limit: parsed.limit, - offset: parsed.offset, - transaction: SequelizeRepository.getTransaction(options), - }) - - rows = await this._populateRelationsForRows(rows, options) - - return { rows, count, limit: parsed.limit, offset: parsed.offset } - } - - static async findAllAutocomplete(query, limit, options: IRepositoryOptions) { - const tenant = SequelizeRepository.getCurrentTenant(options) - - const whereAnd: Array = [ - { - tenantId: tenant.id, + type: QueryTypes.INSERT, }, - ] - - if (query) { - whereAnd.push({ - [Op.or]: [{ id: SequelizeFilterUtils.uuid(query) }], - }) - } - - const where = { [Op.and]: whereAnd } - - const records = await options.database.activity.findAll({ - attributes: ['id', 'id'], - where, - limit: limit ? Number(limit) : undefined, - order: [['id', 'ASC']], - }) - - return records.map((record) => ({ - id: record.id, - label: record.id, - })) - } - - static async _createAuditLog(action, record, data, options: IRepositoryOptions) { - if (log) { - let values = {} - - if (data) { - values = { - ...record.get({ plain: true }), - } - } - - await AuditLogRepository.log( - { - entityName: 'activity', - entityId: record.id, - action, - values, - }, - options, - ) - } - } - - static async _populateRelationsForRows(rows, options: IRepositoryOptions) { - if (!rows) { - return rows - } - - return Promise.all(rows.map((record) => this._populateRelations(record, options))) - } - - static async _populateRelations(record, options: IRepositoryOptions) { - if (!record) { - return record - } - const transaction = SequelizeRepository.getTransaction(options) - - const output = record.get({ plain: true }) - - output.display = ActivityDisplayService.getDisplayOptions( - record, - SegmentRepository.getActivityTypes(options), ) - if (output.parent) { - output.parent.display = ActivityDisplayService.getDisplayOptions( - output.parent, - SegmentRepository.getActivityTypes(options), - ) - } - - output.tasks = await record.getTasks({ - transaction, - joinTableAttributes: [], - }) - - return output + return results[0][0].id } } diff --git a/backend/src/database/repositories/auditLogRepository.ts b/backend/src/database/repositories/auditLogRepository.ts deleted file mode 100644 index 6d669d2256..0000000000 --- a/backend/src/database/repositories/auditLogRepository.ts +++ /dev/null @@ -1,148 +0,0 @@ -import Sequelize, { QueryTypes } from 'sequelize' -import SequelizeRepository from './sequelizeRepository' -import SequelizeFilterUtils from '../utils/sequelizeFilterUtils' -import { IRepositoryOptions } from './IRepositoryOptions' - -const { Op } = Sequelize - -export default class AuditLogRepository { - static get CREATE() { - return 'create' - } - - static get UPDATE() { - return 'update' - } - - static get DELETE() { - return 'delete' - } - - /** - * Saves an Audit Log to the database. - * - * @param {Object} log - The log being saved. - * @param {string} log.entityName - The name of the entity. Ex.: customer - * @param {string} log.entityId - The id of the entity. - * @param {string} log.action - The action [create, update or delete]. - * @param {Object} log.values - The JSON log value with data of the entity. - * - * @param {Object} options - * @param {Object} options.transaction - The current database transaction. - * @param {Object} options.currentUser - The current logged user. - * @param {Object} options.currentTenant - The current currentTenant. - */ - static async log({ entityName, entityId, action, values }, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const log = await options.database.auditLog.create( - { - entityName, - tenantId: currentTenant.id, - entityId, - action, - values, - timestamp: new Date(), - createdById: options && options.currentUser ? options.currentUser.id : null, - createdByEmail: options && options.currentUser ? options.currentUser.email : null, - }, - { transaction }, - ) - - return log - } - - static async cleanUpOldAuditLogs( - maxMonthsToKeep: number, - options: IRepositoryOptions, - ): Promise { - const seq = SequelizeRepository.getSequelize(options) - - await seq.query( - ` - delete from "auditLogs" where timestamp < now() - interval '${maxMonthsToKeep} months' - `, - { - type: QueryTypes.DELETE, - }, - ) - } - - static async findAndCountAll( - { filter, limit = 0, offset = 0, orderBy = '' }, - options: IRepositoryOptions, - ) { - const tenant = SequelizeRepository.getCurrentTenant(options) - - const whereAnd: Array = [] - const include = [] - - whereAnd.push({ - tenantId: tenant.id, - }) - - if (filter) { - if (filter.timestampRange) { - const [start, end] = filter.timestampRange - - if (start !== undefined && start !== null && start !== '') { - whereAnd.push({ - timestamp: { - [Op.gte]: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - whereAnd.push({ - timestamp: { - [Op.lte]: end, - }, - }) - } - } - - if (filter.action) { - whereAnd.push({ - action: filter.action, - }) - } - - if (filter.entityId) { - whereAnd.push({ - entityId: filter.entityId, - }) - } - - if (filter.createdByEmail) { - whereAnd.push({ - [Op.and]: SequelizeFilterUtils.ilikeIncludes( - 'auditLog', - 'createdByEmail', - filter.createdByEmail, - ), - }) - } - - if (filter.entityNames && filter.entityNames.length) { - whereAnd.push({ - entityName: { - [Op.in]: filter.entityNames, - }, - }) - } - } - - const where = { [Op.and]: whereAnd } - - return options.database.auditLog.findAndCountAll({ - where, - include, - limit: limit ? Number(limit) : undefined, - offset: offset ? Number(offset) : undefined, - order: orderBy ? [orderBy.split('_')] : [['timestamp', 'DESC']], - }) - } -} diff --git a/backend/src/database/repositories/automationExecutionRepository.ts b/backend/src/database/repositories/automationExecutionRepository.ts deleted file mode 100644 index a2482c0d88..0000000000 --- a/backend/src/database/repositories/automationExecutionRepository.ts +++ /dev/null @@ -1,170 +0,0 @@ -/* eslint-disable class-methods-use-this */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { QueryTypes } from 'sequelize' -import { IRepositoryOptions } from './IRepositoryOptions' -import { DbAutomationExecutionInsertData } from './types/automationTypes' -import { - AutomationExecution, - AutomationExecutionCriteria, - AutomationExecutionState, -} from '../../types/automationTypes' -import { PageData } from '../../types/common' -import { RepositoryBase } from './repositoryBase' - -export default class AutomationExecutionRepository extends RepositoryBase< - AutomationExecution, - string, - DbAutomationExecutionInsertData, - unknown, - AutomationExecutionCriteria -> { - public constructor(options: IRepositoryOptions) { - super(options, false) - } - - override async create(data: DbAutomationExecutionInsertData): Promise { - const transaction = this.transaction - - return this.database.automationExecution.create( - { - automationId: data.automationId, - type: data.type, - tenantId: data.tenantId, - trigger: data.trigger, - state: data.state, - error: data.error, - executedAt: data.executedAt, - eventId: data.eventId, - payload: data.payload, - }, - { transaction }, - ) - } - - override async findAndCountAll( - criteria: AutomationExecutionCriteria, - ): Promise> { - // get current tenant that was used to make a request - const currentTenant = this.currentTenant - - // get plain sequelize object to use with a raw query - const seq = this.seq - - // construct a query with pagination - const query = ` - select id, - "automationId", - state, - error, - "executedAt", - "eventId", - payload, - count(*) over () as "paginatedItemsCount" - from "automationExecutions" - where "tenantId" = :tenantId - and "automationId" = :automationId - order by "executedAt" desc - limit ${criteria.limit} offset ${criteria.offset} - - ` - - const results = await seq.query(query, { - replacements: { - tenantId: currentTenant.id, - automationId: criteria.automationId, - }, - type: QueryTypes.SELECT, - }) - - if (results.length === 0) { - return { - rows: [], - count: 0, - offset: criteria.offset, - limit: criteria.limit, - } - } - - const count = parseInt((results[0] as any).paginatedItemsCount, 10) - const rows: AutomationExecution[] = results.map((r) => { - const row = r as any - return { - id: row.id, - automationId: row.automationId, - executedAt: row.executedAt, - eventId: row.eventId, - payload: row.payload, - error: row.error, - state: row.state, - } - }) - - return { - rows, - count, - offset: criteria.offset, - limit: criteria.limit, - } - } - - public async hasAlreadyBeenTriggered(automationId: string, eventId: string): Promise { - const seq = this.seq - - const query = ` - select id - from "automationExecutions" - where "automationId" = :automationId - and "eventId" = :eventId - and state = '${AutomationExecutionState.SUCCESS}'; - ` - - const results = await seq.query(query, { - replacements: { - automationId, - eventId, - }, - type: QueryTypes.SELECT, - }) - - return results.length > 0 - } - - override async update(id: string, data: unknown): Promise { - throw new Error('Method not implemented.') - } - - override async destroy(id: string): Promise { - throw new Error('Method not implemented.') - } - - async destroyAllAutomation(automationIds: string[]): Promise { - const transaction = this.transaction - - const seq = this.seq - - const currentTenant = this.currentTenant - - const query = ` - delete - from "automationExecutions" - where "automationId" in (:automationIds) - and "tenantId" = :tenantId;` - - await seq.query(query, { - replacements: { - automationIds, - tenantId: currentTenant.id, - }, - type: QueryTypes.DELETE, - transaction, - }) - } - - override async destroyAll(ids: string[]): Promise { - throw new Error('Method not implemented.') - } - - override async findById(id: string): Promise { - throw new Error('Method not implemented.') - } -} diff --git a/backend/src/database/repositories/automationRepository.ts b/backend/src/database/repositories/automationRepository.ts deleted file mode 100644 index b6708cb8ae..0000000000 --- a/backend/src/database/repositories/automationRepository.ts +++ /dev/null @@ -1,312 +0,0 @@ -import Sequelize, { QueryTypes } from 'sequelize' -import { AutomationState, AutomationSyncTrigger, IAutomation } from '@crowd/types' -import AuditLogRepository from './auditLogRepository' -import { IRepositoryOptions } from './IRepositoryOptions' -import Error404 from '../../errors/Error404' -import { AutomationCriteria, AutomationData } from '../../types/automationTypes' -import { DbAutomationInsertData, DbAutomationUpdateData } from './types/automationTypes' -import { FeatureFlag, PageData } from '../../types/common' -import { RepositoryBase } from './repositoryBase' -import { PLAN_LIMITS } from '@/feature-flags/isFeatureEnabled' - -const { Op } = Sequelize - -export default class AutomationRepository extends RepositoryBase< - AutomationData, - string, - DbAutomationInsertData, - DbAutomationUpdateData, - AutomationCriteria -> { - public constructor(options: IRepositoryOptions) { - super(options, true) - } - - override async create(data: DbAutomationInsertData): Promise { - const currentUser = this.currentUser - - const tenant = this.currentTenant - - const transaction = this.transaction - - const existingActiveAutomations = await this.findAndCountAll({ - state: AutomationState.ACTIVE, - }) - - const record = await this.database.automation.create( - { - name: data.name, - type: data.type, - trigger: data.trigger, - settings: data.settings, - state: - existingActiveAutomations.count >= PLAN_LIMITS[tenant.plan][FeatureFlag.AUTOMATIONS] - ? AutomationState.DISABLED - : data.state, - tenantId: tenant.id, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { - transaction, - }, - ) - - await this.createAuditLog('automation', AuditLogRepository.CREATE, record, data) - - return this.findById(record.id) - } - - override async update(id, data: DbAutomationUpdateData): Promise { - const currentUser = this.currentUser - - const currentTenant = this.currentTenant - - const transaction = this.transaction - - const existingActiveAutomations = await this.findAndCountAll({ - state: AutomationState.ACTIVE, - }) - - if ( - data.state === AutomationState.ACTIVE && - existingActiveAutomations.count >= PLAN_LIMITS[currentTenant.plan][FeatureFlag.AUTOMATIONS] - ) { - throw new Error(`Maximum number of active automations reached for the plan!`) - } - - let record = await this.database.automation.findOne({ - where: { - id, - tenantId: currentTenant.id, - }, - transaction, - }) - - if (!record) { - throw new Error404() - } - - record = await record.update( - { - name: data.name, - trigger: data.trigger, - settings: data.settings, - state: data.state, - updatedById: currentUser.id, - }, - { - transaction, - }, - ) - - await this.createAuditLog('automation', AuditLogRepository.UPDATE, record, data) - - return this.findById(record.id) - } - - override async destroyAll(ids: string[]): Promise { - const transaction = this.transaction - - const currentTenant = this.currentTenant - - const records = await this.database.automation.findAll({ - where: { - id: { - [Op.in]: ids, - }, - tenantId: currentTenant.id, - }, - transaction, - }) - - if (ids.some((id) => records.find((r) => r.id === id) === undefined)) { - throw new Error404() - } - - await Promise.all( - records.flatMap((r) => [ - r.destroy({ transaction }), - this.createAuditLog('automation', AuditLogRepository.DELETE, r, r), - ]), - ) - } - - override async findById(id: string): Promise { - const results = await this.findAndCountAll({ - id, - offset: 0, - limit: 1, - }) - - if (results.count === 1) { - return results.rows[0] - } - - if (results.count === 0) { - throw new Error404() - } - - throw new Error('More than one row returned when fetching by automation unique ID!') - } - - override async findAndCountAll(criteria: AutomationCriteria): Promise> { - // get current tenant that was used to make a request - const currentTenant = this.currentTenant - - // we need transaction if there is one set because some records were perhaps created/updated in the same transaction - const transaction = this.transaction - - // get plain sequelize object to use with a raw query - const seq = this.seq - - // build a where condition based on tenant and other criteria passed as parameter - const conditions = ['a."tenantId" = :tenantId'] - const parameters: any = { - tenantId: currentTenant.id, - } - - if (criteria.id) { - conditions.push('a.id = :id') - parameters.id = criteria.id - } - - if (criteria.state) { - conditions.push('a.state = :state') - parameters.state = criteria.state - } - - if (criteria.type) { - conditions.push('a.type = :type') - parameters.type = criteria.type - } - - if (criteria.trigger) { - conditions.push('a.trigger = :trigger') - parameters.trigger = criteria.trigger - } - - const conditionsString = conditions.join(' and ') - - const query = ` - -- common table expression (CTE) to prepare the last execution information for each automationId - with latest_executions as (select distinct on ("automationId") "automationId", "executedAt", state, error - from "automationExecutions" - order by "automationId", "executedAt" desc) - select a.id, - a.name, - a.type, - a."tenantId", - a.trigger, - a.settings, - a.state, - a."createdAt", - a."updatedAt", - le."executedAt" as "lastExecutionAt", - le.state as "lastExecutionState", - le.error as "lastExecutionError", - count(*) over () as "paginatedItemsCount" - from automations a - left join latest_executions le on a.id = le."automationId" - where ${conditionsString} - order by "updatedAt" desc - ${this.getPaginationString(criteria)} - ` - // fetch all automations for a tenant - // and include the latest execution data if available - const results = await seq.query(query, { - replacements: parameters, - type: QueryTypes.SELECT, - transaction, - }) - - if (results.length === 0) { - return { - rows: [], - count: 0, - offset: criteria.offset, - limit: criteria.limit, - } - } - - const count = parseInt((results[0] as any).paginatedItemsCount, 10) - const rows: AutomationData[] = results.map((r) => { - const row = r as any - return { - id: row.id, - name: row.name, - type: row.type, - tenantId: row.tenantId, - trigger: row.trigger, - settings: row.settings, - state: row.state, - createdAt: row.createdAt, - updatedAt: row.updatedAt, - lastExecutionAt: row.lastExecutionAt, - lastExecutionState: row.lastExecutionState, - lastExecutionError: row.lastExecutionError, - } - }) - - return { - rows, - count, - offset: criteria.offset, - limit: criteria.limit, - } - } - - static async countAllActive(database: any, tenantId: string): Promise { - const automationCount = await database.automation.count({ - where: { - tenantId, - state: AutomationState.ACTIVE, - }, - useMaster: true, - }) - - return automationCount - } - - public async findSyncAutomations( - tenantId: string, - platform: string, - ): Promise { - const seq = this.seq - - const transaction = this.transaction - - const pageSize = 10 - const syncAutomations: IAutomation[] = [] - - let results - let offset - - do { - offset = results ? pageSize + offset : 0 - - results = await seq.query( - `select * from automations - where type = :platform and "tenantId" = :tenantId and trigger in (:syncAutomationTriggers) - limit :limit offset :offset`, - { - replacements: { - tenantId, - platform, - syncAutomationTriggers: [ - AutomationSyncTrigger.MEMBER_ATTRIBUTES_MATCH, - AutomationSyncTrigger.ORGANIZATION_ATTRIBUTES_MATCH, - ], - limit: pageSize, - offset, - }, - type: QueryTypes.SELECT, - transaction, - }, - ) - syncAutomations.push(...results) - } while (results.length > 0) - - return syncAutomations - } -} diff --git a/backend/src/database/repositories/conversationRepository.ts b/backend/src/database/repositories/conversationRepository.ts deleted file mode 100644 index d731eb8514..0000000000 --- a/backend/src/database/repositories/conversationRepository.ts +++ /dev/null @@ -1,661 +0,0 @@ -import lodash from 'lodash' -import Sequelize from 'sequelize' -import { PlatformType } from '@crowd/types' -import { QueryOutput } from './filters/queryTypes' -import SequelizeRepository from './sequelizeRepository' -import AuditLogRepository from './auditLogRepository' -import SequelizeFilterUtils from '../utils/sequelizeFilterUtils' -import Error404 from '../../errors/Error404' -import { IRepositoryOptions } from './IRepositoryOptions' -import snakeCaseNames from '../../utils/snakeCaseNames' -import QueryParser from './filters/queryParser' -import ActivityDisplayService from '../../services/activityDisplayService' -import SegmentRepository from './segmentRepository' - -const Op = Sequelize.Op - -class ConversationRepository { - static async create(data, options: IRepositoryOptions) { - const currentUser = SequelizeRepository.getCurrentUser(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - const transaction = SequelizeRepository.getTransaction(options) - - const segment = SequelizeRepository.getStrictlySingleActiveSegment(options) - - const record = await options.database.conversation.create( - { - ...lodash.pick(data, ['title', 'slug', 'published']), - - tenantId: tenant.id, - segmentId: segment.id, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { - transaction, - }, - ) - - await record.setActivities(data.activities || [], { - transaction, - }) - - await this._createAuditLog(AuditLogRepository.CREATE, record, data, options) - - return this.findById(record.id, options) - } - - static async update(id, data, options: IRepositoryOptions) { - const currentUser = SequelizeRepository.getCurrentUser(options) - - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - let record = await options.database.conversation.findOne({ - where: { - id, - tenantId: currentTenant.id, - segmentId: SequelizeRepository.getSegmentIds(options), - }, - transaction, - }) - - if (!record) { - throw new Error404() - } - - record = await record.update( - { - ...lodash.pick(data, ['title', 'slug', 'published']), - - updatedById: currentUser.id, - }, - { - transaction, - }, - ) - - if (data.activities) { - await record.setActivities(data.activities, { - transaction, - }) - } - - await this._createAuditLog(AuditLogRepository.UPDATE, record, data, options) - - return this.findById(record.id, options) - } - - static async destroy(id, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const record = await options.database.conversation.findOne({ - where: { - id, - tenantId: currentTenant.id, - segmentId: SequelizeRepository.getSegmentIds(options), - }, - transaction, - }) - - if (!record) { - throw new Error404() - } - - await record.destroy({ - transaction, - }) - - await this._createAuditLog(AuditLogRepository.DELETE, record, record, options) - } - - static async findById(id, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const include = [] - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const record = await options.database.conversation.findOne({ - where: { - id, - tenantId: currentTenant.id, - segmentId: SequelizeRepository.getSegmentIds(options), - }, - include, - transaction, - }) - - if (!record) { - throw new Error404() - } - - return this._populateRelations(record, options) - } - - static async filterIdInTenant(id, options: IRepositoryOptions) { - return lodash.get(await this.filterIdsInTenant([id], options), '[0]', null) - } - - static async filterIdsInTenant(ids, options: IRepositoryOptions) { - if (!ids || !ids.length) { - return [] - } - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const where = { - id: { - [Op.in]: ids, - }, - tenantId: currentTenant.id, - } - - const records = await options.database.conversation.findAll({ - attributes: ['id'], - where, - }) - - return records.map((record) => record.id) - } - - static async destroyBulk(ids, options: IRepositoryOptions, force = false) { - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - await options.database.conversation.destroy({ - where: { - id: ids, - tenantId: currentTenant.id, - segmentId: SequelizeRepository.getSegmentIds(options), - }, - force, - transaction, - }) - } - - static async count(filter, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - return options.database.conversation.count({ - where: { - ...filter, - tenantId: tenant.id, - segmentId: SequelizeRepository.getSegmentIds(options), - }, - transaction, - }) - } - - static async findAndCountAll( - { - filter = {} as any, - advancedFilter = null as any, - limit = 0, - offset = 0, - orderBy = '', - lazyLoad = [], - }, - options: IRepositoryOptions, - ) { - let customOrderBy: Array = [] - const include = [ - { - model: options.database.activity, - as: 'activities', - attributes: [], - }, - ] - - // If the advanced filter is empty, we construct it from the query parameter filter - if (!advancedFilter) { - advancedFilter = { and: [] } - // Filter by ID - if (filter.id) { - advancedFilter.and.push({ id: filter.id }) - } - - if ( - filter.published === true || - filter.published === 'true' || - filter.published === false || - filter.published === 'false' - ) { - advancedFilter.and.push({ - published: filter.published === true || filter.published === 'true', - }) - } - - // Filter by title - if (filter.title) { - advancedFilter.and.push({ - title: { - textContains: filter.title, - }, - }) - } - - // Filter by slug - if (filter.slug) { - advancedFilter.and.push({ - slug: { - like: filter.slug, - }, - }) - } - - // Filter by createdAtRange - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange - - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - createdAt: { - gte: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - createdAt: { - lte: end, - }, - }) - } - } - - if (filter.platform) { - advancedFilter.and.push({ - platform: filter.platform, - }) - } - - if (filter.channel) { - advancedFilter.and.push({ - channel: { like: filter.channel }, - }) - } - - if (filter.activityCountRange) { - const [start, end] = filter.activityCountRange - - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - activityCount: { - gte: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - activityCount: { - lte: end, - }, - }) - } - } - - if (filter.lastActiveRange) { - const [start, end] = filter.lastActiveRange - - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - lastActive: { - gte: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - lastActive: { - lte: end, - }, - }) - } - } - } - - // generate customOrderBy array for ordering Sequelize literals - customOrderBy = customOrderBy.concat( - SequelizeFilterUtils.customOrderByIfExists('activityCount', orderBy), - ) - customOrderBy = customOrderBy.concat( - SequelizeFilterUtils.customOrderByIfExists('lastActive', orderBy), - ) - - customOrderBy = customOrderBy.concat( - SequelizeFilterUtils.customOrderByIfExists('channel', orderBy), - ) - - const activityCount = options.database.Sequelize.fn( - 'COUNT', - options.database.Sequelize.col('activities.id'), - ) - - const lastActive = options.database.Sequelize.fn( - 'MAX', - options.database.Sequelize.col('activities.timestamp'), - ) - - const platform = Sequelize.col('activities.platform') - - const parser = new QueryParser( - { - aggregators: { - ...SequelizeFilterUtils.getNativeTableFieldAggregations( - [ - 'id', - 'title', - 'slug', - 'published', - 'createdAt', - 'updatedAt', - 'tenantId', - 'segmentId', - 'createdById', - 'updatedById', - ], - 'conversation', - ), - activityCount, - channel: Sequelize.literal(`"activities"."channel"`), - lastActive, - platform, - }, - }, - options, - ) - - const parsed: QueryOutput = parser.parse({ - filter: advancedFilter, - orderBy: orderBy || ['createdAt_DESC'], - limit, - offset, - }) - - let order = parsed.order - - if (customOrderBy.length > 0) { - order = [customOrderBy] - } else if (orderBy) { - order = [orderBy.split('_')] - } - - // eslint-disable-next-line prefer-const - let { rows, count } = await options.database.conversation.findAndCountAll({ - attributes: [ - ...SequelizeFilterUtils.getLiteralProjections( - [ - 'id', - 'title', - 'slug', - 'published', - 'createdAt', - 'tenantId', - 'segmentId', - 'updatedAt', - 'createdById', - 'updatedById', - ], - 'conversation', - ), - [platform, 'platform'], - [activityCount, 'activityCount'], - [lastActive, 'lastActive'], - [Sequelize.literal(`MAX("activities"."channel")`), 'channel'], - ], - ...(parsed.where ? { where: parsed.where } : {}), - ...(parsed.having ? { having: parsed.having } : {}), - include, - order, - transaction: SequelizeRepository.getTransaction(options), - group: ['conversation.id', 'activities.platform', 'activities.channel'], - limit: parsed.limit, - offset: parsed.offset, - subQuery: false, - distinct: true, - }) - rows = await this._populateRelationsForRows(rows, options, lazyLoad) - return { rows, count: count.length } - } - - static async _createAuditLog(action, record, data, options: IRepositoryOptions) { - let values = {} - - if (data) { - values = { - ...record.get({ plain: true }), - } - } - - await AuditLogRepository.log( - { - entityName: 'conversation', - entityId: record.id, - action, - values, - }, - options, - ) - } - - /** - * Counts distinct members in a conversation - * @param activities Activity list in a conversation - */ - static getTotalMemberCount(activities) { - return ( - activities.reduce((acc, i) => { - if (!acc.ids) { - acc.ids = [] - acc.count = 0 - } - - if (!acc.ids[i.memberId]) { - acc.ids[i.memberId] = true - acc.count += 1 - } - return acc - }, {}).count ?? 0 - ) - } - - static async _populateRelationsForRows(rows, options, lazyLoad = []) { - if (!rows) { - return rows - } - - return Promise.all( - rows.map(async (record) => { - const rec = record.get({ plain: true }) - for (const relationship of lazyLoad) { - if (relationship === 'activities') { - const allActivities = await record.getActivities({ - order: [ - ['timestamp', 'ASC'], - ['createdAt', 'ASC'], - ], - include: ['parent', 'organization'], - }) - - rec.memberCount = ConversationRepository.getTotalMemberCount(allActivities) - - if (allActivities.length > 0) { - let neededActivities = [] - const parentActivity = - allActivities.find((a) => a.parent === null) || allActivities[0] - - if (parentActivity) { - neededActivities = [parentActivity] - } - - if (allActivities.length > 2) { - neededActivities = [ - ...neededActivities, - allActivities[allActivities.length - 2], - allActivities[allActivities.length - 1], - ] - } else { - neededActivities = [...neededActivities, allActivities[allActivities.length - 1]] - } - - const promises = neededActivities.map(async (act) => { - const member = (await act.getMember()).get({ plain: true }) - - let objectMember = null - if (act.objectMemberId) { - objectMember = (await act.getObjectMember()).get({ plain: true }) - } - - act = act.get({ plain: true }) - act.member = member - act.objectMember = objectMember - act.display = ActivityDisplayService.getDisplayOptions( - act, - SegmentRepository.getActivityTypes(options), - ) - - return act - }) - const returnedNeededActivities = await Promise.all(promises) - rec.conversationStarter = returnedNeededActivities[0] - rec.lastReplies = returnedNeededActivities.slice(1) - } else { - rec.conversationStarter = null - rec.lastReplies = [] - } - - if (rec.conversationStarter) { - rec.conversationStarter.display = ActivityDisplayService.getDisplayOptions( - rec.conversationStarter, - SegmentRepository.getActivityTypes(options), - ) - } - } else { - rec[relationship] = (await record[`get${snakeCaseNames(relationship)}`]()).map((a) => - a.get({ plain: true }), - ) - } - } - if (rec.activityCount) { - rec.activityCount = parseInt(rec.activityCount, 10) - if (rec.platform && rec.platform === PlatformType.GITHUB) { - rec.channel = this.extractGitHubRepoPath(rec.channel) - } - } - return rec - }), - ) - } - - static extractGitHubRepoPath(url) { - if (!url) return null - const match = url.match(/^https?:\/\/(www\.)?github.com\/(?[\w.-]+)\/(?[\w.-]+)/) - if (!match || !(match.groups?.owner && match.groups?.name)) return null - return `${match.groups.owner}/${match.groups.name}` - } - - static async _populateRelations(record, options: IRepositoryOptions) { - if (!record) { - return record - } - - const output = record.get({ plain: true }) - - const transaction = SequelizeRepository.getTransaction(options) - - // Fetch the first activity with parent = null - const firstActivity = await record.getActivities({ - where: { - parentId: null, - }, - include: ['member', 'parent', 'objectMember', 'organization'], - transaction, - order: [ - ['timestamp', 'ASC'], - ['createdAt', 'ASC'], - ], - }) - - // Fetch remaining activities with parent != null - const remainingActivities = await record.getActivities({ - where: { - parentId: { - [Sequelize.Op.not]: null, - }, - }, - include: ['member', 'parent', 'objectMember', 'organization'], - order: [ - ['timestamp', 'ASC'], - ['createdAt', 'ASC'], - ], - transaction, - }) - - output.activities = [...firstActivity, ...remainingActivities] - - let memberPromises = output.activities.map(async (act) => { - const member = (await act.getMember()).get({ plain: true }) - act = act.get({ plain: true }) - act.member = member - act.display = ActivityDisplayService.getDisplayOptions( - act, - SegmentRepository.getActivityTypes(options), - ) - return act - }) - - const chunkedPromises = [] - - const CHUNK_PROMISE_SIZE = 50 - - if (memberPromises.length > CHUNK_PROMISE_SIZE) { - while (memberPromises.length > CHUNK_PROMISE_SIZE) { - chunkedPromises.push(memberPromises.slice(0, CHUNK_PROMISE_SIZE)) - memberPromises = memberPromises.slice(CHUNK_PROMISE_SIZE) - } - if (memberPromises.length > 0) { - chunkedPromises.push(memberPromises) - } - } else { - chunkedPromises.push(memberPromises) - } - - output.activities = [] - for (const memberPromiseChunk of chunkedPromises) { - output.activities.push(...(await Promise.all(memberPromiseChunk))) - } - - output.memberCount = ConversationRepository.getTotalMemberCount(output.activities) - output.conversationStarter = output.activities[0] ?? null - output.activityCount = output.activities.length - output.platform = null - output.channel = null - output.lastActive = null - - if (output.activityCount > 0) { - output.platform = output.activities[0].platform ?? null - output.lastActive = output.activities[output.activities.length - 1].timestamp - output.channel = output.activities[0].channel ? output.activities[0].channel : null - output.conversationStarter.display = ActivityDisplayService.getDisplayOptions( - output.conversationStarter, - SegmentRepository.getActivityTypes(options), - ) - } - - return output - } -} - -export default ConversationRepository diff --git a/backend/src/database/repositories/conversationSettingsRepository.ts b/backend/src/database/repositories/conversationSettingsRepository.ts deleted file mode 100644 index 0c3116c558..0000000000 --- a/backend/src/database/repositories/conversationSettingsRepository.ts +++ /dev/null @@ -1,67 +0,0 @@ -import SequelizeRepository from './sequelizeRepository' -import AuditLogRepository from './auditLogRepository' -import { IRepositoryOptions } from './IRepositoryOptions' - -export default class ConversationSettingsRepository { - static async findOrCreateDefault(defaults, options: IRepositoryOptions) { - const currentUser = SequelizeRepository.getCurrentUser(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - const [settings] = await options.database.conversationSettings.findOrCreate({ - where: { id: tenant.id, tenantId: tenant.id }, - defaults: { - ...defaults, - id: tenant.id, - tenantId: tenant.id, - createdById: currentUser ? currentUser.id : null, - }, - transaction: SequelizeRepository.getTransaction(options), - }) - - return this._populateRelations(settings) - } - - static async save(data, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const currentUser = SequelizeRepository.getCurrentUser(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - const [conversationSettings] = await options.database.conversationSettings.findOrCreate({ - where: { id: tenant.id, tenantId: tenant.id }, - defaults: { - ...data, - id: tenant.id, - tenantId: tenant.id, - createdById: currentUser ? currentUser.id : null, - }, - transaction, - }) - - await conversationSettings.update(data, { - transaction, - }) - - await AuditLogRepository.log( - { - entityName: 'conversationSettings', - entityId: conversationSettings.id, - action: AuditLogRepository.UPDATE, - values: data, - }, - options, - ) - - return this._populateRelations(conversationSettings) - } - - static async _populateRelations(record) { - if (!record) { - return record - } - - return record.get({ plain: true }) - } -} diff --git a/backend/src/database/repositories/customViewRepository.ts b/backend/src/database/repositories/customViewRepository.ts index ba25611d1b..0e0155ab44 100644 --- a/backend/src/database/repositories/customViewRepository.ts +++ b/backend/src/database/repositories/customViewRepository.ts @@ -1,9 +1,10 @@ import lodash from 'lodash' import Sequelize from 'sequelize' -import Error404 from '../../errors/Error404' -import SequelizeRepository from './sequelizeRepository' + +import { Error404 } from '@crowd/common' + import { IRepositoryOptions } from './IRepositoryOptions' -import AuditLogRepository from './auditLogRepository' +import SequelizeRepository from './sequelizeRepository' const Op = Sequelize.Op @@ -38,9 +39,6 @@ class CustomViewRepository { }, ) - // adds event to audit log - await this._createAuditLog(AuditLogRepository.CREATE, record, data, options) - return this.findById(record.id, options) } @@ -63,6 +61,11 @@ class CustomViewRepository { throw new Error404() } + // don't allow other users private custom views to be updated + if (record.visibility === 'user' && record.createdById !== currentUser.id) { + throw new Error('Update not allowed as custom view was not created by user!') + } + // we don't allow placement to be updated record = await record.update( { @@ -88,8 +91,6 @@ class CustomViewRepository { ) } - await this._createAuditLog(AuditLogRepository.UPDATE, record, data, options) - return this.findById(record.id, options) } @@ -112,6 +113,11 @@ class CustomViewRepository { throw new Error404() } + // don't allow other users private custom views to be deleted + if (record.visibility === 'user' && record.createdById !== currentUser.id) { + throw new Error('Deletion not allowed as custom view was not created by user!') + } + // update who deleted the custom view await record.update( { @@ -133,8 +139,6 @@ class CustomViewRepository { }, transaction, }) - - await this._createAuditLog(AuditLogRepository.DELETE, record, record, options) } static async findById(id, options: IRepositoryOptions) { @@ -213,24 +217,6 @@ class CustomViewRepository { return customViewRecords } - - static async _createAuditLog(action, record, data, options: IRepositoryOptions) { - let values = {} - - if (data) { - values = record.get({ plain: true }) - } - - await AuditLogRepository.log( - { - entityName: 'customView', - entityId: record.id, - action, - values, - }, - options, - ) - } } export default CustomViewRepository diff --git a/backend/src/database/repositories/eagleEyeActionRepository.ts b/backend/src/database/repositories/eagleEyeActionRepository.ts index cc7932b257..a48484cdd9 100644 --- a/backend/src/database/repositories/eagleEyeActionRepository.ts +++ b/backend/src/database/repositories/eagleEyeActionRepository.ts @@ -1,6 +1,8 @@ import lodash from 'lodash' -import Error404 from '../../errors/Error404' -import { EagleEyeAction, EagleEyeActionType } from '../../types/eagleEyeTypes' + +import { Error404 } from '@crowd/common' +import { EagleEyeAction, EagleEyeActionType } from '@crowd/types' + import { IRepositoryOptions } from './IRepositoryOptions' import SequelizeRepository from './sequelizeRepository' diff --git a/backend/src/database/repositories/eagleEyeContentRepository.ts b/backend/src/database/repositories/eagleEyeContentRepository.ts index a289a1ee2a..b2889488b7 100644 --- a/backend/src/database/repositories/eagleEyeContentRepository.ts +++ b/backend/src/database/repositories/eagleEyeContentRepository.ts @@ -1,12 +1,14 @@ import lodash from 'lodash' import { Op } from 'sequelize' -import SequelizeRepository from './sequelizeRepository' -import Error404 from '../../errors/Error404' + +import { Error404 } from '@crowd/common' +import { EagleEyeContent } from '@crowd/types' + import { IRepositoryOptions } from './IRepositoryOptions' -import { EagleEyeContent } from '../../types/eagleEyeTypes' +import EagleEyeActionRepository from './eagleEyeActionRepository' import QueryParser from './filters/queryParser' import { QueryOutput } from './filters/queryTypes' -import EagleEyeActionRepository from './eagleEyeActionRepository' +import SequelizeRepository from './sequelizeRepository' export default class EagleEyeContentRepository { static async create( diff --git a/backend/src/database/repositories/filters/__tests__/queryParser.test.ts b/backend/src/database/repositories/filters/__tests__/queryParser.test.ts deleted file mode 100644 index d73a7ea3d5..0000000000 --- a/backend/src/database/repositories/filters/__tests__/queryParser.test.ts +++ /dev/null @@ -1,555 +0,0 @@ -import Sequelize from 'sequelize' -import { generateUUIDv4 as uuid } from '@crowd/common' -import SequelizeTestUtils from '../../../utils/sequelizeTestUtils' -import QueryParser from '../queryParser' - -const { Op } = Sequelize -const db = null - -describe('QueryParser tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('Simple tests', () => { - it('With empty values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser({}, mockIRepositoryOptions) - const parsed = parser.parse({ - filter: {}, - orderBy: [], - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - }, - limit: 10, - offset: 0, - order: [], - } - expect(parsed).toStrictEqual(expected) - }) - it('With some filtering values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser({}, mockIRepositoryOptions) - const parsed = parser.parse({ - filter: { - body: { - textContains: 'test', - }, - or: [{ channel: 'dev' }, { channel: 'bugs' }], - }, - orderBy: [], - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - body: { - [Op.iLike]: '%test%', - }, - [Op.or]: [{ channel: 'dev' }, { channel: 'bugs' }], - }, - limit: 10, - offset: 0, - order: [], - } - expect(parsed).toStrictEqual(expected) - }) - it('With some sorting values: list', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser({}, mockIRepositoryOptions) - const parsed = parser.parse({ - filter: {}, - orderBy: ['timestamp_DESC', 'channel_ASC'], - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - }, - limit: 10, - offset: 0, - order: [ - ['timestamp', 'DESC'], - ['channel', 'ASC'], - ], - } - expect(parsed).toStrictEqual(expected) - }) - - it('With some sorting values: string', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser({}, mockIRepositoryOptions) - const parsed = parser.parse({ - filter: {}, - orderBy: 'timestamp_DESC,channel_ASC', - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - }, - limit: 10, - offset: 0, - order: [ - ['timestamp', 'DESC'], - ['channel', 'ASC'], - ], - } - expect(parsed).toStrictEqual(expected) - }) - - it('With offset', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser({}, mockIRepositoryOptions) - const parsed = parser.parse({ - filter: {}, - orderBy: [], - offset: 10, - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - }, - limit: 10, - offset: 10, - order: [], - } - expect(parsed).toStrictEqual(expected) - }) - - it('With limit', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser({}, mockIRepositoryOptions) - const parsed = parser.parse({ - filter: {}, - orderBy: [], - limit: 100, - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - }, - limit: 100, - offset: 0, - order: [], - } - expect(parsed).toStrictEqual(expected) - }) - - it('With too large limit', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser({}, mockIRepositoryOptions) - const parsed = parser.parse({ - filter: {}, - orderBy: [], - limit: 210, - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - }, - limit: 200, - offset: 0, - order: [], - } - expect(parsed).toStrictEqual(expected) - }) - - it('With filtering, sorting, limit and offset', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser({}, mockIRepositoryOptions) - const parsed = parser.parse({ - filter: { - body: { - textContains: 'test', - }, - or: [{ channel: 'dev' }, { channel: 'bugs' }], - }, - orderBy: ['timestamp_DESC', 'channel_ASC'], - limit: 100, - offset: 10, - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - body: { - [Op.iLike]: '%test%', - }, - [Op.or]: [{ channel: 'dev' }, { channel: 'bugs' }], - }, - limit: 100, - offset: 10, - order: [ - ['timestamp', 'DESC'], - ['channel', 'ASC'], - ], - } - expect(parsed).toStrictEqual(expected) - }) - }) - - describe('Complex filtering tests', () => { - it('With nested fields', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser( - { - nestedFields: { - sentiment: 'sentiment.sentiment', - mood: 'sentiment.mood', - }, - }, - mockIRepositoryOptions, - ) - const parsed = parser.parse({ - filter: { - sentiment: { - gte: 0.5, - }, - or: [{ mood: 'happy' }, { mood: 'sad' }], - }, - orderBy: [], - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - 'sentiment.sentiment': { - [Op.gte]: 0.5, - }, - [Op.or]: [{ 'sentiment.mood': 'happy' }, { 'sentiment.mood': 'sad' }], - }, - limit: 10, - offset: 0, - order: [], - } - expect(parsed).toStrictEqual(expected) - }) - it('With complex operators', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser({}, mockIRepositoryOptions) - const parsed = parser.parse({ - filter: { - body: { - textContains: 'test', - }, - }, - orderBy: [], - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - body: { - [Op.iLike]: '%test%', - }, - }, - limit: 10, - offset: 0, - order: [], - } - expect(parsed).toStrictEqual(expected) - }) - it('With aggregators', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser( - { - aggregators: { - count: mockIRepositoryOptions.database.Sequelize.fn( - 'COUNT', - mockIRepositoryOptions.database.Sequelize.col('activities.id'), - ), - platform: Sequelize.literal(`"activities"."platform"`), - }, - }, - mockIRepositoryOptions, - ) - const parsed = parser.parse({ - filter: { - or: [ - { - platform: { - in: ['discord', 'github'], - }, - }, - { - count: { - gte: 10, - }, - }, - ], - }, - orderBy: [], - }) - const expected = { - having: { - [Op.or]: [ - { - [Op.and]: [ - Sequelize.where( - Sequelize.literal(`"activities"."platform"`), - Op.in, - Sequelize.literal(`('discord','github')`), - ), - ], - }, - { - [Op.and]: [ - Sequelize.where( - mockIRepositoryOptions.database.Sequelize.fn( - 'COUNT', - mockIRepositoryOptions.database.Sequelize.col('activities.id'), - ), - Op.gte, - 10, - ), - ], - }, - ], - }, - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - }, - limit: 10, - offset: 0, - order: [], - } - expect(parsed).toStrictEqual(expected) - }) - it('With many to many relations', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser( - { - manyToMany: { - members: { - table: 'tasks', - model: 'task', - relationTable: { - name: 'memberTasks', - from: 'taskId', - to: 'memberId', - }, - }, - activities: { - table: 'tasks', - model: 'task', - relationTable: { - name: 'activityTasks', - from: 'taskId', - to: 'activityId', - }, - }, - }, - }, - mockIRepositoryOptions, - ) - - const aid1 = uuid() - const aid2 = uuid() - const mid1 = uuid() - - const parsed = parser.parse({ - filter: { - or: [ - { - members: [mid1], - activities: [aid1, aid2], - }, - { - status: { - eq: 'some-task-name', - }, - }, - ], - }, - orderBy: [], - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - [Op.or]: [ - { - [Op.and]: [ - Sequelize.where( - Sequelize.literal(`"task"."id"`), - Op.in, - Sequelize.literal( - `(SELECT "tasks"."id" FROM "tasks" INNER JOIN "memberTasks" ON "memberTasks"."taskId" = "tasks"."id" WHERE "memberTasks"."memberId" = '${mid1}')`, - ), - ), - Sequelize.where( - Sequelize.literal(`"task"."id"`), - Op.in, - Sequelize.literal( - `(SELECT "tasks"."id" FROM "tasks" INNER JOIN "activityTasks" ON "activityTasks"."taskId" = "tasks"."id" WHERE "activityTasks"."activityId" = '${aid1}' OR "activityTasks"."activityId" = '${aid2}')`, - ), - ), - ], - }, - { - status: { - [Op.eq]: 'some-task-name', - }, - }, - ], - }, - limit: 10, - offset: 0, - order: [], - } - expect(parsed).toStrictEqual(expected) - }) - it('With nested fields, complex operators, aggregators and many to many', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser( - { - nestedFields: { - sentiment: 'sentiment.sentiment', - mood: 'sentiment.mood', - }, - aggregators: { - count: mockIRepositoryOptions.database.Sequelize.fn( - 'COUNT', - mockIRepositoryOptions.database.Sequelize.col('activities.id'), - ), - platform: Sequelize.literal(`"activities"."platform"`), - }, - manyToMany: { - members: { - table: 'tasks', - model: 'task', - relationTable: { - name: 'memberTasks', - from: 'taskId', - to: 'memberId', - }, - }, - activities: { - table: 'tasks', - model: 'task', - relationTable: { - name: 'activityTasks', - from: 'taskId', - to: 'activityId', - }, - }, - }, - }, - mockIRepositoryOptions, - ) - - const aid1 = uuid() - const aid2 = uuid() - - const parsed = parser.parse({ - filter: { - sentiment: { - gte: 0.5, - }, - or: [ - { - description: { - textContains: 'test', - }, - }, - { - and: [ - { - platform: { - in: ['discord', 'github'], - }, - }, - { - count: { - lt: 10, - }, - }, - ], - }, - { - activities: [aid1, aid2], - }, - ], - }, - orderBy: ['dueDate_DESC', 'createdAt_ASC'], - offset: 102, - limit: 101, - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - }, - having: { - 'sentiment.sentiment': { - [Op.gte]: 0.5, - }, - [Op.or]: [ - { - description: { - [Op.iLike]: '%test%', - }, - }, - { - [Op.and]: [ - { - [Op.and]: [ - Sequelize.where( - Sequelize.literal(`"activities"."platform"`), - Op.in, - Sequelize.literal(`('discord','github')`), - ), - ], - }, - { - [Op.and]: [ - Sequelize.where( - mockIRepositoryOptions.database.Sequelize.fn( - 'COUNT', - mockIRepositoryOptions.database.Sequelize.col('activities.id'), - ), - Op.lt, - 10, - ), - ], - }, - ], - }, - { - [Op.and]: [ - Sequelize.where( - Sequelize.literal(`"task"."id"`), - Op.in, - Sequelize.literal( - `(SELECT "tasks"."id" FROM "tasks" INNER JOIN "activityTasks" ON "activityTasks"."taskId" = "tasks"."id" WHERE "activityTasks"."activityId" = '${aid1}' OR "activityTasks"."activityId" = '${aid2}')`, - ), - ), - ], - }, - ], - }, - limit: 101, - offset: 102, - order: [ - ['dueDate', 'DESC'], - ['createdAt', 'ASC'], - ], - } - expect(parsed).toStrictEqual(expected) - }) - }) -}) diff --git a/backend/src/database/repositories/filters/__tests__/rawQueryParser.test.ts b/backend/src/database/repositories/filters/__tests__/rawQueryParser.test.ts deleted file mode 100644 index c8612f504a..0000000000 --- a/backend/src/database/repositories/filters/__tests__/rawQueryParser.test.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { MemberAttributeType } from '@crowd/types' -import MemberRepository from '../../memberRepository' -import RawQueryParser from '../rawQueryParser' - -describe('RawQueryParser', () => { - it('Should parse simple filter with an empty second operand', () => { - const basicFilter = { - and: [ - { - isOrganization: { - not: true, - }, - }, - {}, - ], - } - - const params: any = {} - - const result = RawQueryParser.parseFilters( - basicFilter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - [], - params, - ) - - expect(result).toEqual( - "((coalesce((m.attributes -> 'isOrganization' -> 'default')::boolean, false) <> :isOrganization_1) and (1=1))", - ) - expect(params.isOrganization_1).toEqual(true) - }) - - it('Should parse simple default filter', () => { - const basicFilter = { - and: [ - { - isOrganization: { - not: true, - }, - }, - { - isBot: { - eq: false, - }, - }, - ], - } - - const params: any = {} - - const result = RawQueryParser.parseFilters( - basicFilter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - [], - params, - ) - - expect(result).toEqual( - "((coalesce((m.attributes -> 'isOrganization' -> 'default')::boolean, false) <> :isOrganization_1) and (coalesce((m.attributes -> 'isBot' -> 'default')::boolean, false) = :isBot_1))", - ) - expect(params.isOrganization_1).toEqual(true) - expect(params.isBot_1).toEqual(false) - }) - - it('Should parse filter with a between condition', () => { - const filter = { - and: [ - { - activityCount: { - between: [10, 100], - }, - }, - {}, - ], - } - - const params: any = {} - - const result = RawQueryParser.parseFilters( - filter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - [], - params, - ) - - expect(result).toEqual( - `((aggs."activityCount" between :activityCount_1 and :activityCount_2) and (1=1))`, - ) - expect(params.activityCount_1).toEqual(10) - expect(params.activityCount_2).toEqual(100) - }) - - it('Should parse filter with a contains condition', () => { - const filter = { - and: [ - { - identities: { - contains: ['github', 'slack'], - }, - }, - {}, - ], - } - - const params: any = {} - - const result = RawQueryParser.parseFilters( - filter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - [], - params, - ) - - expect(result).toEqual(`((aggs.identities @> array[:identities_1, :identities_2]) and (1=1))`) - expect(params.identities_1).toEqual('github') - expect(params.identities_2).toEqual('slack') - }) - - it('Should parse filter with an overlap condition', () => { - const filter = { - and: [ - { - identities: { - overlap: ['github', 'slack'], - }, - }, - {}, - ], - } - - const params: any = {} - - const result = RawQueryParser.parseFilters( - filter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - [], - params, - ) - - expect(result).toEqual(`((aggs.identities && array[:identities_1, :identities_2]) and (1=1))`) - expect(params.identities_1).toEqual('github') - expect(params.identities_2).toEqual('slack') - }) - - it('Should parse filter with an in condition', () => { - const filter = { - and: [ - { - emails: { - in: ['crash@crowd.dev', 'burn@crowd.dev'], - }, - }, - {}, - ], - } - - const params: any = {} - - const result = RawQueryParser.parseFilters( - filter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - [], - params, - ) - - expect(result).toEqual(`((m.emails in (:emails_1, :emails_2)) and (1=1))`) - expect(params.emails_1).toEqual('crash@crowd.dev') - expect(params.emails_2).toEqual('burn@crowd.dev') - }) - - it('Should parse filter with attribute column multiselect filter', () => { - const filter = { - and: [ - { - 'attributes.skills.default': { - contains: ['javascript', 'typescript'], - }, - }, - {}, - ], - } - - const params: any = {} - const result = RawQueryParser.parseFilters( - filter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - [ - { - property: 'attributes', - column: 'm.attributes', - attributeInfos: [ - { - name: 'skills', - type: MemberAttributeType.MULTI_SELECT, - }, - ], - }, - ], - params, - ) - - expect(result).toEqual( - `(((m.attributes -> 'skills' -> 'default') ?& array[:attributes_skills_default_1, :attributes_skills_default_2]) and (1=1))`, - ) - expect(params.attributes_skills_default_1).toEqual('javascript') - expect(params.attributes_skills_default_2).toEqual('typescript') - }) - - it('Should parse filter with attribute column number filter', () => { - const filter = { - and: [ - { - 'attributes.age.default': { - between: [20, 30], - }, - }, - {}, - ], - } - - const params: any = {} - const result = RawQueryParser.parseFilters( - filter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - [ - { - property: 'attributes', - column: 'm.attributes', - attributeInfos: [ - { - name: 'age', - type: MemberAttributeType.NUMBER, - }, - ], - }, - ], - params, - ) - - expect(result).toEqual( - `(((m.attributes -> 'age' -> 'default')::integer between :attributes_age_default_1 and :attributes_age_default_2) and (1=1))`, - ) - expect(params.attributes_age_default_1).toEqual(20) - expect(params.attributes_age_default_2).toEqual(30) - }) - - it('Should parse filter with json column array filter', () => { - const filter = { - and: [ - { - tags: ['c194036e-cf7c-4353-ae16-e8572a208f51'], - }, - {}, - ], - } - - const params: any = {} - const result = RawQueryParser.parseFilters( - filter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - [ - { - property: 'tags', - column: 'mt.all_ids', - attributeInfos: [], - }, - ], - params, - ) - - expect(result).toEqual(`(((mt.all_ids) ?& array[:tags_1]) and (1=1))`) - expect(params.tags_1).toEqual('c194036e-cf7c-4353-ae16-e8572a208f51') - }) - - it('Should parse filter with not operator', () => { - const filter = { - and: [ - { - not: { - displayName: { - textContains: 'test', - }, - }, - }, - {}, - ], - } - - const params: any = {} - const result = RawQueryParser.parseFilters( - filter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - [], - params, - ) - - expect(result).toEqual(`((not (m."displayName" ilike :displayName_1)) and (1=1))`) - expect(params.displayName_1).toEqual('%test%') - }) -}) diff --git a/backend/src/database/repositories/filters/queryParser.ts b/backend/src/database/repositories/filters/queryParser.ts index 5831141169..17a5384fc5 100644 --- a/backend/src/database/repositories/filters/queryParser.ts +++ b/backend/src/database/repositories/filters/queryParser.ts @@ -1,10 +1,13 @@ import lodash from 'lodash' +import Sequelize from 'sequelize' import validator from 'validator' + import { generateUUIDv4 as uuid } from '@crowd/common' -import Sequelize from 'sequelize' + import { IRepositoryOptions } from '../IRepositoryOptions' import SequelizeRepository from '../sequelizeRepository' -import { QueryInput, ManyToManyType } from './queryTypes' + +import { ManyToManyType, QueryInput } from './queryTypes' const { Op } = Sequelize @@ -267,8 +270,7 @@ class QueryParser { // The mapping comes from the manyToMany field for that key const mapping = this.manyToMany[key] - // We construct the items to filter on. For example, if we were filtering tags for members - // "memberTags"."tagId" = '{{id1}}' OR "memberTags"."tagId" = '{{id2}}' + // We construct the items to filter on. const items = value.reduce((acc, item, index) => { if (index === 0) { return `${acc} "${mapping.relationTable.name}"."${ @@ -283,13 +285,11 @@ class QueryParser { const joinField = mapping.overrideJoinField ?? 'id' // Find all the rows in the table that have the items we are filtering on - // For example, find all members that have the tags with id1 or id2 const literal = Sequelize.literal( `(SELECT "${mapping.table}"."${joinField}" FROM "${mapping.table}" INNER JOIN "${mapping.relationTable.name}" ON "${mapping.relationTable.name}"."${mapping.relationTable.from}" = "${mapping.table}"."${joinField}" WHERE ${items})`, ) - // It coudl be that we have more than 1 many to many filter, so we could need to append. For example: - // {tags: [id1, id2], organizations: [id3, id4]} + // It coudl be that we have more than 1 many to many filter, so we could need to append. if (query[Op.and]) { query[Op.and].push( Sequelize.where(Sequelize.literal(`"${mapping.model}"."${joinField}"`), Op.in, literal), diff --git a/backend/src/database/repositories/filters/rawQueryParser.ts b/backend/src/database/repositories/filters/rawQueryParser.ts index b68a96fa57..4d6f2a4ebf 100644 --- a/backend/src/database/repositories/filters/rawQueryParser.ts +++ b/backend/src/database/repositories/filters/rawQueryParser.ts @@ -1,5 +1,6 @@ import { singleOrDefault } from '@crowd/common' import { MemberAttributeType } from '@crowd/types' + import { JsonColumnInfo, Operator, ParsedJsonColumn } from './queryTypes' export default class RawQueryParser { @@ -38,7 +39,10 @@ export default class RawQueryParser { if (jsonColumnInfo) { results.push(this.parseJsonColumnCondition(jsonColumnInfo, filters[key], params)) } else { - results.push(this.parseColumnCondition(key, columnMap.get(key), filters[key], params)) + // handle column maps without quotes/alias to handle postgres camelCase columns + const column = + columnMap.get(key).indexOf('"') === -1 ? `"${columnMap.get(key)}"` : columnMap.get(key) + results.push(this.parseColumnCondition(key, column, filters[key], params)) } } } diff --git a/backend/src/database/repositories/githubInstallationsRepository.ts b/backend/src/database/repositories/githubInstallationsRepository.ts new file mode 100644 index 0000000000..f0d5ff7f50 --- /dev/null +++ b/backend/src/database/repositories/githubInstallationsRepository.ts @@ -0,0 +1,21 @@ +import { QueryTypes } from 'sequelize' + +import { IRepositoryOptions } from './IRepositoryOptions' +import SequelizeRepository from './sequelizeRepository' + +export default class GithubInstallationsRepository { + static async getInstallations(options: IRepositoryOptions) { + const transaction = SequelizeRepository.getTransaction(options) + const seq = SequelizeRepository.getSequelize(options) + + return seq.query( + ` + select * from "githubInstallations" + `, + { + transaction, + type: QueryTypes.SELECT, + }, + ) + } +} diff --git a/backend/src/database/repositories/githubReposRepository.ts b/backend/src/database/repositories/githubReposRepository.ts deleted file mode 100644 index 2c133aaa6b..0000000000 --- a/backend/src/database/repositories/githubReposRepository.ts +++ /dev/null @@ -1,85 +0,0 @@ -import trim from 'lodash/trim' -import { QueryTypes } from 'sequelize' -import { IRepositoryOptions } from './IRepositoryOptions' -import SequelizeRepository from './sequelizeRepository' - -export default class GithubReposRepository { - static async bulkInsert(table, columns, placeholdersFn, values, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - const seq = SequelizeRepository.getSequelize(options) - - columns = columns.map((column) => trim(column, '"')).map((column) => `"${column}"`) - const joinedColumns = columns.join(', ') - - const placeholders = values.map((value, idx) => placeholdersFn(idx)) - - const replacements = values.reduce((acc, value) => { - Object.entries(value).forEach(([key, value]) => { - acc[key] = value - }) - return acc - }, {}) - - return seq.query( - ` - INSERT INTO "${table}" - (${joinedColumns}) - VALUES ${placeholders.join(', ')} - ON CONFLICT ("tenantId", "url") - DO UPDATE SET "segmentId" = EXCLUDED."segmentId", - "integrationId" = EXCLUDED."integrationId" - `, - { - replacements, - transaction, - }, - ) - } - - static async updateMapping(integrationId, mapping, options: IRepositoryOptions) { - const tenantId = options.currentTenant.id - - await GithubReposRepository.bulkInsert( - 'githubRepos', - ['tenantId', 'integrationId', 'segmentId', 'url'], - (idx) => `(:tenantId_${idx}, :integrationId_${idx}, :segmentId_${idx}, :url_${idx})`, - Object.entries(mapping).map(([url, segmentId], idx) => ({ - [`tenantId_${idx}`]: tenantId, - [`integrationId_${idx}`]: integrationId, - [`segmentId_${idx}`]: segmentId, - [`url_${idx}`]: url, - })), - options, - ) - } - - static async getMapping(integrationId, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - const tenantId = options.currentTenant.id - - const results = await options.database.sequelize.query( - ` - SELECT - r.url, - JSONB_BUILD_OBJECT( - 'id', s.id, - 'name', s.name - ) as "segment" - FROM "githubRepos" r - JOIN segments s ON s.id = r."segmentId" - WHERE r."integrationId" = :integrationId - AND r."tenantId" = :tenantId - `, - { - replacements: { - integrationId, - tenantId, - }, - type: QueryTypes.SELECT, - transaction, - }, - ) - - return results - } -} diff --git a/backend/src/database/repositories/incomingWebhookRepository.ts b/backend/src/database/repositories/incomingWebhookRepository.ts index 0972fc7207..3a24061e84 100644 --- a/backend/src/database/repositories/incomingWebhookRepository.ts +++ b/backend/src/database/repositories/incomingWebhookRepository.ts @@ -1,5 +1,7 @@ import { QueryTypes } from 'sequelize' + import { generateUUIDv1 } from '@crowd/common' + import { DbIncomingWebhookInsertData, ErrorWebhook, @@ -8,6 +10,7 @@ import { WebhookState, WebhookType, } from '../../types/webhooks' + import { IRepositoryOptions } from './IRepositoryOptions' import { RepositoryBase } from './repositoryBase' @@ -236,7 +239,7 @@ export default class IncomingWebhookRepository extends RepositoryBase< from "incomingWebhooks" where state = :pending and "createdAt" < now() - interval '1 hour' - and type not in ('GITHUB', 'DISCORD') + and type not in ('GITHUB') limit ${perPage} offset ${(page - 1) * perPage}; ` diff --git a/backend/src/database/repositories/integrationProgressRepository.ts b/backend/src/database/repositories/integrationProgressRepository.ts new file mode 100644 index 0000000000..8c8fdf2588 --- /dev/null +++ b/backend/src/database/repositories/integrationProgressRepository.ts @@ -0,0 +1,194 @@ +import { QueryTypes } from 'sequelize' + +import { Repos } from '@/serverless/integrations/types/regularTypes' +import { GitHubStats } from '@/serverless/integrations/usecases/github/rest/getRemoteStats' + +import { IRepositoryOptions } from './IRepositoryOptions' +import SequelizeRepository from './sequelizeRepository' + +class IntegrationProgressRepository { + static createPayloadWithActivityType( + activityTypes: string[], + repos: Repos, + segments: string[] = [], + ) { + return { + filter: { + and: [ + { platform: { in: ['github'] } }, + { or: repos.map((repo) => ({ channel: { eq: repo.url } })) }, + { type: { in: activityTypes } }, + ], + }, + segmentIds: segments, + } + } + + static async getPendingStreamsCount(integrationId: string, options: IRepositoryOptions) { + const transaction = options.transaction + const seq = SequelizeRepository.getSequelize(options) + + const lastRunId = await IntegrationProgressRepository.getLastRunId(integrationId, options) + + if (!lastRunId) { + return 0 + } + + const result = await seq.query( + ` + select count(*) as "total" + from integration.streams + where "integrationId" = :integrationId + and "runId" = :lastRunId + and "state" = 'pending' + `, + { + replacements: { + integrationId, + lastRunId, + }, + type: QueryTypes.SELECT, + transaction, + }, + ) + + return (result[0] as any).total as number + } + + static async getLastRunId(integrationId: string, options: IRepositoryOptions) { + const transaction = options.transaction + const seq = SequelizeRepository.getSequelize(options) + + const result = await seq.query( + ` + select id + from integration.runs + where "integrationId" = :integrationId + order by "createdAt" desc + limit 1 + `, + { + replacements: { + integrationId, + }, + type: QueryTypes.SELECT, + transaction, + }, + ) + + if (result.length === 0) { + return null + } + + return (result[0] as any).id as string + } + + static async getDbStatsForGithub(): Promise { + // const tb = new TinybirdClient() + + // const promises: Promise<{ data: Counter }>[] = [ + // queryActivitiesCounter( + // IntegrationProgressRepository.createPayloadWithActivityType(['star'], repos, segments), + // tb, + // ), + // queryActivitiesCounter( + // IntegrationProgressRepository.createPayloadWithActivityType(['unstar'], repos, segments), + // tb, + // ), + // queryActivitiesCounter( + // { + // ...IntegrationProgressRepository.createPayloadWithActivityType(['fork'], repos, segments), + // indirectFork: 1, + // }, + // tb, + // ), + // queryActivitiesCounter( + // IntegrationProgressRepository.createPayloadWithActivityType( + // ['issues-opened'], + // repos, + // segments, + // ), + // tb, + // ), + // queryActivitiesCounter( + // IntegrationProgressRepository.createPayloadWithActivityType( + // ['pull_request-opened'], + // repos, + // segments, + // ), + // tb, + // ), + // ] + + // const result = await Promise.all(promises) + + return { + // stars: (result[0]?.data?.[0]?.count ?? 0) - (result[1]?.data?.[0]?.count ?? 0), + // forks: result[2]?.data?.[0]?.count ?? 0, + // totalIssues: result[3]?.data?.[0]?.count ?? 0, + // totalPRs: result[4]?.data?.[0]?.count ?? 0, + stars: 0, + forks: 0, + totalIssues: 0, + totalPRs: 0, + } + } + + static async getAllIntegrationsInProgressForSegment( + options: IRepositoryOptions, + ): Promise { + const transaction = options.transaction + const seq = SequelizeRepository.getSequelize(options) + const segment = SequelizeRepository.getStrictlySingleActiveSegment(options) + + const result = await seq.query( + ` + select id + from integrations + where + "status" = 'in-progress' + and "segmentId" = :segmentId + and "deletedAt" is null + `, + { + replacements: { + segmentId: segment.id, + }, + type: QueryTypes.SELECT, + transaction, + }, + ) + + return result.map((r: any) => r.id) + } + + static async getAllIntegrationsInProgressForMultipleSegments( + options: IRepositoryOptions, + ): Promise { + const transaction = options.transaction + const seq = SequelizeRepository.getSequelize(options) + const segments = SequelizeRepository.getCurrentSegments(options) + + const result = await seq.query( + ` + select id + from integrations + where + "status" = 'in-progress' + and "segmentId" in (:segmentIds) + and "deletedAt" is null + `, + { + replacements: { + segmentIds: segments.map((s) => s.id), + }, + type: QueryTypes.SELECT, + transaction, + }, + ) + + return result.map((r: any) => r.id) + } +} + +export default IntegrationProgressRepository diff --git a/backend/src/database/repositories/integrationRepository.ts b/backend/src/database/repositories/integrationRepository.ts index 3db2d13755..7aca1d55bc 100644 --- a/backend/src/database/repositories/integrationRepository.ts +++ b/backend/src/database/repositories/integrationRepository.ts @@ -1,56 +1,63 @@ import lodash from 'lodash' import Sequelize, { QueryTypes } from 'sequelize' + +import { captureApiChange, integrationConnectAction } from '@crowd/audit-logs' +import { DEFAULT_TENANT_ID, Error404 } from '@crowd/common' +import { + fetchGlobalIntegrations, + fetchGlobalIntegrationsCount, + fetchGlobalIntegrationsStatusCount, + fetchGlobalNotConnectedIntegrations, + fetchGlobalNotConnectedIntegrationsCount, + getNangoMappingsForIntegrations, +} from '@crowd/data-access-layer/src/integrations' +import { QueryExecutor } from '@crowd/data-access-layer/src/queryExecutor' +import { getReposGroupedByOrgForIntegrations } from '@crowd/data-access-layer/src/repositories' +import { getSegmentSubprojectIds } from '@crowd/data-access-layer/src/segments' import { IntegrationRunState, PlatformType } from '@crowd/types' -import SequelizeRepository from './sequelizeRepository' -import AuditLogRepository from './auditLogRepository' + import SequelizeFilterUtils from '../utils/sequelizeFilterUtils' -import Error404 from '../../errors/Error404' + import { IRepositoryOptions } from './IRepositoryOptions' import QueryParser from './filters/queryParser' import { QueryOutput } from './filters/queryTypes' -import AutomationRepository from './automationRepository' -import AutomationExecutionRepository from './automationExecutionRepository' -import MemberSyncRemoteRepository from './memberSyncRemoteRepository' -import OrganizationSyncRemoteRepository from './organizationSyncRemoteRepository' +import SequelizeRepository from './sequelizeRepository' const { Op } = Sequelize -const log: boolean = false class IntegrationRepository { static async create(data, options: IRepositoryOptions) { const currentUser = SequelizeRepository.getCurrentUser(options) - const tenant = SequelizeRepository.getCurrentTenant(options) - const transaction = SequelizeRepository.getTransaction(options) const segment = SequelizeRepository.getStrictlySingleActiveSegment(options) - const record = await options.database.integration.create( - { - ...lodash.pick(data, [ - 'platform', - 'status', - 'limitCount', - 'limitLastResetAt', - 'token', - 'refreshToken', - 'settings', - 'integrationIdentifier', - 'importHash', - 'emailSentAt', - ]), - segmentId: segment.id, - tenantId: tenant.id, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { - transaction, - }, - ) + const toInsert = { + ...lodash.pick(data, [ + 'platform', + 'status', + 'token', + 'refreshToken', + 'settings', + 'integrationIdentifier', + ]), + segmentId: segment.id, + tenantId: DEFAULT_TENANT_ID, + createdById: currentUser.id, + updatedById: currentUser.id, + id: data.id || undefined, + } + const record = await options.database.integration.create(toInsert, { + transaction, + }) - await this._createAuditLog(AuditLogRepository.CREATE, record, data, options) + await captureApiChange( + options, + integrationConnectAction(record.id, async (captureState) => { + captureState(toInsert) + }), + ) return this.findById(record.id, options) } @@ -60,13 +67,15 @@ class IntegrationRepository { const transaction = SequelizeRepository.getTransaction(options) - const currentTenant = SequelizeRepository.getCurrentTenant(options) + const qx = SequelizeRepository.getQueryExecutor(options) + const currentSegments = SequelizeRepository.getSegmentIds(options) + + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) let record = await options.database.integration.findOne({ where: { id, - tenantId: currentTenant.id, - segmentId: SequelizeRepository.getSegmentIds(options), + segmentId: subprojectIds, }, transaction, }) @@ -80,14 +89,10 @@ class IntegrationRepository { ...lodash.pick(data, [ 'platform', 'status', - 'limitCount', - 'limitLastResetAt', 'token', 'refreshToken', 'settings', 'integrationIdentifier', - 'importHash', - 'emailSentAt', ]), updatedById: currentUser.id, @@ -97,20 +102,15 @@ class IntegrationRepository { }, ) - await this._createAuditLog(AuditLogRepository.UPDATE, record, data, options) - return this.findById(record.id, options) } static async destroy(id, options: IRepositoryOptions) { const transaction = SequelizeRepository.getTransaction(options) - const currentTenant = SequelizeRepository.getCurrentTenant(options) - const record = await options.database.integration.findOne({ where: { id, - tenantId: currentTenant.id, }, transaction, }) @@ -125,21 +125,6 @@ class IntegrationRepository { // also mark integration runs as deleted const seq = SequelizeRepository.getSequelize(options) - await seq.query( - `update "integrationRuns" set state = :newState - where "integrationId" = :integrationId and state in (:delayed, :pending, :processing) - `, - { - replacements: { - newState: IntegrationRunState.INTEGRATION_DELETED, - delayed: IntegrationRunState.DELAYED, - pending: IntegrationRunState.PENDING, - processing: IntegrationRunState.PROCESSING, - integrationId: id, - }, - transaction, - }, - ) await seq.query( `update integration.runs set state = :newState @@ -155,32 +140,6 @@ class IntegrationRepository { transaction, }, ) - - // delete syncRemote rows coming from integration - await new MemberSyncRemoteRepository({ ...options, transaction }).destroyAllIntegration([ - record.id, - ]) - await new OrganizationSyncRemoteRepository({ ...options, transaction }).destroyAllIntegration([ - record.id, - ]) - - // destroy existing automations for outgoing integrations - const syncAutomationIds = ( - await new AutomationRepository({ ...options, transaction }).findSyncAutomations( - currentTenant.id, - record.platform, - ) - ).map((a) => a.id) - - if (syncAutomationIds.length > 0) { - await new AutomationExecutionRepository({ ...options, transaction }).destroyAllAutomation( - syncAutomationIds, - ) - } - - await new AutomationRepository({ ...options, transaction }).destroyAll(syncAutomationIds) - - await this._createAuditLog(AuditLogRepository.DELETE, record, record, options) } static async findAllByPlatform(platform, options: IRepositoryOptions) { @@ -188,12 +147,9 @@ class IntegrationRepository { const include = [] - const currentTenant = SequelizeRepository.getCurrentTenant(options) - const records = await options.database.integration.findAll({ where: { platform, - tenantId: currentTenant.id, }, include, transaction, @@ -209,12 +165,9 @@ class IntegrationRepository { const include = [] - const currentTenant = SequelizeRepository.getCurrentTenant(options) - const record = await options.database.integration.findOne({ where: { platform, - tenantId: currentTenant.id, segmentId: segment.id, }, include, @@ -225,16 +178,15 @@ class IntegrationRepository { throw new Error404() } - return this._populateRelations(record) + return this._populateRelations(record, SequelizeRepository.getQueryExecutor(options)) } - static async findActiveIntegrationByPlatform(platform: PlatformType, tenantId: string) { + static async findActiveIntegrationByPlatform(platform: PlatformType) { const options = await SequelizeRepository.getDefaultIRepositoryOptions() const record = await options.database.integration.findOne({ where: { platform, - tenantId, }, }) @@ -242,7 +194,7 @@ class IntegrationRepository { throw new Error404() } - return this._populateRelations(record) + return this._populateRelations(record, SequelizeRepository.getQueryExecutor(options)) } /** @@ -267,7 +219,7 @@ class IntegrationRepository { throw new Error404() } - return Promise.all(records.map((record) => this._populateRelations(record))) + return this._populateRelationsForRows(records, SequelizeRepository.getQueryExecutor(options)) } static async findByStatus( @@ -298,7 +250,6 @@ class IntegrationRepository { /** * Find an integration using the integration identifier and a platform. - * Tenant not needed. * @param identifier The integration identifier * @returns The integration object */ @@ -318,7 +269,7 @@ class IntegrationRepository { throw new Error404() } - return this._populateRelations(record) + return this._populateRelations(record, SequelizeRepository.getQueryExecutor(options)) } static async findById(id, options: IRepositoryOptions) { @@ -326,12 +277,9 @@ class IntegrationRepository { const include = [] - const currentTenant = SequelizeRepository.getCurrentTenant(options) - const record = await options.database.integration.findOne({ where: { id, - tenantId: currentTenant.id, }, include, transaction, @@ -341,47 +289,111 @@ class IntegrationRepository { throw new Error404() } - return this._populateRelations(record) + return this._populateRelations(record, SequelizeRepository.getQueryExecutor(options)) } - static async filterIdInTenant(id, options: IRepositoryOptions) { - return lodash.get(await this.filterIdsInTenant([id], options), '[0]', null) + static async count(filter, options: IRepositoryOptions) { + const transaction = SequelizeRepository.getTransaction(options) + + return options.database.integration.count({ + where: { + ...filter, + }, + transaction, + }) } - static async filterIdsInTenant(ids, options: IRepositoryOptions) { - if (!ids || !ids.length) { - return [] - } + /** + * Finds global integrations based on the provided parameters. + * + * @param {Object} filters - An object containing various filter options. + * @param {string} [filters.platform=null] - The platform to filter integrations by. + * @param {string | string[]} [filters.status=['done']] - The status of the integrations to be filtered. Can be a single status or array of statuses. + * @param {string} [filters.query=''] - The search query to filter integrations. + * @param {number} [filters.limit=20] - The maximum number of integrations to return. + * @param {number} [filters.offset=0] - The offset for pagination. + * @param {string} [filters.segment=null] - The segment to filter integrations by. + * @param {IRepositoryOptions} options - The repository options for querying. + * @returns {Promise} The result containing the rows of integrations and metadata about the query. + */ + static async findGlobalIntegrations( + filters: { + platform?: string | null + status?: string | string[] + query?: string + limit?: number + offset?: number + segment?: string | null + }, + options: IRepositoryOptions, + ) { + const { + platform = null, + status = ['done'], + query = '', + limit = 20, + offset = 0, + segment = null, + } = filters + + const qx = SequelizeRepository.getQueryExecutor(options) + const statusArray = Array.isArray(status) ? status : [status] + const isNotConnectedQuery = statusArray.includes('not-connected') + + // Execute data fetch and count in parallel for better performance + const [rows, [countObj]] = await Promise.all([ + isNotConnectedQuery + ? fetchGlobalNotConnectedIntegrations(qx, platform, query, limit, offset, segment) + : fetchGlobalIntegrations(qx, statusArray, platform, query, limit, offset, segment), + isNotConnectedQuery + ? fetchGlobalNotConnectedIntegrationsCount(qx, platform, query, segment) + : fetchGlobalIntegrationsCount(qx, statusArray, platform, query, segment), + ]) - const currentTenant = SequelizeRepository.getCurrentTenant(options) + // Both functions return an array with count objects, so we take the first element + const count = countObj?.count - const where = { - id: { - [Op.in]: ids, - }, - tenantId: currentTenant.id, + return { + rows, + count: +count || 0, + limit: +limit, + offset: +offset, } - - const records = await options.database.integration.findAll({ - attributes: ['id'], - where, - }) - - return records.map((record) => record.id) } - static async count(filter, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) + /** + * Retrieves the count of global integrations statuses for a specified platform. + * This method aggregates the count of different integration statuses including a 'not-connected' status. + * + * @param {Object} param1 - The optional parameters. + * @param {string|null} [param1.platform=null] - The platform to filter the integrations. Default is null. + * @param {string|null} [param1.segment=null] - The segment to filter the integrations. Default is null. + * @param {IRepositoryOptions} options - The options for the repository operations. + * @return {Promise>} A promise that resolves to an array of objects containing the statuses and their counts. + */ + static async findGlobalIntegrationsStatusCount( + filters: { + platform?: string | null + segment?: string | null + }, + options: IRepositoryOptions, + ) { + const { platform = null, segment = null } = filters + const qx = SequelizeRepository.getQueryExecutor(options) - const tenant = SequelizeRepository.getCurrentTenant(options) + // Execute both queries in parallel for better performance + const [statusCounts, [notConnectedResult]] = await Promise.all([ + fetchGlobalIntegrationsStatusCount(qx, platform, segment), + fetchGlobalNotConnectedIntegrationsCount(qx, platform, '', segment), + ]) - return options.database.integration.count({ - where: { - ...filter, - tenantId: tenant.id, + return [ + ...statusCounts, + { + status: 'not-connected', + count: Number(notConnectedResult?.count) || 0, }, - transaction, - }) + ] } static async findAndCountAll( @@ -412,46 +424,6 @@ class IntegrationRepository { }) } - if (filter.limitCountRange) { - const [start, end] = filter.limitCountRange - - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - limitCount: { - gte: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - limitCount: { - lte: end, - }, - }) - } - } - - if (filter.limitLastResetAtRange) { - const [start, end] = filter.limitLastResetAtRange - - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - limitLastResetAt: { - gte: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - limitLastResetAt: { - lte: end, - }, - }) - } - } - if (filter.integrationIdentifier) { advancedFilter.and.push({ integrationIdentifier: filter.integrationIdentifier, @@ -484,6 +456,10 @@ class IntegrationRepository { nestedFields: { sentiment: 'sentiment.sentiment', }, + // QueryParser filters on req.currentSegments directly (e.g., projectGroupId). + // Since integrations are stored per subproject, segment filtering is applied manually below + // after expanding to subprojectIds. + withSegments: false, }, options, ) @@ -495,63 +471,99 @@ class IntegrationRepository { offset, }) + const qx = SequelizeRepository.getQueryExecutor(options) + const currentSegments = SequelizeRepository.getSegmentIds(options) + + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) + + const segmentWhere = { segmentId: subprojectIds } + const where = parsed.where ? { [Op.and]: [parsed.where, segmentWhere] } : segmentWhere + let { rows, count, // eslint-disable-line prefer-const } = await options.database.integration.findAndCountAll({ - ...(parsed.where ? { where: parsed.where } : {}), + where, ...(parsed.having ? { having: parsed.having } : {}), order: parsed.order, - limit: parsed.limit, - offset: parsed.offset, + limit: limit ? parsed.limit : undefined, + offset: offset ? parsed.offset : undefined, include, transaction: SequelizeRepository.getTransaction(options), }) - rows = await this._populateRelationsForRows(rows) + rows = await this._populateRelationsForRows(rows, SequelizeRepository.getQueryExecutor(options)) // Some integrations (i.e GitHub, Discord, Discourse, Groupsio) receive new data via webhook post-onboarding. // We track their last processedAt separately, and not using updatedAt. const seq = SequelizeRepository.getSequelize(options) const integrationIds = rows.map((row) => row.id) - let results = [] if (integrationIds.length > 0) { - const query = `select "integrationId", max("processedAt") as "processedAt" from "incomingWebhooks" - where "integrationId" in (:integrationIds) and state = 'PROCESSED' - group by "integrationId"` - - results = await seq.query(query, { - replacements: { - integrationIds, - }, - type: QueryTypes.SELECT, - transaction: SequelizeRepository.getTransaction(options), + const webhookQuery = ` + SELECT "integrationId", MAX("processedAt") AS "webhookProcessedAt" + FROM "incomingWebhooks" + WHERE "integrationId" IN (:integrationIds) AND state = 'PROCESSED' + GROUP BY "integrationId" + ` + + const runQuery = ` + SELECT "integrationId", MAX("processedAt") AS "runProcessedAt" + FROM integration.runs + WHERE "integrationId" IN (:integrationIds) + GROUP BY "integrationId" + ` + + const [webhookResults, runResults] = await Promise.all([ + seq.query(webhookQuery, { + replacements: { integrationIds }, + type: QueryTypes.SELECT, + transaction: SequelizeRepository.getTransaction(options), + }), + seq.query(runQuery, { + replacements: { integrationIds }, + type: QueryTypes.SELECT, + transaction: SequelizeRepository.getTransaction(options), + }), + ]) + + const processedAtMap = integrationIds.reduce((map, id) => { + const webhookResult: any = webhookResults.find( + (r: { integrationId: string }) => r.integrationId === id, + ) + const runResult: any = runResults.find( + (r: { integrationId: string }) => r.integrationId === id, + ) + map[id] = { + webhookProcessedAt: webhookResult ? webhookResult.webhookProcessedAt : null, + runProcessedAt: runResult ? runResult.runProcessedAt : null, + } + return map + }, {}) + + rows.forEach((row) => { + const processedAt = processedAtMap[row.id] + // Use the latest processedAt from either webhook or run, or fall back to updatedAt + row.lastProcessedAt = processedAt + ? new Date( + Math.max( + processedAt.webhookProcessedAt + ? new Date(processedAt.webhookProcessedAt).getTime() + : 0, + processedAt.runProcessedAt ? new Date(processedAt.runProcessedAt).getTime() : 0, + new Date(row.updatedAt).getTime(), + ), + ) + : row.updatedAt }) } - const processedAtMap = results.reduce((map, item) => { - map[item.integrationId] = item.processedAt - return map - }, {}) - - rows.forEach((row) => { - // Either use the last processedAt, or fall back updatedAt - row.lastProcessedAt = processedAtMap[row.id] || row.updatedAt - }) - return { rows, count, limit: parsed.limit, offset: parsed.offset } } static async findAllAutocomplete(query, limit, options: IRepositoryOptions) { - const tenant = SequelizeRepository.getCurrentTenant(options) - - const whereAnd: Array = [ - { - tenantId: tenant.id, - }, - ] + const whereAnd: Array = [{}] if (query) { whereAnd.push({ @@ -579,43 +591,117 @@ class IntegrationRepository { })) } - static async _createAuditLog(action, record, data, options: IRepositoryOptions) { - if (log) { - let values = {} + static async _populateRelationsForRows(rows, qx: QueryExecutor) { + if (!rows) { + return rows + } + + const records = rows.map((record) => record.get({ plain: true })) + + const nangoIntegrationIds = records + .filter((r) => r.platform === PlatformType.GITHUB_NANGO) + .map((r) => r.id) + + const githubIntegrationIds = records + .filter( + (r) => + (r.platform === PlatformType.GITHUB || r.platform === PlatformType.GITHUB_NANGO) && + r.settings?.orgs?.length > 0, + ) + .map((r) => r.id) - if (data) { - values = { - ...record.get({ plain: true }), + const [allNangoMappings, allReposByOrg] = await Promise.all([ + getNangoMappingsForIntegrations(qx, nangoIntegrationIds), + getReposGroupedByOrgForIntegrations(qx, githubIntegrationIds), + ]) + + return records.map((output) => { + if (output.platform === PlatformType.GITHUB_NANGO) { + const nangoMapping = allNangoMappings[output.id] + if (nangoMapping && Object.keys(nangoMapping).length > 0) { + output.settings = { ...output.settings, nangoMapping } } } - await AuditLogRepository.log( - { - entityName: 'integration', - entityId: record.id, - action, - values, - }, - options, - ) - } - } + if ( + (output.platform === PlatformType.GITHUB || + output.platform === PlatformType.GITHUB_NANGO) && + output.settings?.orgs?.length > 0 + ) { + const reposByOrg = allReposByOrg[output.id] + + if (reposByOrg && Object.keys(reposByOrg).length > 0) { + output.settings = { + ...output.settings, + orgs: output.settings.orgs.map((org) => ({ + ...org, + repos: (reposByOrg[org.name] || []).map((r) => ({ + url: r.url, + name: r.name, + owner: r.owner, + forkedFrom: r.forkedFrom, + updatedAt: r.updatedAt, + })), + })), + } + } - static async _populateRelationsForRows(rows) { - if (!rows) { - return rows - } + delete output.settings.repos + delete output.settings.unavailableRepos + } - return Promise.all(rows.map((record) => this._populateRelations(record))) + return output + }) } - static async _populateRelations(record) { + static async _populateRelations(record, qx: QueryExecutor) { if (!record) { return record } const output = record.get({ plain: true }) + // For github-nango integrations, populate settings.nangoMapping from dedicated table + if (output.platform === PlatformType.GITHUB_NANGO) { + const allNangoMappings = await getNangoMappingsForIntegrations(qx, [output.id]) + const nangoMapping = allNangoMappings[output.id] || {} + if (Object.keys(nangoMapping).length > 0) { + output.settings = { ...output.settings, nangoMapping } + } + } + + // For both github and github-nango, populate orgs[].repos from repositories table + if ( + (output.platform === PlatformType.GITHUB || output.platform === PlatformType.GITHUB_NANGO) && + output.settings?.orgs?.length > 0 + ) { + const allReposByOrg = await getReposGroupedByOrgForIntegrations(qx, [output.id]) + const reposByOrg = allReposByOrg[output.id] || {} + + // Only overwrite orgs[].repos from the repositories table if there are rows. + // During the 'mapping' phase (legacy github connect), repos live in settings + // before being written to the repositories table via mapGithubRepos. + if (Object.keys(reposByOrg).length > 0) { + output.settings = { + ...output.settings, + orgs: output.settings.orgs.map((org) => ({ + ...org, + repos: (reposByOrg[org.name] || []).map((r) => ({ + url: r.url, + name: r.name, + owner: r.owner, + forkedFrom: r.forkedFrom, + updatedAt: r.updatedAt, + })), + })), + } + } + + // Strip legacy top-level keys that may still exist in the DB column + delete output.settings.repos + delete output.settings.unavailableRepos + } + return output } } diff --git a/backend/src/database/repositories/integrationRunRepository.ts b/backend/src/database/repositories/integrationRunRepository.ts index a92c40076c..3304b22b7c 100644 --- a/backend/src/database/repositories/integrationRunRepository.ts +++ b/backend/src/database/repositories/integrationRunRepository.ts @@ -1,11 +1,14 @@ import { QueryTypes } from 'sequelize' + import { generateUUIDv1 } from '@crowd/common' import { IntegrationRunState } from '@crowd/types' -import { IntegrationRun, DbIntegrationRunCreateData } from '../../types/integrationRunTypes' + +import { INTEGRATION_PROCESSING_CONFIG } from '../../conf' +import { DbIntegrationRunCreateData, IntegrationRun } from '../../types/integrationRunTypes' import { IntegrationStreamState } from '../../types/integrationStreamTypes' + import { IRepositoryOptions } from './IRepositoryOptions' import { RepositoryBase } from './repositoryBase' -import { INTEGRATION_PROCESSING_CONFIG } from '../../conf' export default class IntegrationRunRepository extends RepositoryBase< IntegrationRun, @@ -27,7 +30,6 @@ export default class IntegrationRunRepository extends RepositoryBase< select id, "tenantId", "integrationId", - "microserviceId", onboarding, state, "delayedUntil", @@ -82,7 +84,6 @@ export default class IntegrationRunRepository extends RepositoryBase< select id, "tenantId", "integrationId", - "microserviceId", onboarding, state, "delayedUntil", @@ -113,7 +114,6 @@ export default class IntegrationRunRepository extends RepositoryBase< select id, "tenantId", "integrationId", - "microserviceId", onboarding, state, "delayedUntil", @@ -142,6 +142,69 @@ export default class IntegrationRunRepository extends RepositoryBase< return results[0] as IntegrationRun } + async findStuckIntegrationRuns( + integrationId: string, + hoursThreshold: number = 24, + ): Promise { + const transaction = this.transaction + const seq = this.seq + const replacements: any = { + delayedState: IntegrationRunState.DELAYED, + processingState: IntegrationRunState.PROCESSING, + pendingState: IntegrationRunState.PENDING, + integrationId, + } + const runsWithStuckStreamsQuery = ` + SELECT DISTINCT run.id, run.onboarding + FROM integration.runs run + JOIN integration.streams strm ON run."id" = strm."runId" + WHERE + run.state IN (:delayedState, :processingState, :pendingState) + AND strm.state IN (:delayedState, :pendingState) + AND run."integrationId" = :integrationId + AND run."createdAt" < NOW() - INTERVAL '${hoursThreshold} hours' + ` + const results = await seq.query(runsWithStuckStreamsQuery, { + replacements, + type: QueryTypes.SELECT, + transaction, + }) + + return results as IntegrationRun[] + } + + async cleanupOrphanedIntegrationRuns( + integrationId: string, + hoursThreshold: number = 24, + ): Promise { + const transaction = this.transaction + const seq = this.seq + const replacements: any = { + integrationId, + delayedState: IntegrationRunState.DELAYED, + processingState: IntegrationRunState.PROCESSING, + pendingState: IntegrationRunState.PENDING, + } + + const query = ` + DELETE FROM integration.runs run + WHERE NOT EXISTS ( + SELECT 1 + FROM integration.streams strm + WHERE strm."runId" = run."id" + ) + AND run."integrationId" = :integrationId + AND run."createdAt" < NOW() - INTERVAL '${hoursThreshold} hours' + AND run.state IN (:delayedState, :processingState, :pendingState); + ` + + await seq.query(query, { + replacements, + type: QueryTypes.DELETE, + transaction, + }) + } + async findLastProcessingRunInNewFramework(integrationId: string): Promise { const transaction = this.transaction @@ -179,7 +242,6 @@ export default class IntegrationRunRepository extends RepositoryBase< async findLastProcessingRun( integrationId?: string, - microserviceId?: string, ignoreId?: string, ): Promise { const transaction = this.transaction @@ -196,11 +258,8 @@ export default class IntegrationRunRepository extends RepositoryBase< if (integrationId) { condition = ` "integrationId" = :integrationId ` replacements.integrationId = integrationId - } else if (microserviceId) { - condition = ` "microserviceId" = :microserviceId ` - replacements.microserviceId = microserviceId } else { - throw new Error(`Either integrationId or microserviceId must be provided!`) + throw new Error(`integrationId must be provided!`) } if (ignoreId) { @@ -212,7 +271,6 @@ export default class IntegrationRunRepository extends RepositoryBase< select id, "tenantId", "integrationId", - "microserviceId", onboarding, state, "delayedUntil", @@ -248,7 +306,6 @@ export default class IntegrationRunRepository extends RepositoryBase< select id, "tenantId", "integrationId", - "microserviceId", onboarding, state, "delayedUntil", @@ -282,8 +339,8 @@ export default class IntegrationRunRepository extends RepositoryBase< const id = generateUUIDv1() const query = ` - insert into "integrationRuns"(id, "tenantId", "integrationId", "microserviceId", onboarding, state) - values(:id, :tenantId, :integrationId, :microserviceId, :onboarding, :state) + insert into "integrationRuns"(id, "tenantId", "integrationId", onboarding, state) + values(:id, :tenantId, :integrationId, :onboarding, :state) returning "createdAt"; ` @@ -292,7 +349,6 @@ export default class IntegrationRunRepository extends RepositoryBase< id, tenantId: data.tenantId, integrationId: data.integrationId || null, - microserviceId: data.microserviceId || null, onboarding: data.onboarding, state: data.state, }, @@ -308,7 +364,6 @@ export default class IntegrationRunRepository extends RepositoryBase< id, tenantId: data.tenantId, integrationId: data.integrationId, - microserviceId: data.microserviceId, onboarding: data.onboarding, state: data.state, delayedUntil: null, diff --git a/backend/src/database/repositories/integrationStreamRepository.ts b/backend/src/database/repositories/integrationStreamRepository.ts index 00cf57473c..e85de2d226 100644 --- a/backend/src/database/repositories/integrationStreamRepository.ts +++ b/backend/src/database/repositories/integrationStreamRepository.ts @@ -1,14 +1,17 @@ -import { QueryTypes } from 'sequelize' import lodash from 'lodash' +import { QueryTypes } from 'sequelize' + import { generateUUIDv1 } from '@crowd/common' + +import { INTEGRATION_PROCESSING_CONFIG } from '../../conf' import { DbIntegrationStreamCreateData, IntegrationStream, IntegrationStreamState, } from '../../types/integrationStreamTypes' + import { IRepositoryOptions } from './IRepositoryOptions' import { RepositoryBase } from './repositoryBase' -import { INTEGRATION_PROCESSING_CONFIG } from '../../conf' export default class IntegrationStreamRepository extends RepositoryBase< IntegrationStream, @@ -31,7 +34,6 @@ export default class IntegrationStreamRepository extends RepositoryBase< "runId", "tenantId", "integrationId", - "microserviceId", state, name, metadata, @@ -94,7 +96,6 @@ export default class IntegrationStreamRepository extends RepositoryBase< "runId", "tenantId", "integrationId", - "microserviceId", state, name, metadata, @@ -127,7 +128,7 @@ export default class IntegrationStreamRepository extends RepositoryBase< const results: IntegrationStream[] = [] const query = ` - insert into "integrationStreams"(id, "runId", "tenantId", "integrationId", "microserviceId", state, name, metadata) + insert into "integrationStreams"(id, "runId", "tenantId", "integrationId", state, name, metadata) values ` @@ -139,14 +140,13 @@ export default class IntegrationStreamRepository extends RepositoryBase< for (const item of batch) { const id = generateUUIDv1() values.push( - `(:id${i}, :runId${i}, :tenantId${i}, :integrationId${i}, :microserviceId${i}, :state${i}, :name${i}, :metadata${i})`, + `(:id${i}, :runId${i}, :tenantId${i}, :integrationId${i}, :state${i}, :name${i}, :metadata${i})`, ) replacements[`id${i}`] = id replacements[`runId${i}`] = item.runId replacements[`tenantId${i}`] = item.tenantId replacements[`state${i}`] = IntegrationStreamState.PENDING replacements[`integrationId${i}`] = item.integrationId || null - replacements[`microserviceId${i}`] = item.microserviceId || null replacements[`name${i}`] = item.name replacements[`metadata${i}`] = JSON.stringify(item.metadata || {}) i++ @@ -175,7 +175,6 @@ export default class IntegrationStreamRepository extends RepositoryBase< tenantId: item.tenantId, state: IntegrationStreamState.PENDING, integrationId: item.integrationId, - microserviceId: item.microserviceId, name: item.name, metadata: item.metadata || {}, createdAt, @@ -198,8 +197,8 @@ export default class IntegrationStreamRepository extends RepositoryBase< const id = generateUUIDv1() const query = ` - insert into "integrationStreams"(id, "runId", "tenantId", "integrationId", "microserviceId", state, name, metadata) - values(:id, :runId, :tenantId, :integrationId, :microserviceId, :state, :name, :metadata) + insert into "integrationStreams"(id, "runId", "tenantId", "integrationId", state, name, metadata) + values(:id, :runId, :tenantId, :integrationId, :state, :name, :metadata) returning "createdAt"; ` @@ -210,7 +209,6 @@ export default class IntegrationStreamRepository extends RepositoryBase< tenantId: data.tenantId, state: IntegrationStreamState.PENDING, integrationId: data.integrationId || null, - microserviceId: data.microserviceId || null, name: data.name, metadata: JSON.stringify(data.metadata || {}), }, @@ -228,7 +226,6 @@ export default class IntegrationStreamRepository extends RepositoryBase< tenantId: data.tenantId, state: IntegrationStreamState.PENDING, integrationId: data.integrationId, - microserviceId: data.microserviceId, name: data.name, metadata: data.metadata || {}, createdAt: (result[0] as any).createdAt, @@ -368,7 +365,6 @@ export default class IntegrationStreamRepository extends RepositoryBase< "runId", "tenantId", "integrationId", - "microserviceId", state, name, metadata, diff --git a/backend/src/database/repositories/member/memberAffiliationsRepository.ts b/backend/src/database/repositories/member/memberAffiliationsRepository.ts new file mode 100644 index 0000000000..7d4488e957 --- /dev/null +++ b/backend/src/database/repositories/member/memberAffiliationsRepository.ts @@ -0,0 +1,108 @@ +import { OrganizationField, queryOrgs } from '@crowd/data-access-layer' +import { + deleteMemberSegmentAffiliations, + fetchMemberAffiliations, + insertMemberAffiliations, +} from '@crowd/data-access-layer/src/member_segment_affiliations' +import { fetchManySegments } from '@crowd/data-access-layer/src/segments' +import { IMemberAffiliation, IOrganization, SegmentData } from '@crowd/types' + +import { IRepositoryOptions } from '../IRepositoryOptions' +import SequelizeRepository from '../sequelizeRepository' + +class MemberAffiliationsRepository { + static async list(memberId: string, options: IRepositoryOptions) { + const transaction = await SequelizeRepository.createTransaction(options) + try { + const txOptions = { ...options, transaction } + const qx = SequelizeRepository.getQueryExecutor(txOptions) + + // Fetch member affiliations + const affiliations = await fetchMemberAffiliations(qx, memberId) + const orgIds = affiliations.map((a) => a.organizationId) + const segmentIds = affiliations.map((a) => a.segmentId) + + // Fetch organizations + let orgObject: Record = {} + if (orgIds.length > 0) { + const organizations = await queryOrgs(qx, { + filter: { + [OrganizationField.ID]: { + in: orgIds, + }, + }, + fields: [OrganizationField.ID, OrganizationField.DISPLAY_NAME, OrganizationField.LOGO], + }) + orgObject = organizations.reduce((acc, org) => { + acc[org.id] = org + return acc + }, {}) + } + + // Fetch organizations + let segmentsObject: Record = {} + if (segmentIds.length > 0) { + const segments = await fetchManySegments(qx, segmentIds, 'id, "slug", "name", "parentName"') + segmentsObject = segments.reduce((acc, seg) => { + acc[seg.id] = seg + return acc + }, {}) + } + + // Map affiliations + const list = affiliations.map((affiliation) => { + const org = orgObject[affiliation.organizationId] + const segment = segmentsObject[affiliation.segmentId] + + return { + ...affiliation, + segmentSlug: segment?.slug, + segmentName: segment?.name, + segmentParentName: segment?.parentName, + organizationName: org?.displayName, + organizationLogo: org?.logo, + } + }) + + await SequelizeRepository.commitTransaction(transaction) + + return list + } catch (err) { + if (transaction) { + await SequelizeRepository.rollbackTransaction(transaction) + } + throw err + } + } + + static async upsertMultiple( + memberId: string, + data: Partial[], + options: IRepositoryOptions, + ) { + const transaction = await SequelizeRepository.createTransaction(options) + try { + const txOptions = { ...options, transaction } + const qx = SequelizeRepository.getQueryExecutor(txOptions) + + // Delete all member affiliations + await deleteMemberSegmentAffiliations(qx, { memberId }) + + if (data?.length > 0) { + // Insert multiple member affiliations + await insertMemberAffiliations(qx, memberId, data) + } + + await SequelizeRepository.commitTransaction(transaction) + + return await this.list(memberId, options) + } catch (err) { + if (transaction) { + await SequelizeRepository.rollbackTransaction(transaction) + } + throw err + } + } +} + +export default MemberAffiliationsRepository diff --git a/backend/src/database/repositories/member/memberOrganizationAffiliationOverridesRepository.ts b/backend/src/database/repositories/member/memberOrganizationAffiliationOverridesRepository.ts new file mode 100644 index 0000000000..cef8495642 --- /dev/null +++ b/backend/src/database/repositories/member/memberOrganizationAffiliationOverridesRepository.ts @@ -0,0 +1,51 @@ +import { + changeMemberOrganizationAffiliationOverrides, + fetchMemberOrganizationById, + findMemberAffiliationOverrides, + findPrimaryWorkExperiencesOfMember, +} from '@crowd/data-access-layer' +import { deleteMemberSegmentAffiliations } from '@crowd/data-access-layer/src/member_segment_affiliations' +import { + IChangeAffiliationOverrideData, + IMemberOrganizationAffiliationOverride, +} from '@crowd/types' + +import { IRepositoryOptions } from '../IRepositoryOptions' +import SequelizeRepository from '../sequelizeRepository' + +class MemberOrganizationAffiliationOverridesRepository { + static async changeOverride(data: IChangeAffiliationOverrideData, options: IRepositoryOptions) { + const qx = SequelizeRepository.getQueryExecutor(options) + + await changeMemberOrganizationAffiliationOverrides(qx, [data]) + + const { allowAffiliation, memberId, memberOrganizationId } = data + + if (allowAffiliation === false && memberId && memberOrganizationId) { + const memberOrganization = await fetchMemberOrganizationById(qx, memberOrganizationId) + + if (memberOrganization?.organizationId) { + await deleteMemberSegmentAffiliations(qx, { + memberId, + organizationId: memberOrganization.organizationId, + }) + } + } + + const overrides = await findMemberAffiliationOverrides(qx, data.memberId, [ + data.memberOrganizationId, + ]) + + return overrides[0] + } + + static async findPrimaryWorkExperiences( + memberId: string, + options: IRepositoryOptions, + ): Promise { + const qx = SequelizeRepository.getQueryExecutor(options) + return findPrimaryWorkExperiencesOfMember(qx, memberId) + } +} + +export default MemberOrganizationAffiliationOverridesRepository diff --git a/backend/src/database/repositories/memberAffiliationRepository.ts b/backend/src/database/repositories/memberAffiliationRepository.ts deleted file mode 100644 index 55bb182387..0000000000 --- a/backend/src/database/repositories/memberAffiliationRepository.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { QueryTypes } from 'sequelize' -import { IRepositoryOptions } from './IRepositoryOptions' -import SequelizeRepository from './sequelizeRepository' - -class MemberAffiliationRepository { - static async update(memberId: string, options: IRepositoryOptions) { - const seq = SequelizeRepository.getSequelize(options) - const transaction = SequelizeRepository.getTransaction(options) - - const query = ` - WITH new_activities_organizations AS ( - SELECT - a.id, - - -- this 000000 magic is to differentiate between nothing to LEFT JOIN with and real individial affiliation - -- we want to keep NULL in 'organizationId' if there is an affiliation configured, - -- but if there is no manual affiliation, we know this by 'msa.id' being NULL, and then using 000000 as a marker, - -- which we remove afterwards - (ARRAY_REMOVE(ARRAY_AGG(CASE WHEN msa.id IS NULL THEN '00000000-0000-0000-0000-000000000000' ELSE msa."organizationId" END), '00000000-0000-0000-0000-000000000000') - || ARRAY_REMOVE(ARRAY_AGG(mo."organizationId" ORDER BY mo."dateStart" DESC, mo.id), NULL) - || ARRAY_REMOVE(ARRAY_AGG(mo1."organizationId" ORDER BY mo1."createdAt" DESC, mo1.id), NULL) - || ARRAY_REMOVE(ARRAY_AGG(mo2."organizationId" ORDER BY mo2."createdAt", mo2.id), NULL) - )[1] AS new_org - FROM activities a - LEFT JOIN "memberSegmentAffiliations" msa ON msa."memberId" = a."memberId" AND a."segmentId" = msa."segmentId" AND ( - a.timestamp BETWEEN msa."dateStart" AND msa."dateEnd" - OR (a.timestamp >= msa."dateStart" AND msa."dateEnd" IS NULL) - ) - LEFT JOIN "memberOrganizations" mo ON mo."memberId" = a."memberId" - AND ( - a.timestamp BETWEEN mo."dateStart" AND mo."dateEnd" - OR (a.timestamp >= mo."dateStart" AND mo."dateEnd" IS NULL) - ) - AND mo."deletedAt" IS NULL - LEFT JOIN "memberOrganizations" mo1 ON mo1."memberId" = a."memberId" - AND mo1."dateStart" IS NULL AND mo1."dateEnd" IS NULL - AND mo1."createdAt" <= a.timestamp - AND mo1."deletedAt" IS NULL - LEFT JOIN "memberOrganizations" mo2 ON mo2."memberId" = a."memberId" - AND mo2."dateStart" IS NULL AND mo2."dateEnd" IS NULL - AND mo2."deletedAt" IS NULL - WHERE a."memberId" = :memberId - GROUP BY a.id - ) - UPDATE activities a1 - SET "organizationId" = nao.new_org - FROM new_activities_organizations nao - WHERE a1.id = nao.id - AND ("organizationId" != nao.new_org - OR ("organizationId" IS NULL AND nao.new_org IS NOT NULL) - OR ("organizationId" IS NOT NULL AND nao.new_org IS NULL)) - RETURNING a1.id, a1."organizationId", nao.new_org - ` - await seq.query(query, { - replacements: { - memberId, - }, - type: QueryTypes.UPDATE, - transaction, - }) - } -} - -export default MemberAffiliationRepository diff --git a/backend/src/database/repositories/memberAttributeSettingsRepository.ts b/backend/src/database/repositories/memberAttributeSettingsRepository.ts index 34602004e8..1d4d00aa67 100644 --- a/backend/src/database/repositories/memberAttributeSettingsRepository.ts +++ b/backend/src/database/repositories/memberAttributeSettingsRepository.ts @@ -1,17 +1,19 @@ import Sequelize from 'sequelize' + +import { Error400, Error404 } from '@crowd/common' import { RedisCache } from '@crowd/redis' -import SequelizeRepository from './sequelizeRepository' -import { IRepositoryOptions } from './IRepositoryOptions' -import Error404 from '../../errors/Error404' -import SequelizeFilterUtils from '../utils/sequelizeFilterUtils' + import { AttributeData } from '../attributes/attribute' +import SequelizeFilterUtils from '../utils/sequelizeFilterUtils' + +import { IRepositoryOptions } from './IRepositoryOptions' +import SequelizeRepository from './sequelizeRepository' import { MemberAttributeSettingsCreateData, - MemberAttributeSettingsUpdateData, MemberAttributeSettingsCriteria, MemberAttributeSettingsCriteriaResult, + MemberAttributeSettingsUpdateData, } from './types/memberAttributeSettingsTypes' -import Error400 from '../../errors/Error400' const Op = Sequelize.Op diff --git a/backend/src/database/repositories/memberEnrichmentCacheRepository.ts b/backend/src/database/repositories/memberEnrichmentCacheRepository.ts deleted file mode 100644 index 84e9e49fdc..0000000000 --- a/backend/src/database/repositories/memberEnrichmentCacheRepository.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { QueryTypes } from 'sequelize' -import { EnrichmentCache } from '../../services/premium/enrichment/types/memberEnrichmentTypes' -import { IRepositoryOptions } from './IRepositoryOptions' -import SequelizeRepository from './sequelizeRepository' - -class MemberEnrichmentCacheRepository { - /** - * Inserts enrichment data into cache. If a member - * already has an enrichment entry, row is updated with the latest given data. - * Returns null if data is falsy or an empty object. - * @param memberId enriched member's id - * @param data enrichment data - * @param options - * @returns - */ - static async upsert( - memberId: string, - data: any, - options: IRepositoryOptions, - ): Promise { - if (data && Object.keys(data).length > 0) { - const transaction = SequelizeRepository.getTransaction(options) - await options.database.sequelize.query( - `INSERT INTO "memberEnrichmentCache" ("createdAt", "updatedAt", "memberId", "data") - VALUES - (now(), now(), :memberId, :data) - ON CONFLICT ("memberId") DO UPDATE - SET data = :data, "updatedAt" = now() - `, - { - replacements: { - memberId, - data: JSON.stringify(data), - }, - type: QueryTypes.UPSERT, - transaction, - }, - ) - } - - const cacheUpserted = await MemberEnrichmentCacheRepository.findByMemberId(memberId, options) - return cacheUpserted - } - - /** - * Finds member enrichment cache given memberId - * Returns null if not found. - * @param memberId enriched member's id - * @param options - * @returns - */ - static async findByMemberId( - memberId: string, - options: IRepositoryOptions, - ): Promise { - const transaction = SequelizeRepository.getTransaction(options) - const records = await options.database.sequelize.query( - `select * - from "memberEnrichmentCache" - where "memberId" = :memberId; - `, - { - replacements: { - memberId, - }, - type: QueryTypes.SELECT, - transaction, - }, - ) - - if (records.length === 0) { - return null - } - - return records[0] - } -} - -export default MemberEnrichmentCacheRepository diff --git a/backend/src/database/repositories/memberOrganizationRepository.ts b/backend/src/database/repositories/memberOrganizationRepository.ts new file mode 100644 index 0000000000..2e3b573e0c --- /dev/null +++ b/backend/src/database/repositories/memberOrganizationRepository.ts @@ -0,0 +1,113 @@ +import { QueryTypes } from 'sequelize' + +import { EntityField } from '@crowd/data-access-layer' +import { IMemberOrganization } from '@crowd/types' + +import { IRepositoryOptions } from './IRepositoryOptions' +import SequelizeRepository from './sequelizeRepository' + +class MemberOrganizationRepository { + static async findRolesBelongingToBothEntities( + primaryId: string, + secondaryId: string, + entityIdField: EntityField, + intersectBasedOnField: EntityField, + options: IRepositoryOptions, + ): Promise { + const transaction = SequelizeRepository.getTransaction(options) + const sequelize = SequelizeRepository.getSequelize(options) + + const results = await sequelize.query( + ` + SELECT mo.* + FROM "memberOrganizations" AS mo + WHERE mo."deletedAt" is null and + mo."${intersectBasedOnField}" IN ( + SELECT "${intersectBasedOnField}" + FROM "memberOrganizations" + WHERE "${entityIdField}" = :primaryId + ) + AND mo."${intersectBasedOnField}" IN ( + SELECT "${intersectBasedOnField}" + FROM "memberOrganizations" + WHERE "${entityIdField}" = :secondaryId) + AND mo."${entityIdField}" IN (:primaryId, :secondaryId); + + `, + { + replacements: { + primaryId, + secondaryId, + }, + type: QueryTypes.SELECT, + transaction, + }, + ) + + return results as IMemberOrganization[] + } + + static async findNonIntersectingRoles( + primaryId: string, + secondaryId: string, + entityIdField: EntityField, + intersectBasedOnField: EntityField, + options: IRepositoryOptions, + ): Promise { + const seq = SequelizeRepository.getSequelize(options) + const transaction = SequelizeRepository.getTransaction(options) + + const remainingRoles = (await seq.query( + ` + SELECT * + FROM "memberOrganizations" + WHERE "${entityIdField}" = :secondaryId + AND "deletedAt" IS NULL + AND "${intersectBasedOnField}" NOT IN ( + SELECT "${intersectBasedOnField}" + FROM "memberOrganizations" + WHERE "${entityIdField}" = :primaryId + AND "deletedAt" IS NULL + ); + `, + { + replacements: { + primaryId, + secondaryId, + }, + type: QueryTypes.SELECT, + transaction, + }, + )) as IMemberOrganization[] + + return remainingRoles + } + + static async findRolesInOrganization( + organizationId: string, + options: IRepositoryOptions, + ): Promise { + const seq = SequelizeRepository.getSequelize(options) + const transaction = SequelizeRepository.getTransaction(options) + + const rolesInOrganization = (await seq.query( + ` + SELECT * + FROM "memberOrganizations" + WHERE "organizationId" = :organizationId + AND "deletedAt" IS NULL; + `, + { + replacements: { + organizationId, + }, + type: QueryTypes.SELECT, + transaction, + }, + )) as IMemberOrganization[] + + return rolesInOrganization + } +} + +export default MemberOrganizationRepository diff --git a/backend/src/database/repositories/memberRepository.ts b/backend/src/database/repositories/memberRepository.ts index 14440c2d06..5a6f668d81 100644 --- a/backend/src/database/repositories/memberRepository.ts +++ b/backend/src/database/repositories/memberRepository.ts @@ -1,149 +1,201 @@ +import lodash, { chunk, uniq } from 'lodash' +import Sequelize, { QueryTypes } from 'sequelize' + +import { + captureApiChange, + memberCreateAction, + memberEditAffiliationsAction, + memberEditProfileAction, +} from '@crowd/audit-logs' +import { + DEFAULT_TENANT_ID, + Error400, + Error404, + Error409, + RawQueryParser, + groupBy, +} from '@crowd/common' +import { BotDetectionService, CommonMemberService } from '@crowd/common_services' +import { + OrganizationField, + createMemberIdentity, + deleteMemberIdentities, + deleteMemberIdentitiesByCombinations, + findAlreadyExistingVerifiedIdentities, + getLastActivitiesForMembers, + queryActivityRelations, + queryOrgs, + updateVerifiedFlag, +} from '@crowd/data-access-layer' +import { findManyLfxMemberships } from '@crowd/data-access-layer/src/lfx_memberships' +import { findMaintainerRoles } from '@crowd/data-access-layer/src/maintainers' +import { addMemberNoMerge, removeMemberToMerge } from '@crowd/data-access-layer/src/member_merge' +import { + deleteMemberSegmentAffiliations, + findMemberAffiliations, + insertMemberAffiliations, +} from '@crowd/data-access-layer/src/member_segment_affiliations' +import { + MemberField, + fetchManyMemberIdentities, + fetchManyMemberOrgs, + fetchManyMemberSegments, + fetchMemberIdentities, + fetchMemberOrganizations, + findMemberById, + queryMembersAdvanced, +} from '@crowd/data-access-layer/src/members' +import { + fetchAbsoluteMemberAggregates, + includeMemberToSegments, +} from '@crowd/data-access-layer/src/members/segments' +import { IDbMemberData } from '@crowd/data-access-layer/src/members/types' +import { optionsQx } from '@crowd/data-access-layer/src/queryExecutor' +import { fetchManySegments, getSegmentSubprojectIds } from '@crowd/data-access-layer/src/segments' +import { ActivityDisplayService } from '@crowd/integrations' import { ALL_PLATFORM_TYPES, ActivityDisplayVariant, + IMemberIdentity, + IMemberUsername, MemberAttributeType, - OpenSearchIndex, + MemberBotDetection, + MemberIdentityType, + MemberSegmentAffiliation, + MemberSegmentAffiliationJoined, PlatformType, - SyncStatus, - OrganizationSource, + SegmentType, + TemporalWorkflowId, } from '@crowd/types' -import lodash, { chunk } from 'lodash' -import moment from 'moment' -import Sequelize, { QueryTypes } from 'sequelize' -import { FieldTranslatorFactory, OpensearchQueryParser } from '@crowd/opensearch' import { KUBE_MODE, SERVICE } from '@/conf' -import { ServiceType } from '../../conf/configTypes' -import Error404 from '../../errors/Error404' -import isFeatureEnabled from '../../feature-flags/isFeatureEnabled' +import { ServiceType } from '@/conf/configTypes' +import { IFetchMemberMergeSuggestionArgs, SimilarityScoreRange } from '@/types/mergeSuggestionTypes' + import { PlatformIdentities } from '../../serverless/integrations/types/messageTypes' -import ActivityDisplayService from '../../services/activityDisplayService' -import { FeatureFlag, PageData } from '../../types/common' -import { - MemberSegmentAffiliation, - MemberSegmentAffiliationJoined, -} from '../../types/memberSegmentAffiliationTypes' -import { - SegmentData, - SegmentProjectGroupNestedData, - SegmentProjectNestedData, -} from '../../types/segmentTypes' import { AttributeData } from '../attributes/attribute' -import SequelizeFilterUtils from '../utils/sequelizeFilterUtils' + import { IRepositoryOptions } from './IRepositoryOptions' -import AuditLogRepository from './auditLogRepository' -import QueryParser from './filters/queryParser' -import { JsonColumnInfo, QueryOutput } from './filters/queryTypes' -import RawQueryParser from './filters/rawQueryParser' -import MemberSegmentAffiliationRepository from './memberSegmentAffiliationRepository' +import MemberAttributeSettingsRepository from './memberAttributeSettingsRepository' import SegmentRepository from './segmentRepository' import SequelizeRepository from './sequelizeRepository' import TenantRepository from './tenantRepository' -import { - IActiveMemberData, - IActiveMemberFilter, - IMemberIdentity, - IMemberMergeSuggestion, - mapUsernameToIdentities, -} from './types/memberTypes' -import Error400 from '../../errors/Error400' -import OrganizationRepository from './organizationRepository' -import MemberSyncRemoteRepository from './memberSyncRemoteRepository' -import MemberAffiliationRepository from './memberAffiliationRepository' +import { IMemberMergeSuggestion, mapUsernameToIdentities } from './types/memberTypes' const { Op } = Sequelize -const log: boolean = false - class MemberRepository { - static async create(data, options: IRepositoryOptions, doPopulateRelations = true) { - if (!data.username) { + static async create(data, options: IRepositoryOptions) { + if (!data.username && !data.identities) { throw new Error('Username not set when creating member!') } - const platforms = Object.keys(data.username) as PlatformType[] - if (platforms.length === 0) { - throw new Error('Username object does not have any platforms!') - } + const currentUser = SequelizeRepository.getCurrentUser(options) - data.username = mapUsernameToIdentities(data.username) + const transaction = SequelizeRepository.getTransaction(options) - const currentUser = SequelizeRepository.getCurrentUser(options) + const botDetectionService = new BotDetectionService(options.log) + const botDetection = botDetectionService.isMemberBot( + data.identities, + data.attributes || {}, + data.displayName, + ) - const tenant = SequelizeRepository.getCurrentTenant(options) + if (botDetection === MemberBotDetection.CONFIRMED_BOT) { + options.log.debug({ memberIdentities: data.identities }, 'Member confirmed as bot!') - const transaction = SequelizeRepository.getTransaction(options) + const existingIsBot = (data.attributes?.isBot as Record) || {} - const segment = SequelizeRepository.getStrictlySingleActiveSegment(options) - const record = await options.database.member.create( - { - ...lodash.pick(data, [ - 'displayName', - 'attributes', - 'emails', - 'lastEnriched', - 'enrichedBy', - 'contributions', - 'score', - 'reach', - 'joinedAt', - 'manuallyCreated', - 'importHash', - ]), - tenantId: tenant.id, - createdById: currentUser.id, - updatedById: currentUser.id, - segmentId: segment.id, - }, - { - transaction, - }, + // add default and system flags only if no active flag exists + if (!Object.values(existingIsBot).some(Boolean)) { + if (!data.attributes) { + data.attributes = {} + } + // When bot detection confirms a bot, set system flag and don't preserve custom flag + // Custom flag should only be set when user manually marks as bot, not when system detects it + data.attributes.isBot = { default: true, system: true } + } + } + + const toInsert = { + ...lodash.pick(data, [ + 'id', + 'displayName', + 'attributes', + 'emails', + 'enrichedBy', + 'contributions', + 'score', + 'reach', + 'joinedAt', + 'manuallyCreated', + 'importHash', + ]), + tenantId: DEFAULT_TENANT_ID, + createdById: currentUser.id, + updatedById: currentUser.id, + } + + const record = await options.database.member.create(toInsert, { + transaction, + }) + + await captureApiChange( + options, + memberCreateAction(record.id, async (captureNewState) => { + captureNewState(toInsert) + }), ) - const username: PlatformIdentities = data.username + const qx = SequelizeRepository.getQueryExecutor(options) + const currentSegments = SequelizeRepository.getSegmentIds(options) - const seq = SequelizeRepository.getSequelize(options) - const query = ` - insert into "memberIdentities"("memberId", platform, username, "sourceId", "tenantId", "integrationId") - values(:memberId, :platform, :username, :sourceId, :tenantId, :integrationId); - ` + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) - for (const platform of Object.keys(username) as PlatformType[]) { - const identities: any[] = username[platform] - for (const identity of identities) { - await seq.query(query, { - replacements: { + if (data.identities) { + for (const i of data.identities as IMemberIdentity[]) { + await createMemberIdentity(qx, { + memberId: record.id, + platform: i.platform, + type: i.type, + value: i.value, + sourceId: i.sourceId || null, + integrationId: i.integrationId || null, + verified: i.verified, + source: i.source, + }) + } + } else if (data.username) { + const username: PlatformIdentities = mapUsernameToIdentities(data.username) + + for (const platform of Object.keys(username) as PlatformType[]) { + const identities: any[] = username[platform] + for (const identity of identities) { + await createMemberIdentity(qx, { memberId: record.id, platform, - username: identity.username, + value: identity.value ? identity.value : identity.username, + type: identity.type ? identity.type : MemberIdentityType.USERNAME, + verified: true, sourceId: identity.sourceId || null, integrationId: identity.integrationId || null, - tenantId: tenant.id, - }, - type: QueryTypes.INSERT, - transaction, - }) + source: identity.source || 'ui', + }) + } } } - await MemberRepository.includeMemberToSegments(record.id, options) - - await record.setActivities(data.activities || [], { - transaction, - }) - await record.setTags(data.tags || [], { - transaction, - }) - - await MemberRepository.updateMemberOrganizations(record, data.organizations, true, options) + await includeMemberToSegments(qx, record.id, subprojectIds) - await record.setTasks(data.tasks || [], { - transaction, - }) + const memberService = new CommonMemberService(optionsQx(options), options.temporal, options.log) - await record.setNotes(data.notes || [], { - transaction, - }) + await memberService.updateMemberOrganizations( + record.id, + data.organizations, + true, + subprojectIds, + options, + ) await record.setNoMerge(data.noMerge || [], { transaction, @@ -156,39 +208,22 @@ class MemberRepository { await this.setAffiliations(record.id, data.affiliations, options) } - await this._createAuditLog(AuditLogRepository.CREATE, record, data, options) - - return this.findById(record.id, options, true, doPopulateRelations) - } - - static async includeMemberToSegments(memberId: string, options: IRepositoryOptions) { - const seq = SequelizeRepository.getSequelize(options) - - const transaction = SequelizeRepository.getTransaction(options) - - let bulkInsertMemberSegments = `INSERT INTO "memberSegments" ("memberId","segmentId", "tenantId", "createdAt") VALUES ` - const replacements = { - memberId, - tenantId: options.currentTenant.id, - } - - for (let idx = 0; idx < options.currentSegments.length; idx++) { - bulkInsertMemberSegments += ` (:memberId, :segmentId${idx}, :tenantId, now()) ` - - replacements[`segmentId${idx}`] = options.currentSegments[idx].id - - if (idx !== options.currentSegments.length - 1) { - bulkInsertMemberSegments += `,` - } + if (botDetection === MemberBotDetection.SUSPECTED_BOT) { + options.log.debug({ memberId: record.id }, 'Member suspected as bot, running LLM check!') + await options.temporal.workflow.start('processMemberBotAnalysisWithLLM', { + taskQueue: 'profiles', + workflowId: `${TemporalWorkflowId.MEMBER_BOT_ANALYSIS_WITH_LLM}/${record.id}`, + retry: { + maximumAttempts: 10, + }, + args: [{ memberId: record.id }], + searchAttributes: { + TenantId: [DEFAULT_TENANT_ID], + }, + }) } - bulkInsertMemberSegments += ` ON CONFLICT DO NOTHING` - - await seq.query(bulkInsertMemberSegments, { - replacements, - type: QueryTypes.INSERT, - transaction, - }) + return this.findById(record.id, options) } static async excludeMembersFromSegments(memberIds: string[], options: IRepositoryOptions) { @@ -198,167 +233,300 @@ class MemberRepository { const bulkDeleteMemberSegments = `DELETE FROM "memberSegments" WHERE "memberId" in (:memberIds) and "segmentId" in (:segmentIds);` + const qx = SequelizeRepository.getQueryExecutor(options) + const currentSegments = SequelizeRepository.getSegmentIds(options) + + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) + await seq.query(bulkDeleteMemberSegments, { replacements: { memberIds, - segmentIds: SequelizeRepository.getSegmentIds(options), + segmentIds: subprojectIds, }, type: QueryTypes.DELETE, transaction, }) } - static async findSampleDataMemberIds(options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - const currentTenant = SequelizeRepository.getCurrentTenant(options) - const sampleMemberIds = await options.database.sequelize.query( - `select m.id from members m - where (m.attributes->'sample'->'default')::boolean is true - and m."tenantId" = :tenantId; - `, + static async countMemberMergeSuggestions( + memberFilter: string, + similarityFilter: string, + displayNameFilter: string, + replacements: { + segmentIds: string[] + memberId?: string + displayName?: string + }, + options: IRepositoryOptions, + ): Promise { + const membersJoin = displayNameFilter + ? `JOIN members m ON m.id = mtm."memberId" + JOIN members m2 ON m2.id = mtm."toMergeId"` + : '' + + const totalCount = await options.database.sequelize.query( + ` + SELECT + COUNT(*) AS count + FROM "memberToMerge" mtm + ${membersJoin} + WHERE EXISTS ( + SELECT 1 FROM "memberSegmentsAgg" ms + WHERE ms."memberId" = mtm."memberId" AND ms."segmentId" IN (:segmentIds) + ) + AND EXISTS ( + SELECT 1 FROM "memberSegmentsAgg" ms2 + WHERE ms2."memberId" = mtm."toMergeId" AND ms2."segmentId" IN (:segmentIds) + ) + ${memberFilter} + ${similarityFilter} + ${displayNameFilter} + `, { - replacements: { - tenantId: currentTenant.id, - }, + replacements, type: QueryTypes.SELECT, - transaction, }, ) - return sampleMemberIds.map((i) => i.id) + return totalCount[0]?.count || 0 } static async findMembersWithMergeSuggestions( - { limit = 20, offset = 0 }, + args: IFetchMemberMergeSuggestionArgs, options: IRepositoryOptions, ) { - const currentTenant = SequelizeRepository.getCurrentTenant(options) + const HIGH_CONFIDENCE_LOWER_BOUND = 0.9 + const MEDIUM_CONFIDENCE_LOWER_BOUND = 0.7 + + // Member segments are aggregated at each hierarchy level (group -> project -> subproject). + // Match the selected segment ID directly; do not expand to leaf subprojects. const segmentIds = SequelizeRepository.getSegmentIds(options) + if (segmentIds.length === 0) { + return args.countOnly + ? { count: '0' } + : { + rows: [{ members: [], similarity: 0 }], + count: 0, + limit: args.limit, + offset: args.offset, + } + } + + let similarityFilter = '' + const similarityConditions = [] + + for (const similarity of args.filter?.similarity || []) { + if (similarity === SimilarityScoreRange.HIGH) { + similarityConditions.push(`(mtm.similarity >= ${HIGH_CONFIDENCE_LOWER_BOUND})`) + } else if (similarity === SimilarityScoreRange.MEDIUM) { + similarityConditions.push( + `(mtm.similarity >= ${MEDIUM_CONFIDENCE_LOWER_BOUND} and mtm.similarity < ${HIGH_CONFIDENCE_LOWER_BOUND})`, + ) + } else if (similarity === SimilarityScoreRange.LOW) { + similarityConditions.push(`(mtm.similarity < ${MEDIUM_CONFIDENCE_LOWER_BOUND})`) + } + } + + if (similarityConditions.length > 0) { + similarityFilter = ` and (${similarityConditions.join(' or ')})` + } + + const memberFilter = args.filter?.memberId + ? ` and (mtm."memberId" = :memberId OR mtm."toMergeId" = :memberId)` + : '' + + const displayNameFilter = args.filter?.displayName + ? ` and (m."displayName" ilike :displayName OR m2."displayName" ilike :displayName)` + : '' + + let order = 'mtm."activityEstimate" desc, mtm.similarity desc, mtm."memberId", mtm."toMergeId"' + + if (args.orderBy?.length > 0) { + order = '' + for (const orderBy of args.orderBy) { + const [field, direction] = orderBy.split('_') + if ( + ['similarity', 'activityEstimate'].includes(field) && + ['asc', 'desc'].includes(direction.toLowerCase()) + ) { + order += `mtm.${field} ${direction}, ` + } + } + + order += 'mtm."memberId", mtm."toMergeId"' + } + + if (args.countOnly) { + const totalCount = await this.countMemberMergeSuggestions( + memberFilter, + similarityFilter, + displayNameFilter, + { + segmentIds, + displayName: args?.filter?.displayName ? `${args.filter.displayName}%` : undefined, + memberId: args?.filter?.memberId, + }, + options, + ) + + return { count: totalCount } + } + const mems = await options.database.sequelize.query( - `SELECT - "membersToMerge".id, - "membersToMerge"."toMergeId", - "membersToMerge"."total_count", - "membersToMerge"."similarity" - FROM - ( - SELECT DISTINCT ON (Greatest(Hashtext(Concat(mem.id, mtm."toMergeId")), Hashtext(Concat(mtm."toMergeId", mem.id)))) - mem.id, - mtm."toMergeId", - mem."joinedAt", - COUNT(*) OVER() AS total_count, - mtm."similarity" - FROM members mem - INNER JOIN "memberToMerge" mtm ON mem.id = mtm."memberId" - JOIN "memberSegments" ms ON ms."memberId" = mem.id - WHERE mem."tenantId" = :tenantId - AND ms."segmentId" IN (:segmentIds) - ) AS "membersToMerge" - ORDER BY - "membersToMerge"."joinedAt" DESC - LIMIT :limit OFFSET :offset - `, + ` + SELECT + mtm."memberId" AS id, + mtm."toMergeId", + mtm.similarity, + mtm."activityEstimate", + m."displayName" as "primaryDisplayName", + m.attributes->'avatarUrl'->>'default' as "primaryAvatarUrl", + m2."displayName" as "toMergeDisplayName", + m2.attributes->'avatarUrl'->>'default' as "toMergeAvatarUrl" + FROM "memberToMerge" mtm + JOIN members m ON m.id = mtm."memberId" + JOIN members m2 ON m2.id = mtm."toMergeId" + WHERE EXISTS ( + SELECT 1 FROM "memberSegmentsAgg" ms + WHERE ms."memberId" = mtm."memberId" AND ms."segmentId" IN (:segmentIds) + ) + AND EXISTS ( + SELECT 1 FROM "memberSegmentsAgg" ms2 + WHERE ms2."memberId" = mtm."toMergeId" AND ms2."segmentId" IN (:segmentIds) + ) + AND mtm.similarity IS NOT NULL + ${memberFilter} + ${similarityFilter} + ${displayNameFilter} + ORDER BY ${order} + LIMIT :limit + OFFSET :offset + `, { replacements: { - tenantId: currentTenant.id, segmentIds, - limit, - offset, + limit: args.limit, + offset: args.offset, + displayName: args?.filter?.displayName ? `${args.filter.displayName}%` : undefined, + memberId: args?.filter?.memberId, }, type: QueryTypes.SELECT, }, ) if (mems.length > 0) { - const memberPromises = [] - const toMergePromises = [] + let result + + if (args.detail) { + const memberPromises = [] + const toMergePromises = [] + + const findMemberInfo = async (memberId: string) => { + const qx = SequelizeRepository.getQueryExecutor(options) + + const [member, identities, aggregates, memberOrgs] = await Promise.all([ + findMemberById(qx, memberId, [ + MemberField.ID, + MemberField.DISPLAY_NAME, + MemberField.ATTRIBUTES, + MemberField.JOINED_AT, + ]), + fetchMemberIdentities(qx, memberId), + fetchAbsoluteMemberAggregates(qx, memberId), + fetchMemberOrganizations(qx, memberId), + ]) - for (const mem of mems) { - memberPromises.push(MemberRepository.findById(mem.id, options)) - toMergePromises.push(MemberRepository.findById(mem.toMergeId, options)) - } + const orgIds = memberOrgs.map((o) => o.organizationId) - const memberResults = await Promise.all(memberPromises) - const memberToMergeResults = await Promise.all(toMergePromises) + let orgExtraInfo = [] + let lfxMemberships = [] - const result = memberResults.map((i, idx) => ({ - members: [i, memberToMergeResults[idx]], - similarity: mems[idx].similarity, - })) - return { rows: result, count: mems[0].total_count / 2, limit, offset } - } + if (orgIds.length > 0) { + orgExtraInfo = await queryOrgs(qx, { + filter: { + [OrganizationField.ID]: { in: orgIds }, + }, + fields: [ + OrganizationField.ID, + OrganizationField.DISPLAY_NAME, + OrganizationField.LOGO, + ], + }) - return { rows: [{ members: [], similarity: 0 }], count: 0, limit, offset } - } + lfxMemberships = await findManyLfxMemberships(qx, { + organizationIds: orgIds, + }) + } - static async moveIdentitiesBetweenMembers( - fromMemberId: string, - toMemberId: string, - identitiesToMove: any[], - options: IRepositoryOptions, - ): Promise { - const transaction = SequelizeRepository.getTransaction(options) + return { + ...member, + identities, + ...{ + activityCount: aggregates?.activityCount, + lastActive: aggregates?.lastActive, + }, + organizations: memberOrgs.map((o) => ({ + ...orgExtraInfo.find((oei) => oei.id === o.organizationId), + lfxMembership: lfxMemberships.find((lm) => lm.organizationId === o.organizationId), + memberOrganizations: o, + })), + } + } - const seq = SequelizeRepository.getSequelize(options) + for (const mem of mems) { + memberPromises.push(findMemberInfo(mem.id)) + toMergePromises.push(findMemberInfo(mem.toMergeId)) + } - const tenant = SequelizeRepository.getCurrentTenant(options) + const memberResults: { id: string }[] = await Promise.all(memberPromises) + const memberToMergeResults = await Promise.all(toMergePromises) - const query = ` - update "memberIdentities" - set - "memberId" = :newMemberId - where - "tenantId" = :tenantId and - "memberId" = :oldMemberId and - platform = :platform and - username = :username; - ` + result = memberResults.map((i, idx) => ({ + members: [i, memberToMergeResults[idx]], + similarity: mems[idx].similarity, + })) + } else { + result = mems.map((i) => ({ + members: [ + { + id: i.id, + displayName: i.primaryDisplayName, + activityCount: i.primaryActivityCount, + avatarUrl: i.primaryAvatarUrl, + }, + { + id: i.toMergeId, + displayName: i.toMergeDisplayName, + activityCount: i.toActivityCount, + avatarUrl: i.toMergeAvatarUrl, + }, + ], + similarity: i.similarity, + })) + } - for (const identity of identitiesToMove) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [_, count] = await seq.query(query, { - replacements: { - tenantId: tenant.id, - oldMemberId: fromMemberId, - newMemberId: toMemberId, - platform: identity.platform, - username: identity.username, + const totalCount = await this.countMemberMergeSuggestions( + memberFilter, + similarityFilter, + displayNameFilter, + { + segmentIds, + memberId: args?.filter?.memberId, + displayName: args?.filter?.displayName ? `${args.filter.displayName}%` : undefined, }, - type: QueryTypes.UPDATE, - transaction, - }) + options, + ) - if (count !== 1) { - throw new Error('One row should be updated!') - } + return { rows: result, count: totalCount, limit: args.limit, offset: args.offset } } - } - - static async moveActivitiesBetweenMembers( - fromMemberId: string, - toMemberId: string, - options: IRepositoryOptions, - ): Promise { - const transaction = SequelizeRepository.getTransaction(options) - - const seq = SequelizeRepository.getSequelize(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - const query = ` - update activities set "memberId" = :toMemberId where "memberId" = :fromMemberId and "tenantId" = :tenantId; - ` - await seq.query(query, { - replacements: { - fromMemberId, - toMemberId, - tenantId: tenant.id, - }, - type: QueryTypes.UPDATE, - transaction, - }) + return { + rows: [{ members: [], similarity: 0 }], + count: 0, + limit: args.limit, + offset: args.offset, + } } static async addToMerge( @@ -412,7 +580,7 @@ class MemberRepository { const query = ` INSERT INTO "memberToMerge" ("memberId", "toMergeId", "similarity", "createdAt", "updatedAt") - VALUES ${placeholders.join(', ')}; + VALUES ${placeholders.join(', ')} on conflict do nothing; ` try { await seq.query(query, { @@ -428,45 +596,15 @@ class MemberRepository { } static async removeToMerge(id, toMergeId, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const returnPlain = false - - const member = await this.findById(id, options, returnPlain) - - const toMergeMember = await this.findById(toMergeId, options, returnPlain) + const qx = SequelizeRepository.getQueryExecutor(options) - await member.removeToMerge(toMergeMember, { transaction }) - - return this.findById(id, options) + await removeMemberToMerge(qx, id, toMergeId) } static async addNoMerge(id, toMergeId, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const returnPlain = false - - const member = await this.findById(id, options, returnPlain) - - const toMergeMember = await this.findById(toMergeId, options, returnPlain) - - await member.addNoMerge(toMergeMember, { transaction }) - - return this.findById(id, options) - } - - static async removeNoMerge(id, toMergeId, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const returnPlain = false - - const member = await this.findById(id, options, returnPlain) + const qx = SequelizeRepository.getQueryExecutor(options) - const toMergeMember = await this.findById(toMergeId, options, returnPlain) - - await member.removeNoMerge(toMergeMember, { transaction }) - - return this.findById(id, options) + await addMemberNoMerge(qx, id, toMergeId) } static async memberExists( @@ -477,8 +615,6 @@ class MemberRepository { ) { const transaction = SequelizeRepository.getTransaction(options) - const currentTenant = SequelizeRepository.getCurrentTenant(options) - const seq = SequelizeRepository.getSequelize(options) const usernames: string[] = [] @@ -497,17 +633,18 @@ class MemberRepository { ` select mi."memberId" from "memberIdentities" mi - where mi."tenantId" = :tenantId and - mi.platform = :platform and - mi.username in (:usernames) and + where mi.platform = :platform and + mi.type = :type and + mi.value in (:usernames) and + mi."deletedAt" is null and exists (select 1 from "memberSegments" ms where ms."memberId" = mi."memberId") `, { type: Sequelize.QueryTypes.SELECT, replacements: { - tenantId: currentTenant.id, platform, usernames, + type: MemberIdentityType.USERNAME, }, transaction, }, @@ -540,11 +677,11 @@ class MemberRepository { array_agg(username) as usernames from (select "memberId", platform, - username, + value as username, "createdAt", row_number() over (partition by "memberId", platform order by "createdAt" desc) = 1 as is_latest - from "memberIdentities" where "memberId" = :memberId) sub + from "memberIdentities" where "memberId" = :memberId and type = '${MemberIdentityType.USERNAME}' and "deletedAt" is null) sub group by "memberId", platform) mi group by mi."memberId"), member_organizations as ( @@ -576,7 +713,6 @@ class MemberRepository { m."attributes", m."emails", m."score", - m."lastEnriched", m."enrichedBy", m."contributions", m."reach", @@ -585,7 +721,6 @@ class MemberRepository { m."createdAt", m."updatedAt", m."deletedAt", - m."tenantId", m."createdById", m."updatedById", i.username, @@ -609,83 +744,165 @@ class MemberRepository { throw new Error('Invalid number of records found!') } - return records[0] + return records[0] as IDbMemberData } - static async update(id, data, options: IRepositoryOptions, doPopulateRelations = true) { + static MEMBER_UPDATE_COLUMNS = [ + 'displayName', + 'attributes', + 'emails', + 'contributions', + 'score', + 'reach', + 'importHash', + ] + + static isEqual = { + displayName: (a, b) => a === b, + attributes: (a, b) => lodash.isEqual(a, b), + emails: (a, b) => lodash.isEqual(a, b), + contributions: (a, b) => lodash.isEqual(a, b), + score: (a, b) => a === b, + reach: (a, b) => lodash.isEqual(a, b), + importHash: (a, b) => a === b, + } + + static async update( + id, + data, + options: IRepositoryOptions, + { + manualChange = false, + }: { + manualChange?: boolean + } = {}, + ) { const currentUser = SequelizeRepository.getCurrentUser(options) const transaction = SequelizeRepository.getTransaction(options) - const currentTenant = SequelizeRepository.getCurrentTenant(options) + const seq = SequelizeRepository.getSequelize(options) - let record = await options.database.member.findOne({ - where: { - id, - tenantId: currentTenant.id, - }, - transaction, - }) + const record = await captureApiChange( + options, + memberEditProfileAction(id, async (captureOldState, captureNewState) => { + const record = await options.database.member.findOne({ + where: { + id, + }, + transaction, + }) - if (!record) { - throw new Error404() - } + captureOldState(record.get({ plain: true })) - // exclude syncRemote attributes, since these are populated from memberSyncRemote table - if (data.attributes?.syncRemote) { - delete data.attributes.syncRemote - } + if (!record) { + throw new Error404() + } - record = await record.update( - { - ...lodash.pick(data, [ - 'displayName', - 'attributes', - 'emails', - 'lastEnriched', - 'enrichedBy', - 'contributions', - 'score', - 'reach', - 'joinedAt', - 'importHash', - ]), + // exclude syncRemote attributes, since these are populated from memberSyncRemote table + if (data.attributes?.syncRemote) { + delete data.attributes.syncRemote + } - updatedById: currentUser.id, - }, - { - transaction, - }, - ) + if (manualChange) { + const manuallyChangedFields: string[] = record.manuallyChangedFields || [] + + for (const column of this.MEMBER_UPDATE_COLUMNS) { + let changed = false + + // only check fields that are in the data object that will be updated + if (column in data) { + if ( + record[column] !== null && + column in data && + (data[column] === null || data[column] === undefined) + ) { + // column was removed in the update -> will be set to null by sequelize + changed = true + } else if ( + record[column] === null && + data[column] !== null && + data[column] !== undefined && + // also ignore empty arrays + (!Array.isArray(data[column]) || data[column].length > 0) + ) { + // column was null before now it's not anymore + changed = true + } else if ( + this.isEqual[column] && + this.isEqual[column](record[column], data[column]) === false + ) { + // column value has changed + changed = true + } + } - if (data.activities) { - await record.setActivities(data.activities || [], { - transaction, - }) - } + if (changed && !manuallyChangedFields.includes(column)) { + // handle attributes, keep each changed attribute separately + if (column === 'attributes') { + for (const key of Object.keys(data.attributes)) { + if (!record.attributes[key]) { + manuallyChangedFields.push(`attributes.${key}`) + } else if ( + !lodash.isEqual(record.attributes[key].default, data.attributes[key].default) + ) { + manuallyChangedFields.push(`attributes.${key}`) + } + } + } else { + manuallyChangedFields.push(column) + } + } + } - if (data.tags) { - await record.setTags(data.tags || [], { - transaction, - }) - } + data.manuallyChangedFields = manuallyChangedFields + } else { + // ignore columns that were manually changed + // by rewriting them with db data + const manuallyChangedFields: string[] = record.manuallyChangedFields || [] + for (const manuallyChangedColumn of manuallyChangedFields) { + if (data.attributes && manuallyChangedColumn.startsWith('attributes')) { + const attributeKey = manuallyChangedColumn.split('.')[1] + data.attributes[attributeKey] = record.attributes[attributeKey] + } else { + data[manuallyChangedColumn] = record[manuallyChangedColumn] + } + } - if (data.tasks) { - await record.setTasks(data.tasks || [], { - transaction, - }) - } + data.manuallyChangedFields = manuallyChangedFields + } - if (data.notes) { - await record.setNotes(data.notes || [], { - transaction, - }) - } + const updatedMember = { + ...lodash.pick(data, this.MEMBER_UPDATE_COLUMNS), + updatedById: currentUser.id, + manuallyChangedFields: data.manuallyChangedFields, + } + + await options.database.member.update(captureNewState(updatedMember), { + where: { + id: record.id, + }, + transaction, + }) + + return record + }), + !manualChange, // no need to track for audit if it's not a manual change + ) + + const qx = SequelizeRepository.getQueryExecutor(options) + const subprojectIds = await getSegmentSubprojectIds( + qx, + SequelizeRepository.getSegmentIds(options), + ) - await MemberRepository.updateMemberOrganizations( - record, + const memberService = new CommonMemberService(optionsQx(options), options.temporal, options.log) + + await memberService.updateMemberOrganizations( + record.id, data.organizations, data.organizationsReplace, + subprojectIds, options, ) @@ -706,36 +923,131 @@ class MemberRepository { } if (options.currentSegments && options.currentSegments.length > 0) { - await MemberRepository.includeMemberToSegments(record.id, options) + await includeMemberToSegments(qx, record.id, subprojectIds) } - const seq = SequelizeRepository.getSequelize(options) + // Before upserting identities, check if they already exist + const checkIdentities = [...(data.identitiesToCreate || []), ...(data.identitiesToUpdate || [])] + if (checkIdentities.length > 0) { + for (const i of checkIdentities) { + const query = ` + select "memberId" + from "memberIdentities" + where "platform" = :platform and + "value" = :value and + "type" = :type and + "deletedAt" is null + ` + + const data: IMemberIdentity[] = await seq.query(query, { + replacements: { + platform: i.platform, + value: i.value, + type: i.type || MemberIdentityType.USERNAME, + }, + type: QueryTypes.SELECT, + transaction, + }) + + if (data.length > 0 && data[0].memberId !== record.id) { + const memberSegment = (await seq.query( + ` + select distinct ms."segmentId", ms."memberId" + from "memberSegments" ms where ms."memberId" = :memberId + limit 1 + `, + { + replacements: { + memberId: data[0].memberId, + }, + type: QueryTypes.SELECT, + transaction, + }, + )) as any[] + + if (memberSegment.length === 0) { + throw new Error('Member with same identity already exists!') + } + + const segmentInfo = (await seq.query( + ` + select s.id, pd.id as "parentId", gpd.id as "grandParentId" + from segments s + inner join segments pd + on pd.slug = s."parentSlug" and pd."grandparentSlug" is null and + pd."parentSlug" is not null + inner join segments gpd on gpd.slug = s."grandparentSlug" and + gpd."grandparentSlug" is null and gpd."parentSlug" is null + where s.id = :segmentId; + `, + { + replacements: { + segmentId: memberSegment[0].segmentId, + }, + type: QueryTypes.SELECT, + transaction, + }, + )) as any[] + + throw new Error409( + options.language, + 'errors.alreadyExists', + // @ts-ignore + JSON.stringify({ + memberId: data[0].memberId, + grandParentId: segmentInfo[0].grandParentId, + }), + ) + } + } + } + + if (data.identitiesToCreate && data.identitiesToCreate.length > 0) { + for (const i of data.identitiesToCreate) { + await createMemberIdentity(qx, { + memberId: record.id, + platform: i.platform, + value: i.value, + type: i.type ? i.type : MemberIdentityType.USERNAME, + sourceId: i.sourceId || null, + integrationId: i.integrationId || null, + verified: i.verified !== undefined ? i.verified : !!manualChange, + source: i.source, + }) + } + } + + if (data.identitiesToUpdate && data.identitiesToUpdate.length > 0) { + for (const i of data.identitiesToUpdate) { + await updateVerifiedFlag(qx, { + memberId: record.id, + platform: i.platform, + value: i.value, + type: i.type ? i.type : MemberIdentityType.USERNAME, + verified: i.verified !== undefined ? i.verified : !!manualChange, + }) + } + } + + if (data.identitiesToDelete && data.identitiesToDelete.length > 0) { + for (const i of data.identitiesToDelete) { + await deleteMemberIdentities(qx, { + memberId: record.id, + platform: i.platform, + value: i.value, + type: i.type ? i.type : MemberIdentityType.USERNAME, + }) + } + } if (data.username) { data.username = mapUsernameToIdentities(data.username) const platforms = Object.keys(data.username) as PlatformType[] if (platforms.length > 0) { - const query = ` - insert into "memberIdentities"("memberId", platform, username, "sourceId", "tenantId", "integrationId") - values (:memberId, :platform, :username, :sourceId, :tenantId, :integrationId); - ` - const deleteQuery = ` - delete from "memberIdentities" - where ("memberId", "tenantId", "platform", "username") in - (select mi."memberId", mi."tenantId", mi."platform", mi."username" - from "memberIdentities" mi - join (select :memberId::uuid as memberid, - :tenantId::uuid as tenantid, - unnest(:platforms::text[]) as platform, - unnest(:usernames::text[]) as username) as combinations - on mi."memberId" = combinations.memberid - and mi."tenantId" = combinations.tenantid - and mi."platform" = combinations.platform - and mi."username" = combinations.username);` - const platformsToDelete: string[] = [] - const usernamesToDelete: string[] = [] + const valuesToDelete: string[] = [] + const typesToDelete: MemberIdentityType[] = [] for (const platform of platforms) { const identities = data.username[platform] @@ -743,58 +1055,57 @@ class MemberRepository { for (const identity of identities) { if (identity.delete) { platformsToDelete.push(identity.platform) - usernamesToDelete.push(identity.username) - } else { - await seq.query(query, { - replacements: { - memberId: record.id, - platform, - username: identity.username, - sourceId: identity.sourceId || null, - integrationId: identity.integrationId || null, - tenantId: currentTenant.id, - }, - type: QueryTypes.INSERT, - transaction, + if (identity.value) { + valuesToDelete.push(identity.value) + typesToDelete.push(identity.type) + } else { + valuesToDelete.push(identity.username) + typesToDelete.push(MemberIdentityType.USERNAME) + } + } else if ( + (identity.username && identity.username !== '') || + (identity.value && identity.value !== '') + ) { + await createMemberIdentity(qx, { + memberId: record.id, + platform, + value: identity.value ? identity.value : identity.username, + type: identity.type ? identity.type : MemberIdentityType.USERNAME, + sourceId: identity.sourceId || null, + integrationId: identity.integrationId || null, + verified: identity.verified !== undefined ? identity.verified : !!manualChange, + source: identity.source || 'ui', }) } } } if (platformsToDelete.length > 0) { - await seq.query(deleteQuery, { - replacements: { - tenantId: currentTenant.id, - memberId: record.id, - platforms: `{${platformsToDelete.join(',')}}`, - usernames: `{${usernamesToDelete.join(',')}}`, - }, - type: QueryTypes.DELETE, - transaction, + await deleteMemberIdentitiesByCombinations(qx, { + memberId: record.id, + platforms: platformsToDelete, + values: valuesToDelete, + types: typesToDelete, }) } } } - await this._createAuditLog(AuditLogRepository.UPDATE, record, data, options) - - return this.findById(record.id, options, true, doPopulateRelations) + return this.findById(record.id, options) } static async destroy(id, options: IRepositoryOptions, force = false) { const transaction = SequelizeRepository.getTransaction(options) - const currentTenant = SequelizeRepository.getCurrentTenant(options) - await MemberRepository.excludeMembersFromSegments([id], { ...options, transaction }) - const member = await this.findById(id, options, true, false) + const qx = SequelizeRepository.getQueryExecutor(options) + const memberSegments = await fetchAbsoluteMemberAggregates(qx, id) // if member doesn't belong to any other segment anymore, remove it - if (member.segments.length === 0) { + if (!memberSegments) { const record = await options.database.member.findOne({ where: { id, - tenantId: currentTenant.id, }, transaction, }) @@ -807,63 +1118,52 @@ class MemberRepository { force, transaction, }) - await this._createAuditLog(AuditLogRepository.DELETE, record, record, options) } } static async destroyBulk(ids, options: IRepositoryOptions, force = false) { const transaction = SequelizeRepository.getTransaction(options) - const currentTenant = SequelizeRepository.getCurrentTenant(options) - await MemberRepository.excludeMembersFromSegments(ids, { ...options, transaction }) await options.database.member.destroy({ where: { id: ids, - tenantId: currentTenant.id, }, force, transaction, }) } - static async getMemberSegments( + static async setAffiliations( memberId: string, + data: MemberSegmentAffiliation[], options: IRepositoryOptions, - ): Promise { - const transaction = SequelizeRepository.getTransaction(options) - const seq = SequelizeRepository.getSequelize(options) - const segmentRepository = new SegmentRepository(options) - - const query = ` - SELECT "segmentId" - FROM "memberSegments" - WHERE "memberId" = :memberId - ORDER BY "createdAt"; - ` + ): Promise { + const qx = optionsQx(options) + await captureApiChange( + options, + memberEditAffiliationsAction(memberId, async (captureOldState, captureNewState) => { + const oldOnes = await findMemberAffiliations(qx, memberId) + captureOldState( + oldOnes.map((item) => ({ + segmentId: item.segmentId, + organizationId: item.organizationId, + dateStart: item.dateStart, + dateEnd: item.dateEnd, + })), + ) - const data = await seq.query(query, { - replacements: { - memberId, - }, - type: QueryTypes.SELECT, - transaction, - }) + captureNewState(data) - const segmentIds = (data as any[]).map((item) => item.segmentId) - const segments = await segmentRepository.findInIds(segmentIds) + await deleteMemberSegmentAffiliations(qx, { memberId }) - return segments - } + if (data.length === 0) { + return + } - static async setAffiliations( - memberId: string, - data: MemberSegmentAffiliation[], - options: IRepositoryOptions, - ): Promise { - const affiliationRepository = new MemberSegmentAffiliationRepository(options) - await affiliationRepository.setForMember(memberId, data) - await MemberAffiliationRepository.update(memberId, options) + await insertMemberAffiliations(qx, memberId, data) + }), + ) } static async getAffiliations( @@ -874,21 +1174,22 @@ class MemberRepository { const seq = SequelizeRepository.getSequelize(options) const query = ` - select + select msa.id, - s.id as "segmentId", + s.id as "segmentId", s.slug as "segmentSlug", s.name as "segmentName", - s."parentName" as "segmentParentName", - o.id as "organizationId", + s."parentName" as "segmentParentName", + o.id as "organizationId", o."displayName" as "organizationName", o.logo as "organizationLogo", msa."dateStart" as "dateStart", msa."dateEnd" as "dateEnd" - from "memberSegmentAffiliations" msa + from "memberSegmentAffiliations" msa left join organizations o on o.id = msa."organizationId" join segments s on s.id = msa."segmentId" where msa."memberId" = :memberId + and msa."deletedAt" is null ` const data = await seq.query(query, { @@ -902,192 +1203,219 @@ class MemberRepository { return data as MemberSegmentAffiliationJoined[] } - static async getIdentities( - memberIds: string[], - options: IRepositoryOptions, - ): Promise> { - const results = new Map() - - const transaction = SequelizeRepository.getTransaction(options) - const seq = SequelizeRepository.getSequelize(options) - - const query = ` - select "memberId", platform, username, "sourceId", "integrationId", "createdAt" from "memberIdentities" where "memberId" in (:memberIds) - order by "createdAt" asc; - ` - - const data = await seq.query(query, { - replacements: { - memberIds, - }, - type: QueryTypes.SELECT, - transaction, - }) - - for (const id of memberIds) { - results.set(id, []) - } - - for (const res of data as any[]) { - const { memberId, platform, username, sourceId, integrationId, createdAt } = res - const identities = results.get(memberId) - - identities.push({ - platform, - username, - sourceId, - integrationId, - createdAt, - }) - } - - return results - } - static async findById( id, options: IRepositoryOptions, - returnPlain = true, - doPopulateRelations = true, - ignoreTenant = false, + { + segmentId, + }: { + segmentId?: string + } = {}, + include: Record = {}, + includeAllAttributes = false, ) { - const transaction = SequelizeRepository.getTransaction(options) - - const include = [ - { - model: options.database.organization, - attributes: ['id', 'displayName'], - as: 'organizations', - order: [['createdAt', 'ASC']], - through: { - attributes: ['memberId', 'organizationId', 'dateStart', 'dateEnd', 'title', 'source'], - where: { - deletedAt: null, - }, - }, - }, - { - model: options.database.segment, - as: 'segments', - through: { - attributes: [], - }, + let memberResponse = null + + const qx = optionsQx(options) + const bgQx = optionsQx({ ...options, transaction: null }) + + memberResponse = await queryMembersAdvanced(qx, bgQx, options.redis, { + filter: { id: { eq: id } }, + limit: 1, + offset: 0, + segmentId, + includeAllAttributes, + include: { + memberOrganizations: false, + lfxMemberships: true, + identities: false, + segments: true, + onlySubProjects: true, + maintainers: true, + ...include, }, - ] - - const where: any = { - id, - } - - if (!ignoreTenant) { - const currentTenant = SequelizeRepository.getCurrentTenant(options) - where.tenantId = currentTenant.id - } - - const record = await options.database.member.findOne({ - where, - include, - transaction, }) - if (!record) { - throw new Error404() - } + if (memberResponse.count === 0) { + // try it again without segment information (no aggregates) + // for members without activities + memberResponse = await queryMembersAdvanced(qx, bgQx, options.redis, { + filter: { id: { eq: id } }, + limit: 1, + offset: 0, + includeAllAttributes, + include: { + lfxMemberships: true, + segments: true, + maintainers: true, + ...include, + }, + }) - if (doPopulateRelations) { - return this._populateRelations(record, options, returnPlain) - } - const data = record.get({ plain: returnPlain }) + if (memberResponse.count === 0) { + throw new Error404() + } - MemberRepository.sortOrganizations(data.organizations) + memberResponse.rows[0].activityCount = 0 + memberResponse.rows[0].lastActive = null + memberResponse.rows[0].activityTypes = [] + memberResponse.rows[0].activeOn = [] + memberResponse.rows[0].averageSentiment = null + } - const identities = (await this.getIdentities([data.id], options)).get(data.id) + const [data] = memberResponse.rows + return data + } - data.username = {} - for (const identity of identities) { - if (data.username[identity.platform]) { - data.username[identity.platform].push(identity.username) + static getUsernameFromIdentities(identities: IMemberIdentity[]): IMemberUsername { + const username = {} + for (const identity of identities.filter((i) => i.type === MemberIdentityType.USERNAME)) { + if (username[identity.platform]) { + username[identity.platform].push(identity.value) } else { - data.username[identity.platform] = [identity.username] + username[identity.platform] = [identity.value] } } - data.affiliations = await MemberRepository.getAffiliations(id, options) - - return data - } - - static async filterIdInTenant(id, options: IRepositoryOptions) { - return lodash.get(await this.filterIdsInTenant([id], options), '[0]', null) + return username } - static async filterIdsInTenant(ids, options: IRepositoryOptions) { - if (!ids || !ids.length) { - return [] - } - + static async count(filter, options: IRepositoryOptions) { const transaction = SequelizeRepository.getTransaction(options) - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const where = { - id: { - [Op.in]: ids, + return options.database.member.count({ + where: { + ...filter, }, - tenantId: currentTenant.id, - } - - const records = await options.database.member.findAll({ - attributes: ['id'], - where, transaction, }) - - return records.map((record) => record.id) } - static async count(filter, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) + static async countMembersPerSegment(options: IRepositoryOptions, segmentIds: string[]) { + const qx = SequelizeRepository.getQueryExecutor(options) + const result = await queryActivityRelations(qx, { + filter: { + and: [ + { + segmentId: { + in: segmentIds, + }, + }, + ], + }, + countOnly: true, + }) - const tenant = SequelizeRepository.getCurrentTenant(options) + return result.count + } - return options.database.member.count({ - where: { - ...filter, - tenantId: tenant.id, + static async countMembers(options: IRepositoryOptions, segmentIds: string[]) { + const countQuery = ` + SELECT + COUNT(DISTINCT msa."memberId") AS "totalCount", + msa."segmentId" + FROM "memberSegmentsAgg" msa + WHERE msa."segmentId" IN (:segmentIds) + GROUP BY msa."segmentId"; + ` + + const seq = SequelizeRepository.getSequelize(options) + return seq.query(countQuery, { + replacements: { + segmentIds, }, - transaction, + type: QueryTypes.SELECT, }) } - static async findAndCountActiveOpensearch( - filter: IActiveMemberFilter, - limit: number, - offset: number, - orderBy: string, - options: IRepositoryOptions, - attributesSettings = [] as AttributeData[], - segments: string[] = [], - ): Promise> { - const tenant = SequelizeRepository.getCurrentTenant(options) + public static QUERY_FILTER_COLUMN_MAP: Map = + new Map([ + // id fields + ['id', { name: 'm.id' }], + ['segmentId', { name: 'msa."segmentId"' }], + + // member fields + ['displayName', { name: 'm."displayName"' }], + ['reach', { name: 'm.reach' }], + ['joinedAt', { name: 'm."joinedAt"' }], + ['jobTitle', { name: `m.attributes -> 'jobTitle' ->> 'default'` }], + [ + 'numberOfOpenSourceContributions', + { + name: "CASE WHEN jsonb_typeof(m.contributions) = 'array' THEN jsonb_array_length(m.contributions) ELSE 0 END", + }, + ], + ['isBot', { name: `COALESCE((m.attributes -> 'isBot' ->> 'default')::BOOLEAN, FALSE)` }], + [ + 'isTeamMember', + { name: `COALESCE((m.attributes -> 'isTeamMember' ->> 'default')::BOOLEAN, FALSE)` }, + ], + [ + 'isOrganization', + { name: `COALESCE((m.attributes -> 'isOrganization' ->> 'default')::BOOLEAN, FALSE)` }, + ], - const segmentsEnabled = await isFeatureEnabled(FeatureFlag.SEGMENTS, options) + // member agg fields + ['lastActive', { name: 'msa."lastActive"' }], + ['identityPlatforms', { name: 'msa."activeOn"' }], + ['score', { name: 'm.score' }], + ['averageSentiment', { name: 'msa."averageSentiment"' }], + ['activityTypes', { name: 'msa."activityTypes"' }], + ['activeOn', { name: 'msa."activeOn"' }], + ['activityCount', { name: 'msa."activityCount"' }], - let originalSegment + // others + ['organizations', { name: 'mo."organizationId"', queryable: false }], - if (segmentsEnabled) { - if (segments.length !== 1) { - throw new Error400( - `This operation can have exactly one segment. Found ${segments.length} segments.`, - ) - } - originalSegment = segments[0] + // fields for querying + ['attributes', { name: 'm.attributes' }], + ]) + + static async findAndCountAll( + { + filter = {} as any, + search = null, + limit = 20, + offset = 0, + orderBy = 'joinedAt_DESC', + segmentId = undefined, + countOnly = false, + fields = [...MemberRepository.QUERY_FILTER_COLUMN_MAP.keys()], + include = { + identities: true, + segments: false, + onlySubProjects: false, + lfxMemberships: false, + memberOrganizations: false, + attributes: true, + maintainers: true, + } as { + identities?: boolean + segments?: boolean + onlySubProjects?: boolean + lfxMemberships?: boolean + memberOrganizations?: boolean + attributes?: boolean + maintainers?: boolean + }, + attributesSettings = [] as AttributeData[], + }, + options: IRepositoryOptions, + ) { + if (!attributesSettings) { + attributesSettings = (await MemberAttributeSettingsRepository.findAndCountAll({}, options)) + .rows + } - const segmentRepository = new SegmentRepository(options) + const qx = SequelizeRepository.getQueryExecutor(options) - const segment = await segmentRepository.findById(originalSegment) + const withAggregates = !!segmentId + let segment + if (withAggregates) { + segment = await new SegmentRepository(options).findById(segmentId) if (segment === null) { + options.log.info('No segment found for member query. Returning empty result.') return { rows: [], count: 0, @@ -1095,1504 +1423,291 @@ class MemberRepository { offset, } } - - if (SegmentRepository.isProjectGroup(segment)) { - segments = (segment as SegmentProjectGroupNestedData).projects.reduce((acc, p) => { - acc.push(...p.subprojects.map((sp) => sp.id)) - return acc - }, []) - } else if (SegmentRepository.isProject(segment)) { - segments = (segment as SegmentProjectNestedData).subprojects.map((sp) => sp.id) - } else { - segments = [originalSegment] - } - } else { - originalSegment = (await new SegmentRepository(options).getDefaultSegment()).id } - const activityPageSize = 10000 - let activityOffset = 0 + const params = { + limit, + offset, + segmentId: segment?.id, + } - const activityQuery = { - query: { - bool: { - must: [ - { - range: { - date_timestamp: { - gte: filter.activityTimestampFrom, - lte: filter.activityTimestampTo, - }, - }, - }, + const filterString = RawQueryParser.parseFilters( + filter, + new Map( + [...MemberRepository.QUERY_FILTER_COLUMN_MAP.entries()].map(([key, { name }]) => [ + key, + name, + ]), + ), + [ + { + property: 'attributes', + column: 'm.attributes', + attributeInfos: [ + ...attributesSettings, { - term: { - uuid_tenantId: tenant.id, - }, + name: 'jobTitle', + type: MemberAttributeType.STRING, }, ], }, - }, - aggs: { - group_by_member: { - terms: { - field: 'uuid_memberId', - size: 10000000, - }, - aggs: { - activity_count: { - value_count: { - field: 'uuid_id', - }, - }, - active_days_count: { - cardinality: { - field: 'date_timestamp', - script: { - source: "doc['date_timestamp'].value.toInstant().toEpochMilli()/86400000", - }, - }, - }, - active_members_bucket_sort: { - bucket_sort: { - sort: [{ activity_count: { order: 'desc' } }], - size: activityPageSize, - from: activityOffset, - }, - }, - }, + { + property: 'username', + column: 'aggs.username', + attributeInfos: ALL_PLATFORM_TYPES.map((p) => ({ + name: p, + type: MemberAttributeType.STRING, + })), }, - }, - size: 0, - } as any - - if (filter.platforms) { - const subQueries = filter.platforms.map((p) => ({ match_phrase: { keyword_platform: p } })) + ], + params, + { pgPromiseFormat: true }, + ) - activityQuery.query.bool.must.push({ - bool: { - should: subQueries, - }, - }) - } + const order = (function prepareOrderBy( + orderBy = withAggregates ? 'activityCount_DESC' : 'id_DESC', + ) { + const orderSplit = orderBy.split('_') - if (filter.activityIsContribution === true) { - activityQuery.query.bool.must.push({ - term: { - bool_isContribution: true, - }, - }) + const orderField = MemberRepository.QUERY_FILTER_COLUMN_MAP.get(orderSplit[0])?.name + if (!orderField) { + return withAggregates ? 'msa."activityCount" DESC' : 'm.id DESC' + } + const orderDirection = ['DESC', 'ASC'].includes(orderSplit[1]) ? orderSplit[1] : 'DESC' + + return `${orderField} ${orderDirection}` + })(orderBy) + + const withSearch = !!search + let searchCTE = '' + let searchJoin = '' + let searchFilter = '1=1' + + if (withSearch) { + search = search.toLowerCase() + searchCTE = ` + , + member_search AS ( + SELECT + DISTINCT "memberId" + FROM "memberIdentities" mi + where (verified and lower("value") like '%${search}%') and "deletedAt" is null + ) + ` + searchJoin = ` LEFT JOIN member_search ms ON ms."memberId" = m.id ` + searchFilter = ` + (ms."memberId" IS NOT NULL OR lower(m."displayName") like '%${search}%') + ` } - if (segmentsEnabled) { - const subQueries = segments.map((s) => ({ term: { uuid_segmentId: s } })) - - activityQuery.query.bool.must.push({ - bool: { - should: subQueries, - }, - }) - } - - const direction = orderBy.split('_')[1].toLowerCase() === 'desc' ? 'desc' : 'asc' - if (orderBy.startsWith('activityCount')) { - activityQuery.aggs.group_by_member.aggs.active_members_bucket_sort.bucket_sort.sort = [ - { activity_count: { order: direction } }, - ] - } else if (orderBy.startsWith('activeDaysCount')) { - activityQuery.aggs.group_by_member.aggs.active_members_bucket_sort.bucket_sort.sort = [ - { active_days_count: { order: direction } }, - ] - } else { - throw new Error(`Invalid order by: ${orderBy}`) - } - - const memberIds = [] - let memberMap = {} - let activities - - do { - activities = await options.opensearch.search({ - index: OpenSearchIndex.ACTIVITIES, - body: activityQuery, - }) - - memberIds.push(...activities.body.aggregations.group_by_member.buckets.map((b) => b.key)) - - memberMap = { - ...memberMap, - ...activities.body.aggregations.group_by_member.buckets.reduce((acc, b) => { - acc[b.key] = { - activityCount: b.activity_count, - activeDaysCount: b.active_days_count, - } - - return acc - }, {}), - } - - activityOffset += activityPageSize - - // update page - activityQuery.aggs.group_by_member.aggs.active_members_bucket_sort.bucket_sort.from = - activityOffset - } while (activities.body.aggregations.group_by_member.buckets.length === activityPageSize) - - if (memberIds.length === 0) { - return { - rows: [], - count: 0, - limit, - offset, - } - } - - const memberQueryPayload = { - and: [ - { - id: { - in: memberIds, - }, - }, - ], - } as any - - if (filter.isBot === true) { - memberQueryPayload.and.push({ - isBot: { - eq: true, - }, - }) - } else if (filter.isBot === false) { - memberQueryPayload.and.push({ - isBot: { - not: true, - }, - }) - } - - if (filter.isTeamMember === true) { - memberQueryPayload.and.push({ - isTeamMember: { - eq: true, - }, - }) - } else if (filter.isTeamMember === false) { - memberQueryPayload.and.push({ - isTeamMember: { - not: true, - }, - }) - } - - if (filter.isOrganization === true) { - memberQueryPayload.and.push({ - isOrganization: { - eq: true, - }, - }) - } else if (filter.isOrganization === false) { - memberQueryPayload.and.push({ - isOrganization: { - not: true, - }, - }) - } - - // to retain the sort came from activity query - const customSortFunction = { - _script: { - type: 'number', - script: { - lang: 'painless', - source: ` - def memberId = doc['uuid_memberId'].value; - return params.memberIds.indexOf(memberId); - `, - params: { - memberIds: memberIds.map((i) => `${i}`), - }, - }, - order: 'asc', - }, - } - - const members = await this.findAndCountAllOpensearch( - { - filter: memberQueryPayload, - attributesSettings, - segments: [originalSegment], - countOnly: false, - limit, - offset, - customSortFunction, - }, - options, - ) - - return { - rows: members.rows.map((m) => { - m.activityCount = memberMap[m.id].activityCount.value - m.activeDaysCount = memberMap[m.id].activeDaysCount.value - return m - }), - count: members.count, - offset, - limit, - } - } - - static async findAndCountActive( - filter: IActiveMemberFilter, - limit: number, - offset: number, - orderBy: string, - options: IRepositoryOptions, - ): Promise> { - const tenant = SequelizeRepository.getCurrentTenant(options) - const segmentIds = SequelizeRepository.getSegmentIds(options) - - const transaction = SequelizeRepository.getTransaction(options) - - const seq = SequelizeRepository.getSequelize(options) - - const conditions = ['m."tenantId" = :tenantId', 'ms."segmentId" in (:segmentIds)'] - const parameters: any = { - tenantId: tenant.id, - segmentIds, - periodStart: filter.activityTimestampFrom, - periodEnd: filter.activityTimestampTo, - } - - if (filter.isTeamMember === true) { - conditions.push("COALESCE((m.attributes->'isTeamMember'->'default')::boolean, false) = true") - } else if (filter.isTeamMember === false) { - conditions.push("COALESCE((m.attributes->'isTeamMember'->'default')::boolean, false) = false") - } - - if (filter.isBot === true) { - conditions.push("COALESCE((m.attributes->'isBot'->'default')::boolean, false) = true") - } else if (filter.isBot === false) { - conditions.push("COALESCE((m.attributes->'isBot'->'default')::boolean, false) = false") - } - - if (filter.isOrganization === true) { - conditions.push( - "COALESCE((m.attributes->'isOrganization'->'default')::boolean, false) = true", - ) - } else if (filter.isOrganization === false) { - conditions.push( - "COALESCE((m.attributes->'isOrganization'->'default')::boolean, false) = false", - ) - } - - const activityConditions = ['1=1'] - - if (filter.platforms && filter.platforms.length > 0) { - activityConditions.push('platform in (:platforms)') - parameters.platforms = filter.platforms - } - - if (filter.activityIsContribution) { - activityConditions.push('"isContribution" = (:isContribution)') - parameters.isContribution = filter.activityIsContribution - } - - const conditionsString = conditions.join(' and ') - const activityConditionsString = activityConditions.join(' and ') - - const direction = orderBy.split('_')[1].toLowerCase() === 'desc' ? 'desc' : 'asc' - let orderString: string - if (orderBy.startsWith('activityCount')) { - orderString = `ad."activityCount" ${direction}` - } else if (orderBy.startsWith('activeDaysCount')) { - orderString = `ad."activeDaysCount" ${direction}` - } else { - throw new Error(`Invalid order by: ${orderBy}`) - } - - const limitCondition = `limit ${limit} offset ${offset}` - const query = ` - WITH - orgs AS ( - SELECT mo."memberId", JSON_AGG(ROW_TO_JSON(o.*)) AS organizations - FROM "memberOrganizations" mo - INNER JOIN organizations o ON mo."organizationId" = o.id - WHERE mo."deletedAt" IS NULL - GROUP BY mo."memberId" - ), - activity_data AS ( - SELECT - "memberId", - COUNT(id) AS "activityCount", - COUNT(DISTINCT timestamp::DATE) AS "activeDaysCount" - FROM activities - WHERE ${activityConditionsString} - AND timestamp >= :periodStart - AND timestamp < :periodEnd - GROUP BY "memberId" - ), - identities AS ( - SELECT - mi."memberId", - ARRAY_AGG(DISTINCT mi.platform) AS identities, - JSONB_OBJECT_AGG(mi.platform, mi.usernames) AS username - FROM ( - SELECT - "memberId", - platform, - ARRAY_AGG(username) AS usernames - FROM ( - SELECT - "memberId", - platform, - username, - "createdAt", - ROW_NUMBER() OVER (PARTITION BY "memberId", platform ORDER BY "createdAt" DESC) = - 1 AS is_latest - FROM "memberIdentities" - WHERE "tenantId" = :tenantId - ) sub - WHERE is_latest - GROUP BY "memberId", platform - ) mi - GROUP BY mi."memberId" - ) - SELECT - m.id, - m."displayName", - i.username, - i.identities, - m.attributes, - ad."activityCount", - ad."activeDaysCount", - m."joinedAt", - COALESCE(o.organizations, JSON_BUILD_ARRAY()) AS organizations, - COUNT(*) OVER () AS "totalCount" - FROM members m - INNER JOIN activity_data ad ON ad."memberId" = m.id - INNER JOIN identities i ON i."memberId" = m.id - LEFT JOIN orgs o ON o."memberId" = m.id - JOIN "memberSegments" ms ON ms."memberId" = m.id - WHERE ${conditionsString} - ORDER BY ${orderString} - ${limitCondition}; - ` - - options.log.debug( - { query, filter, orderBy, limit, offset, test: orderBy.split('_')[1].toLowerCase() }, - 'Active members query!', - ) - - const results = await seq.query(query, { - replacements: parameters, - type: QueryTypes.SELECT, - transaction, - }) - - if (results.length === 0) { - return { - rows: [], - count: 0, - offset, - limit, - } - } - - const count = parseInt((results[0] as any).totalCount, 10) - const rows: IActiveMemberData[] = results.map((r) => { - const row = r as any - return { - id: row.id, - displayName: row.displayName, - username: row.username, - attributes: row.attributes, - organizations: row.organizations, - activityCount: parseInt(row.activityCount, 10), - activeDaysCount: parseInt(row.activeDaysCount, 10), - joinedAt: row.joinedAt, - } - }) - - return { - rows, - count, - offset, - limit, - } - } - - public static MEMBER_QUERY_FILTER_COLUMN_MAP: Map = new Map([ - ['isOrganization', "coalesce((m.attributes -> 'isOrganization' -> 'default')::boolean, false)"], - ['isTeamMember', "coalesce((m.attributes -> 'isTeamMember' -> 'default')::boolean, false)"], - ['isBot', "coalesce((m.attributes -> 'isBot' -> 'default')::boolean, false)"], - ['activeOn', 'aggs."activeOn"'], - ['activityCount', 'aggs."activityCount"'], - ['activityTypes', 'aggs."activityTypes"'], - ['activeDaysCount', 'aggs."activeDaysCount"'], - ['lastActive', 'aggs."lastActive"'], - ['averageSentiment', 'aggs."averageSentiment"'], - ['identities', 'aggs.identities'], - ['reach', "(m.reach -> 'total')::integer"], - ['numberOfOpenSourceContributions', 'coalesce(jsonb_array_length(m.contributions), 0)'], - - ['id', 'm.id'], - ['displayName', 'm."displayName"'], - ['tenantId', 'm."tenantId"'], - ['score', 'm.score'], - ['lastEnriched', 'm."lastEnriched"'], - ['joinedAt', 'm."joinedAt"'], - ['importHash', 'm."importHash"'], - ['createdAt', 'm."createdAt"'], - ['updatedAt', 'm."updatedAt"'], - ['emails', 'm.emails'], - ]) - - static async countMembersPerSegment(options: IRepositoryOptions, segmentIds: string[]) { - const countResults = await MemberRepository.countMembers(options, segmentIds) - return countResults.reduce((acc, curr: any) => { - acc[curr.segmentId] = parseInt(curr.totalCount, 10) - return acc - }, {}) - } - - static async countMembers( - options: IRepositoryOptions, - segmentIds: string[], - filterString: string = '1=1', - params: any = {}, - ) { - const countQuery = ` - WITH - member_tags AS ( - SELECT - mt."memberId", - JSONB_AGG(t.id) AS all_ids - FROM "memberTags" mt - INNER JOIN members m ON mt."memberId" = m.id - JOIN "memberSegments" ms ON ms."memberId" = m.id - INNER JOIN tags t ON mt."tagId" = t.id - WHERE m."tenantId" = :tenantId - AND m."deletedAt" IS NULL - AND ms."segmentId" IN (:segmentIds) - AND t."tenantId" = :tenantId - AND t."deletedAt" IS NULL - GROUP BY mt."memberId" - ), - member_organizations AS ( - SELECT - mo."memberId", - JSONB_AGG(o.id) AS all_ids - FROM "memberOrganizations" mo - INNER JOIN members m ON mo."memberId" = m.id - INNER JOIN organizations o ON mo."organizationId" = o.id - JOIN "memberSegments" ms ON ms."memberId" = m.id - JOIN "organizationSegments" os ON o.id = os."organizationId" - WHERE m."tenantId" = :tenantId - AND m."deletedAt" IS NULL - AND ms."segmentId" IN (:segmentIds) - AND o."tenantId" = :tenantId - AND o."deletedAt" IS NULL - AND os."segmentId" IN (:segmentIds) - AND mo."deletedAt" IS NULL - GROUP BY mo."memberId" - ) - SELECT - COUNT(m.id) AS "totalCount", - ms."segmentId" - FROM members m - JOIN "memberSegments" ms ON ms."memberId" = m.id - INNER JOIN "memberActivityAggregatesMVs" aggs ON aggs.id = m.id AND aggs."segmentId" = ms."segmentId" - LEFT JOIN member_tags mt ON m.id = mt."memberId" - LEFT JOIN member_organizations mo ON m.id = mo."memberId" - WHERE m."deletedAt" IS NULL - AND m."tenantId" = :tenantId - AND ms."segmentId" IN (:segmentIds) - AND ${filterString} - GROUP BY ms."segmentId"; - ` - - const seq = SequelizeRepository.getSequelize(options) - return seq.query(countQuery, { - replacements: { - tenantId: options.currentTenant.id, - segmentIds, - ...params, - }, - type: QueryTypes.SELECT, - }) - } - - static async findAndCountAllv2( - { - filter = {} as any, - limit = 20, - offset = 0, - orderBy = 'joinedAt_DESC', - countOnly = false, - attributesSettings = [] as AttributeData[], - }, - options: IRepositoryOptions, - ): Promise> { - const tenant = SequelizeRepository.getCurrentTenant(options) - const segmentIds = SequelizeRepository.getSegmentIds(options) - const seq = SequelizeRepository.getSequelize(options) - - const params: any = { - tenantId: tenant.id, - segmentIds, - limit, - offset, - } - - let orderByString = '' - const orderByParts = orderBy.split('_') - const direction = orderByParts[1].toLowerCase() - switch (orderByParts[0]) { - case 'joinedAt': - orderByString = 'm."joinedAt"' - break - case 'displayName': - orderByString = 'm."displayName"' - break - case 'reach': - orderByString = "(m.reach ->> 'total')::int" - break - case 'score': - orderByString = 'm.score' - break - case 'lastActive': - orderByString = 'aggs."lastActive"' - break - case 'averageSentiment': - orderByString = 'aggs."averageSentiment"' - break - case 'activeDaysCount': - orderByString = 'aggs."activeDaysCount"' - break - case 'activityCount': - orderByString = 'aggs."activityCount"' - break - case 'numberOfOpenSourceContributions': - orderByString = '"numberOfOpenSourceContributions"' - break - - default: - throw new Error(`Invalid order by: ${orderBy}!`) - } - orderByString = `${orderByString} ${direction}` - - const jsonColumnInfos: JsonColumnInfo[] = [ - { - property: 'attributes', - column: 'm.attributes', - attributeInfos: attributesSettings, - }, - { - property: 'username', - column: 'aggs.username', - attributeInfos: ALL_PLATFORM_TYPES.map((p) => ({ - name: p, - type: MemberAttributeType.STRING, - })), - }, - { - property: 'tags', - column: 'mt.all_ids', - attributeInfos: [], - }, - { - property: 'organizations', - column: 'mo.all_ids', - attributeInfos: [], - }, - ] - - let filterString = RawQueryParser.parseFilters( - filter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - jsonColumnInfos, - params, - ) - if (filterString.trim().length === 0) { - filterString = '1=1' - } - - const query = ` - WITH - to_merge_data AS ( - SELECT mtm."memberId", STRING_AGG(DISTINCT mtm."toMergeId"::TEXT, ',') AS to_merge_ids - FROM "memberToMerge" mtm - INNER JOIN members m ON mtm."memberId" = m.id - INNER JOIN members m2 ON mtm."toMergeId" = m2.id - JOIN "memberSegments" ms ON m.id = ms."memberId" - JOIN "memberSegments" ms2 ON m2.id = ms2."memberId" - WHERE m."tenantId" = :tenantId - AND m2."deletedAt" IS NULL - AND ms."segmentId" IN (:segmentIds) - AND ms2."segmentId" IN (:segmentIds) - GROUP BY mtm."memberId" - ), - no_merge_data AS ( - SELECT mnm."memberId", STRING_AGG(DISTINCT mnm."noMergeId"::TEXT, ',') AS no_merge_ids - FROM "memberNoMerge" mnm - INNER JOIN members m ON mnm."memberId" = m.id - INNER JOIN members m2 ON mnm."noMergeId" = m2.id - JOIN "memberSegments" ms ON m.id = ms."memberId" - JOIN "memberSegments" ms2 ON m2.id = ms2."memberId" - WHERE m."tenantId" = :tenantId - AND m2."deletedAt" IS NULL - AND ms."segmentId" IN (:segmentIds) - AND ms2."segmentId" IN (:segmentIds) - GROUP BY mnm."memberId" - ), - member_tags AS ( - SELECT - mt."memberId", - JSON_AGG( - DISTINCT JSONB_BUILD_OBJECT( - 'id', t.id, - 'name', t.name - ) - ) AS all_tags, - JSONB_AGG(t.id) AS all_ids - FROM "memberTags" mt - INNER JOIN members m ON mt."memberId" = m.id - JOIN "memberSegments" ms ON ms."memberId" = m.id - INNER JOIN tags t ON mt."tagId" = t.id - WHERE m."tenantId" = :tenantId - AND m."deletedAt" IS NULL - AND ms."segmentId" IN (:segmentIds) - AND t."tenantId" = :tenantId - AND t."deletedAt" IS NULL - GROUP BY mt."memberId" - ), - member_organizations AS ( - SELECT - mo."memberId", - JSON_AGG( - ROW_TO_JSON(o.*) - ) AS all_organizations, - JSONB_AGG(o.id) AS all_ids - FROM "memberOrganizations" mo - INNER JOIN members m ON mo."memberId" = m.id - INNER JOIN organizations o ON mo."organizationId" = o.id - JOIN "memberSegments" ms ON ms."memberId" = m.id - JOIN "organizationSegments" os ON o.id = os."organizationId" - WHERE m."tenantId" = :tenantId - AND m."deletedAt" IS NULL - AND ms."segmentId" IN (:segmentIds) - AND o."tenantId" = :tenantId - AND o."deletedAt" IS NULL - AND os."segmentId" IN (:segmentIds) - AND ms."segmentId" = os."segmentId" - AND mo."deletedAt" IS NULL - GROUP BY mo."memberId" - ), - aggs AS ( - SELECT - id, - MAX("lastActive") AS "lastActive", - ARRAY(SELECT DISTINCT UNNEST(ARRAY_AGG(identities))) AS identities, - jsonb_merge_agg(username) AS username, - SUM("activityCount") AS "activityCount", - ARRAY(SELECT DISTINCT UNNEST(array_accum("activityTypes"))) AS "activityTypes", - ARRAY(SELECT DISTINCT UNNEST(array_accum("activeOn"))) AS "activeOn", - SUM("activeDaysCount") AS "activeDaysCount", - ROUND(SUM("averageSentiment" * "activityCount") / SUM("activityCount"), 2) AS "averageSentiment", - ARRAY_AGG(DISTINCT "segmentId") AS "segmentIds" - FROM "memberActivityAggregatesMVs" - WHERE "segmentId" IN (:segmentIds) - GROUP BY id - ) - SELECT - m.id, - m."displayName", - m.attributes, - m.emails, - m."tenantId", - m.score, - m."lastEnriched", - m.contributions, - m."joinedAt", - m."importHash", - m."createdAt", - m."updatedAt", - m.reach, - tmd.to_merge_ids AS "toMergeIds", - nmd.no_merge_ids AS "noMergeIds", - aggs.username, - aggs.identities, - aggs."activeOn", - aggs."activityCount", - aggs."activityTypes", - aggs."activeDaysCount", - aggs."lastActive", - aggs."averageSentiment", - aggs."segmentIds", - COALESCE(mt.all_tags, JSON_BUILD_ARRAY()) AS tags, - COALESCE(mo.all_organizations, JSON_BUILD_ARRAY()) AS organizations, - COALESCE(JSONB_ARRAY_LENGTH(m.contributions), 0) AS "numberOfOpenSourceContributions" - FROM members m - INNER JOIN aggs ON aggs.id = m.id - LEFT JOIN to_merge_data tmd ON m.id = tmd."memberId" - LEFT JOIN no_merge_data nmd ON m.id = nmd."memberId" - LEFT JOIN member_tags mt ON m.id = mt."memberId" - LEFT JOIN member_organizations mo ON m.id = mo."memberId" - WHERE m."deletedAt" IS NULL - AND m."tenantId" = :tenantId - AND ${filterString} - ORDER BY ${orderByString} - LIMIT :limit OFFSET :offset; - ` - - const sumMemberCount = (countResults) => - countResults.map((row) => parseInt(row.totalCount, 10)).reduce((a, b) => a + b, 0) - - if (countOnly) { - const countResults = await MemberRepository.countMembers( - options, - segmentIds, - filterString, - params, - ) - const count = sumMemberCount(countResults) - - return { - rows: [], - count, - limit, - offset, - } - } - - const [results, countResults] = await Promise.all([ - seq.query(query, { - replacements: params, - type: QueryTypes.SELECT, - }), - MemberRepository.countMembers(options, segmentIds, filterString, params), - ]) - - const memberIds = results.map((r) => (r as any).id) - if (memberIds.length > 0) { - const lastActivities = await seq.query( - ` - WITH - raw_data AS ( - SELECT *, ROW_NUMBER() OVER (PARTITION BY "memberId" ORDER BY timestamp DESC) AS rn - FROM activities - WHERE "tenantId" = :tenantId - AND "memberId" IN (:memberIds) - AND "segmentId" IN (:segmentIds) - ) - SELECT * - FROM raw_data - WHERE rn = 1; - `, - { - replacements: { - tenantId: tenant.id, - segmentIds, - memberIds, - }, - type: QueryTypes.SELECT, - }, - ) - - for (const row of results) { - const r = row as any - r.lastActivity = lastActivities.find((a) => (a as any).memberId === r.id) - if (r.lastActivity) { - r.lastActivity.display = ActivityDisplayService.getDisplayOptions( - r.lastActivity, - SegmentRepository.getActivityTypes(options), - [ActivityDisplayVariant.SHORT, ActivityDisplayVariant.CHANNEL], - ) - } - } - } - - const count = sumMemberCount(countResults) - - return { - rows: results, - count, - limit, - offset, - } - } - - static async findAndCountAllOpensearch( - { - filter = {} as any, - limit = 20, - offset = 0, - orderBy = 'joinedAt_DESC', - countOnly = false, - attributesSettings = [] as AttributeData[], - segments = [] as string[], - customSortFunction = undefined, - }, - options: IRepositoryOptions, - ): Promise> { - const tenant = SequelizeRepository.getCurrentTenant(options) - - const segmentsEnabled = await isFeatureEnabled(FeatureFlag.SEGMENTS, options) - - const segment = segments[0] - - const translator = FieldTranslatorFactory.getTranslator( - OpenSearchIndex.MEMBERS, - attributesSettings, - [ - 'default', - 'custom', - 'crowd', - 'enrichment', - ...(await TenantRepository.getAvailablePlatforms(options.currentTenant.id, options)).map( - (p) => p.platform, - ), - ], - ) - - const parsed = OpensearchQueryParser.parse( - { filter, limit, offset, orderBy }, - OpenSearchIndex.MEMBERS, - translator, - ) - - // add tenant filter to parsed query - parsed.query.bool.must.push({ - term: { - uuid_tenantId: tenant.id, - }, - }) - - if (segmentsEnabled) { - // add segment filter - parsed.query.bool.must.push({ - term: { - uuid_segmentId: segment, - }, - }) - } - - if (customSortFunction) { - parsed.sort = customSortFunction - } - - if (filter.organizations && filter.organizations.length > 0) { - parsed.query.bool.must = parsed.query.bool.must.filter( - (d) => d.nested?.query?.term?.['nested_organizations.uuid_id'] === undefined, - ) - - // add organizations filter manually for now - - for (const organizationId of filter.organizations) { - parsed.query.bool.must.push({ - nested: { - path: 'nested_organizations', - query: { - bool: { - must: [ - { - term: { - 'nested_organizations.uuid_id': organizationId, - }, - }, - { - bool: { - must_not: { - exists: { - field: 'nested_organizations.obj_memberOrganizations.date_dateEnd', - }, - }, - }, - }, - ], - }, - }, - }, - }) - } - } - - const countResponse = await options.opensearch.count({ - index: OpenSearchIndex.MEMBERS, - body: { query: parsed.query }, - }) - - if (countOnly) { - return { - rows: [], - count: countResponse.body.count, - limit, - offset, - } - } - - const response = await options.opensearch.search({ - index: OpenSearchIndex.MEMBERS, - body: parsed, - }) - - const translatedRows = response.body.hits.hits.map((o) => - translator.translateObjectToCrowd(o._source), - ) - - for (const row of translatedRows) { - const identities = [] - const username: {} = {} - - for (const identity of row.identities) { - identities.push(identity.platform) - if (identity.platform in username) { - username[identity.platform].push(identity.username) - } else { - username[identity.platform] = [identity.username] - } - } - - row.identities = identities - row.username = username - row.activeDaysCount = parseInt(row.activeDaysCount, 10) - row.activityCount = parseInt(row.activityCount, 10) - } - - const memberIds = translatedRows.map((r) => r.id) - if (memberIds.length > 0) { - const seq = SequelizeRepository.getSequelize(options) - const segmentIds = segments - - const lastActivities = await seq.query( - ` - WITH - raw_data AS ( - SELECT *, ROW_NUMBER() OVER (PARTITION BY "memberId" ORDER BY timestamp DESC) AS rn - FROM activities - WHERE "tenantId" = :tenantId - AND "memberId" IN (:memberIds) - AND "segmentId" IN (:segmentIds) - ) - SELECT * - FROM raw_data - WHERE rn = 1; - `, - { - replacements: { - tenantId: tenant.id, - segmentIds, - memberIds, - }, - type: QueryTypes.SELECT, - }, - ) - - for (const row of translatedRows) { - const r = row as any - r.lastActivity = lastActivities.find((a) => (a as any).memberId === r.id) - if (r.lastActivity) { - r.lastActivity.display = ActivityDisplayService.getDisplayOptions( - r.lastActivity, - SegmentRepository.getActivityTypes(options), - [ActivityDisplayVariant.SHORT, ActivityDisplayVariant.CHANNEL], - ) - } - } - } - - return { rows: translatedRows, count: countResponse.body.count, limit, offset } - } - - static async findAndCountAll( - { - filter = {} as any, - advancedFilter = null as any, - limit = 0, - offset = 0, - orderBy = '', - attributesSettings = [] as AttributeData[], - exportMode = false, - }, - - options: IRepositoryOptions, - ) { - let customOrderBy: Array = [] - const include = [ - { - model: options.database.memberActivityAggregatesMV, - attributes: [], - required: true, - as: 'memberActivityAggregatesMVs', - }, - { - model: options.database.member, - as: 'toMerge', - attributes: [], - through: { - attributes: [], - }, - }, - { - model: options.database.member, - as: 'noMerge', - attributes: [], - through: { - attributes: [], - }, - }, - ] - - customOrderBy = customOrderBy.concat( - SequelizeFilterUtils.customOrderByIfExists('activityCount', orderBy), - ) - - customOrderBy = customOrderBy.concat( - SequelizeFilterUtils.customOrderByIfExists('activeDaysCount', orderBy), - ) - - customOrderBy = customOrderBy.concat( - SequelizeFilterUtils.customOrderByIfExists('lastActive', orderBy), - ) - - customOrderBy = customOrderBy.concat( - SequelizeFilterUtils.customOrderByIfExists('averageSentiment', orderBy), - ) - - customOrderBy = customOrderBy.concat( - SequelizeFilterUtils.customOrderByIfExists('numberOfOpenSourceContributions', orderBy), - ) - - if (orderBy.includes('reach')) { - customOrderBy = customOrderBy.concat([ - Sequelize.literal(`("member".reach->'total')::int`), - orderBy.split('_')[1], - ]) - } - - if (!advancedFilter) { - advancedFilter = { and: [] } - - if (filter) { - if (filter.id) { - advancedFilter.and.push({ id: filter.id }) - } - - if (filter.platform) { - advancedFilter.and.push({ - platform: { - jsonContains: filter.platform, - }, - }) - } - - if (filter.tags) { - advancedFilter.and.push({ - tags: filter.tags, - }) - } - - if (filter.organizations) { - advancedFilter.and.push({ - organizations: filter.organizations, - }) - } - - // TODO: member identitites FIX - if (filter.username) { - advancedFilter.and.push({ username: { jsonContains: filter.username } }) - } - - if (filter.displayName) { - advancedFilter.and.push({ - displayName: { - textContains: filter.displayName, - }, - }) - } - - if (filter.emails) { - advancedFilter.and.push({ - emails: { - contains: filter.emails, - }, - }) - } - - if (filter.scoreRange) { - const [start, end] = filter.scoreRange - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - score: { - gte: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - score: { - lte: end, - }, - }) - } - } - - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - createdAt: { - gte: start, - }, - }) - } + const createQuery = (fields) => ` + WITH member_orgs AS ( + SELECT + "memberId", + ARRAY_AGG("organizationId")::TEXT[] AS "organizationId" + FROM "memberOrganizations" + WHERE "deletedAt" IS NULL + GROUP BY 1 + ) + ${searchCTE} + SELECT + ${fields} + FROM members m + ${ + withAggregates + ? ` JOIN "memberSegmentsAgg" msa ON msa."memberId" = m.id AND msa."segmentId" = $(segmentId)` + : '' + } + LEFT JOIN member_orgs mo ON mo."memberId" = m.id + ${searchJoin} + WHERE (${filterString}) + AND (${searchFilter}) + ` - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - createdAt: { - lte: end, - }, - }) - } - } + if (countOnly) { + return { + rows: [], + count: parseInt((await qx.selectOne(createQuery('COUNT(*)'), params)).count, 10), + limit, + offset, + } + } - if (filter.reachRange) { - const [start, end] = filter.reachRange - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - reach: { - gte: start, - }, - }) - } + const results = await Promise.all([ + qx.select( + ` + ${createQuery( + (function prepareFields(fields) { + return `${fields + .map((f) => { + const mappedField = MemberRepository.QUERY_FILTER_COLUMN_MAP.get(f) + if (!mappedField) { + throw new Error400(options.language, `Invalid field: ${f}`) + } + + return { + alias: f, + ...mappedField, + } + }) + .filter((mappedField) => mappedField.queryable !== false) + .filter((mappedField) => { + if (!withAggregates && mappedField.name.includes('msa.')) { + return false + } + if (!include.memberOrganizations && mappedField.name.includes('mo.')) { + return false + } + if (!include.attributes && mappedField.name === 'm.attributes') { + return false + } + return true + }) + .map((mappedField) => `${mappedField.name} AS "${mappedField.alias}"`) + .join(',\n')}` + })(fields), + )} + ORDER BY ${order} NULLS LAST + LIMIT $(limit) + OFFSET $(offset) + `, + params, + ), + qx.selectOne(createQuery('COUNT(*)'), params), + ]) - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - reach: { - lte: end, - }, - }) - } - } + const rows = results[0] + const count = parseInt(results[1].count, 10) - if (filter.activityCountRange) { - const [start, end] = filter.activityCountRange - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - activityCount: { - gte: start, - }, - }) - } + const memberIds = rows.map((org) => org.id) + if (memberIds.length === 0) { + return { rows: [], count, limit, offset } + } - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - activityCount: { - lte: end, + if (include.memberOrganizations) { + const memberOrganizations = await fetchManyMemberOrgs(qx, memberIds) + const orgIds = uniq( + memberOrganizations.reduce((acc, mo) => { + acc.push(...mo.organizations.map((o) => o.organizationId)) + return acc + }, []), + ) + const orgExtra = orgIds.length + ? await queryOrgs(qx, { + filter: { + [OrganizationField.ID]: { + in: orgIds, }, - }) - } - } - - if (filter.activityTypes) { - advancedFilter.and.push({ - activityTypes: { - overlap: filter.activityTypes.split(','), }, + fields: [OrganizationField.ID, OrganizationField.DISPLAY_NAME, OrganizationField.LOGO], }) - } - if (filter.activeDaysCountRange) { - const [start, end] = filter.activeDaysCountRange - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - activeDaysCount: { - gte: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - activeDaysCount: { - lte: end, - }, - }) - } - } - - if (filter.joinedAtRange) { - const [start, end] = filter.joinedAtRange - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - joinedAt: { - gte: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - joinedAt: { - lte: end, - }, - }) - } - } - - if (filter.lastActiveRange) { - const [start, end] = filter.lastActiveRange - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - lastActive: { - gte: start, - }, - }) - } + : [] + + rows.forEach((member) => { + member.organizations = ( + memberOrganizations.find((o) => o.memberId === member.id)?.organizations || [] + ).map((o) => ({ + id: o.organizationId, + ...orgExtra.find((odn) => odn.id === o.organizationId), + memberOrganizations: o, + })) + + // sort organizations + MemberRepository.sortOrganizations(member.organizations) + }) + } + if (include.lfxMemberships) { + const lfxMemberships = await findManyLfxMemberships(qx, { + organizationIds: uniq( + rows.reduce((acc, r) => { + if (r.organizations) { + acc.push(...r.organizations.map((o) => o.id)) + } + return acc + }, []), + ), + }) - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - lastActive: { - lte: end, - }, - }) - } + rows.forEach((member) => { + if (member.organizations) { + member.organizations.forEach((o) => { + o.lfxMembership = lfxMemberships.find((m) => m.organizationId === o.id) + }) } + }) + } + if (include.identities) { + const identities = await fetchManyMemberIdentities(qx, memberIds) - if (filter.averageSentimentRange) { - const [start, end] = filter.averageSentimentRange - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - averageSentiment: { - gte: start, - }, - }) - } + rows.forEach((member) => { + member.identities = identities.find((i) => i.memberId === member.id)?.identities || [] + }) + } + if (include.segments) { + const memberSegments = await fetchManyMemberSegments(qx, memberIds) + const segmentIds = uniq( + memberSegments.reduce((acc, ms) => { + acc.push(...ms.segments.map((s) => s.segmentId)) + return acc + }, []), + ) + const segmentsInfo = await fetchManySegments(qx, segmentIds) - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - averageSentiment: { - lte: end, - }, - }) - } - } + rows.forEach((member) => { + member.segments = (memberSegments.find((i) => i.memberId === member.id)?.segments || []) + .map((segment) => { + const segmentInfo = segmentsInfo.find((s) => s.id === segment.segmentId) - if (filter.numberOfOpenSourceContributionsRange) { - const [start, end] = filter.numberOfOpenSourceContributionsRange - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - numberOfOpenSourceContributions: { - gte: start, - }, - }) - } + // include only subprojects if flag is set + if (include.onlySubProjects && segmentInfo?.type !== SegmentType.SUB_PROJECT) { + return null + } - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - numberOfOpenSourceContributions: { - lte: end, - }, - }) + return { + id: segment.segmentId, + name: segmentInfo?.name, + activityCount: segment.activityCount, + } + }) + .filter(Boolean) + }) + } + if (include.maintainers) { + const maintainerRoles = await findMaintainerRoles(qx, memberIds) + const segmentIds = uniq(maintainerRoles.map((m) => m.segmentId)) + const segmentsInfo = await fetchManySegments(qx, segmentIds) + + const groupedMaintainers = groupBy(maintainerRoles, (m) => m.memberId) + rows.forEach((member) => { + member.maintainerRoles = (groupedMaintainers.get(member.id) || []).map((role) => { + const segmentInfo = segmentsInfo.find((s) => s.id === role.segmentId) + return { + ...role, + segmentName: segmentInfo?.name, } - } - } + }) + }) } - const { - dynamicAttributesDefaultNestedFields, - dynamicAttributesPlatformNestedFields, - dynamicAttributesProjection, - } = await MemberRepository.getDynamicAttributesLiterals(attributesSettings, options) - - const activityCount = Sequelize.literal(`"memberActivityAggregatesMVs"."activityCount"`) - const activityTypes = Sequelize.literal(`"memberActivityAggregatesMVs"."activityTypes"`) - const activeDaysCount = Sequelize.literal(`"memberActivityAggregatesMVs"."activeDaysCount"`) - const lastActive = Sequelize.literal(`"memberActivityAggregatesMVs"."lastActive"`) - const activeOn = Sequelize.literal(`"memberActivityAggregatesMVs"."activeOn"`) - - const averageSentiment = Sequelize.literal(`"memberActivityAggregatesMVs"."averageSentiment"`) - const identities = Sequelize.literal(`"memberActivityAggregatesMVs"."identities"`) - const username = Sequelize.literal(`"memberActivityAggregatesMVs"."username"`) - - const toMergeArray = Sequelize.literal(`STRING_AGG( distinct "toMerge"."id"::text, ',')`) - const noMergeArray = Sequelize.literal(`STRING_AGG( distinct "noMerge"."id"::text, ',')`) - - const numberOfOpenSourceContributions = Sequelize.literal( - `COALESCE(jsonb_array_length("member"."contributions"), 0)`, - ) - - const parser = new QueryParser( - { - nestedFields: { - ...dynamicAttributesDefaultNestedFields, - reach: 'reach.total', - username: 'username.asString', - }, - aggregators: { - activityCount, - activityTypes, - activeDaysCount, - lastActive, - averageSentiment, - activeOn, - identities, - username, - numberOfOpenSourceContributions, - ...dynamicAttributesPlatformNestedFields, - 'reach.total': Sequelize.literal(`("member".reach->'total')::int`), - 'username.asString': Sequelize.literal( - `CAST("memberActivityAggregatesMVs"."username" AS TEXT)`, - ), - ...SequelizeFilterUtils.getNativeTableFieldAggregations( - [ - 'id', - 'attributes', - 'displayName', - 'emails', - 'score', - 'lastEnriched', - 'enrichedBy', - 'contributions', - 'joinedAt', - 'importHash', - 'reach', - 'createdAt', - 'updatedAt', - 'createdById', - 'updatedById', - ], - 'member', - ), - }, - manyToMany: { - tags: { - table: 'members', - model: 'member', - relationTable: { - name: 'memberTags', - from: 'memberId', - to: 'tagId', - }, - }, - organizations: { - table: 'members', - model: 'member', - relationTable: { - name: 'memberOrganizations', - from: 'memberId', - to: 'organizationId', - }, - }, - segments: { - table: 'members', - model: 'member', - relationTable: { - name: 'memberSegments', - from: 'memberId', - to: 'segmentId', - }, - }, - }, - // TODO: member identitites FIX - // customOperators: { - // username: { - // model: 'member', - // column: 'username', - // }, - // platform: { - // model: 'member', - // column: 'username', - // }, - // }, - exportMode, - }, - options, - ) - - const parsed: QueryOutput = parser.parse({ - filter: advancedFilter, - orderBy: orderBy || ['joinedAt_DESC'], - limit, - offset, - }) - - let order = parsed.order + if (memberIds.length > 0) { + const activityTypes = SegmentRepository.getActivityTypes(options) + const lastActivities = await getLastActivitiesForMembers(qx, memberIds, activityTypes, [ + segmentId, + ]) - if (customOrderBy.length > 0) { - order = [customOrderBy] + rows.forEach((r) => { + r.lastActivity = lastActivities.find((a) => a.memberId === r.id) + if (r.lastActivity) { + r.lastActivity.display = ActivityDisplayService.getDisplayOptions( + r.lastActivity, + SegmentRepository.getActivityTypes(options), + [ActivityDisplayVariant.SHORT, ActivityDisplayVariant.CHANNEL], + ) + } + }) } - let { - rows, - count, // eslint-disable-line prefer-const - } = await options.database.member.findAndCountAll({ - where: parsed.where ? parsed.where : {}, - having: parsed.having ? parsed.having : {}, - include, - attributes: [ - ...SequelizeFilterUtils.getLiteralProjections( - [ - 'id', - 'attributes', - 'displayName', - 'emails', - 'tenantId', - 'score', - 'lastEnriched', - 'enrichedBy', - 'contributions', - 'joinedAt', - 'importHash', - 'createdAt', - 'updatedAt', - 'createdById', - 'updatedById', - 'reach', - ], - 'member', - ), - [activeOn, 'activeOn'], - [identities, 'identities'], - [username, 'username'], - [activityCount, 'activityCount'], - [activityTypes, 'activityTypes'], - [activeDaysCount, 'activeDaysCount'], - [lastActive, 'lastActive'], - [averageSentiment, 'averageSentiment'], - [toMergeArray, 'toMergeIds'], - [noMergeArray, 'noMergeIds'], - [numberOfOpenSourceContributions, 'numberOfOpenSourceContributions'], - ...dynamicAttributesProjection, - ], - limit: parsed.limit || 50, - offset: offset ? Number(offset) : 0, - order, - subQuery: false, - group: [ - 'member.id', - 'memberActivityAggregatesMVs.activeOn', - 'memberActivityAggregatesMVs.activityCount', - 'memberActivityAggregatesMVs.activityTypes', - 'memberActivityAggregatesMVs.activeDaysCount', - 'memberActivityAggregatesMVs.lastActive', - 'memberActivityAggregatesMVs.averageSentiment', - 'memberActivityAggregatesMVs.username', - 'memberActivityAggregatesMVs.identities', - 'toMerge.id', - ], - distinct: true, - }) - - rows = await this._populateRelationsForRows(rows, attributesSettings, exportMode) - - return { - rows, - count: count.length, - limit: limit ? Number(limit) : 50, - offset: offset ? Number(offset) : 0, - } + return { rows, count, limit, offset } } /** @@ -2609,9 +1724,7 @@ class MemberRepository { const availableDynamicAttributePlatformKeys = [ 'default', 'custom', - ...(await TenantRepository.getAvailablePlatforms(options.currentTenant.id, options)).map( - (p) => p.platform, - ), + ...(await TenantRepository.getAvailablePlatforms(options)).map((p) => p.platform), ] const dynamicAttributesDefaultNestedFields = memberAttributeSettings.reduce( @@ -2674,13 +1787,7 @@ class MemberRepository { } static async findAllAutocomplete(query, limit, options: IRepositoryOptions) { - const tenant = SequelizeRepository.getCurrentTenant(options) - - const whereAnd: Array = [ - { - tenantId: tenant.id, - }, - ] + const whereAnd: Array = [{}] if (query) { whereAnd.push({ @@ -2694,300 +1801,49 @@ class MemberRepository { }) } - const where = { [Op.and]: whereAnd } - - const records = await options.database.member.findAll({ - attributes: ['id', 'displayName', 'attributes', 'emails'], - where, - limit: limit ? Number(limit) : undefined, - order: [['displayName', 'ASC']], - include: [ - { - model: options.database.organization, - attributes: ['id', 'displayName'], - as: 'organizations', - }, - { - model: options.database.segment, - as: 'segments', - where: { - id: SequelizeRepository.getSegmentIds(options), - }, - }, - ], - }) - - return records.map((record) => ({ - id: record.id, - label: record.displayName, - email: record.emails.length > 0 ? record.emails[0] : null, - avatar: record.attributes?.avatarUrl?.default || null, - organizations: record.organizations.map((org) => ({ - id: org.id, - name: org.name, - })), - })) - } - - static async mergeSuggestionsByUsername( - numberOfHours, - options: IRepositoryOptions, - ): Promise { - const transaction = SequelizeRepository.getTransaction(options) - - const seq = SequelizeRepository.getSequelize(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - const segmentIds = SequelizeRepository.getSegmentIds(options) - - const query = ` - -- Define a CTE named "new_members" to get members created in the last 2 hours with a specific tenantId - WITH new_members AS ( - SELECT m.id, m."tenantId" - FROM members m - JOIN "memberSegments" ms ON ms."memberId" = m.id - WHERE m."createdAt" >= now() - INTERVAL :numberOfHours - AND m."tenantId" = :tenantId - AND ms."segmentId" IN (:segmentIds) - ), - -- Define a CTE named "identity_join" to find members with the same usernames across different platforms - identity_join AS ( - SELECT - m1.id AS m1_id, - m2.id AS m2_id, - i1.platform AS i1_platform, - i2.platform AS i2_platform, - i1.username AS i1_username, - i2.username AS i2_username - FROM new_members m1 - -- Join memberIdentities and members to get related records - JOIN "memberIdentities" i1 ON m1.id = i1."memberId" - JOIN "memberIdentities" i2 ON i1.username = i2.username AND i1.platform <> i2.platform - JOIN members m2 ON m2.id = i2."memberId" - -- Filter out records where tenantId is different and memberIds are the same - WHERE m1."tenantId" = m2."tenantId" - AND m1.id <> m2.id - -- Filter out records present in memberToMerge table - AND NOT EXISTS ( - SELECT 1 - FROM "memberToMerge" - WHERE ( - "memberId" = m1.id - AND "toMergeId" = m2.id - ) OR ( - "memberId" = m2.id - AND "toMergeId" = m1.id - ) - ) - -- Filter out records present in memberNoMerge table - AND NOT EXISTS ( - SELECT 1 - FROM "memberNoMerge" - WHERE ( - "memberId" = m1.id - AND "noMergeId" = m2.id - ) OR ( - "memberId" = m2.id - AND "noMergeId" = m1.id - ) - ) - ) - -- Select everything from the final CTE "identity_join" - SELECT * - FROM identity_join;` - - const suggestions = await seq.query(query, { - replacements: { - tenantId: tenant.id, - segmentIds, - numberOfHours: `${numberOfHours} hours`, - }, - type: QueryTypes.SELECT, - transaction, - }) - - return suggestions.map((suggestion: any) => ({ - members: [suggestion.m1_id, suggestion.m2_id], - // 100% confidence only from emails - similarity: 0.95, - })) - } - - static async mergeSuggestionsByEmail( - numberOfHours, - options: IRepositoryOptions, - ): Promise { - const transaction = SequelizeRepository.getTransaction(options) - - const seq = SequelizeRepository.getSequelize(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - const segmentIds = SequelizeRepository.getSegmentIds(options) - - const query = ` - -- Define a CTE named "new_members" to get members created in the last 7 days with a specific tenantId and their emails - WITH new_members AS ( - SELECT m.id, m."tenantId", m.emails - FROM members m - JOIN "memberSegments" ms ON ms."memberId" = m.id - WHERE m."createdAt" >= now() - INTERVAL :numberOfHours - AND m."tenantId" = :tenantId - AND ms."segmentId" IN (:segmentIds) - ), - -- Define a CTE named "email_join" to find overlapping emails across different members - email_join AS ( - SELECT - m1.id AS m1_id, -- Member 1 ID - m2.id AS m2_id, -- Member 2 ID - m1.emails AS m1_emails, -- Member 1 emails - m2.emails AS m2_emails -- Member 2 emails - FROM new_members m1 - -- Join the members table on the tenantId field and ensuring the IDs are different - JOIN members m2 ON m1."tenantId" = m2."tenantId" - AND m1.id <> m2.id - -- Filter for overlapping emails - WHERE m1.emails && m2.emails - -- Exclude pairs that are already in the memberToMerge table - AND NOT EXISTS ( - SELECT 1 - FROM "memberToMerge" - WHERE ( - "memberId" = m1.id - AND "toMergeId" = m2.id - ) OR ( - "memberId" = m2.id - AND "toMergeId" = m1.id - ) - ) - -- Exclude pairs that are in the memberNoMerge table - AND NOT EXISTS ( - SELECT 1 - FROM "memberNoMerge" - WHERE ( - "memberId" = m1.id - AND "noMergeId" = m2.id - ) OR ( - "memberId" = m2.id - AND "noMergeId" = m1.id - ) - ) - ) - -- Select all columns from the email_join CTE - SELECT * - FROM email_join;` - - const suggestions = await seq.query(query, { - replacements: { - tenantId: tenant.id, - segmentIds, - numberOfHours: `${numberOfHours} hours`, - }, - type: QueryTypes.SELECT, - transaction, - }) - return suggestions.map((suggestion: any) => ({ - members: [suggestion.m1_id, suggestion.m2_id], - similarity: 1, - })) - } - - static async mergeSuggestionsBySimilarity( - numberOfHours, - options: IRepositoryOptions, - ): Promise { - const transaction = SequelizeRepository.getTransaction(options) - - const seq = SequelizeRepository.getSequelize(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - const segmentIds = SequelizeRepository.getSegmentIds(options) - - const query = ` - -- Define a CTE named "new_members" to get members created in the last 7 days with a specific tenantId - WITH new_members AS ( - SELECT m.* - FROM members m - JOIN "memberSegments" ms ON ms."memberId" = m.id - WHERE m."createdAt" >= now() - INTERVAL :numberOfHours - AND m."tenantId" = :tenantId - AND ms."segmentId" IN (:segmentIds) - LIMIT 1000 - ), - -- Define a CTE named "identity_join" to find similar identities across platforms - identity_join AS ( - -- Select distinct pairs of memberIds and relevant information, along with the similarity score - SELECT DISTINCT ON(m1_id, m2_id) - m1.id AS m1_id, - m2.id AS m2_id, - similarity(i1.username, i2.username) AS similarity - FROM new_members m1 - -- Join memberIdentities and members to get related records - JOIN "memberIdentities" i1 ON m1.id = i1."memberId" - JOIN "memberIdentities" i2 ON i1.platform <> i2.platform - JOIN members m2 ON m2.id = i2."memberId" - -- Filter out records where tenantId is different and memberIds are the same - WHERE m1."tenantId" = m2."tenantId" - AND m1.id <> m2.id - -- Consider only records with similarity > 0.5 (adjust this threshold as needed) - AND similarity(i1.username, i2.username) > 0.5 - -- Order by similarity descending to get the most similar records first - ORDER BY m1_id, m2_id, similarity DESC - ), - -- Define a CTE named "exclude_already_processed" to remove the already processed members - exclude_already_processed AS ( - SELECT * - FROM identity_join - -- Filter out records present in memberToMerge table - WHERE NOT EXISTS ( - SELECT 1 - FROM "memberToMerge" - WHERE ( - "memberId" = identity_join.m1_id - AND "toMergeId" = identity_join.m2_id - ) OR ( - "memberId" = identity_join.m2_id - AND "toMergeId" = identity_join.m1_id - ) - -- Filter out records present in memberNoMerge table - ) AND NOT EXISTS ( - SELECT 1 - FROM "memberNoMerge" - WHERE ( - "memberId" = identity_join.m1_id - AND "noMergeId" = identity_join.m2_id - ) OR ( - "memberId" = identity_join.m2_id - AND "noMergeId" = identity_join.m1_id - ) - ) - ) - -- Select everything from the final CTE "exclude_already_processed" - SELECT * - FROM exclude_already_processed;` + const where = { [Op.and]: whereAnd } - const suggestions = await seq.query(query, { - replacements: { - tenantId: tenant.id, - segmentIds, - numberOfHours: `${numberOfHours} hours`, - }, - type: QueryTypes.SELECT, - transaction, + const qx = SequelizeRepository.getQueryExecutor(options) + const currentSegments = SequelizeRepository.getSegmentIds(options) + + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) + + const records = await options.database.member.findAll({ + attributes: ['id', 'displayName', 'attributes'], + where, + limit: limit ? Number(limit) : undefined, + order: [['displayName', 'ASC']], + include: [ + { + model: options.database.organization, + attributes: ['id', 'displayName'], + as: 'organizations', + }, + { + model: options.database.segment, + as: 'segments', + where: { + id: subprojectIds, + }, + }, + ], }) - return suggestions.map((suggestion: any) => ({ - members: [suggestion.m1_id, suggestion.m2_id], - // 100% confidence only from emails - similarity: suggestion.similarity > 0.95 ? 0.95 : suggestion.similarity, + return records.map((record) => ({ + id: record.id, + label: record.displayName, + avatar: record.attributes?.avatarUrl?.default || null, + organizations: record.organizations.map((org) => ({ + id: org.id, + name: org.name, + })), })) } - static async addToWeakIdentities( + static async addAsUnverifiedIdentity( memberIds: string[], - username: string, + value: string, + type: MemberIdentityType, platform: string, options: IRepositoryOptions, ): Promise { @@ -2995,52 +1851,24 @@ class MemberRepository { const seq = SequelizeRepository.getSequelize(options) - const tenant = SequelizeRepository.getCurrentTenant(options) - const query = ` - update members - set "weakIdentities" = "weakIdentities" || jsonb_build_object('username', :username, 'platform', :platform)::jsonb - where id in (:memberIds) - and not exists (select 1 - from jsonb_array_elements("weakIdentities") as wi - where wi ->> 'username' = :username - and wi ->> 'platform' = :platform); + insert into "memberIdentities"("memberId", platform, type, value, "tenantId", verified) + values(:memberId, :platform, :type, :value, :tenantId, false) + on conflict do nothing; ` - await seq.query(query, { - replacements: { - memberIds, - username, - platform, - tenantId: tenant.id, - }, - type: QueryTypes.UPDATE, - transaction, - }) - } - - static async _createAuditLog(action, record, data, options: IRepositoryOptions) { - if (log) { - let values = {} - - if (data) { - values = { - ...record.get({ plain: true }), - activitiesIds: data.activities, - tagsIds: data.tags, - noMergeIds: data.noMerge, - } - } - - await AuditLogRepository.log( - { - entityName: 'member', - entityId: record.id, - action, - values, + for (const memberId of memberIds) { + await seq.query(query, { + replacements: { + memberId, + value, + type, + platform, + tenantId: DEFAULT_TENANT_ID, }, - options, - ) + type: QueryTypes.INSERT, + transaction, + }) } } @@ -3049,13 +1877,9 @@ class MemberRepository { return rows } - // No need for lazyloading tags for integrations or microservices if ( - (KUBE_MODE && - (SERVICE === ServiceType.NODEJS_WORKER || SERVICE === ServiceType.JOB_GENERATOR) && - !exportMode) || - process.env.SERVICE === 'integrations' || - process.env.SERVICE === 'microservices-nodejs' + (KUBE_MODE && SERVICE === ServiceType.JOB_GENERATOR && !exportMode) || + process.env.SERVICE === 'integrations' ) { return rows.map((record) => { const plainRecord = record.get({ plain: true }) @@ -3104,353 +1928,12 @@ class MemberRepository { plainRecord.organizations = await record.getOrganizations({ joinTableAttributes: [], }) - plainRecord.tags = await record.getTags({ - joinTableAttributes: [], - }) - if (exportMode) { - plainRecord.notes = await record.getNotes({ - joinTableAttributes: [], - }) - } return plainRecord }), ) } - /** - * Fill a record with the relations and files (if any) - * @param record Record to get relations and files for - * @param options IRepository options - * @param returnPlain If true: return object, otherwise return model - * @returns The model/object with filled relations and files - */ - static async _populateRelations(record, options: IRepositoryOptions, returnPlain = true) { - if (!record) { - return record - } - - let output - - if (returnPlain) { - output = record.get({ plain: true }) - } else { - output = record - } - - const transaction = SequelizeRepository.getTransaction(options) - - output.activities = await record.getActivities({ - order: [['timestamp', 'DESC']], - limit: 20, - transaction, - }) - - output.lastActivity = output.activities[0]?.get({ plain: true }) ?? null - - output.lastActive = output.activities[0]?.timestamp ?? null - - output.activeOn = [...new Set(output.activities.map((i) => i.platform))] - - output.activityCount = output.activities.length - - output.numberOfOpenSourceContributions = output.contributions?.length ?? 0 - - output.activityTypes = [...new Set(output.activities.map((i) => `${i.platform}:${i.type}`))] - output.activeDaysCount = - output.activities.reduce((acc, activity) => { - if (!acc.datetimeHashmap) { - acc.datetimeHashmap = {} - acc.count = 0 - } - // strip hours from timestamp - const date = activity.timestamp.toISOString().split('T')[0] - - if (!acc.datetimeHashmap[date]) { - acc.count += 1 - acc.datetimeHashmap[date] = true - } - - return acc - }, {}).count ?? 0 - - output.averageSentiment = - output.activityCount > 0 - ? Math.round( - (output.activities.reduce((acc, i) => { - if (i.sentiment && 'sentiment' in i.sentiment) { - acc += i.sentiment.sentiment - } - return acc - }, 0) / - output.activities.filter((i) => i.sentiment && 'sentiment' in i.sentiment).length) * - 100, - ) / 100 - : 0 - - output.tags = await record.getTags({ - transaction, - order: [['createdAt', 'ASC']], - joinTableAttributes: [], - }) - - output.organizations = await record.getOrganizations({ - transaction, - order: [['createdAt', 'ASC']], - joinTableAttributes: ['dateStart', 'dateEnd', 'title', 'source'], - through: { - where: { - deletedAt: null, - }, - }, - }) - MemberRepository.sortOrganizations(output.organizations) - - output.tasks = await record.getTasks({ - transaction, - order: [['createdAt', 'ASC']], - joinTableAttributes: [], - }) - - output.notes = await record.getNotes({ - transaction, - joinTableAttributes: [], - }) - - output.noMerge = ( - await record.getNoMerge({ - transaction, - }) - ).map((i) => i.id) - - output.toMerge = ( - await record.getToMerge({ - transaction, - }) - ).map((i) => i.id) - - const memberIdentities = (await this.getIdentities([record.id], options)).get(record.id) - - output.username = {} - - for (const identity of memberIdentities) { - if (output.username[identity.platform]) { - output.username[identity.platform].push(identity.username) - } else { - output.username[identity.platform] = [identity.username] - } - } - - output.identities = Object.keys(output.username) - - output.affiliations = await this.getAffiliations(record.id, options) - - const manualSyncRemote = await new MemberSyncRemoteRepository(options).findMemberManualSync( - record.id, - ) - - for (const syncRemote of manualSyncRemote) { - if (output.attributes?.syncRemote) { - output.attributes.syncRemote[syncRemote.platform] = syncRemote.status === SyncStatus.ACTIVE - } else { - output.attributes.syncRemote = { - [syncRemote.platform]: syncRemote.status === SyncStatus.ACTIVE, - } - } - } - - return output - } - - static async updateMemberOrganizations( - record, - organizations, - replace, - options: IRepositoryOptions, - ) { - if (!organizations) { - return - } - - function iso(v) { - return moment(v).toISOString() - } - - if (replace) { - const originalOrgs = await MemberRepository.fetchWorkExperiences(record.id, options) - - const toDelete = originalOrgs.filter( - (originalOrg: any) => - !organizations.find( - (newOrg) => - originalOrg.organizationId === newOrg.id && - originalOrg.title === (newOrg.title || null) && - iso(originalOrg.dateStart) === iso(newOrg.startDate || null) && - iso(originalOrg.dateEnd) === iso(newOrg.endDate || null), - ), - ) - - for (const item of toDelete) { - await MemberRepository.deleteWorkExperience((item as any).id, options) - } - } - - for (const item of organizations) { - const org = typeof item === 'string' ? { id: item } : item - await MemberRepository.createOrUpdateWorkExperience( - { - memberId: record.id, - organizationId: org.id, - title: org.title, - dateStart: org.startDate, - dateEnd: org.endDate, - source: org.source, - }, - options, - ) - await OrganizationRepository.includeOrganizationToSegments(org.id, options) - } - } - - static async createOrUpdateWorkExperience( - { - memberId, - organizationId, - source, - title = null, - dateStart = null, - dateEnd = null, - updateAffiliation = true, - }, - options: IRepositoryOptions, - ) { - const seq = SequelizeRepository.getSequelize(options) - const transaction = SequelizeRepository.getTransaction(options) - - if (dateStart) { - // clean up organizations without dates if we're getting ones with dates - await seq.query( - ` - UPDATE "memberOrganizations" - SET "deletedAt" = NOW() - WHERE "memberId" = :memberId - AND "organizationId" = :organizationId - AND "dateStart" IS NULL - AND "dateEnd" IS NULL - `, - { - replacements: { - memberId, - organizationId, - }, - type: QueryTypes.UPDATE, - transaction, - }, - ) - } else { - const rows = await seq.query( - ` - SELECT COUNT(*) AS count FROM "memberOrganizations" - WHERE "memberId" = :memberId - AND "organizationId" = :organizationId - AND "dateStart" IS NOT NULL - AND "deletedAt" IS NULL - `, - { - replacements: { - memberId, - organizationId, - }, - type: QueryTypes.SELECT, - transaction, - }, - ) - const row = rows[0] as any - if (row.count > 0) { - // if we're getting organization without dates, but there's already one with dates, don't insert - return - } - } - - let conflictCondition = `("memberId", "organizationId", "dateStart", "dateEnd")` - if (!dateEnd) { - conflictCondition = `("memberId", "organizationId", "dateStart") WHERE "dateEnd" IS NULL` - } - if (!dateStart) { - conflictCondition = `("memberId", "organizationId") WHERE "dateStart" IS NULL AND "dateEnd" IS NULL` - } - - const onConflict = - source === OrganizationSource.UI - ? `ON CONFLICT ${conflictCondition} DO UPDATE SET "title" = :title, "dateStart" = :dateStart, "dateEnd" = :dateEnd, "deletedAt" = NULL, "source" = :source` - : 'ON CONFLICT DO NOTHING' - - await seq.query( - ` - INSERT INTO "memberOrganizations" ("memberId", "organizationId", "createdAt", "updatedAt", "title", "dateStart", "dateEnd", "source") - VALUES (:memberId, :organizationId, NOW(), NOW(), :title, :dateStart, :dateEnd, :source) - ${onConflict} - `, - { - replacements: { - memberId, - organizationId, - title: title || null, - dateStart: dateStart || null, - dateEnd: dateEnd || null, - source: source || null, - }, - type: QueryTypes.INSERT, - transaction, - }, - ) - - if (updateAffiliation) { - await MemberAffiliationRepository.update(memberId, options) - } - } - - static async deleteWorkExperience(id, options: IRepositoryOptions) { - const seq = SequelizeRepository.getSequelize(options) - const transaction = SequelizeRepository.getTransaction(options) - - await seq.query( - ` - UPDATE "memberOrganizations" - SET "deletedAt" = NOW() - WHERE "id" = :id - `, - { - replacements: { - id, - }, - type: QueryTypes.UPDATE, - transaction, - }, - ) - } - - static async fetchWorkExperiences(memberId: string, options: IRepositoryOptions) { - const seq = SequelizeRepository.getSequelize(options) - const transaction = SequelizeRepository.getTransaction(options) - - const query = ` - SELECT * FROM "memberOrganizations" - WHERE "memberId" = :memberId - AND "deletedAt" IS NULL - ` - - const records = await seq.query(query, { - replacements: { - memberId, - }, - type: QueryTypes.SELECT, - transaction, - }) - - return records - } - static async findWorkExperience( memberId: string, timestamp: string, @@ -3594,81 +2077,60 @@ class MemberRepository { }) } - static async getMemberIdsandCount( - { limit = 20, offset = 0, orderBy = 'joinedAt_DESC', countOnly = false }, + static async moveSelectedAffiliationsBetweenMembers( + fromMemberId: string, + toMemberId: string, + memberSegmentAffiliationIds: string[], options: IRepositoryOptions, - ) { - const tenant = SequelizeRepository.getCurrentTenant(options) - const segmentIds = SequelizeRepository.getSegmentIds(options) + ): Promise { + const transaction = SequelizeRepository.getTransaction(options) + const seq = SequelizeRepository.getSequelize(options) const params: any = { - tenantId: tenant.id, - segmentIds, - limit, - offset, - } - - let orderByString = '' - const orderByParts = orderBy.split('_') - const direction = orderByParts[1].toLowerCase() - switch (orderByParts[0]) { - case 'joinedAt': - orderByString = 'm."joinedAt"' - break - case 'displayName': - orderByString = 'm."displayName"' - break - case 'reach': - orderByString = "(m.reach ->> 'total')::int" - break - case 'score': - orderByString = 'm.score' - break - - default: - throw new Error(`Invalid order by: ${orderBy}!`) + fromMemberId, + toMemberId, + memberSegmentAffiliationIds, } - orderByString = `${orderByString} ${direction}` - const countQuery = ` - SELECT count(*) FROM ( - SELECT m.id - FROM members m - JOIN "memberSegments" ms ON ms."memberId" = m.id - WHERE m."tenantId" = :tenantId - AND ms."segmentId" IN (:segmentIds) - ) as count + const updateQuery = ` + update "memberSegmentAffiliations" set "memberId" = :toMemberId where "memberId" = :fromMemberId + and "id" in (:memberSegmentAffiliationIds); ` - const memberCount = await seq.query(countQuery, { + await seq.query(updateQuery, { replacements: params, - type: QueryTypes.SELECT, + type: QueryTypes.UPDATE, + transaction, }) + } - if (countOnly) { - return { - count: (memberCount[0] as any).count, - ids: [], - } + static async removeIdentitiesFromMember( + memberId: string, + identities: IMemberIdentity[], + options: IRepositoryOptions, + ): Promise { + const qx = SequelizeRepository.getQueryExecutor(options) + + for (const identity of identities) { + await deleteMemberIdentities(qx, { + memberId, + value: identity.value, + type: identity.type, + platform: identity.platform, + }) } + } - const members = await seq.query( - `SELECT m.id FROM members m - JOIN "memberSegments" ms ON ms."memberId" = m.id - WHERE m."tenantId" = :tenantId and ms."segmentId" in (:segmentIds) - ORDER BY ${orderByString} - LIMIT :limit OFFSET :offset`, - { - replacements: params, - type: QueryTypes.SELECT, - }, - ) + static async findAlreadyExistingIdentities( + identities: IMemberIdentity[], + options: IRepositoryOptions, + ): Promise { + const qx = SequelizeRepository.getQueryExecutor(options) - return { - count: (memberCount[0] as any).count, - ids: members.map((i: any) => i.id), - } + const existingIdentities = await findAlreadyExistingVerifiedIdentities(qx, { identities }) + + return existingIdentities } } diff --git a/backend/src/database/repositories/memberSegmentAffiliationRepository.ts b/backend/src/database/repositories/memberSegmentAffiliationRepository.ts deleted file mode 100644 index 632001679d..0000000000 --- a/backend/src/database/repositories/memberSegmentAffiliationRepository.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { v4 as uuid } from 'uuid' -import { QueryTypes } from 'sequelize' -import { - MemberSegmentAffiliation, - MemberSegmentAffiliationCreate, - MemberSegmentAffiliationUpdate, -} from '../../types/memberSegmentAffiliationTypes' -import { IRepositoryOptions } from './IRepositoryOptions' -import { RepositoryBase } from './repositoryBase' -import Error404 from '../../errors/Error404' -import SequelizeRepository from './sequelizeRepository' - -class MemberSegmentAffiliationRepository extends RepositoryBase< - MemberSegmentAffiliation, - string, - MemberSegmentAffiliationCreate, - MemberSegmentAffiliationUpdate, - unknown -> { - public constructor(options: IRepositoryOptions) { - super(options, true) - } - - async createOrUpdate(data: MemberSegmentAffiliationCreate): Promise { - if (!data.memberId) { - throw new Error('memberId is required when creating a member segment affiliation.') - } - - if (!data.segmentId) { - throw new Error('segmentId is required when creating a member segment affiliation.') - } - - if (data.organizationId === undefined) { - throw new Error('organizationId is required when creating a member segment affiliation.') - } - - const transaction = this.transaction - - const affiliationInsertResult = await this.options.database.sequelize.query( - `INSERT INTO "memberSegmentAffiliations" ("id", "memberId", "segmentId", "organizationId", "dateStart", "dateEnd") - VALUES - (:id, :memberId, :segmentId, :organizationId, :dateStart, :dateEnd) - RETURNING "id" - `, - { - replacements: { - id: uuid(), - memberId: data.memberId, - segmentId: data.segmentId, - organizationId: data.organizationId, - dateStart: data.dateStart || null, - dateEnd: data.dateEnd || null, - }, - type: QueryTypes.INSERT, - transaction, - }, - ) - - await this.updateAffiliation(data.memberId, data.segmentId, data.organizationId) - - return this.findById(affiliationInsertResult[0][0].id) - } - - async setForMember(memberId: string, data: MemberSegmentAffiliationCreate[]): Promise { - const seq = SequelizeRepository.getSequelize(this.options) - const transaction = SequelizeRepository.getTransaction(this.options) - - await seq.query( - ` - DELETE FROM "memberSegmentAffiliations" - WHERE "memberId" = :memberId - `, - { - replacements: { - memberId, - }, - type: QueryTypes.DELETE, - transaction, - }, - ) - - if (data.length === 0) { - return - } - - const valuePlaceholders = data - .map( - (_, i) => - `(:id_${i}, :memberId_${i}, :segmentId_${i}, :organizationId_${i}, :dateStart_${i}, :dateEnd_${i})`, - ) - .join(', ') - - await seq.query( - ` - INSERT INTO "memberSegmentAffiliations" ("id", "memberId", "segmentId", "organizationId", "dateStart", "dateEnd") - VALUES ${valuePlaceholders} - `, - { - replacements: data.reduce((acc, item, i) => { - acc[`id_${i}`] = uuid() - acc[`memberId_${i}`] = memberId - acc[`segmentId_${i}`] = item.segmentId - acc[`organizationId_${i}`] = item.organizationId - acc[`dateStart_${i}`] = item.dateStart || null - acc[`dateEnd_${i}`] = item.dateEnd || null - - return acc - }, {}), - type: QueryTypes.INSERT, - transaction, - }, - ) - } - - override async findById(id: string): Promise { - const transaction = this.transaction - - const records = await this.options.database.sequelize.query( - `SELECT * - FROM "memberSegmentAffiliations" - WHERE id = :id - `, - { - replacements: { - id, - }, - type: QueryTypes.SELECT, - transaction, - }, - ) - - if (records.length === 0) { - return null - } - - return records[0] - } - - override async update( - id: string, - data: MemberSegmentAffiliationUpdate, - ): Promise { - const transaction = this.transaction - - if (data.organizationId === undefined) { - throw new Error('When updating member segment affiliation, organizationId is required.') - } - - const affiliation = await this.findById(id) - - if (!affiliation) { - throw new Error404() - } - - await this.options.database.sequelize.query( - `UPDATE "memberSegmentAffiliations" SET "organizationId" = :organizationId`, - { - replacements: { - organizationId: data.organizationId, - }, - type: QueryTypes.UPDATE, - transaction, - }, - ) - - return this.findById(id) - } - - override async destroyAll(ids: string[]): Promise { - const transaction = this.transaction - - const records = await this.findInIds(ids) - - if (ids.some((id) => records.find((r) => r.id === id) === undefined)) { - throw new Error404() - } - - await this.options.database.sequelize.query( - `DELETE FROM "memberSegmentAffiliations" WHERE id in (:ids) - `, - { - replacements: { - ids, - }, - type: QueryTypes.SELECT, - transaction, - }, - ) - } - - async findInIds(ids: string[]): Promise { - const transaction = this.transaction - - const records = await this.options.database.sequelize.query( - `SELECT * - FROM "memberSegmentAffiliations" - WHERE id in (:ids) - `, - { - replacements: { - ids, - }, - type: QueryTypes.SELECT, - transaction, - }, - ) - - return records - } - - async findForMember(memberId: string, timestamp: string): Promise { - const transaction = SequelizeRepository.getTransaction(this.options) - - const segment = SequelizeRepository.getStrictlySingleActiveSegment(this.options) - - const seq = SequelizeRepository.getSequelize(this.options) - - const records = await seq.query( - ` - SELECT * FROM "memberSegmentAffiliations" - WHERE "memberId" = :memberId - AND "segmentId" = :segmentId - AND ( - ("dateStart" <= :timestamp AND "dateEnd" >= :timestamp) - OR ("dateStart" <= :timestamp AND "dateEnd" IS NULL) - ) - ORDER BY "dateStart" DESC, id - LIMIT 1 - `, - { - replacements: { - memberId, - segmentId: segment.id, - timestamp, - }, - type: QueryTypes.SELECT, - transaction, - }, - ) - - if (records.length === 0) { - return null - } - - return records[0] as MemberSegmentAffiliation - } - - private async updateAffiliation(memberId, segmentId, organizationId) { - const transaction = this.transaction - - const query = ` - UPDATE activities - SET "organizationId" = :organizationId - WHERE "memberId" = :memberId - AND "segmentId" = :segmentId - ` - - await this.options.database.sequelize.query(query, { - replacements: { - memberId, - segmentId, - organizationId, - }, - type: QueryTypes.UPDATE, - transaction, - }) - } -} - -export default MemberSegmentAffiliationRepository diff --git a/backend/src/database/repositories/memberSyncRemoteRepository.ts b/backend/src/database/repositories/memberSyncRemoteRepository.ts deleted file mode 100644 index 8d70daa232..0000000000 --- a/backend/src/database/repositories/memberSyncRemoteRepository.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { generateUUIDv1 as uuid } from '@crowd/common' -import { IMemberSyncRemoteData, SyncStatus } from '@crowd/types' -import { QueryTypes } from 'sequelize' -import { IRepositoryOptions } from './IRepositoryOptions' -import { RepositoryBase } from './repositoryBase' -import SequelizeRepository from './sequelizeRepository' - -class MemberSyncRemoteRepository extends RepositoryBase< - IMemberSyncRemoteData, - string, - IMemberSyncRemoteData, - unknown, - unknown -> { - public constructor(options: IRepositoryOptions) { - super(options, true) - } - - async stopSyncingAutomation(automationId: string) { - await this.options.database.sequelize.query( - `update "membersSyncRemote" set status = :status where "syncFrom" = :automationId - `, - { - replacements: { - status: SyncStatus.STOPPED, - automationId, - }, - type: QueryTypes.UPDATE, - }, - ) - } - - async findRemoteSync(integrationId: string, memberId: string, syncFrom: string) { - const transaction = SequelizeRepository.getTransaction(this.options) - - const records = await this.options.database.sequelize.query( - `SELECT * - FROM "membersSyncRemote" - WHERE "integrationId" = :integrationId and "memberId" = :memberId and "syncFrom" = :syncFrom; - `, - { - replacements: { - integrationId, - memberId, - syncFrom, - }, - type: QueryTypes.SELECT, - transaction, - }, - ) - - if (records.length === 0) { - return null - } - - return records[0] - } - - async startManualSync(id: string, sourceId: string) { - const transaction = SequelizeRepository.getTransaction(this.options) - - await this.options.database.sequelize.query( - `update "membersSyncRemote" set status = :status, "sourceId" = :sourceId where "id" = :id - `, - { - replacements: { - status: SyncStatus.ACTIVE, - id, - sourceId: sourceId || null, - }, - type: QueryTypes.UPDATE, - transaction, - }, - ) - } - - async stopMemberManualSync(memberId: string) { - await this.options.database.sequelize.query( - `update "membersSyncRemote" set status = :status where "memberId" = :memberId and "syncFrom" = :manualSync - `, - { - replacements: { - status: SyncStatus.STOPPED, - memberId, - manualSync: 'manual', - }, - type: QueryTypes.UPDATE, - }, - ) - } - - async destroyAllAutomation(automationIds: string[]): Promise { - const transaction = this.transaction - - const seq = this.seq - - const query = ` - delete - from "membersSyncRemote" - where "syncFrom" in (:automationIds);` - - await seq.query(query, { - replacements: { - automationIds, - }, - type: QueryTypes.DELETE, - transaction, - }) - } - - async destroyAllIntegration(integrationIds: string[]): Promise { - const transaction = this.transaction - - const seq = this.seq - - const query = ` - delete - from "membersSyncRemote" - where "integrationId" in (:integrationIds);` - - await seq.query(query, { - replacements: { - integrationIds, - }, - type: QueryTypes.DELETE, - transaction, - }) - } - - async markMemberForSyncing(data: IMemberSyncRemoteData): Promise { - const transaction = SequelizeRepository.getTransaction(this.options) - - const existingSyncRemote = await this.findByMemberId(data.memberId) - - if (existingSyncRemote) { - data.sourceId = existingSyncRemote.sourceId - } - - const existingManualSyncRemote = await this.findRemoteSync( - data.integrationId, - data.memberId, - data.syncFrom, - ) - - if (existingManualSyncRemote) { - await this.startManualSync(existingManualSyncRemote.id, data.sourceId) - return existingManualSyncRemote - } - - const memberSyncRemoteInserted = await this.options.database.sequelize.query( - `insert into "membersSyncRemote" ("id", "memberId", "sourceId", "integrationId", "syncFrom", "metaData", "lastSyncedAt", "status") - values - (:id, :memberId, :sourceId, :integrationId, :syncFrom, :metaData, :lastSyncedAt, :status) - returning "id" - `, - { - replacements: { - id: uuid(), - memberId: data.memberId, - integrationId: data.integrationId, - syncFrom: data.syncFrom, - metaData: data.metaData, - lastSyncedAt: data.lastSyncedAt || null, - sourceId: data.sourceId || null, - status: SyncStatus.ACTIVE, - }, - type: QueryTypes.INSERT, - transaction, - }, - ) - - const memberSyncRemote = await this.findById(memberSyncRemoteInserted[0][0].id) - return memberSyncRemote - } - - async findMemberManualSync(memberId: string) { - const transaction = SequelizeRepository.getTransaction(this.options) - - const records = await this.options.database.sequelize.query( - `select i.platform, msr.status from "membersSyncRemote" msr - inner join integrations i on msr."integrationId" = i.id - where msr."syncFrom" = :syncFrom and msr."memberId" = :memberId; - `, - { - replacements: { - memberId, - syncFrom: 'manual', - }, - type: QueryTypes.SELECT, - transaction, - }, - ) - - return records - } - - async findByMemberId(memberId: string): Promise { - const transaction = SequelizeRepository.getTransaction(this.options) - - const records = await this.options.database.sequelize.query( - `SELECT * - FROM "membersSyncRemote" - WHERE "memberId" = :memberId - and "sourceId" is not null - limit 1; - `, - { - replacements: { - memberId, - }, - type: QueryTypes.SELECT, - transaction, - }, - ) - - if (records.length === 0) { - return null - } - - return records[0] - } - - async findById(id: string): Promise { - const transaction = SequelizeRepository.getTransaction(this.options) - - const records = await this.options.database.sequelize.query( - `SELECT * - FROM "membersSyncRemote" - WHERE id = :id; - `, - { - replacements: { - id, - }, - type: QueryTypes.SELECT, - transaction, - }, - ) - - if (records.length === 0) { - return null - } - - return records[0] - } -} - -export default MemberSyncRemoteRepository diff --git a/backend/src/database/repositories/mergeActionsRepository.ts b/backend/src/database/repositories/mergeActionsRepository.ts new file mode 100644 index 0000000000..9f2836f54a --- /dev/null +++ b/backend/src/database/repositories/mergeActionsRepository.ts @@ -0,0 +1,278 @@ +import { QueryTypes } from 'sequelize' + +import { + IMemberIdentity, + IMemberUnmergeBackup, + IMergeAction, + IOrganizationIdentity, + IOrganizationUnmergeBackup, + IUnmergeBackup, + MemberIdentityType, + MergeActionType, + OrganizationIdentityType, +} from '@crowd/types' + +import { IRepositoryOptions } from './IRepositoryOptions' +import SequelizeRepository from './sequelizeRepository' + +class MergeActionsRepository { + static async findById(id: string, options: IRepositoryOptions): Promise { + const transaction = SequelizeRepository.getTransaction(options) + + const record = await options.database.sequelize.query( + ` + SELECT * + FROM "mergeActions" + WHERE id = :id; + `, + { + replacements: { id }, + type: QueryTypes.SELECT, + transaction, + }, + ) + + if (record.length === 1) { + const data = record[0] as IMergeAction + + // fix old identities to use the new format + if (data.type === MergeActionType.MEMBER && data.unmergeBackup) { + const backup = data.unmergeBackup as IUnmergeBackup + if (backup.primary) { + for (const identity of backup.primary.identities) { + if ('username' in identity) { + identity.value = (identity as any).username + identity.type = MemberIdentityType.USERNAME + delete (identity as any).username + } + } + } + + if (backup.secondary) { + for (const identity of backup.secondary.identities) { + if ('username' in identity) { + identity.value = (identity as any).username + identity.type = MemberIdentityType.USERNAME + delete (identity as any).username + } + } + } + } + + return data + } + + return null + } + + static async findMergeBackup( + primaryMemberId: string, + type: MergeActionType, + identity: IMemberIdentity | IOrganizationIdentity, + options: IRepositoryOptions, + ): Promise { + const transaction = SequelizeRepository.getTransaction(options) + + let records + + if (type === MergeActionType.MEMBER) { + const memberIdentity = identity as IMemberIdentity + records = await options.database.sequelize.query( + ` + select * + from "mergeActions" ma + where ma."primaryId" = :primaryMemberId + and exists( + select 1 + from jsonb_array_elements(ma."unmergeBackup" -> 'secondary' -> 'identities') as identities + where (identities ->> 'username' = :secondaryMemberIdentityValue or (identities ->> 'type' = :secondaryMemberIdentityType and identities ->> 'value' = :secondaryMemberIdentityValue)) + and identities ->> 'platform' = :secondaryMemberIdentityPlatform + ); + `, + { + replacements: { + primaryMemberId, + secondaryMemberIdentityValue: memberIdentity.value, + secondaryMemberIdentityType: memberIdentity.type, + secondaryMemberIdentityPlatform: memberIdentity.platform, + }, + type: QueryTypes.SELECT, + transaction, + }, + ) + + // fix old identities to use the new format + for (const record of records) { + const data = record as IMergeAction + + // fix old identities to use the new format + if (data.type === MergeActionType.MEMBER && data.unmergeBackup) { + const backup = data.unmergeBackup as IUnmergeBackup + + if (backup.primary) { + for (const identity of backup.primary.identities) { + if ('username' in identity) { + identity.value = (identity as any).username + identity.type = MemberIdentityType.USERNAME + delete (identity as any).username + } + } + } + + if (backup.secondary) { + for (const identity of backup.secondary.identities) { + if ('username' in identity) { + identity.value = (identity as any).username + identity.type = MemberIdentityType.USERNAME + delete (identity as any).username + } + } + } + } + } + } else if (type === MergeActionType.ORG) { + const organizationIdentity = identity as IOrganizationIdentity + records = await options.database.sequelize.query( + ` + select * + from "mergeActions" ma + where ma."primaryId" = :primaryMemberId + and exists( + select 1 + from jsonb_array_elements(ma."unmergeBackup" -> 'secondary' -> 'identities') as identities + where (identities ->> 'name' = :secondaryOrgIdentityValue or (identities ->> 'type' = :secondaryOrgIdentityType and identities ->> 'value' = :secondaryOrgIdentityValue)) + and identities ->> 'platform' = :secondaryOrgIdentityPlatform + ); + `, + { + replacements: { + primaryMemberId, + secondaryOrgIdentityType: organizationIdentity.type, + secondaryOrgIdentityValue: organizationIdentity.value, + secondaryOrgIdentityPlatform: organizationIdentity.platform, + }, + type: QueryTypes.SELECT, + transaction, + }, + ) + + // fix old identities to use the new format + for (const record of records) { + const data = record as IMergeAction + + // fix old identities to use the new format + if (data.type === MergeActionType.ORG && data.unmergeBackup) { + const backup = data.unmergeBackup as IUnmergeBackup + + if (backup.primary) { + for (const identity of backup.primary.identities) { + if ('name' in identity) { + identity.value = (identity as any).name + identity.type = OrganizationIdentityType.USERNAME + delete (identity as any).name + } + } + + if ((backup.primary as any).website) { + backup.primary.identities.push({ + type: OrganizationIdentityType.PRIMARY_DOMAIN, + value: (backup.primary as any).website, + platform: 'custom', + verified: true, + source: null, + sourceId: null, + integrationId: null, + }) + } + + if ((backup.primary as any).alternativeDomains) { + for (const domain of (backup.primary as any).alternativeDomains) { + backup.primary.identities.push({ + type: OrganizationIdentityType.ALTERNATIVE_DOMAIN, + value: domain, + platform: 'enrichment', + verified: false, + source: null, + sourceId: null, + integrationId: null, + }) + } + } + + if ((backup.primary as any).affiliatedProfiles) { + for (const profile of (backup.primary as any).affiliatedProfiles) { + backup.primary.identities.push({ + type: OrganizationIdentityType.AFFILIATED_PROFILE, + value: profile, + platform: 'enrichment', + verified: false, + source: null, + sourceId: null, + integrationId: null, + }) + } + } + } + + if (backup.secondary) { + for (const identity of backup.secondary.identities) { + if ('name' in identity) { + identity.value = (identity as any).name + identity.type = OrganizationIdentityType.USERNAME + delete (identity as any).name + } + } + + if ((backup.secondary as any).website) { + backup.secondary.identities.push({ + type: OrganizationIdentityType.PRIMARY_DOMAIN, + value: (backup.secondary as any).website, + platform: 'custom', + verified: true, + source: null, + sourceId: null, + integrationId: null, + }) + } + + if ((backup.secondary as any).alternativeDomains) { + for (const domain of (backup.secondary as any).alternativeDomains) { + backup.secondary.identities.push({ + type: OrganizationIdentityType.ALTERNATIVE_DOMAIN, + value: domain, + platform: 'enrichment', + verified: false, + source: null, + sourceId: null, + integrationId: null, + }) + } + } + + if ((backup.secondary as any).affiliatedProfiles) { + for (const profile of (backup.secondary as any).affiliatedProfiles) { + backup.secondary.identities.push({ + type: OrganizationIdentityType.AFFILIATED_PROFILE, + value: profile, + platform: 'enrichment', + verified: false, + source: null, + sourceId: null, + integrationId: null, + }) + } + } + } + } + } + } + + if (records.length === 0) { + return null + } + + return records[0] + } +} + +export { MergeActionsRepository } diff --git a/backend/src/database/repositories/microserviceRepository.ts b/backend/src/database/repositories/microserviceRepository.ts deleted file mode 100644 index c8a7647549..0000000000 --- a/backend/src/database/repositories/microserviceRepository.ts +++ /dev/null @@ -1,358 +0,0 @@ -import lodash from 'lodash' -import Sequelize from 'sequelize' -import SequelizeRepository from './sequelizeRepository' -import AuditLogRepository from './auditLogRepository' -import SequelizeFilterUtils from '../utils/sequelizeFilterUtils' -import Error404 from '../../errors/Error404' -import { IRepositoryOptions } from './IRepositoryOptions' -import QueryParser from './filters/queryParser' -import { QueryOutput } from './filters/queryTypes' - -const Op = Sequelize.Op - -class MicroserviceRepository { - static async create(data, options: IRepositoryOptions) { - const currentUser = SequelizeRepository.getCurrentUser(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - const transaction = SequelizeRepository.getTransaction(options) - - const record = await options.database.microservice.create( - { - ...lodash.pick(data, ['init', 'running', 'type', 'variant', 'settings', 'importHash']), - - tenantId: tenant.id, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { - transaction, - }, - ) - - await this._createAuditLog(AuditLogRepository.CREATE, record, data, options) - - return this.findById(record.id, options) - } - - /** - * Find all microservices available for a type - * @param type The microservice type to filter - * @returns All active integrations for the platform - */ - static async findAllByType(type: string, page: number, perPage: number) { - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - - const records = await options.database.microservice.findAll({ - where: { - running: false, - type, - }, - limit: perPage, - offset: (page - 1) * perPage, - }) - - if (!records) { - throw new Error404() - } - - return Promise.all(records.map((record) => this._populateRelations(record))) - } - - static async update(id, data, options: IRepositoryOptions) { - const currentUser = SequelizeRepository.getCurrentUser(options) - - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - let record = await options.database.microservice.findOne({ - where: { - id, - tenantId: currentTenant.id, - }, - transaction, - }) - - if (!record) { - throw new Error404() - } - - record = await record.update( - { - ...lodash.pick(data, ['init', 'running', 'type', 'variant', 'settings', 'importHash']), - - updatedById: currentUser.id, - }, - { - transaction, - }, - ) - - await this._createAuditLog(AuditLogRepository.UPDATE, record, data, options) - - return this.findById(record.id, options) - } - - static async destroy(id, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const record = await options.database.microservice.findOne({ - where: { - id, - tenantId: currentTenant.id, - }, - transaction, - }) - - if (!record) { - throw new Error404() - } - - await record.destroy({ - transaction, - }) - - await this._createAuditLog(AuditLogRepository.DELETE, record, record, options) - } - - static async findById(id, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const include = [] - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const record = await options.database.microservice.findOne({ - where: { - id, - tenantId: currentTenant.id, - }, - include, - transaction, - }) - - if (!record) { - throw new Error404() - } - - return this._populateRelations(record) - } - - static async filterIdInTenant(id, options: IRepositoryOptions) { - return lodash.get(await this.filterIdsInTenant([id], options), '[0]', null) - } - - static async filterIdsInTenant(ids, options: IRepositoryOptions) { - if (!ids || !ids.length) { - return [] - } - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const where = { - id: { - [Op.in]: ids, - }, - tenantId: currentTenant.id, - } - - const records = await options.database.microservice.findAll({ - attributes: ['id'], - where, - }) - - return records.map((record) => record.id) - } - - static async count(filter, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - return options.database.microservice.count({ - where: { - ...filter, - tenantId: tenant.id, - }, - transaction, - }) - } - - static async findAndCountAll( - { filter = {} as any, advancedFilter = null as any, limit = 0, offset = 0, orderBy = '' }, - options: IRepositoryOptions, - ) { - const include = [] - - // If the advanced filter is empty, we construct it from the query parameter filter - if (!advancedFilter) { - advancedFilter = { and: [] } - - if (filter.id) { - advancedFilter.and.push({ - id: SequelizeFilterUtils.uuid(filter.id), - }) - } - - if ( - filter.init === true || - filter.init === 'true' || - filter.init === false || - filter.init === 'false' - ) { - advancedFilter.and.push({ - init: filter.init === true || filter.init === 'true', - }) - } - - if ( - filter.running === true || - filter.running === 'true' || - filter.running === false || - filter.running === 'false' - ) { - advancedFilter.and.push({ - running: filter.running === true || filter.running === 'true', - }) - } - - if (filter.type) { - advancedFilter.and.push({ - type: filter.type, - }) - } - - if (filter.variant) { - advancedFilter.and.push({ - variant: filter.variant, - }) - } - - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange - - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - createdAt: { - gte: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - createdAt: { - lte: end, - }, - }) - } - } - } - - const parser = new QueryParser( - { - nestedFields: { - sentiment: 'sentiment.sentiment', - }, - withSegments: false, - }, - options, - ) - - const parsed: QueryOutput = parser.parse({ - filter: advancedFilter, - orderBy: orderBy || ['createdAt_DESC'], - limit, - offset, - }) - - // eslint-disable-next-line prefer-const - let { rows, count } = await options.database.microservice.findAndCountAll({ - ...(parsed.where ? { where: parsed.where } : {}), - ...(parsed.having ? { having: parsed.having } : {}), - order: parsed.order, - limit: parsed.limit, - offset: parsed.offset, - include, - transaction: SequelizeRepository.getTransaction(options), - }) - - rows = await this._populateRelationsForRows(rows) - - return { rows, count, limit: parsed.limit, offset: parsed.offset } - } - - static async findAllAutocomplete(query, limit, options: IRepositoryOptions) { - const tenant = SequelizeRepository.getCurrentTenant(options) - - const whereAnd: Array = [ - { - tenantId: tenant.id, - }, - ] - - if (query) { - whereAnd.push({ - [Op.or]: [{ id: SequelizeFilterUtils.uuid(query) }], - }) - } - - const where = { [Op.and]: whereAnd } - - const records = await options.database.microservice.findAll({ - attributes: ['id', 'id'], - where, - limit: limit ? Number(limit) : undefined, - order: [['id', 'ASC']], - }) - - return records.map((record) => ({ - id: record.id, - label: record.id, - })) - } - - static async _createAuditLog(action, record, data, options: IRepositoryOptions) { - let values = {} - - if (data) { - values = { - ...record.get({ plain: true }), - } - } - - await AuditLogRepository.log( - { - entityName: 'microservice', - entityId: record.id, - action, - values, - }, - options, - ) - } - - static async _populateRelationsForRows(rows) { - if (!rows) { - return rows - } - - return Promise.all(rows.map((record) => this._populateRelations(record))) - } - - static async _populateRelations(record) { - if (!record) { - return record - } - - const output = record.get({ plain: true }) - - return output - } -} - -export default MicroserviceRepository diff --git a/backend/src/database/repositories/noteRepository.ts b/backend/src/database/repositories/noteRepository.ts deleted file mode 100644 index fdaf19873b..0000000000 --- a/backend/src/database/repositories/noteRepository.ts +++ /dev/null @@ -1,377 +0,0 @@ -import sanitizeHtml from 'sanitize-html' -import lodash from 'lodash' -import Sequelize from 'sequelize' -import SequelizeRepository from './sequelizeRepository' -import AuditLogRepository from './auditLogRepository' -import SequelizeFilterUtils from '../utils/sequelizeFilterUtils' -import Error404 from '../../errors/Error404' -import { IRepositoryOptions } from './IRepositoryOptions' -import QueryParser from './filters/queryParser' -import { QueryOutput } from './filters/queryTypes' -import UserRepository from './userRepository' - -const { Op } = Sequelize - -class NoteRepository { - static async create(data, options: IRepositoryOptions) { - const currentUser = SequelizeRepository.getCurrentUser(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - const transaction = SequelizeRepository.getTransaction(options) - - if (data.body) { - data.body = sanitizeHtml(data.body).trim() - } - - const record = await options.database.note.create( - { - ...lodash.pick(data, ['body', 'importHash']), - - tenantId: tenant.id, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { - transaction, - }, - ) - - await record.setMembers(data.members || [], { - transaction, - }) - - await this._createAuditLog(AuditLogRepository.CREATE, record, data, options) - - return this.findById(record.id, options) - } - - static async update(id, data, options: IRepositoryOptions) { - const currentUser = SequelizeRepository.getCurrentUser(options) - - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - let record = await options.database.note.findOne({ - where: { - id, - tenantId: currentTenant.id, - }, - transaction, - }) - - if (!record) { - throw new Error404() - } - - if (data.body) { - data.body = sanitizeHtml(data.body).trim() - } - - record = await record.update( - { - ...lodash.pick(data, ['body', 'importHash']), - - updatedById: currentUser.id, - }, - { - transaction, - }, - ) - - await record.setMembers(data.members || [], { - transaction, - }) - - await this._createAuditLog(AuditLogRepository.UPDATE, record, data, options) - - return this.findById(record.id, options) - } - - static async destroy(id, options: IRepositoryOptions, force = false) { - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const record = await options.database.note.findOne({ - where: { - id, - tenantId: currentTenant.id, - }, - transaction, - }) - - if (!record) { - throw new Error404() - } - - await record.destroy({ - transaction, - force, - }) - - await this._createAuditLog(AuditLogRepository.DELETE, record, record, options) - } - - static async findById(id, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const include = [] - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const record = await options.database.note.findOne({ - where: { - id, - tenantId: currentTenant.id, - }, - include, - transaction, - }) - - if (!record) { - throw new Error404() - } - - return this._populateRelations(record, options) - } - - static async filterIdInTenant(id, options: IRepositoryOptions) { - return lodash.get(await this.filterIdsInTenant([id], options), '[0]', null) - } - - static async filterIdsInTenant(ids, options: IRepositoryOptions) { - if (!ids || !ids.length) { - return [] - } - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const where = { - id: { - [Op.in]: ids, - }, - tenantId: currentTenant.id, - } - - const records = await options.database.note.findAll({ - attributes: ['id'], - where, - }) - - return records.map((record) => record.id) - } - - static async count(filter, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - return options.database.note.count({ - where: { - ...filter, - tenantId: tenant.id, - }, - transaction, - }) - } - - static async findAndCountAll( - { filter = {} as any, advancedFilter = null as any, limit = 0, offset = 0, orderBy = '' }, - options: IRepositoryOptions, - ) { - const tenant = SequelizeRepository.getCurrentTenant(options) - - const whereAnd: Array = [] - const include = [ - { - model: options.database.member, - as: 'members', - include: [ - { - model: options.database.segment, - as: 'segments', - where: { - id: SequelizeRepository.getSegmentIds(options), - }, - }, - ], - }, - ] - - whereAnd.push({ - tenantId: tenant.id, - }) - - if (!advancedFilter) { - advancedFilter = { and: [] } - if (filter.id) { - advancedFilter.and.push({ - id: filter.id, - }) - } - - if (filter.body) { - advancedFilter.and.push({ - body: { - textContains: filter.body, - }, - }) - } - - if (filter.members) { - advancedFilter.and.push({ - members: filter.members, - }) - } - - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange - - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - createdAt: { - gte: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - createdAt: { - lte: end, - }, - }) - } - } - } - - const parser = new QueryParser( - { - manyToMany: { - members: { - table: 'notes', - model: 'note', - relationTable: { - name: 'memberNotes', - from: 'noteId', - to: 'memberId', - }, - }, - }, - withSegments: false, - }, - options, - ) - const parsed: QueryOutput = parser.parse({ - filter: advancedFilter, - orderBy: orderBy || ['createdAt_DESC'], - limit, - offset, - }) - - let { - rows, - count, // eslint-disable-line prefer-const - } = await options.database.note.findAndCountAll({ - ...(parsed.where ? { where: parsed.where } : {}), - ...(parsed.having ? { having: parsed.having } : {}), - order: parsed.order, - limit: parsed.limit, - offset: parsed.offset, - include, - transaction: SequelizeRepository.getTransaction(options), - }) - - rows = await this._populateRelationsForRows(rows, options) - - return { rows, count, limit: parsed.limit, offset: parsed.offset } - } - - static async findAllAutocomplete(query, limit, options: IRepositoryOptions) { - const tenant = SequelizeRepository.getCurrentTenant(options) - - const whereAnd: Array = [ - { - tenantId: tenant.id, - }, - ] - - if (query) { - whereAnd.push({ - [Op.or]: [ - { id: SequelizeFilterUtils.uuid(query) }, - { - [Op.and]: SequelizeFilterUtils.ilikeIncludes('note', 'body', query), - }, - ], - }) - } - - const where = { [Op.and]: whereAnd } - - const records = await options.database.note.findAll({ - attributes: ['id', 'body'], - where, - limit: limit ? Number(limit) : undefined, - order: [['body', 'ASC']], - }) - - return records.map((record) => ({ - id: record.id, - label: record.body, - })) - } - - static async _createAuditLog(action, record, data, options: IRepositoryOptions) { - let values = {} - - if (data) { - values = { - ...record.get({ plain: true }), - memberIds: data.members, - } - } - - await AuditLogRepository.log( - { - entityName: 'note', - entityId: record.id, - action, - values, - }, - options, - ) - } - - static async _populateRelationsForRows(rows, options: IRepositoryOptions) { - if (!rows) { - return rows - } - - return Promise.all(rows.map((record) => this._populateRelations(record, options))) - } - - static async _populateRelations(record, options: IRepositoryOptions) { - if (!record) { - return record - } - - const output = record.get({ plain: true }) - - const transaction = SequelizeRepository.getTransaction(options) - - output.members = await record.getMembers({ - transaction, - joinTableAttributes: [], - }) - - const user = await UserRepository.findById(record.createdById, options) - output.createdBy = { id: user.id, fullName: user.fullName, avatarUrl: null } - - return output - } -} - -export default NoteRepository diff --git a/backend/src/database/repositories/organizationCacheRepository.ts b/backend/src/database/repositories/organizationCacheRepository.ts deleted file mode 100644 index 90b6a04637..0000000000 --- a/backend/src/database/repositories/organizationCacheRepository.ts +++ /dev/null @@ -1,259 +0,0 @@ -import lodash from 'lodash' -import SequelizeRepository from './sequelizeRepository' -import AuditLogRepository from './auditLogRepository' -import Error404 from '../../errors/Error404' -import { IRepositoryOptions } from './IRepositoryOptions' - -class OrganizationCacheRepository { - static async create(data, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const record = await options.database.organizationCache.create( - { - ...lodash.pick(data, [ - 'name', - 'url', - 'description', - 'emails', - 'phoneNumbers', - 'logo', - 'tags', - 'twitter', - 'linkedin', - 'crunchbase', - 'employees', - 'revenueRange', - 'importHash', - 'enriched', - 'website', - 'github', - 'location', - 'employeeCountByCountry', - 'type', - 'ticker', - 'headline', - 'profiles', - 'naics', - 'industry', - 'founded', - 'address', - 'size', - 'lastEnrichedAt', - 'manuallyCreated', - ]), - }, - { - transaction, - }, - ) - - await this._createAuditLog(AuditLogRepository.CREATE, record, data, options) - - return this.findById(record.id, options) - } - - static async update(id, data, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - let record = await options.database.organizationCache.findOne({ - where: { - id, - }, - transaction, - }) - - if (!record) { - throw new Error404() - } - - record = await record.update( - { - ...lodash.pick(data, [ - 'name', - 'url', - 'description', - 'emails', - 'phoneNumbers', - 'logo', - 'tags', - 'twitter', - 'linkedin', - 'crunchbase', - 'employees', - 'revenueRange', - 'importHash', - 'enriched', - 'website', - 'github', - 'location', - 'geoLocation', - 'employeeCountByCountry', - 'geoLocation', - 'address', - 'type', - 'ticker', - 'headline', - 'profiles', - 'naics', - 'industry', - 'founded', - 'size', - 'employees', - 'twitter', - 'lastEnrichedAt', - ]), - }, - { - transaction, - }, - ) - - if (!record) { - throw new Error404() - } - - await this._createAuditLog(AuditLogRepository.UPDATE, record, data, options) - - return this.findById(record.id, options) - } - - static async bulkUpdate( - data: any[], - options: IRepositoryOptions, - isEnrichment: boolean = false, - ): Promise { - const transaction = SequelizeRepository.getTransaction(options) - - if (isEnrichment) { - // Fetch existing organizations - const existingRecords = await options.database.organizationCache.findAll({ - where: { - id: { - [options.database.Sequelize.Op.in]: data.map((x) => x.id), - }, - }, - transaction, - }) - - // Merge existing tags with new tags instead of overwriting - data = data.map((org) => { - const existingOrg = existingRecords.find((record) => record.id === org.id) - if (existingOrg && existingOrg.tags) { - // Merge existing and new tags without duplicates - org.tags = lodash.uniq([...org.tags, ...existingOrg.tags]) - } - return org - }) - } - - for (const org of data) { - this.update(org.id, org, options) - } - } - - static async destroy(id, options: IRepositoryOptions, force = false) { - const transaction = SequelizeRepository.getTransaction(options) - - const record = await options.database.organizationCache.findOne({ - where: { - id, - }, - transaction, - }) - - if (!record) { - throw new Error404() - } - - await record.destroy({ - transaction, - force, - }) - - await this._createAuditLog(AuditLogRepository.DELETE, record, record, options) - } - - static async findById(id, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const include = [] - - const record = await options.database.organizationCache.findOne({ - where: { - id, - }, - include, - transaction, - }) - - if (!record) { - throw new Error404() - } - const output = record.get({ plain: true }) - return output - } - - static async findByUrl(url, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const include = [] - - const record = await options.database.organizationCache.findOne({ - where: { - url, - }, - include, - transaction, - }) - - if (!record) { - return undefined - } - - const output = record.get({ plain: true }) - return output - } - - static async findByName(name, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const include = [] - - const record = await options.database.organizationCache.findOne({ - where: { - name, - }, - include, - transaction, - }) - - if (!record) { - return undefined - } - - const output = record.get({ plain: true }) - return output - } - - static async _createAuditLog(action, record, data, options: IRepositoryOptions) { - let values = {} - - if (data) { - values = { - ...record.get({ plain: true }), - } - } - - await AuditLogRepository.log( - { - entityName: 'organizationCache', - entityId: record.id, - action, - values, - }, - options, - ) - } -} - -export default OrganizationCacheRepository diff --git a/backend/src/database/repositories/organizationRepository.ts b/backend/src/database/repositories/organizationRepository.ts index 5e6992668c..e64d83eb8d 100644 --- a/backend/src/database/repositories/organizationRepository.ts +++ b/backend/src/database/repositories/organizationRepository.ts @@ -1,233 +1,108 @@ -import lodash, { chunk } from 'lodash' -import { get as getLevenshteinDistance } from 'fast-levenshtein' +import lodash, { uniq } from 'lodash' +import { QueryTypes } from 'sequelize' import validator from 'validator' -import { FieldTranslatorFactory, OpensearchQueryParser } from '@crowd/opensearch' -import { PageData } from '@crowd/common' + +import { + captureApiChange, + organizationCreateAction, + organizationEditIdentitiesAction, + organizationUpdateAction, +} from '@crowd/audit-logs' +import { Error400, Error404, Error409, RawQueryParser } from '@crowd/common' +import { queryActivities, queryActivityRelations } from '@crowd/data-access-layer' +import { findManyLfxMemberships } from '@crowd/data-access-layer/src/lfx_memberships' import { - IEnrichableOrganization, - IMemberOrganization, - IOrganization, + IDbOrgAttribute, + IDbOrganization, + OrgIdentityField, + OrganizationField, + addOrgIdentity, + addOrgsToSegments, + cleanUpOrgIdentities, + cleanupForOganization, + deleteOrganizationAttributes, + fetchManyOrgIdentities, + fetchManyOrgSegments, + fetchOrgIdentities, + findManyOrgAttributes, + findOrgAttributes, + findOrgById, + markOrgAttributeDefault, + queryOrgIdentities, + updateOrgIdentityVerifiedFlag, + upsertOrgAttributes, +} from '@crowd/data-access-layer/src/organizations' +import { findAttribute } from '@crowd/data-access-layer/src/organizations/attributesConfig' +import { optionsQx } from '@crowd/data-access-layer/src/queryExecutor' +import { findSegmentById, getSegmentSubprojectIds } from '@crowd/data-access-layer/src/segments' +import { + IMemberRenderFriendlyRole, + IMemberRoleWithOrganization, IOrganizationIdentity, - IOrganizationMergeSuggestion, - OpenSearchIndex, - SyncStatus, + MergeActionState, + MergeActionType, + OrganizationIdentityType, + SegmentData, } from '@crowd/types' -import Sequelize, { QueryTypes } from 'sequelize' -import SequelizeRepository from './sequelizeRepository' -import AuditLogRepository from './auditLogRepository' -import SequelizeFilterUtils from '../utils/sequelizeFilterUtils' -import Error404 from '../../errors/Error404' -import { IRepositoryOptions } from './IRepositoryOptions' -import QueryParser from './filters/queryParser' -import { QueryOutput } from './filters/queryTypes' -import OrganizationSyncRemoteRepository from './organizationSyncRemoteRepository' -import isFeatureEnabled from '@/feature-flags/isFeatureEnabled' -import { FeatureFlag } from '@/types/common' -import { SegmentData } from '@/types/segmentTypes' -import SegmentRepository from './segmentRepository' - -const { Op } = Sequelize - -interface IOrganizationIdentityOpensearch { - string_platform: string - string_name: string -} -interface IOrganizationPartialAggregatesOpensearch { - _source: { - uuid_organizationId: string - nested_identities: { - string_platform: string - string_name: string - }[] - uuid_arr_noMergeIds: string[] - } -} +import { + IFetchOrganizationMergeSuggestionArgs, + SimilarityScoreRange, +} from '@/types/mergeSuggestionTypes' -interface ISimilarOrganization { - _score: number - _source: { - uuid_organizationId: string - nested_identities: IOrganizationIdentityOpensearch[] - nested_weakIdentities: IOrganizationIdentityOpensearch[] - } -} +import { IRepositoryOptions } from './IRepositoryOptions' +import { OrganizationQueryCache } from './organizationsQueryCache' +import SegmentRepository from './segmentRepository' +import SequelizeRepository from './sequelizeRepository' interface IOrganizationId { id: string } -interface IOrganizationNoMerge { - organizationId: string - noMergeId: string -} - class OrganizationRepository { - static async filterByPayingTenant( - tenantId: string, - limit: number, - options: IRepositoryOptions, - ): Promise { - const database = SequelizeRepository.getSequelize(options) - const transaction = SequelizeRepository.getTransaction(options) - const query = ` - with org_activities as (select a."organizationId", count(a.id) as "orgActivityCount" - from activities a - where a."tenantId" = :tenantId - and a."deletedAt" is null - and a."isContribution" = true - group by a."organizationId" - having count(id) > 0), - identities as (select oi."organizationId", jsonb_agg(oi) as "identities" - from "organizationIdentities" oi - where oi."tenantId" = :tenantId - group by oi."organizationId") - select org.id, - i.identities, - org."displayName", - org."location", - org."website", - org."lastEnrichedAt", - org."twitter", - org."employees", - org."size", - org."founded", - org."industry", - org."naics", - org."profiles", - org."headline", - org."ticker", - org."type", - org."address", - org."geoLocation", - org."employeeCountByCountry", - org."twitter", - org."linkedin", - org."crunchbase", - org."github", - org."description", - org."revenueRange", - org."tags", - org."affiliatedProfiles", - org."allSubsidiaries", - org."alternativeDomains", - org."alternativeNames", - org."averageEmployeeTenure", - org."averageTenureByLevel", - org."averageTenureByRole", - org."directSubsidiaries", - org."employeeChurnRate", - org."employeeCountByMonth", - org."employeeGrowthRate", - org."employeeCountByMonthByLevel", - org."employeeCountByMonthByRole", - org."gicsSector", - org."grossAdditionsByMonth", - org."grossDeparturesByMonth", - org."ultimateParent", - org."immediateParent", - activity."orgActivityCount" - from "organizations" as org - join org_activities activity on activity."organizationId" = org."id" - join identities i on i."organizationId" = org.id - where :tenantId = org."tenantId" - and (org."lastEnrichedAt" is null or date_part('month', age(now(), org."lastEnrichedAt")) >= 6) - order by org."lastEnrichedAt" asc, org."website", activity."orgActivityCount" desc, org."createdAt" desc - limit :limit - ` - const orgs: IEnrichableOrganization[] = await database.query(query, { - type: QueryTypes.SELECT, - transaction, - replacements: { - tenantId, - limit, - }, - }) - return orgs - } - - static async filterByActiveLastYear( - tenantId: string, - limit: number, - options: IRepositoryOptions, - ): Promise { - const database = SequelizeRepository.getSequelize(options) - const transaction = SequelizeRepository.getTransaction(options) - const query = ` - with org_activities as (select a."organizationId", count(a.id) as "orgActivityCount" - from activities a - where a."tenantId" = :tenantId - and a."deletedAt" is null - and a."isContribution" = true - and a."createdAt" > (CURRENT_DATE - INTERVAL '1 year') - group by a."organizationId" - having count(id) > 0), - identities as (select oi."organizationId", jsonb_agg(oi) as "identities" - from "organizationIdentities" oi - where oi."tenantId" = :tenantId - group by oi."organizationId") - select org.id, - i.identities, - org."displayName", - org."location", - org."website", - org."lastEnrichedAt", - org."twitter", - org."employees", - org."size", - org."founded", - org."industry", - org."naics", - org."profiles", - org."headline", - org."ticker", - org."type", - org."address", - org."geoLocation", - org."employeeCountByCountry", - org."twitter", - org."linkedin", - org."crunchbase", - org."github", - org."description", - org."revenueRange", - org."tags", - org."affiliatedProfiles", - org."allSubsidiaries", - org."alternativeDomains", - org."alternativeNames", - org."averageEmployeeTenure", - org."averageTenureByLevel", - org."averageTenureByRole", - org."directSubsidiaries", - org."employeeChurnRate", - org."employeeCountByMonth", - org."employeeGrowthRate", - org."employeeCountByMonthByLevel", - org."employeeCountByMonthByRole", - org."gicsSector", - org."grossAdditionsByMonth", - org."grossDeparturesByMonth", - org."ultimateParent", - org."immediateParent", - activity."orgActivityCount" - from "organizations" as org - join org_activities activity on activity."organizationId" = org."id" - join identities i on i."organizationId" = org.id - where :tenantId = org."tenantId" - order by org."lastEnrichedAt" asc, org."website", activity."orgActivityCount" desc, org."createdAt" desc - limit :limit - ` - const orgs: IEnrichableOrganization[] = await database.query(query, { - type: QueryTypes.SELECT, - transaction, - replacements: { - tenantId, - limit, - }, - }) - return orgs - } + public static QUERY_FILTER_COLUMN_MAP: Map = new Map([ + // id fields + ['id', 'o.id'], + ['segmentId', 'osa."segmentId"'], + + // basic fields for filtering + ['size', 'o.size'], + ['industry', 'o.industry'], + ['employees', 'o."employees"'], + ['founded', 'o."founded"'], + ['headline', 'o."headline"'], + ['location', 'o."location"'], + ['tags', 'o."tags"'], + ['type', 'o."type"'], + ['isTeamOrganization', 'o."isTeamOrganization"'], + ['isAffiliationBlocked', 'o."isAffiliationBlocked"'], + + // basic fields for querying + ['displayName', 'o."displayName"'], + ['revenueRange', 'o."revenueRange"'], + ['employeeGrowthRate', 'o."employeeGrowthRate"'], + + // derived fields + ['employeeChurnRate12Month', `(o."employeeChurnRate"->>'12_month')::decimal`], + ['employeeGrowthRate12Month', `(o."employeeGrowthRate"->>'12_month')::decimal`], + ['revenueRangeMin', `(o."revenueRange"->>'min')::integer`], + ['revenueRangeMax', `(o."revenueRange"->>'max')::integer`], + + // aggregated fields + ['activityCount', 'coalesce(osa."activityCount", 0)::integer'], + ['memberCount', 'coalesce(osa."memberCount", 0)::integer'], + ['activeOn', 'coalesce(osa."activeOn", \'{}\'::text[])'], + ['joinedAt', 'osa."joinedAt"'], + ['lastActive', 'osa."lastActive"'], + ['avgContributorEngagement', 'coalesce(osa."avgContributorEngagement", 0)::integer'], + + // org fields for display + ['logo', 'o."logo"'], + ['description', 'o."description"'], + + // enrichment + ['lastEnrichedAt', 'oe."lastUpdatedAt"'], + ]) static async create(data, options: IRepositoryOptions) { const currentUser = SequelizeRepository.getCurrentUser(options) @@ -239,245 +114,63 @@ class OrganizationRepository { if (!data.displayName) { data.displayName = data.identities[0].name } + const toInsert = { + ...lodash.pick(data, [ + 'displayName', + 'description', + 'headline', + 'logo', + 'importHash', + 'isTeamOrganization', + 'isAffiliationBlocked', + 'lastEnrichedAt', + 'manuallyCreated', + ]), + + tenantId: tenant.id, + createdById: currentUser.id, + updatedById: currentUser.id, + } - const record = await options.database.organization.create( - { - ...lodash.pick(data, [ - 'displayName', - 'description', - 'emails', - 'phoneNumbers', - 'logo', - 'tags', - 'website', - 'location', - 'github', - 'twitter', - 'linkedin', - 'crunchbase', - 'employees', - 'revenueRange', - 'importHash', - 'isTeamOrganization', - 'employeeCountByCountry', - 'type', - 'ticker', - 'headline', - 'profiles', - 'naics', - 'industry', - 'founded', - 'size', - 'lastEnrichedAt', - 'manuallyCreated', - 'affiliatedProfiles', - 'allSubsidiaries', - 'alternativeDomains', - 'alternativeNames', - 'averageEmployeeTenure', - 'averageTenureByLevel', - 'averageTenureByRole', - 'directSubsidiaries', - 'employeeChurnRate', - 'employeeCountByMonth', - 'employeeGrowthRate', - 'employeeCountByMonthByLevel', - 'employeeCountByMonthByRole', - 'gicsSector', - 'grossAdditionsByMonth', - 'grossDeparturesByMonth', - 'ultimateParent', - 'immediateParent', - ]), - - tenantId: tenant.id, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { - transaction, - }, - ) - - await record.setMembers(data.members || [], { + const record = await options.database.organization.create(toInsert, { transaction, }) - if (data.identities && data.identities.length > 0) { - await OrganizationRepository.setIdentities(record.id, data.identities, options) - } - - await OrganizationRepository.includeOrganizationToSegments(record.id, options) - - await this._createAuditLog(AuditLogRepository.CREATE, record, data, options) - - return this.findById(record.id, options) - } - - static async bulkUpdate( - data: T, - fields: string[], - options: IRepositoryOptions, - isEnrichment: boolean = false, - ): Promise { - const transaction = SequelizeRepository.getTransaction(options) - - // Ensure every organization has a non-undefine primary ID - const isValid = new Set(data.filter((org) => org.id).map((org) => org.id)).size !== data.length - if (isValid) return [] as T - - if (isEnrichment) { - // Fetch existing organizations - const existingOrgs = await options.database.organization.findAll({ - where: { - id: { - [options.database.Sequelize.Op.in]: data.map((org) => org.id), - }, - }, - }) + // prepare attributes object + const attributes = {} as any - // Append new tags to existing tags instead of overwriting - if (fields.includes('tags')) { - // @ts-ignore - data = data.map((org) => { - const existingOrg = existingOrgs.find((o) => o.id === org.id) - if (existingOrg && existingOrg.tags) { - // Merge existing and new tags without duplicates - org.tags = lodash.uniq([...existingOrg.tags, ...org.tags]) - } - return org - }) + if (data.logo) { + attributes.logo = { + custom: [data.logo], + default: data.logo, } } - // Using bulk insert to update on duplicate primary ID - try { - const orgs = await options.database.organization.bulkCreate(data, { - fields: ['id', 'tenantId', ...fields], - updateOnDuplicate: fields, - returning: fields, - transaction, - }) - return orgs - } catch (error) { - options.log.error('Error while bulk updating organizations!', error) - throw error - } - } - - static async checkIdentities( - data: IOrganization, - options: IRepositoryOptions, - organizationId?: string, - ): Promise { - // convert non-existing weak identities to strong ones - if (data.weakIdentities && data.weakIdentities.length > 0) { - const strongNotOwnedIdentities = await OrganizationRepository.findIdentities( - data.weakIdentities, - options, - organizationId, - ) - - const strongIdentities = [] + await this.updateOrgAttributes(record.id, { attributes }, options) - // find weak identities in the payload that doesn't exist as a strong identity yet - for (const weakIdentity of data.weakIdentities) { - if (!strongNotOwnedIdentities.has(`${weakIdentity.platform}:${weakIdentity.name}`)) { - strongIdentities.push(weakIdentity) - } - } + await captureApiChange( + options, + organizationCreateAction(record.id, async (captureState) => { + captureState(toInsert) + }), + ) - // exclude identities that are converted to a strong one from weakIdentities - if (strongIdentities.length > 0) { - data.weakIdentities = data.weakIdentities.filter( - (i) => - strongIdentities.find((s) => s.platform === i.platform && s.name === i.name) === - undefined, - ) - // push new strong identities to the payload - for (const identity of strongIdentities) { - if ( - data.identities.find( - (i) => i.platform === identity.platform && i.name === identity.name, - ) === undefined - ) { - data.identities.push(identity) - } - } - } - } + await record.setMembers(data.members || [], { + transaction, + }) - // convert already existing strong identities to weak ones if (data.identities && data.identities.length > 0) { - const strongNotOwnedIdentities = await OrganizationRepository.findIdentities( - data.identities, - options, - organizationId, - ) - - const weakIdentities: IOrganizationIdentity[] = [] - - // find strong identities in payload that already exist in some other organization, and convert these to weak - for (const identity of data.identities) { - if (strongNotOwnedIdentities.has(`${identity.platform}:${identity.name}`)) { - weakIdentities.push(identity) - } - } - - // exclude identities that are converted to a weak one from strong identities - if (weakIdentities.length > 0) { - data.identities = data.identities.filter( - (i) => - weakIdentities.find((w) => w.platform === i.platform && w.name === i.name) === - undefined, - ) - - // push new weak identities to the payload - for (const weakIdentity of weakIdentities) { - if (!data.weakIdentities) { - data.weakIdentities = [] - } - - if ( - data.weakIdentities.find( - (w) => w.platform === weakIdentity.platform && w.name === weakIdentity.name, - ) === undefined - ) { - data.weakIdentities.push(weakIdentity) - } - } - } - } - } - - static async includeOrganizationToSegments(organizationId: string, options: IRepositoryOptions) { - const seq = SequelizeRepository.getSequelize(options) - - const transaction = SequelizeRepository.getTransaction(options) - - let bulkInsertOrganizationSegments = `INSERT INTO "organizationSegments" ("organizationId","segmentId", "tenantId", "createdAt") VALUES ` - const replacements = { - organizationId, - tenantId: options.currentTenant.id, + await OrganizationRepository.setIdentities(record.id, data.identities, options) } - for (let idx = 0; idx < options.currentSegments.length; idx++) { - bulkInsertOrganizationSegments += ` (:organizationId, :segmentId${idx}, :tenantId, now()) ` + const currentSegments = SequelizeRepository.getSegmentIds(options) - replacements[`segmentId${idx}`] = options.currentSegments[idx].id - - if (idx !== options.currentSegments.length - 1) { - bulkInsertOrganizationSegments += `,` - } - } + const qx = SequelizeRepository.getQueryExecutor(options) + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) - bulkInsertOrganizationSegments += ` ON CONFLICT DO NOTHING` + await addOrgsToSegments(qx, subprojectIds, [record.id]) - await seq.query(bulkInsertOrganizationSegments, { - replacements, - type: QueryTypes.INSERT, - transaction, - }) + return this.findById(record.id, options) } static async excludeOrganizationsFromSegments( @@ -490,10 +183,15 @@ class OrganizationRepository { const bulkDeleteOrganizationSegments = `DELETE FROM "organizationSegments" WHERE "organizationId" in (:organizationIds) and "segmentId" in (:segmentIds);` + const currentSegments = SequelizeRepository.getSegmentIds(options) + + const qx = SequelizeRepository.getQueryExecutor(options) + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) + await seq.query(bulkDeleteOrganizationSegments, { replacements: { organizationIds, - segmentIds: SequelizeRepository.getSegmentIds(options), + segmentIds: subprojectIds, }, type: QueryTypes.DELETE, transaction, @@ -519,149 +217,208 @@ class OrganizationRepository { }) } - static async removeMemberRole(role: IMemberOrganization, options: IRepositoryOptions) { - const seq = SequelizeRepository.getSequelize(options) - const transaction = SequelizeRepository.getTransaction(options) + static ORGANIZATION_UPDATE_COLUMNS = [ + 'importHash', + 'isTeamOrganization', + 'isAffiliationBlocked', + 'headline', + 'lastEnrichedAt', + + // default attributes + 'type', + 'industry', + 'founded', + 'size', + 'employees', + 'displayName', + 'description', + 'logo', + 'tags', + 'location', + 'employees', + 'revenueRange', + 'employeeChurnRate', + 'employeeGrowthRate', + ] + + static isEqual = { + displayName: (a, b) => a === b, + description: (a, b) => a === b, + emails: (a, b) => lodash.isEqual((a || []).sort(), (b || []).sort()), + phoneNumbers: (a, b) => lodash.isEqual((a || []).sort(), (b || []).sort()), + logo: (a, b) => a === b, + location: (a, b) => a === b, + isTeamOrganization: (a, b) => a === b, + isAffiliationBlocked: (a, b) => a === b, + attributes: (a, b) => lodash.isEqual(a, b), + } + + static convertOrgAttributesForInsert(data: any) { + const orgAttributes = [] + const defaultColumns = {} + + for (const [name, attribute] of Object.entries(data.attributes)) { + const attributeDefinition = findAttribute(name) + + if (!(attribute as any).custom) { + continue // eslint-disable-line no-continue + } + + for (const value of (attribute as any).custom) { + const isDefault = value === (attribute as any).default + + orgAttributes.push({ + type: attributeDefinition.type, + name, + source: 'custom', + default: isDefault, + value, + }) - let deleteMemberRole = `DELETE FROM "memberOrganizations" - WHERE - "organizationId" = :organizationId and - "memberId" = :memberId` - - const replacements = { - organizationId: role.organizationId, - memberId: role.memberId, - } as any - - if (role.dateStart === null) { - deleteMemberRole += ` and "dateStart" is null ` - } else { - deleteMemberRole += ` and "dateStart" = :dateStart ` - replacements.dateStart = (role.dateStart as Date).toISOString() + if (isDefault && attributeDefinition.defaultColumn) { + defaultColumns[attributeDefinition.defaultColumn] = value + } + } } - if (role.dateEnd === null) { - deleteMemberRole += ` and "dateEnd" is null ` - } else { - deleteMemberRole += ` and "dateEnd" = :dateEnd ` - replacements.dateEnd = (role.dateEnd as Date).toISOString() + return { + orgAttributes, + defaultColumns, } + } - await seq.query(deleteMemberRole, { - replacements, - type: QueryTypes.DELETE, - transaction, - }) + static convertOrgAttributesForDisplay(attributes: IDbOrgAttribute[]) { + return attributes.reduce((acc, a) => { + if (!acc[a.name]) { + acc[a.name] = {} + } + if (!acc[a.name][a.source]) { + acc[a.name][a.source] = [] + } + + acc[a.name][a.source].push(a.value) + if (a.default) { + acc[a.name].default = a.value + } + return acc + }, {}) } - static async addMemberRole( - role: IMemberOrganization, - options: IRepositoryOptions, - ): Promise { - const transaction = SequelizeRepository.getTransaction(options) - const sequelize = SequelizeRepository.getSequelize(options) + static async updateOrgAttributes(organizationId: string, data: any, options: IRepositoryOptions) { + const qx = SequelizeRepository.getQueryExecutor(options) - const query = ` - insert into "memberOrganizations" ("memberId", "organizationId", "createdAt", "updatedAt", "title", "dateStart", "dateEnd", "source") - values (:memberId, :organizationId, NOW(), NOW(), :title, :dateStart, :dateEnd, :source) - on conflict do nothing; - ` + const { orgAttributes, defaultColumns } = + OrganizationRepository.convertOrgAttributesForInsert(data) - await sequelize.query(query, { - replacements: { - memberId: role.memberId, - organizationId: role.organizationId, - title: role.title || null, - dateStart: role.dateStart, - dateEnd: role.dateEnd, - source: role.source || null, - }, - type: QueryTypes.INSERT, - transaction, - }) + await upsertOrgAttributes(qx, organizationId, orgAttributes) + for (const attr of orgAttributes) { + if (attr.default) { + await markOrgAttributeDefault(qx, organizationId, attr) + } + } + + return defaultColumns } - static async update(id, data, options: IRepositoryOptions, overrideIdentities = false) { + static async update( + id, + data, + options: IRepositoryOptions, + overrideIdentities = false, + manualChange = false, + ) { const currentUser = SequelizeRepository.getCurrentUser(options) const transaction = SequelizeRepository.getTransaction(options) const currentTenant = SequelizeRepository.getCurrentTenant(options) - let record = await options.database.organization.findOne({ - where: { - id, - tenantId: currentTenant.id, - }, - transaction, - }) + const seq = SequelizeRepository.getSequelize(options) - if (!record) { - throw new Error404() - } + const record = await captureApiChange( + options, + organizationUpdateAction(id, async (captureOldState, captureNewState) => { + const record = await options.database.organization.findOne({ + where: { + id, + tenantId: currentTenant.id, + }, + transaction, + }) - // exclude syncRemote attributes, since these are populated from organizationSyncRemote table - if (data.attributes?.syncRemote) { - delete data.attributes.syncRemote - } + if (!record) { + throw new Error404() + } - record = await record.update( - { - ...lodash.pick(data, [ - 'displayName', - 'description', - 'emails', - 'phoneNumbers', - 'logo', - 'tags', - 'website', - 'location', - 'github', - 'twitter', - 'linkedin', - 'crunchbase', - 'employees', - 'revenueRange', - 'importHash', - 'isTeamOrganization', - 'employeeCountByCountry', - 'type', - 'ticker', - 'headline', - 'profiles', - 'naics', - 'industry', - 'founded', - 'size', - 'employees', - 'twitter', - 'lastEnrichedAt', - 'affiliatedProfiles', - 'allSubsidiaries', - 'alternativeDomains', - 'alternativeNames', - 'averageEmployeeTenure', - 'averageTenureByLevel', - 'averageTenureByRole', - 'directSubsidiaries', - 'employeeChurnRate', - 'employeeCountByMonth', - 'employeeGrowthRate', - 'employeeCountByMonthByLevel', - 'employeeCountByMonthByRole', - 'gicsSector', - 'grossAdditionsByMonth', - 'grossDeparturesByMonth', - 'ultimateParent', - 'immediateParent', - 'attributes', - 'weakIdentities', - ]), - updatedById: currentUser.id, - }, - { - transaction, - }, + captureOldState(record.get({ plain: true })) + + if (data.identities) { + const primaryDomainIdentity = data.identities.find( + (i) => i.type === OrganizationIdentityType.PRIMARY_DOMAIN && i.verified, + ) + + // check if domain already exists in another organization in the same tenant + if (primaryDomainIdentity) { + const existingOrg = (await seq.query( + ` + select "organizationId" + from "organizationIdentities" + where + "tenantId" = :tenantId and + "organizationId" <> :id and + type = :type and + value = :value and + verified = true + `, + { + replacements: { + tenantId: currentTenant.id, + id: record.id, + type: OrganizationIdentityType.PRIMARY_DOMAIN, + value: primaryDomainIdentity.value, + }, + type: QueryTypes.SELECT, + transaction, + }, + )) as any[] + + // ensure that it's not the same organization + if (existingOrg && existingOrg.length > 0) { + throw new Error409( + options.language, + 'errors.alreadyExists', + existingOrg[0].organizationId, + ) + } + } + } + + if (data.attributes) { + const defaultColumns = await OrganizationRepository.updateOrgAttributes( + record.id, + data, + options, + ) + for (const col of Object.keys(defaultColumns)) { + data[col] = defaultColumns[col] + } + } + + const updatedData = { + ...lodash.pick(data, this.ORGANIZATION_UPDATE_COLUMNS), + updatedById: currentUser.id, + } + captureNewState(updatedData) + await options.database.organization.update(updatedData, { + where: { + id: record.id, + }, + transaction, + }) + + return record + }), + !manualChange, // skip audit log if not a manual change ) if (data.members) { await record.setMembers(data.members || [], { @@ -679,20 +436,64 @@ class OrganizationRepository { } if (data.segments) { - await OrganizationRepository.includeOrganizationToSegments(record.id, options) + const qx = SequelizeRepository.getQueryExecutor(options) + const currentSegments = SequelizeRepository.getSegmentIds(options) + + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) + + await addOrgsToSegments(qx, subprojectIds, [record.id]) } - if (data.identities && data.identities.length > 0) { - if (overrideIdentities) { - await this.setIdentities(id, data.identities, options) - } else { - for (const identity of data.identities) { - await this.addIdentity(id, identity, options) + await captureApiChange( + options, + organizationEditIdentitiesAction(id, async (captureOldState, captureNewState) => { + const qx = SequelizeRepository.getQueryExecutor(options) + const initialIdentities = await fetchOrgIdentities(qx, id) + + function convertIdentitiesForAudit(identities: IOrganizationIdentity[]) { + return identities.reduce((acc, r) => { + if (!acc[r.platform]) { + acc[r.platform] = [] + } + + acc[r.platform].push({ + value: r.value, + type: r.type, + verified: r.verified, + }) + + acc[r.platform] = acc[r.platform].sort((a, b) => + `${a.value}:${a.type}:${a.verified}`.localeCompare( + `${b.value}:${b.type}:${b.verified}`, + ), + ) + + return acc + }, {}) } - } - } - await this._createAuditLog(AuditLogRepository.UPDATE, record, data, options) + captureOldState(convertIdentitiesForAudit(initialIdentities)) + + if (data.identities && data.identities.length > 0) { + if (overrideIdentities) { + captureNewState( + convertIdentitiesForAudit( + data.identities.map((i) => ({ + platform: i.platform, + value: i.value, + type: i.type, + verified: i.verified, + })), + ), + ) + await this.setIdentities(id, data.identities, options) + } else { + captureNewState(convertIdentitiesForAudit([...initialIdentities, ...data.identities])) + await OrganizationRepository.addIdentities(id, data.identities, options) + } + } + }), + ) return this.findById(record.id, options) } @@ -731,12 +532,7 @@ class OrganizationRepository { ) } - static async destroy( - id, - options: IRepositoryOptions, - force = false, - destroyIfOnlyNoSegmentsLeft = true, - ) { + static async destroy(id, options: IRepositoryOptions, force = false) { const transaction = SequelizeRepository.getTransaction(options) const currentTenant = SequelizeRepository.getCurrentTenant(options) @@ -753,32 +549,21 @@ class OrganizationRepository { throw new Error404() } - if (destroyIfOnlyNoSegmentsLeft) { - await OrganizationRepository.excludeOrganizationsFromSegments([id], { - ...options, - transaction, - }) - const org = await this.findById(id, options) + await OrganizationRepository.excludeOrganizationsFromAllSegments([id], { + ...options, + transaction, + }) - if (org.segments.length === 0) { - await record.destroy({ - transaction, - force, - }) - } - } else { - await OrganizationRepository.excludeOrganizationsFromAllSegments([id], { - ...options, - transaction, - }) + const qx = SequelizeRepository.getQueryExecutor(options) - await record.destroy({ - transaction, - force, - }) - } + await cleanupForOganization(qx, id) + + await deleteOrganizationAttributes(qx, [id]) - await this._createAuditLog(AuditLogRepository.DELETE, record, record, options) + await record.destroy({ + transaction, + force, + }) } static async setIdentities( @@ -786,56 +571,55 @@ class OrganizationRepository { identities: IOrganizationIdentity[], options: IRepositoryOptions, ): Promise { - const transaction = SequelizeRepository.getTransaction(options) - const sequelize = SequelizeRepository.getSequelize(options) - const currentTenant = SequelizeRepository.getCurrentTenant(options) + const qx = SequelizeRepository.getQueryExecutor(options) - await sequelize.query( - `delete from "organizationIdentities" where "organizationId" = :organizationId and "tenantId" = :tenantId`, - { - replacements: { - organizationId, - tenantId: currentTenant.id, - }, - type: QueryTypes.DELETE, - transaction, - }, - ) + await cleanUpOrgIdentities(qx, organizationId) + await OrganizationRepository.addIdentities(organizationId, identities, options) + } + + static async addIdentities( + organizationId: string, + identities: IOrganizationIdentity[], + options: IRepositoryOptions, + ) { for (const identity of identities) { await OrganizationRepository.addIdentity(organizationId, identity, options) } } - static async addIdentity( + static async updateIdentity( organizationId: string, identity: IOrganizationIdentity, options: IRepositoryOptions, ): Promise { - const transaction = SequelizeRepository.getTransaction(options) - const sequelize = SequelizeRepository.getSequelize(options) - const currentTenant = SequelizeRepository.getCurrentTenant(options) + const qx = SequelizeRepository.getQueryExecutor(options) - const query = ` - insert into - "organizationIdentities"("organizationId", "platform", "name", "url", "sourceId", "tenantId", "integrationId", "createdAt") - values - (:organizationId, :platform, :name, :url, :sourceId, :tenantId, :integrationId, now()) - on conflict do nothing; - ` + await updateOrgIdentityVerifiedFlag(qx, { + organizationId, + platform: identity.platform, + value: identity.value, + type: identity.type, + verified: identity.verified, + }) + } - await sequelize.query(query, { - replacements: { - organizationId, - platform: identity.platform, - sourceId: identity.sourceId || null, - url: identity.url || null, - tenantId: currentTenant.id, - integrationId: identity.integrationId || null, - name: identity.name, - }, - type: QueryTypes.INSERT, - transaction, + static async addIdentity( + organizationId: string, + identity: IOrganizationIdentity, + options: IRepositoryOptions, + ): Promise { + const qx = SequelizeRepository.getQueryExecutor(options) + + await addOrgIdentity(qx, { + organizationId, + platform: identity.platform, + source: identity.source, + sourceId: identity.sourceId || null, + value: identity.value, + type: identity.type, + verified: identity.verified, + integrationId: identity.integrationId || null, }) } @@ -845,17 +629,15 @@ class OrganizationRepository { ): Promise { const transaction = SequelizeRepository.getTransaction(options) const sequelize = SequelizeRepository.getSequelize(options) - const currentTenant = SequelizeRepository.getCurrentTenant(options) const results = await sequelize.query( ` - select "sourceId", "platform", "name", "integrationId", "organizationId" from "organizationIdentities" - where "tenantId" = :tenantId and "organizationId" in (:organizationIds) + select "sourceId", "source", platform, value, type, verified, "integrationId", "organizationId" from "organizationIdentities" + where "organizationId" in (:organizationIds) `, { replacements: { organizationIds, - tenantId: currentTenant.id, }, type: QueryTypes.SELECT, transaction, @@ -875,28 +657,28 @@ class OrganizationRepository { const seq = SequelizeRepository.getSequelize(options) - const tenant = SequelizeRepository.getCurrentTenant(options) - const query = ` - update "organizationIdentities" - set + update "organizationIdentities" + set "organizationId" = :newOrganizationId - where - "tenantId" = :tenantId and - "organizationId" = :oldOrganizationId and - platform = :platform and - name = :name; + where + "organizationId" = :oldOrganizationId and + platform = :platform and + value = :value and + type = :type and + verified = :verified; ` for (const identity of identitiesToMove) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_, count] = await seq.query(query, { replacements: { - tenantId: tenant.id, oldOrganizationId: fromOrganizationId, newOrganizationId: toOrganizationId, platform: identity.platform, - name: identity.name, + value: identity.value, + type: identity.type, + verified: identity.verified, }, type: QueryTypes.UPDATE, transaction, @@ -918,7 +700,7 @@ class OrganizationRepository { const query = ` insert into "organizationNoMerge" ("organizationId", "noMergeId", "createdAt", "updatedAt") - values + values (:organizationId, :noMergeId, now(), now()), (:noMergeId, :organizationId, now(), now()) on conflict do nothing; @@ -1006,809 +788,296 @@ class OrganizationRepository { } } - static async findNoMergeIds(id: string, options: IRepositoryOptions): Promise { - const transaction = SequelizeRepository.getTransaction(options) - const seq = SequelizeRepository.getSequelize(options) - - const query = `select onm."organizationId", onm."noMergeId" from "organizationNoMerge" onm - where onm."organizationId" = :id or onm."noMergeId" = :id;` - - try { - const results: IOrganizationNoMerge[] = await seq.query(query, { - type: QueryTypes.SELECT, - replacements: { - id, - }, - transaction, - }) - - return Array.from( - results.reduce((acc, r) => { - if (id === r.organizationId) { - acc.add(r.noMergeId) - } else if (id === r.noMergeId) { - acc.add(r.organizationId) - } - return acc - }, new Set()), - ) - } catch (error) { - options.log.error('error while getting non existing organizations from db', error) - throw error - } - } - - static async addToMerge( - suggestions: IOrganizationMergeSuggestion[], - options: IRepositoryOptions, - ): Promise { - const transaction = SequelizeRepository.getTransaction(options) - const seq = SequelizeRepository.getSequelize(options) - - // Remove possible duplicates - suggestions = lodash.uniqWith(suggestions, (a, b) => - lodash.isEqual(lodash.sortBy(a.organizations), lodash.sortBy(b.organizations)), - ) - - // check all suggestion ids exists in the db - const uniqueOrganizationIds = Array.from( - suggestions.reduce((acc, suggestion) => { - acc.add(suggestion.organizations[0]) - acc.add(suggestion.organizations[1]) - return acc - }, new Set()), - ) - - // filter non existing org ids from suggestions - const nonExistingIds = await OrganizationRepository.findNonExistingIds( - uniqueOrganizationIds, - options, - ) - - suggestions = suggestions.filter( - (s) => - !nonExistingIds.includes(s.organizations[0]) && - !nonExistingIds.includes(s.organizations[1]), - ) - - // Process suggestions in chunks of 100 or less - const suggestionChunks: IOrganizationMergeSuggestion[][] = chunk(suggestions, 100) - - const insertValues = ( - organizationId: string, - toMergeId: string, - similarity: number | null, - index: number, - ) => { - const idPlaceholder = (key: string) => `${key}${index}` - return { - query: `(:${idPlaceholder('organizationId')}, :${idPlaceholder( - 'toMergeId', - )}, :${idPlaceholder('similarity')}, NOW(), NOW())`, - replacements: { - [idPlaceholder('organizationId')]: organizationId, - [idPlaceholder('toMergeId')]: toMergeId, - [idPlaceholder('similarity')]: similarity === null ? null : similarity, - }, - } - } - - for (const suggestionChunk of suggestionChunks) { - const placeholders: string[] = [] - let replacements: Record = {} - - suggestionChunk.forEach((suggestion, index) => { - const { query, replacements: chunkReplacements } = insertValues( - suggestion.organizations[0], - suggestion.organizations[1], - suggestion.similarity, - index, - ) - placeholders.push(query) - replacements = { ...replacements, ...chunkReplacements } - }) - - const query = ` - INSERT INTO "organizationToMerge" ("organizationId", "toMergeId", "similarity", "createdAt", "updatedAt") - VALUES ${placeholders.join(', ')} - on conflict do nothing; - ` - try { - await seq.query(query, { - replacements, - type: QueryTypes.INSERT, - transaction, - }) - } catch (error) { - options.log.error('error adding organizations to merge', error) - throw error - } - } - } - - static async findMembersBelongToBothOrganizations( - organizationId1: string, - organizationId2: string, + static async countOrganizationMergeSuggestions( + organizationFilter: string, + similarityFilter: string, + displayNameFilter: string, + replacements: { + segmentIds: string[] + organizationId?: string + displayName?: string + mergeActionType: MergeActionType + mergeActionStatus: MergeActionState + }, options: IRepositoryOptions, - ): Promise { - const transaction = SequelizeRepository.getTransaction(options) - const sequelize = SequelizeRepository.getSequelize(options) + ): Promise { + const organizationsJoin = displayNameFilter + ? `JOIN organizations o1 ON o1.id = otm."organizationId" + JOIN organizations o2 ON o2.id = otm."toMergeId"` + : '' - const results = await sequelize.query( + const result = await options.database.sequelize.query( ` - SELECT mo.* - FROM "memberOrganizations" AS mo - WHERE mo."deletedAt" is null and - mo."memberId" IN ( - SELECT "memberId" - FROM "memberOrganizations" - WHERE "organizationId" = :organizationId1 + SELECT COUNT(DISTINCT Greatest( + Hashtext(Concat(otm."organizationId", otm."toMergeId")), + Hashtext(Concat(otm."toMergeId", otm."organizationId")) + )) AS total_count + FROM "organizationToMerge" otm + ${organizationsJoin} + LEFT JOIN "mergeActions" ma + ON ma.type = :mergeActionType + AND ( + (ma."primaryId" = otm."organizationId" AND ma."secondaryId" = otm."toMergeId") + OR (ma."primaryId" = otm."toMergeId" AND ma."secondaryId" = otm."organizationId") + ) + WHERE EXISTS ( + SELECT 1 FROM "organizationSegmentsAgg" os1 + WHERE os1."organizationId" = otm."organizationId" AND os1."segmentId" IN (:segmentIds) ) - AND mo."memberId" IN ( - SELECT "memberId" - FROM "memberOrganizations" - WHERE "organizationId" = :organizationId2); - `, + AND EXISTS ( + SELECT 1 FROM "organizationSegmentsAgg" os2 + WHERE os2."organizationId" = otm."toMergeId" AND os2."segmentId" IN (:segmentIds) + ) + AND (ma.id IS NULL OR ma.state = :mergeActionStatus) + ${organizationFilter} + ${similarityFilter} + ${displayNameFilter} + `, { - replacements: { - organizationId1, - organizationId2, - }, + replacements, type: QueryTypes.SELECT, - transaction, }, ) - return results as IMemberOrganization[] + return result[0]?.total_count || 0 } - static async moveActivitiesBetweenOrganizations( - fromOrganizationId: string, - toOrganizationId: string, + static async findOrganizationsWithMergeSuggestions( + args: IFetchOrganizationMergeSuggestionArgs, options: IRepositoryOptions, - batchSize = 10000, - ): Promise { - const transaction = SequelizeRepository.getTransaction(options) - const seq = SequelizeRepository.getSequelize(options) - const tenant = SequelizeRepository.getCurrentTenant(options) + ) { + const HIGH_CONFIDENCE_LOWER_BOUND = 0.9 + const MEDIUM_CONFIDENCE_LOWER_BOUND = 0.7 - let updatedRowsCount = 0 + // Organization segments are aggregated at each hierarchy level (group -> project -> subproject). + // Match the selected segment ID(s) directly; do not expand to leaf subprojects. + const segmentIds = SequelizeRepository.getSegmentIds(options) - do { - options.log.info( - `[Move Activities] - Moving maximum of ${batchSize} activities from ${fromOrganizationId} to ${toOrganizationId}.`, - ) + let similarityFilter = '' + const similarityConditions = [] - const query = ` - UPDATE "activities" - SET "organizationId" = :newOrganizationId - WHERE id IN ( - SELECT id - FROM "activities" - WHERE "tenantId" = :tenantId - AND "organizationId" = :oldOrganizationId - LIMIT :limit + for (const similarity of args.filter?.similarity || []) { + if (similarity === SimilarityScoreRange.HIGH) { + similarityConditions.push(`(otm.similarity >= ${HIGH_CONFIDENCE_LOWER_BOUND})`) + } else if (similarity === SimilarityScoreRange.MEDIUM) { + similarityConditions.push( + `(otm.similarity >= ${MEDIUM_CONFIDENCE_LOWER_BOUND} and otm.similarity < ${HIGH_CONFIDENCE_LOWER_BOUND})`, ) - ` - - const [, rowCount] = await seq.query(query, { - replacements: { - tenantId: tenant.id, - oldOrganizationId: fromOrganizationId, - newOrganizationId: toOrganizationId, - limit: batchSize, - }, - type: QueryTypes.UPDATE, - transaction, - }) - - updatedRowsCount = rowCount ?? 0 - } while (updatedRowsCount === batchSize) - } - - static async *getMergeSuggestions( - options: IRepositoryOptions, - ): AsyncGenerator { - const BATCH_SIZE = 100 - - const YIELD_CHUNK_SIZE = 100 - - let yieldChunk: IOrganizationMergeSuggestion[] = [] - - const prefixLength = (string: string) => { - if (string.length > 5 && string.length < 8) { - return 6 + } else if (similarity === SimilarityScoreRange.LOW) { + similarityConditions.push(`(otm.similarity < ${MEDIUM_CONFIDENCE_LOWER_BOUND})`) } - - return 10 } - const calculateSimilarity = ( - primaryOrganization: IOrganizationPartialAggregatesOpensearch, - similarOrganization: ISimilarOrganization, - ): number => { - let smallestEditDistance: number = null - - let similarPrimaryIdentity: IOrganizationIdentityOpensearch = null - - // find the smallest edit distance between both identity arrays - for (const primaryIdentity of primaryOrganization._source.nested_identities) { - // similar organization has a weakIdentity as one of primary organization's strong identity, return score 95 - if ( - similarOrganization._source.nested_weakIdentities.length > 0 && - similarOrganization._source.nested_weakIdentities.some( - (weakIdentity) => - weakIdentity.string_name === primaryIdentity.string_name && - weakIdentity.string_platform === primaryIdentity.string_platform, - ) - ) { - return 0.95 - } - for (const secondaryIdentity of similarOrganization._source.nested_identities) { - const currentLevenstheinDistance = getLevenshteinDistance( - primaryIdentity.string_name, - secondaryIdentity.string_name, - ) - if (smallestEditDistance === null || smallestEditDistance > currentLevenstheinDistance) { - smallestEditDistance = currentLevenstheinDistance - similarPrimaryIdentity = primaryIdentity - } - } - } - - // calculate similarity percentage - const identityLength = similarPrimaryIdentity.string_name.length - - if (identityLength < smallestEditDistance) { - // if levensthein distance is bigger than the word itself, it might be a prefix match, return medium similarity - return (Math.floor(Math.random() * 21) + 20) / 100 - } - - return Math.floor(((identityLength - smallestEditDistance) / identityLength) * 100) / 100 + if (similarityConditions.length > 0) { + similarityFilter = ` and (${similarityConditions.join(' or ')})` } - const tenant = SequelizeRepository.getCurrentTenant(options) + const organizationFilter = args.filter?.organizationId + ? ` AND ("otm"."organizationId" = :organizationId OR "otm"."toMergeId" = :organizationId)` + : '' - const queryBody = { - from: 0, - size: BATCH_SIZE, - query: {}, - sort: { - [`uuid_organizationId`]: 'asc', - }, - collapse: { - field: 'uuid_organizationId', - }, - _source: ['uuid_organizationId', 'nested_identities', 'uuid_arr_noMergeIds'], - } + const displayNameFilter = args.filter?.displayName + ? ` and (o1."displayName" ilike :displayName OR o2."displayName" ilike :displayName)` + : '' - let organizations: IOrganizationPartialAggregatesOpensearch[] = [] - let lastUuid: string + let order = + '"organizationsToMerge".similarity desc, "organizationsToMerge"."id", "organizationsToMerge"."toMergeId"' - do { - if (organizations.length > 0) { - queryBody.query = { - bool: { - filter: [ - { - bool: { - should: [ - { - range: { - int_activityCount: { - gt: 0, - }, - }, - }, - { - term: { - bool_manuallyCreated: true, - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - term: { - uuid_tenantId: tenant.id, - }, - }, - { - range: { - uuid_organizationId: { - gt: lastUuid, - }, - }, - }, - ], - }, - } - } else { - queryBody.query = { - bool: { - filter: [ - { - bool: { - should: [ - { - range: { - int_activityCount: { - gt: 0, - }, - }, - }, - { - term: { - bool_manuallyCreated: true, - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - term: { - uuid_tenantId: tenant.id, - }, - }, - ], - }, + if (args.orderBy?.length > 0) { + order = '' + for (const orderBy of args.orderBy) { + const [field, direction] = orderBy.split('_') + if (['similarity'].includes(field) && ['asc', 'desc'].includes(direction.toLowerCase())) { + order += `"organizationsToMerge".${field} ${direction}, ` } } - organizations = - ( - await options.opensearch.search({ - index: OpenSearchIndex.ORGANIZATIONS, - body: queryBody, - }) - ).body?.hits?.hits || [] - - if (organizations.length > 0) { - lastUuid = organizations[organizations.length - 1]._source.uuid_organizationId - } - - for (const organization of organizations) { - if ( - organization._source.nested_identities && - organization._source.nested_identities.length > 0 - ) { - const identitiesPartialQuery = { - should: [ - { - nested: { - path: 'nested_weakIdentities', - query: { - bool: { - should: [], - boost: 1000, - minimum_should_match: 1, - }, - }, - }, - }, - { - nested: { - path: 'nested_identities', - query: { - bool: { - should: [], - boost: 1, - minimum_should_match: 1, - }, - }, - }, - }, - ], - minimum_should_match: 1, - must_not: [ - { - term: { - uuid_organizationId: organization._source.uuid_organizationId, - }, - }, - ], - must: [ - { - term: { - uuid_tenantId: tenant.id, - }, - }, - { - bool: { - should: [ - { - range: { - int_activityCount: { - gt: 0, - }, - }, - }, - { - term: { - bool_manuallyCreated: true, - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - } - - let hasFuzzySearch = false - - for (const identity of organization._source.nested_identities) { - if (identity.string_name.length > 0) { - // weak identity search - identitiesPartialQuery.should[0].nested.query.bool.should.push({ - bool: { - must: [ - { match: { [`nested_weakIdentities.keyword_name`]: identity.string_name } }, - { - match: { - [`nested_weakIdentities.string_platform`]: identity.string_platform, - }, - }, - ], - }, - }) - - // some identities have https? in the beginning, resulting in false positive suggestions - // remove these when making fuzzy, wildcard and prefix searches - const cleanedIdentityName = identity.string_name.replace(/^https?:\/\//, '') - - // only do fuzzy/wildcard/partial search when identity name is not all numbers (like linkedin organization profiles) - if (Number.isNaN(Number(identity.string_name))) { - hasFuzzySearch = true - // fuzzy search for identities - identitiesPartialQuery.should[1].nested.query.bool.should.push({ - match: { - [`nested_identities.keyword_name`]: { - query: cleanedIdentityName, - prefix_length: 1, - fuzziness: 'auto', - }, - }, - }) - - // also check for prefix for identities that has more than 5 characters and no whitespace - if (identity.string_name.length > 5 && identity.string_name.indexOf(' ') === -1) { - identitiesPartialQuery.should[1].nested.query.bool.should.push({ - prefix: { - [`nested_identities.keyword_name`]: { - value: cleanedIdentityName.slice(0, prefixLength(cleanedIdentityName)), - }, - }, - }) - } - } - } - } - - // check if we have any actual identity searches, if not remove it from the query - if (!hasFuzzySearch) { - identitiesPartialQuery.should.pop() - } - - const noMergeIds = await OrganizationRepository.findNoMergeIds( - organization._source.uuid_organizationId, - options, - ) - - if (noMergeIds && noMergeIds.length > 0) { - for (const noMergeId of noMergeIds) { - identitiesPartialQuery.must_not.push({ - term: { - uuid_organizationId: noMergeId, - }, - }) - } - } - - const sameOrganizationsQueryBody = { - query: { - bool: identitiesPartialQuery, - }, - collapse: { - field: 'uuid_organizationId', - }, - _source: ['uuid_organizationId', 'nested_identities', 'nested_weakIdentities'], - } - - const organizationsToMerge: ISimilarOrganization[] = - ( - await options.opensearch.search({ - index: OpenSearchIndex.ORGANIZATIONS, - body: sameOrganizationsQueryBody, - }) - ).body?.hits?.hits || [] - - /* - const { maxScore, minScore } = organizationsToMerge.reduce( - (acc, organizationToMerge) => { - if (!acc.minScore || organizationToMerge._score < acc.minScore) { - acc.minScore = organizationToMerge._score - } - - if (!acc.maxScore || organizationToMerge._score > acc.maxScore) { - acc.maxScore = organizationToMerge._score - } - - return acc - }, - { maxScore: null, minScore: null }, - ) - */ - - for (const organizationToMerge of organizationsToMerge) { - yieldChunk.push({ - similarity: calculateSimilarity(organization, organizationToMerge), - organizations: [ - organization._source.uuid_organizationId, - organizationToMerge._source.uuid_organizationId, - ], - }) - } + order += '"organizationsToMerge"."id", "organizationsToMerge"."toMergeId"' + } - if (yieldChunk.length >= YIELD_CHUNK_SIZE) { - yield yieldChunk - yieldChunk = [] - } - } - } - } while (organizations.length > 0) + if (args.countOnly) { + const totalCount = await this.countOrganizationMergeSuggestions( + organizationFilter, + similarityFilter, + displayNameFilter, + { + segmentIds, + displayName: args?.filter?.displayName ? `${args.filter.displayName}%` : undefined, + organizationId: args?.filter?.organizationId, + mergeActionType: MergeActionType.ORG, + mergeActionStatus: MergeActionState.ERROR, + }, + options, + ) - if (yieldChunk.length > 0) { - yield yieldChunk + return { count: totalCount } } - } - - static async findOrganizationsWithMergeSuggestions( - { limit = 20, offset = 0 }, - options: IRepositoryOptions, - ) { - const currentTenant = SequelizeRepository.getCurrentTenant(options) - const segmentIds = SequelizeRepository.getSegmentIds(options) const orgs = await options.database.sequelize.query( `WITH cte AS ( SELECT - Greatest(Hashtext(Concat(org.id, otm."toMergeId")), Hashtext(Concat(otm."toMergeId", org.id))) as hash, - org.id, + Greatest(Hashtext(Concat(otm."organizationId", otm."toMergeId")), Hashtext(Concat(otm."toMergeId", otm."organizationId"))) as hash, + otm."organizationId" as id, otm."toMergeId", - org."createdAt", - otm."similarity" - FROM organizations org - JOIN "organizationToMerge" otm ON org.id = otm."organizationId" - JOIN "organizationSegments" os ON os."organizationId" = org.id - JOIN "organizationSegments" to_merge_segments on to_merge_segments."organizationId" = otm."toMergeId" - WHERE org."tenantId" = :tenantId - AND os."segmentId" IN (:segmentIds) - AND to_merge_segments."segmentId" IN (:segmentIds) + o1."createdAt", + otm."similarity", + o1."displayName" as "primaryDisplayName", + o1.logo as "primaryLogo", + o2."displayName" as "secondaryDisplayName", + o2.logo as "secondaryLogo", + (SELECT os1."segmentId" FROM "organizationSegmentsAgg" os1 + WHERE os1."organizationId" = otm."organizationId" AND os1."segmentId" IN (:segmentIds) + LIMIT 1) as "primarySegmentId", + (SELECT os2."segmentId" FROM "organizationSegmentsAgg" os2 + WHERE os2."organizationId" = otm."toMergeId" AND os2."segmentId" IN (:segmentIds) + LIMIT 1) as "secondarySegmentId" + FROM "organizationToMerge" otm + JOIN organizations o1 ON o1.id = otm."organizationId" + JOIN organizations o2 ON o2.id = otm."toMergeId" + LEFT JOIN "mergeActions" ma + ON ma.type = :mergeActionType + AND ( + (ma."primaryId" = otm."organizationId" AND ma."secondaryId" = otm."toMergeId") + OR (ma."primaryId" = otm."toMergeId" AND ma."secondaryId" = otm."organizationId") + ) + WHERE EXISTS ( + SELECT 1 FROM "organizationSegmentsAgg" os1 + WHERE os1."organizationId" = otm."organizationId" AND os1."segmentId" IN (:segmentIds) + ) + AND EXISTS ( + SELECT 1 FROM "organizationSegmentsAgg" os2 + WHERE os2."organizationId" = otm."toMergeId" AND os2."segmentId" IN (:segmentIds) + ) + AND (ma.id IS NULL OR ma.state = :mergeActionStatus) + ${organizationFilter} + ${similarityFilter} + ${displayNameFilter} ), - + count_cte AS ( SELECT COUNT(DISTINCT hash) AS total_count FROM cte ), - + final_select AS ( SELECT DISTINCT ON (hash) id, "toMergeId", + "primaryDisplayName", + "primaryLogo", + "secondaryDisplayName", + "secondaryLogo", "createdAt", - "similarity" + "similarity", + "primarySegmentId", + "secondarySegmentId" FROM cte ORDER BY hash, id ) - + SELECT "organizationsToMerge".id, "organizationsToMerge"."toMergeId", + "organizationsToMerge"."primaryDisplayName", + "organizationsToMerge"."primaryLogo", + "organizationsToMerge"."secondaryDisplayName", + "organizationsToMerge"."secondaryLogo", + "organizationsToMerge"."primarySegmentId", + "organizationsToMerge"."secondarySegmentId", count_cte."total_count", "organizationsToMerge"."similarity" FROM final_select AS "organizationsToMerge", count_cte ORDER BY - "organizationsToMerge"."similarity" DESC, "organizationsToMerge".id + ${order} LIMIT :limit OFFSET :offset `, { replacements: { - tenantId: currentTenant.id, segmentIds, - limit, - offset, + limit: args.limit, + offset: args.offset, + displayName: args?.filter?.displayName ? `${args.filter.displayName}%` : undefined, + mergeActionType: MergeActionType.ORG, + mergeActionStatus: MergeActionState.ERROR, + organizationId: args?.filter?.organizationId, }, type: QueryTypes.SELECT, }, ) if (orgs.length > 0) { - const organizationPromises = [] - const toMergePromises = [] + let result + + if (args.detail) { + const organizationPromises = [] + const toMergePromises = [] + + for (const org of orgs) { + organizationPromises.push( + OrganizationRepository.findById(org.id, options, org.primarySegmentId), + ) + toMergePromises.push( + OrganizationRepository.findById(org.toMergeId, options, org.secondarySegmentId), + ) + } + + const organizationResults = await Promise.all(organizationPromises) + const organizationToMergeResults = await Promise.all(toMergePromises) - for (const org of orgs) { - organizationPromises.push(OrganizationRepository.findById(org.id, options)) - toMergePromises.push(OrganizationRepository.findById(org.toMergeId, options)) + result = organizationResults.map((i, idx) => ({ + organizations: [i, organizationToMergeResults[idx]], + similarity: orgs[idx].similarity, + })) + } else { + result = orgs.map((o) => ({ + organizations: [ + { + id: o.id, + displayName: o.primaryDisplayName, + logo: o.primaryLogo, + }, + { + id: o.toMergeId, + displayName: o.secondaryDisplayName, + logo: o.secondaryLogo, + }, + ], + similarity: o.similarity, + })) } - const organizationResults = await Promise.all(organizationPromises) - const organizationToMergeResults = await Promise.all(toMergePromises) + const qx = SequelizeRepository.getQueryExecutor(options) + const organizationIds = uniq(result.map((r) => r.organizations[0].id)) + const lfxMemberships = await findManyLfxMemberships(qx, { + organizationIds, + }) + result.forEach((r) => { + r.organizations.forEach((org) => { + org.lfxMembership = lfxMemberships.find((m) => m.organizationId === org.id) + }) + }) - const result = organizationResults.map((i, idx) => ({ - organizations: [i, organizationToMergeResults[idx]], - similarity: orgs[idx].similarity, - })) - return { rows: result, count: orgs[0].total_count, limit, offset } + return { rows: result, count: orgs[0].total_count, limit: args.limit, offset: args.offset } } - return { rows: [{ organizations: [], similarity: 0 }], count: 0, limit, offset } + return { + rows: [{ organizations: [], similarity: 0 }], + count: 0, + limit: args.limit, + offset: args.offset, + } } - static async moveMembersBetweenOrganizations( - fromOrganizationId: string, - toOrganizationId: string, + static async getOrganizationSegments( + organizationId: string, options: IRepositoryOptions, - ): Promise { - const seq = SequelizeRepository.getSequelize(options) - + ): Promise { const transaction = SequelizeRepository.getTransaction(options) - - let removeRoles: IMemberOrganization[] = [] - - let addRoles: IMemberOrganization[] = [] - - // first, handle members that belong to both organizations, - // then make a full update on remaining org2 members (that doesn't belong to o1) - const memberRolesWithBothOrganizations = await this.findMembersBelongToBothOrganizations( - fromOrganizationId, - toOrganizationId, - options, - ) - - const primaryOrganizationMemberRoles = memberRolesWithBothOrganizations.filter( - (m) => m.organizationId === toOrganizationId, - ) - const secondaryOrganizationMemberRoles = memberRolesWithBothOrganizations.filter( - (m) => m.organizationId === fromOrganizationId, - ) - - for (const memberOrganization of secondaryOrganizationMemberRoles) { - // if dateEnd and dateStart isn't available, we don't need to move but delete it from org2 - if (memberOrganization.dateStart === null && memberOrganization.dateEnd === null) { - removeRoles.push(memberOrganization) - } - // it's a current role, also check org1 to see which one starts earlier - else if (memberOrganization.dateStart !== null && memberOrganization.dateEnd === null) { - const currentRoles = primaryOrganizationMemberRoles.filter( - (mo) => - mo.memberId === memberOrganization.memberId && - mo.dateStart !== null && - mo.dateEnd === null, - ) - if (currentRoles.length === 0) { - // no current role in org1, add the memberOrganization to org1 - addRoles.push(memberOrganization) - } else if (currentRoles.length === 1) { - const currentRole = currentRoles[0] - if (new Date(memberOrganization.dateStart) <= new Date(currentRoles[0].dateStart)) { - // add a new role with earlier dateStart - addRoles.push({ - id: currentRole.id, - dateStart: (memberOrganization.dateStart as Date).toISOString(), - dateEnd: null, - memberId: currentRole.memberId, - organizationId: currentRole.organizationId, - title: currentRole.title, - source: currentRole.source, - }) - - // remove current role - removeRoles.push(currentRole) - } - - // delete role from org2 - removeRoles.push(memberOrganization) - } else { - throw new Error(`Member ${memberOrganization.memberId} has more than one current roles.`) - } - } else if (memberOrganization.dateStart === null && memberOrganization.dateEnd !== null) { - throw new Error(`Member organization with dateEnd and without dateStart!`) - } else { - // both dateStart and dateEnd exists - const foundIntersectingRoles = primaryOrganizationMemberRoles.filter((mo) => { - const primaryStart = new Date(mo.dateStart) - const primaryEnd = new Date(mo.dateEnd) - const secondaryStart = new Date(memberOrganization.dateStart) - const secondaryEnd = new Date(memberOrganization.dateEnd) - - return ( - mo.memberId === memberOrganization.memberId && - ((secondaryStart < primaryStart && secondaryEnd > primaryStart) || - (primaryStart < secondaryStart && secondaryEnd < primaryEnd) || - (secondaryStart < primaryStart && secondaryEnd > primaryEnd) || - (primaryStart < secondaryStart && secondaryEnd > primaryEnd)) - ) - }) - - // rebuild dateRanges using intersecting roles coming from primary and secondary organizations - const startDates = [...foundIntersectingRoles, memberOrganization].map((org) => - new Date(org.dateStart).getTime(), - ) - const endDates = [...foundIntersectingRoles, memberOrganization].map((org) => - new Date(org.dateEnd).getTime(), - ) - - addRoles.push({ - dateStart: new Date(Math.min.apply(null, startDates)).toISOString(), - dateEnd: new Date(Math.max.apply(null, endDates)).toISOString(), - memberId: memberOrganization.memberId, - organizationId: toOrganizationId, - title: - foundIntersectingRoles.length > 0 - ? foundIntersectingRoles[0].title - : memberOrganization.title, - source: - foundIntersectingRoles.length > 0 - ? foundIntersectingRoles[0].source - : memberOrganization.source, - }) - - // we'll delete all roles that intersect with incoming org member roles and create a merged role - for (const r of foundIntersectingRoles) { - removeRoles.push(r) - } - } - - for (const removeRole of removeRoles) { - await this.removeMemberRole(removeRole, options) - } - - for (const addRole of addRoles) { - await this.addMemberRole(addRole, options) - } - - addRoles = [] - removeRoles = [] - } - - // update rest of the o2 members - await seq.query( - ` - UPDATE "memberOrganizations" - SET "organizationId" = :toOrganizationId - WHERE "organizationId" = :fromOrganizationId - AND "deletedAt" IS NULL - AND "memberId" NOT IN ( - SELECT "memberId" - FROM "memberOrganizations" - WHERE "organizationId" = :toOrganizationId - AND "deletedAt" IS NULL - ); - `, - { - replacements: { - toOrganizationId, - fromOrganizationId, - }, - type: QueryTypes.UPDATE, - transaction, - }, - ) - } - - static async getOrganizationSegments( - organizationId: string, - options: IRepositoryOptions, - ): Promise { - const transaction = SequelizeRepository.getTransaction(options) - const seq = SequelizeRepository.getSequelize(options) - const segmentRepository = new SegmentRepository(options) + const seq = SequelizeRepository.getSequelize(options) + const segmentRepository = new SegmentRepository(options) const query = ` SELECT "segmentId" @@ -1831,412 +1100,120 @@ class OrganizationRepository { return segments } - static async findByIdentity( - identity: IOrganizationIdentity, + static async findByVerifiedIdentities( + identities: IOrganizationIdentity[], options: IRepositoryOptions, - ): Promise { - const transaction = SequelizeRepository.getTransaction(options) - const sequelize = SequelizeRepository.getSequelize(options) - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const results = await sequelize.query( - ` - with - "organizationsWithIdentity" as ( - select oi."organizationId" - from "organizationIdentities" oi - where - oi.platform = :platform - and oi.name = :name - ) - select o.id, - o.description, - o.emails, - o.logo, - o.tags, - o.github, - o.twitter, - o.linkedin, - o.crunchbase, - o.employees, - o.location, - o.website, - o.type, - o.size, - o.headline, - o.industry, - o.founded, - o.attributes - from organizations o - where o."tenantId" = :tenantId - and o.id in (select "organizationId" from "organizationsWithIdentity"); - `, - { - replacements: { - tenantId: currentTenant.id, - name: identity.name, - platform: identity.platform, - }, - type: QueryTypes.SELECT, - transaction, + ): Promise { + const qx = SequelizeRepository.getQueryExecutor(options) + + const foundOrgs = await queryOrgIdentities(qx, { + fields: [OrgIdentityField.ORGANIZATION_ID], + filter: { + or: identities.map((identity) => ({ + and: [ + { platform: { eq: identity.platform } }, + { value: { eq: identity.value } }, + { type: { eq: identity.type } }, + { verified: { eq: true } }, + ], + })), }, - ) + }) - if (results.length === 0) { + if (foundOrgs.length === 0) { return null } - const result = results[0] as IOrganization - - return result - } - - static async findByDomain(domain: string, options: IRepositoryOptions): Promise { - const transaction = SequelizeRepository.getTransaction(options) - const sequelize = SequelizeRepository.getSequelize(options) - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const results = await sequelize.query( - ` - SELECT - o.id, - o.description, - o.emails, - o.logo, - o.tags, - o.github, - o.twitter, - o.linkedin, - o.crunchbase, - o.employees, - o.location, - o.website, - o.type, - o.size, - o.headline, - o.industry, - o.founded, - o.attributes, - o."weakIdentities" - FROM - organizations o - WHERE - o."tenantId" = :tenantId AND - o.website = :domain - `, - { - replacements: { - tenantId: currentTenant.id, - domain, - }, - type: QueryTypes.SELECT, - transaction, - }, + const foundOrgsIdentities = await fetchManyOrgIdentities( + qx, + foundOrgs.map((o) => o.organizationId), ) - if (results.length === 0) { - return null - } - - const result = results[0] as IOrganization + const orgIdWithMostIdentities = foundOrgsIdentities.sort( + (a, b) => b.identities.length - a.identities.length, + )[0].organizationId + + const result = await findOrgById(qx, orgIdWithMostIdentities, [ + OrganizationField.ID, + OrganizationField.DISPLAY_NAME, + OrganizationField.DESCRIPTION, + OrganizationField.LOGO, + OrganizationField.TAGS, + OrganizationField.EMPLOYEES, + OrganizationField.REVENUE_RANGE, + OrganizationField.IMPORT_HASH, + OrganizationField.LOCATION, + OrganizationField.TYPE, + OrganizationField.SIZE, + OrganizationField.HEADLINE, + OrganizationField.INDUSTRY, + OrganizationField.FOUNDED, + OrganizationField.IS_TEAM_ORGANIZATION, + OrganizationField.IS_AFFILIATION_BLOCKED, + OrganizationField.MANUALLY_CREATED, + ]) return result } - static async findIdentities( - identities: IOrganizationIdentity[], - options: IRepositoryOptions, - organizationId?: string, - ): Promise> { - const transaction = SequelizeRepository.getTransaction(options) - const sequelize = SequelizeRepository.getSequelize(options) - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const params = { - tenantId: currentTenant.id, - } as any - - const condition = organizationId ? 'and "organizationId" <> :organizationId' : '' - - if (organizationId) { - params.organizationId = organizationId - } - - const identityParams = identities - .map((identity, index) => `(:platform${index}, :name${index})`) - .join(', ') - - identities.forEach((identity, index) => { - params[`platform${index}`] = identity.platform - params[`name${index}`] = identity.name - }) + static async findById(id: string, options: IRepositoryOptions, segmentId?: string) { + let orgResponse = null - const results = (await sequelize.query( - ` - with input_identities (platform, name) as ( - values ${identityParams} - ) - select "organizationId", i.platform, i.name - from "organizationIdentities" oi - inner join input_identities i on oi.platform = i.platform and oi.name = i.name - where oi."tenantId" = :tenantId ${condition} - `, + orgResponse = await OrganizationRepository.findAndCountAll( { - replacements: params, - type: QueryTypes.SELECT, - transaction, + filter: { id: { eq: id } }, + limit: 1, + offset: 0, + segmentId, + include: { + aggregates: true, + attributes: true, + lfxMemberships: true, + identities: true, + segments: true, + }, }, - )) as IOrganizationIdentity[] - - const resultMap = new Map() - results.forEach((row) => { - resultMap.set(`${row.platform}:${row.name}`, row.organizationId) - }) - - return resultMap - } - - static async findById(id: string, options: IRepositoryOptions, segmentId?: string) { - const transaction = SequelizeRepository.getTransaction(options) - const sequelize = SequelizeRepository.getSequelize(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const replacements: Record = { - id, - tenantId: currentTenant.id, - } - - // query for all leaf segment ids - let extraCTEs = ` - leaf_segment_ids AS ( - select id - from segments - where "tenantId" = :tenantId and "parentSlug" is not null and "grandparentSlug" is not null - ), - ` - - if (segmentId) { - // we load data for a specific segment (can be leaf, parent or grand parent id) - replacements.segmentId = segmentId - extraCTEs = ` - input_segment AS ( - select - id, - slug, - "parentSlug", - "grandparentSlug" - from segments - where id = :segmentId - and "tenantId" = :tenantId - ), - segment_level AS ( - select - case - when "parentSlug" is not null and "grandparentSlug" is not null - then 'child' - when "parentSlug" is not null and "grandparentSlug" is null - then 'parent' - when "parentSlug" is null and "grandparentSlug" is null - then 'grandparent' - end as level, - id, - slug, - "parentSlug", - "grandparentSlug" - from input_segment - ), - leaf_segment_ids AS ( - select s.id - from segments s - join segment_level sl on (sl.level = 'child' and s.id = sl.id) - or (sl.level = 'parent' and s."parentSlug" = sl.slug and s."grandparentSlug" is not null) - or (sl.level = 'grandparent' and s."grandparentSlug" = sl.slug) - ), - ` - } - - const query = ` - WITH - ${extraCTEs} - member_data AS ( - select - a."organizationId", - count(distinct a."memberId") as "memberCount", - count(distinct a.id) as "activityCount", - case - when array_agg(distinct a.platform::TEXT) = array [null] then array []::text[] - else array_agg(distinct a.platform::TEXT) end as "activeOn", - max(a.timestamp) as "lastActive", - min(a.timestamp) filter ( where a.timestamp <> '1970-01-01T00:00:00.000Z' ) as "joinedAt" - from leaf_segment_ids ls - join mv_activities_cube a on a."segmentId" = ls.id and a."organizationId" = :id - group by a."organizationId" - ), - organization_segments as ( - select "organizationId", array_agg("segmentId") as "segments" - from "organizationSegments" - where "organizationId" = :id - group by "organizationId" - ), - identities as ( - SELECT oi."organizationId", jsonb_agg(oi) AS "identities" - FROM "organizationIdentities" oi - WHERE oi."organizationId" = :id - GROUP BY "organizationId" - ) - select - o.*, - coalesce(md."activityCount", 0)::integer as "activityCount", - coalesce(md."memberCount", 0)::integer as "memberCount", - coalesce(md."activeOn", '{}') as "activeOn", - coalesce(i.identities, '{}') as identities, - coalesce(os.segments, '{}') as segments, - md."lastActive", - md."joinedAt" - from organizations o - left join member_data md on md."organizationId" = o.id - left join organization_segments os on os."organizationId" = o.id - left join identities i on i."organizationId" = o.id - where o.id = :id - and o."tenantId" = :tenantId; - ` - - const results = await sequelize.query(query, { - replacements, - type: QueryTypes.SELECT, - transaction, - }) - - if (results.length === 0) { - throw new Error404() - } - - const result = results[0] as any - - const manualSyncRemote = await new OrganizationSyncRemoteRepository( options, - ).findOrganizationManualSync(result.id) - - for (const syncRemote of manualSyncRemote) { - if (result.attributes?.syncRemote) { - result.attributes.syncRemote[syncRemote.platform] = syncRemote.status === SyncStatus.ACTIVE - } else { - result.attributes.syncRemote = { - [syncRemote.platform]: syncRemote.status === SyncStatus.ACTIVE, - } - } - } - - // compatibility issue - delete result.searchSyncedAt - - return result - } - - static async findByName(name, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const include = [] - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const record = await options.database.organization.findOne({ - where: { - name, - tenantId: currentTenant.id, - }, - include, - transaction, - }) - - if (!record) { - return null - } - - return record.get({ plain: true }) - } - - static async findByUrl(url, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const include = [] - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const record = await options.database.organization.findOne({ - where: { - url, - tenantId: currentTenant.id, - }, - include, - transaction, - }) - - if (!record) { - return null - } - - return record.get({ plain: true }) - } - - static async findOrCreateByDomain(domain, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - // Check if organization exists - let organization = await options.database.organization.findOne({ - attributes: ['id'], - where: { - website: domain, - tenantId: currentTenant.id, - }, - transaction, - }) + ) - if (!organization) { - const data = { - displayName: domain, - website: domain, - identities: [ - { - name: domain, - platform: 'email', + if (orgResponse.count === 0) { + // try it again without segment information (no aggregates) + // for orgs without activities + orgResponse = await OrganizationRepository.findAndCountAll( + { + filter: { id: { eq: id } }, + limit: 1, + offset: 0, + include: { + aggregates: false, + attributes: true, + lfxMemberships: true, + identities: true, + segments: true, }, - ], - tenantId: currentTenant.id, - } - organization = await this.create(data, options) - } - - return organization.id - } + }, + options, + ) - static async filterIdInTenant(id, options: IRepositoryOptions) { - return lodash.get(await this.filterIdsInTenant([id], options), '[0]', null) - } + if (orgResponse.count === 0) { + throw new Error404() + } - static async filterIdsInTenant(ids, options: IRepositoryOptions) { - if (!ids || !ids.length) { - return [] + orgResponse.rows[0].joinedAt = null + orgResponse.rows[0].lastActive = null + orgResponse.rows[0].activityCount = 0 + orgResponse.rows[0].memberCount = 0 + orgResponse.rows[0].avgContributorEngagement = null + orgResponse.rows[0].activeOn = null } - const currentTenant = SequelizeRepository.getCurrentTenant(options) + const organization = orgResponse.rows[0] - const where = { - id: { - [Op.in]: ids, - }, - tenantId: currentTenant.id, - } - - const records = await options.database.organization.findAll({ - attributes: ['id'], - where, - }) + const qx = SequelizeRepository.getQueryExecutor(options) + const attributes = await findOrgAttributes(qx, id) + organization.attributes = OrganizationRepository.convertOrgAttributesForDisplay(attributes) - return records.map((record) => record.id) + return organization } static async destroyBulk(ids, options: IRepositoryOptions, force = false) { @@ -2273,552 +1250,385 @@ class OrganizationRepository { }) } - static async findOrganizationActivities( - organizationId: string, - limit: number, - offset: number, - options: IRepositoryOptions, - ): Promise { - const seq = SequelizeRepository.getSequelize(options) - - const results = await seq.query( - `select "id", "organizationId" - from "activities" - where "organizationId" = :organizationId - order by "createdAt" - limit :limit offset :offset`, - { - replacements: { - organizationId, - limit, - offset, - }, - type: QueryTypes.SELECT, - }, - ) + private static removeLfxMembershipFromFilters( + filtersArray: [], + index: number, + filterName: string, + ) { + const lfxFilterObj = Object.assign(filtersArray[filterName][index])?.lfxMembership + filtersArray[filterName].splice(index, 1) - return results + if (filtersArray[filterName].length === 0) + // edge case when "lfxMembership" is the only filter + delete filtersArray[filterName] + return lfxFilterObj } - static async findAndCountAllOpensearch( - { - filter = {} as any, - limit = 20, - offset = 0, - orderBy = 'joinedAt_DESC', - countOnly = false, - segments = [] as string[], - customSortFunction = undefined, - }, - options: IRepositoryOptions, - ): Promise> { - if (orderBy.length === 0) { - orderBy = 'joinedAt_DESC' + private static handleLfxMembershipFilter(filter: any): { + lfxMembershipFilter: object | null + updatedfilter: object + } { + if (!filter) { + return { lfxMembershipFilter: null, updatedfilter: filter } } - const tenant = SequelizeRepository.getCurrentTenant(options) - - const segmentsEnabled = await isFeatureEnabled(FeatureFlag.SEGMENTS, options) - - const segment = segments[0] + let lfxMembershipFilter = null + const updatedfilter = Object.assign(filter) - const translator = FieldTranslatorFactory.getTranslator(OpenSearchIndex.ORGANIZATIONS) + // handle nested "and" filters \\ "or" inside "and" + if (updatedfilter.and && Array.isArray(updatedfilter.and)) + for (let i = 0; i < updatedfilter.and.length; i++) { + if (Object.hasOwn(updatedfilter.and[i], 'lfxMembership')) { + lfxMembershipFilter = this.removeLfxMembershipFromFilters(updatedfilter, i, 'and') + return { lfxMembershipFilter, updatedfilter } + } + if ( + Object.hasOwn(updatedfilter.and[i], 'and') || + Object.hasOwn(updatedfilter.and[i], 'or') + ) { + const result = this.handleLfxMembershipFilter(updatedfilter.and[i]) + lfxMembershipFilter = result.lfxMembershipFilter + } + } - if (filter.and) { - filter.and.push({ - or: [ - { - manuallyCreated: { - eq: true, - }, - }, - { - activityCount: { - gt: 0, - }, - }, - ], - }) - } + // "or" filters cannot be nested, we can only have "or" inside parent "and" filter + if (updatedfilter.or && Array.isArray(updatedfilter.or)) + for (let i = 0; i < updatedfilter.or.length; i++) + if (Object.hasOwn(updatedfilter.or[i], 'lfxMembership')) { + lfxMembershipFilter = this.removeLfxMembershipFromFilters(updatedfilter, i, 'or') + return { lfxMembershipFilter, updatedfilter } + } - const parsed = OpensearchQueryParser.parse( - { filter, limit, offset, orderBy }, - OpenSearchIndex.ORGANIZATIONS, - translator, - ) + return { lfxMembershipFilter, updatedfilter } + } - // add tenant filter to parsed query - parsed.query.bool.must.push({ - term: { - uuid_tenantId: tenant.id, + static async findAndCountAll( + { + countOnly = false, + fields = [...OrganizationRepository.QUERY_FILTER_COLUMN_MAP.keys()], + filter = {} as any, + include = { + identities: true, + lfxMemberships: true, + segments: false, + attributes: false, + } as { + aggregates?: boolean + identities?: boolean + lfxMemberships?: boolean + segments?: boolean + attributes?: boolean }, + limit = 20, + offset = 0, + orderBy = undefined, + search = undefined as string | undefined, + segmentId = undefined, + }, + options: IRepositoryOptions, + ) { + // Initialize cache + const cache = new OrganizationQueryCache(options.redis) + + // Build cache key + const cacheKey = OrganizationQueryCache.buildCacheKey({ + countOnly, + fields, + filter, + include, + limit, + offset, + orderBy, + search, + segmentId, }) - if (segmentsEnabled) { - // add segment filter - parsed.query.bool.must.push({ - term: { - uuid_segmentId: segment, + // Try to get from cache first + const cachedResult = countOnly ? null : await cache.get(cacheKey) + const cachedCount = countOnly ? await cache.getCount(cacheKey) : null + + if (cachedResult) { + this.refreshCacheInBackground( + cache, + cacheKey, + { + filter, + search, + limit, + offset, + orderBy, + segmentId, + countOnly: false, + fields, + include, }, - }) - } - - // exclude empty filters if any - parsed.query.bool.must = parsed.query.bool.must.filter((obj) => { - // Check if the object has a non-empty 'term' property - if (obj.term) { - return Object.keys(obj.term).length !== 0 - } - return true - }) + options, + ) - if (customSortFunction) { - parsed.sort = customSortFunction + options.log.info(`Organizations advanced query cache hit: ${cacheKey}`) + return cachedResult } - const countResponse = await options.opensearch.count({ - index: OpenSearchIndex.ORGANIZATIONS, - body: { query: parsed.query }, - }) + if (countOnly && cachedCount !== null) { + this.refreshCountCacheInBackground( + cache, + cacheKey, + { + filter, + search, + segmentId, + include, + }, + options, + ) - if (countOnly) { + options.log.info(`Organizations advanced count query cache hit: ${cacheKey}`) return { rows: [], - count: countResponse.body.count, + count: cachedCount, limit, offset, } } - const response = await options.opensearch.search({ - index: OpenSearchIndex.ORGANIZATIONS, - body: parsed, - }) - - const translatedRows = response.body.hits.hits.map((o) => - translator.translateObjectToCrowd(o._source), + return this.executeQuery( + cache, + cacheKey, + { + filter, + search, + limit, + offset, + orderBy, + segmentId, + countOnly, + fields, + include, + }, + options, ) - - return { rows: translatedRows, count: countResponse.body.count, limit, offset } } - static async findAndCountAll( + private static async executeQuery( + cache: OrganizationQueryCache, + cacheKey: string, { filter = {} as any, - advancedFilter = null as any, - limit = 0, + search = undefined as string | undefined, + limit = 20, offset = 0, - orderBy = '', - includeOrganizationsWithoutMembers = true, + orderBy = undefined, + segmentId = undefined, + fields = [...OrganizationRepository.QUERY_FILTER_COLUMN_MAP.keys()], + include = { + identities: true, + lfxMemberships: true, + segments: false, + attributes: false, + } as { + aggregates?: boolean + identities?: boolean + lfxMemberships?: boolean + segments?: boolean + attributes?: boolean + }, + countOnly = false, }, options: IRepositoryOptions, ) { - let customOrderBy: Array = [] + const qx = SequelizeRepository.getQueryExecutor(options) - const include = [ - { - model: options.database.member, - as: 'members', - required: !includeOrganizationsWithoutMembers, - attributes: [], - through: { - attributes: [], - where: { - deletedAt: null, - }, - }, - include: [ - { - model: options.database.activity, - as: 'activities', - attributes: [], - }, - { - model: options.database.memberIdentity, - as: 'memberIdentities', - attributes: [], - }, - ], - }, - { - model: options.database.segment, - as: 'segments', - attributes: [], - through: { - attributes: [], - }, - }, - ] - - const activeOn = Sequelize.literal( - `array_agg( distinct ("members->activities".platform) ) filter (where "members->activities".platform is not null)`, - ) + const withAggregates = include.aggregates - // TODO: member identitites FIX - const identities = Sequelize.literal( - `array_agg( distinct "members->memberIdentities".platform)`, - ) - - const lastActive = Sequelize.literal(`MAX("members->activities".timestamp)`) - - const joinedAt = Sequelize.literal(` - MIN( - CASE - WHEN "members->activities".timestamp != '1970-01-01T00:00:00.000Z' - THEN "members->activities".timestamp - END - ) - `) + const { lfxMembershipFilter, updatedfilter } = + OrganizationRepository.handleLfxMembershipFilter(filter) + filter = updatedfilter // updated filter without lfxMembershipFilter + let lfxMembershipFilterWhereClause = '' - const memberCount = Sequelize.literal(`COUNT(DISTINCT "members".id)::integer`) - - const activityCount = Sequelize.literal(`COUNT("members->activities".id)::integer`) - - const segments = Sequelize.literal( - `ARRAY_AGG(DISTINCT "segments->organizationSegments"."segmentId")`, - ) - - // If the advanced filter is empty, we construct it from the query parameter filter - if (!advancedFilter) { - advancedFilter = { and: [] } - - if (filter.id) { - advancedFilter.and.push({ - id: filter.id, - }) - } - - if (filter.displayName) { - advancedFilter.and.push({ - displayName: { - textContains: filter.displayName, - }, - }) + if (lfxMembershipFilter) { + const filterKey = Object.keys(lfxMembershipFilter)[0] + if (filterKey === 'ne') { + lfxMembershipFilterWhereClause = `AND EXISTS (SELECT 1 FROM "lfxMemberships" lm WHERE lm."organizationId" = o.id AND lm."tenantId" = $(tenantId))` + } else if (filterKey === 'eq') { + lfxMembershipFilterWhereClause = `AND NOT EXISTS (SELECT 1 FROM "lfxMemberships" lm WHERE lm."organizationId" = o.id AND lm."tenantId" = $(tenantId))` } + } - if (filter.description) { - advancedFilter.and.push({ - description: { - textContains: filter.description, - }, - }) - } + if (segmentId) { + const segment = (await findSegmentById(optionsQx(options), segmentId)) as any - if (filter.emails) { - if (typeof filter.emails === 'string') { - filter.emails = filter.emails.split(',') + if (segment === null) { + options.log.info('No segment found for organization') + return { + rows: [], + count: 0, + limit, + offset, } - advancedFilter.and.push({ - emails: { - overlap: filter.emails, - }, - }) } - if (filter.phoneNumbers) { - if (typeof filter.phoneNumbers === 'string') { - filter.phoneNumbers = filter.phoneNumbers.split(',') - } - advancedFilter.and.push({ - phoneNumbers: { - overlap: filter.phoneNumbers, - }, - }) - } + segmentId = segment.id + } - if (filter.tags) { - if (typeof filter.tags === 'string') { - filter.tags = filter.tags.split(',') - } - advancedFilter.and.push({ - tags: { - overlap: filter.tags, - }, - }) - } + const params: Record = { + limit, + offset, + segmentId, + tenantId: options.currentTenant.id, + } - if (filter.twitter) { - advancedFilter.and.push({ - twitter: { - textContains: filter.twitter, - }, - }) - } + let searchWhereClause = '' + if (search) { + params.searchTerm = `%${search}%` + searchWhereClause = `AND o."displayName" ILIKE $(searchTerm)` + } - if (filter.linkedin) { - advancedFilter.and.push({ - linkedin: { - textContains: filter.linkedin, - }, - }) - } + const filterString = RawQueryParser.parseFilters( + filter, + OrganizationRepository.QUERY_FILTER_COLUMN_MAP, + [], + params, + { pgPromiseFormat: true }, + ) - if (filter.crunchbase) { - advancedFilter.and.push({ - crunchbase: { - textContains: filter.crunchbase, - }, - }) - } + const order = (function prepareOrderBy( + orderBy = withAggregates ? 'lastActive_DESC' : 'id_DESC', + ) { + const orderSplit = orderBy.split('_') - if (filter.employeesRange) { - const [start, end] = filter.employeesRange + const orderField = OrganizationRepository.QUERY_FILTER_COLUMN_MAP.get(orderSplit[0]) + if (!orderField) { + return withAggregates ? 'osa."lastActive" DESC' : 'o.id DESC' + } + const orderDirection = ['DESC', 'ASC'].includes(orderSplit[1]) ? orderSplit[1] : 'DESC' - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - employees: { - gte: start, - }, - }) - } + return `${orderField} ${orderDirection}` + })(orderBy ?? 'id_DESC') - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - employees: { - lte: end, - }, - }) - } + const createQuery = (fields) => ` + SELECT + ${fields} + FROM organizations o + LEFT JOIN "organizationSegmentsAgg" osa ON osa."organizationId" = o.id AND ${ + segmentId ? `osa."segmentId" = $(segmentId)` : `osa."segmentId" IS NULL` } + LEFT JOIN "organizationEnrichments" oe ON oe."organizationId" = o.id + WHERE 1=1 + AND o."tenantId" = $(tenantId) + ${lfxMembershipFilterWhereClause} + ${searchWhereClause} + AND (${filterString}) + ` + const countQuery = createQuery('COUNT(*)') - if (filter.revenueMin) { - advancedFilter.and.push({ - revenueMin: { - gte: filter.revenueMin, - }, - }) - } + if (countOnly) { + const result = await qx.selectOne(countQuery, params) + const count = parseInt(result.count, 10) - if (filter.revenueMax) { - advancedFilter.and.push({ - revenueMax: { - lte: filter.revenueMax, - }, - }) - } + // Cache the count + await cache.setCount(cacheKey, count, 21600) // 6 hours TTL - if (filter.members) { - advancedFilter.and.push({ - members: filter.members, - }) + return { + rows: [], + count, + limit, + offset, } + } - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange + const query = ` + ${createQuery( + (function prepareFields(fields) { + return fields + .map((f) => { + const mappedField = OrganizationRepository.QUERY_FILTER_COLUMN_MAP.get(f) + if (!mappedField) { + throw new Error400(options.language, `Invalid field: ${f}`) + } + return `${mappedField} as "${f}"` + }) + .filter((f) => { + if (withAggregates) { + return true + } + return !f.includes('osa.') + }) + .join(',\n') + })(fields), + )} + ORDER BY ${order} NULLS LAST + LIMIT $(limit) + OFFSET $(offset) + ` + + const results = await Promise.all([qx.select(query, params), qx.selectOne(countQuery, params)]) + + const rows = results[0] + const count = parseInt(results[1].count, 10) + + const orgIds = rows.map((org) => org.id) + if (orgIds.length === 0) { + return { rows: [], count: 0, limit, offset } + } - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - createdAt: { - gte: start, - }, - }) - } + if (include.lfxMemberships) { + const lfxMemberships = await findManyLfxMemberships(qx, { + organizationIds: orgIds, + }) - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - createdAt: { - lte: end, - }, - }) - } - } + rows.forEach((org) => { + const membership = lfxMemberships.find((lm) => lm.organizationId === org.id) + org.lfxMembership = !!membership + }) } - customOrderBy = customOrderBy.concat( - SequelizeFilterUtils.customOrderByIfExists('lastActive', orderBy), - ) - customOrderBy = customOrderBy.concat( - SequelizeFilterUtils.customOrderByIfExists('joinedAt', orderBy), - ) - customOrderBy = customOrderBy.concat( - SequelizeFilterUtils.customOrderByIfExists('activityCount', orderBy), - ) + if (include.identities) { + const identities = await fetchManyOrgIdentities(qx, orgIds) - customOrderBy = customOrderBy.concat( - SequelizeFilterUtils.customOrderByIfExists('memberCount', orderBy), - ) + rows.forEach((org) => { + const orgIdentities = identities.find((i) => i.organizationId === org.id)?.identities || [] - const parser = new QueryParser( - { - nestedFields: { - twitter: 'twitter.handle', - linkedin: 'linkedin.handle', - crunchbase: 'crunchbase.handle', - revenueMin: 'revenueRange.min', - revenueMax: 'revenueRange.max', - revenue: 'revenueRange.min', - }, - aggregators: { - ...SequelizeFilterUtils.getNativeTableFieldAggregations( - [ - 'id', - 'displayName', - 'description', - 'emails', - 'phoneNumbers', - 'logo', - 'tags', - 'website', - 'location', - 'github', - 'twitter', - 'linkedin', - 'crunchbase', - 'employees', - 'revenueRange', - 'importHash', - 'createdAt', - 'updatedAt', - 'deletedAt', - 'tenantId', - 'createdById', - 'updatedById', - 'isTeamOrganization', - 'type', - 'attributes', - 'manuallyCreated', - ], - 'organization', - ), - activeOn, - identities, - lastActive, - joinedAt, - memberCount, - activityCount, - segments, - }, - manyToMany: { - members: { - table: 'organizations', - model: 'organization', - relationTable: { - name: 'memberOrganizations', - from: 'organizationId', - to: 'memberId', - }, - }, - segments: { - table: 'organizations', - model: 'organization', - relationTable: { - name: 'organizationSegments', - from: 'organizationId', - to: 'segmentId', - }, - }, - }, - }, - options, - ) + org.identities = orgIdentities.map((identity) => ({ + type: identity.type, + value: identity.value, + platform: identity.platform, + verified: identity.verified, + })) + }) + } - const parsed: QueryOutput = parser.parse({ - filter: advancedFilter, - orderBy: orderBy || ['createdAt_DESC'], - limit, - offset, - }) + if (include.segments) { + const orgSegments = await fetchManyOrgSegments(qx, orgIds) + + rows.forEach((org) => { + org.segments = + orgSegments + .find((i) => i.organizationId === org.id) + ?.segments.filter((segment) => segment !== null) || [] + }) + } - let order = parsed.order + if (include.attributes) { + const attributes = await findManyOrgAttributes(qx, orgIds) - if (customOrderBy.length > 0) { - order = [customOrderBy] - } else if (orderBy) { - order = [orderBy.split('_')] + rows.forEach((org) => { + org.attributes = attributes.find((a) => a.organizationId === org.id)?.attributes || [] + }) } - let { - rows, - count, // eslint-disable-line prefer-const - } = await options.database.organization.findAndCountAll({ - ...(parsed.where ? { where: parsed.where } : {}), - ...(parsed.having ? { having: parsed.having } : {}), - attributes: [ - ...SequelizeFilterUtils.getLiteralProjections( - [ - 'id', - 'displayName', - 'description', - 'emails', - 'phoneNumbers', - 'logo', - 'tags', - 'website', - 'location', - 'github', - 'twitter', - 'linkedin', - 'crunchbase', - 'employees', - 'revenueRange', - 'importHash', - 'createdAt', - 'updatedAt', - 'deletedAt', - 'tenantId', - 'createdById', - 'updatedById', - 'isTeamOrganization', - 'type', - 'ticker', - 'size', - 'naics', - 'lastEnrichedAt', - 'industry', - 'headline', - 'geoLocation', - 'founded', - 'employeeCountByCountry', - 'address', - 'profiles', - 'attributes', - 'manuallyCreated', - 'affiliatedProfiles', - 'allSubsidiaries', - 'alternativeDomains', - 'alternativeNames', - 'averageEmployeeTenure', - 'averageTenureByLevel', - 'averageTenureByRole', - 'directSubsidiaries', - 'employeeChurnRate', - 'employeeCountByMonth', - 'employeeGrowthRate', - 'employeeCountByMonthByLevel', - 'employeeCountByMonthByRole', - 'gicsSector', - 'grossAdditionsByMonth', - 'grossDeparturesByMonth', - 'ultimateParent', - 'immediateParent', - ], - 'organization', - ), - [activeOn, 'activeOn'], - [identities, 'identities'], - [lastActive, 'lastActive'], - [joinedAt, 'joinedAt'], - [memberCount, 'memberCount'], - [activityCount, 'activityCount'], - [segments, 'segmentIds'], - ], - order, - limit: parsed.limit, - offset: parsed.offset, - include, - subQuery: false, - group: ['organization.id'], - transaction: SequelizeRepository.getTransaction(options), - }) + const result = { rows, count, limit, offset } - rows = await this._populateRelationsForRows(rows) + await cache.set(cacheKey, result, 21600) // 6 hours TTL - return { rows, count: count.length, limit: parsed.limit, offset: parsed.offset } + return result } static async findAllAutocomplete(query, limit, options: IRepositoryOptions) { const tenant = SequelizeRepository.getCurrentTenant(options) - const segmentIds = SequelizeRepository.getSegmentIds(options) + const qx = SequelizeRepository.getQueryExecutor(options) + const currentSegments = SequelizeRepository.getSegmentIds(options) + + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) const records = await options.database.sequelize.query( ` @@ -2842,7 +1652,7 @@ class OrganizationRepository { replacements: { limit: limit ? Number(limit) : 20, tenantId: tenant.id, - segmentIds, + segmentIds: subprojectIds, queryLike: `%${query}%`, queryExact: query, uuid: validator.isUUID(query) ? query : null, @@ -2855,39 +1665,184 @@ class OrganizationRepository { return records } - static async _createAuditLog(action, record, data, options: IRepositoryOptions) { - let values = {} + private static async refreshCacheInBackground( + cache: OrganizationQueryCache, + cacheKey: string, + params: { + // TODO: REMOVE this any + filter?: any + search?: string + limit: number + offset: number + orderBy?: string + segmentId?: string + countOnly: boolean + fields: string[] + include: any + }, + options: IRepositoryOptions, + ): Promise { + try { + await this.executeQuery(cache, cacheKey, params, options) + } catch (error) { + options.log.warn('Background cache refresh failed:', error) + } + } - if (data) { - values = { - ...record.get({ plain: true }), - memberIds: data.members, - } + private static async refreshCountCacheInBackground( + cache: OrganizationQueryCache, + cacheKey: string, + params: { + filter?: any + search?: string + segmentId?: string + include: any + }, + options: IRepositoryOptions, + ): Promise { + try { + options.log.info(`Refreshing organizations advanced count cache in background: ${cacheKey}`) + await this.executeQuery( + cache, + cacheKey, + { + ...params, + countOnly: true, + fields: [...OrganizationRepository.QUERY_FILTER_COLUMN_MAP.keys()], + limit: 20, + offset: 0, + }, + options, + ) + } catch (error) { + options.log.warn('Background count cache refresh failed:', error) } + } - await AuditLogRepository.log( + static async findByIds(ids: string[], options: IRepositoryOptions) { + const records = await options.database.sequelize.query( + ` + SELECT + o."id", + o."displayName", + o."logo" + FROM "organizations" AS o + WHERE o."id" IN (:ids); + `, { - entityName: 'organization', - entityId: record.id, - action, - values, + replacements: { + ids, + }, + type: QueryTypes.SELECT, + raw: true, }, - options, ) + + return records } - static async _populateRelationsForRows(rows) { - if (!rows) { - return rows + static calculateRenderFriendlyOrganizations( + memberOrganizations: IMemberRoleWithOrganization[], + ): IMemberRenderFriendlyRole[] { + const organizations: IMemberRenderFriendlyRole[] = [] + + for (const role of memberOrganizations) { + organizations.push({ + id: role.organizationId, + displayName: role.organizationName, + logo: role.organizationLogo, + memberOrganizations: role, + }) } - return rows.map((record) => { - const rec = record.get({ plain: true }) - rec.activeOn = rec.activeOn ?? [] - rec.segments = rec.segmentIds ?? [] - delete rec.segmentIds - return rec + return organizations + } + + static async getActivityCountInPlatform( + organizationId: string, + platform: string, + options: IRepositoryOptions, + ): Promise { + const currentSegments = SequelizeRepository.getSegmentIds(options) + const qx = SequelizeRepository.getQueryExecutor(options) + const activityTypes = SegmentRepository.getActivityTypes(options) + + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) + + const result = await queryActivities( + { + segmentIds: subprojectIds, + countOnly: true, + filter: { + and: [ + { + organizationId: { + eq: organizationId, + }, + platform: { + eq: platform, + }, + }, + ], + }, + }, + qx, + activityTypes, + ) + + return result.count + } + + static async getMemberCountInPlatform( + organizationId: string, + platform: string, + options: IRepositoryOptions, + ): Promise { + const qx = SequelizeRepository.getQueryExecutor(options) + const rows = await queryActivityRelations(qx, { + filter: { + and: [ + { + organizationId: { + eq: organizationId, + }, + platform: { + eq: platform, + }, + }, + ], + }, + countOnly: true, }) + + return rows.count + } + + static async removeIdentitiesFromOrganization( + organizationId: string, + identities: IOrganizationIdentity[], + options: IRepositoryOptions, + ): Promise { + const transaction = SequelizeRepository.getTransaction(options) + + const seq = SequelizeRepository.getSequelize(options) + + const query = ` + delete from "organizationIdentities" where "organizationId" = :organizationId and platform = :platform and value = :value and type = :type; + ` + + for (const identity of identities) { + await seq.query(query, { + replacements: { + organizationId, + value: identity.value, + type: identity.type, + platform: identity.platform, + }, + type: QueryTypes.DELETE, + transaction, + }) + } } } diff --git a/backend/src/database/repositories/organizationSyncRemoteRepository.ts b/backend/src/database/repositories/organizationSyncRemoteRepository.ts deleted file mode 100644 index da2db18882..0000000000 --- a/backend/src/database/repositories/organizationSyncRemoteRepository.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { generateUUIDv1 as uuid } from '@crowd/common' -import { IOrganizationSyncRemoteData, SyncStatus } from '@crowd/types' -import { QueryTypes } from 'sequelize' -import { IRepositoryOptions } from './IRepositoryOptions' -import { RepositoryBase } from './repositoryBase' -import SequelizeRepository from './sequelizeRepository' - -class OrganizationSyncRemoteRepository extends RepositoryBase< - IOrganizationSyncRemoteData, - string, - IOrganizationSyncRemoteData, - unknown, - unknown -> { - public constructor(options: IRepositoryOptions) { - super(options, true) - } - - async stopSyncingAutomation(automationId: string) { - await this.options.database.sequelize.query( - `update "organizationsSyncRemote" set status = :status where "syncFrom" = :automationId - `, - { - replacements: { - status: SyncStatus.STOPPED, - automationId, - }, - type: QueryTypes.UPDATE, - }, - ) - } - - async stopOrganizationManualSync(organizationId: string) { - await this.options.database.sequelize.query( - `update "organizationsSyncRemote" set status = :status where "organizationId" = :organizationId and "syncFrom" = :manualSync - `, - { - replacements: { - status: SyncStatus.STOPPED, - organizationId, - manualSync: 'manual', - }, - type: QueryTypes.UPDATE, - }, - ) - } - - async startManualSync(id: string, sourceId: string) { - const transaction = SequelizeRepository.getTransaction(this.options) - - await this.options.database.sequelize.query( - `update "organizationsSyncRemote" set status = :status, "sourceId" = :sourceId where "id" = :id - `, - { - replacements: { - status: SyncStatus.ACTIVE, - id, - sourceId: sourceId || null, - }, - type: QueryTypes.UPDATE, - transaction, - }, - ) - } - - async findRemoteSync(integrationId: string, organizationId: string, syncFrom: string) { - const transaction = SequelizeRepository.getTransaction(this.options) - - const records = await this.options.database.sequelize.query( - `SELECT * - FROM "organizationsSyncRemote" - WHERE "integrationId" = :integrationId and "organizationId" = :organizationId and "syncFrom" = :syncFrom; - `, - { - replacements: { - integrationId, - organizationId, - syncFrom, - }, - type: QueryTypes.SELECT, - transaction, - }, - ) - - if (records.length === 0) { - return null - } - - return records[0] - } - - async markOrganizationForSyncing( - data: IOrganizationSyncRemoteData, - ): Promise { - const transaction = SequelizeRepository.getTransaction(this.options) - - const existingSyncRemote = await this.findByOrganizationId(data.organizationId) - - if (existingSyncRemote) { - data.sourceId = existingSyncRemote.sourceId - } - - const existingManualSyncRemote = await this.findRemoteSync( - data.integrationId, - data.organizationId, - data.syncFrom, - ) - - if (existingManualSyncRemote) { - await this.startManualSync(existingManualSyncRemote.id, data.sourceId) - return existingManualSyncRemote - } - - const organizationSyncRemoteInserted = await this.options.database.sequelize.query( - `insert into "organizationsSyncRemote" ("id", "organizationId", "sourceId", "integrationId", "syncFrom", "metaData", "lastSyncedAt", "status") - VALUES - (:id, :organizationId, :sourceId, :integrationId, :syncFrom, :metaData, :lastSyncedAt, :status) - returning "id" - `, - { - replacements: { - id: uuid(), - organizationId: data.organizationId, - integrationId: data.integrationId, - syncFrom: data.syncFrom, - metaData: data.metaData, - lastSyncedAt: data.lastSyncedAt || null, - sourceId: data.sourceId || null, - status: SyncStatus.ACTIVE, - }, - type: QueryTypes.INSERT, - transaction, - }, - ) - - const organizationSyncRemote = await this.findById(organizationSyncRemoteInserted[0][0].id) - return organizationSyncRemote - } - - async destroyAllAutomation(automationIds: string[]): Promise { - const transaction = this.transaction - - const seq = this.seq - - const query = ` - delete - from "organizationsSyncRemote" - where "syncFrom" in (:automationIds);` - - await seq.query(query, { - replacements: { - automationIds, - }, - type: QueryTypes.DELETE, - transaction, - }) - } - - async destroyAllIntegration(integrationIds: string[]): Promise { - const transaction = this.transaction - - const seq = this.seq - - const query = ` - delete - from "organizationsSyncRemote" - where "integrationId" in (:integrationIds);` - - await seq.query(query, { - replacements: { - integrationIds, - }, - type: QueryTypes.DELETE, - transaction, - }) - } - - async findOrganizationManualSync(organizationId: string) { - const transaction = SequelizeRepository.getTransaction(this.options) - - const records = await this.options.database.sequelize.query( - `select i.platform, osr.status from "organizationsSyncRemote" osr - inner join integrations i on osr."integrationId" = i.id - where osr."syncFrom" = :syncFrom and osr."organizationId" = :organizationId; - `, - { - replacements: { - organizationId, - syncFrom: 'manual', - }, - type: QueryTypes.SELECT, - transaction, - }, - ) - - return records - } - - async findByOrganizationId(organizationId: string): Promise { - const transaction = SequelizeRepository.getTransaction(this.options) - - const records = await this.options.database.sequelize.query( - `SELECT * - FROM "organizationsSyncRemote" - WHERE "organizationId" = :organizationId - and "sourceId" is not null - limit 1; - `, - { - replacements: { - organizationId, - }, - type: QueryTypes.SELECT, - transaction, - }, - ) - - if (records.length === 0) { - return null - } - - return records[0] - } - - async findById(id: string): Promise { - const transaction = SequelizeRepository.getTransaction(this.options) - - const records = await this.options.database.sequelize.query( - `SELECT * - FROM "organizationsSyncRemote" - WHERE id = :id; - `, - { - replacements: { - id, - }, - type: QueryTypes.SELECT, - transaction, - }, - ) - - if (records.length === 0) { - return null - } - - return records[0] - } -} - -export default OrganizationSyncRemoteRepository diff --git a/backend/src/database/repositories/organizationsQueryCache.ts b/backend/src/database/repositories/organizationsQueryCache.ts new file mode 100644 index 0000000000..259d0e24ef --- /dev/null +++ b/backend/src/database/repositories/organizationsQueryCache.ts @@ -0,0 +1,104 @@ +import { createHash } from 'crypto' + +import { PageData } from '@crowd/common' +import { getServiceLogger } from '@crowd/logging' +import { RedisCache, RedisClient } from '@crowd/redis' + +interface IOrganizationData { + id: string + displayName: string + [key: string]: any +} + +interface IncludeOptions { + aggregates?: boolean + identities?: boolean + lfxMemberships?: boolean + segments?: boolean + attributes?: boolean +} + +const log = getServiceLogger() + +export class OrganizationQueryCache { + private cache: RedisCache + + private countCache: RedisCache + + constructor(redis: RedisClient) { + this.cache = new RedisCache('organizations-advanced', redis, log) + this.countCache = new RedisCache('organizations-count', redis, log) + } + + static buildCacheKey(params: { + countOnly?: boolean + fields?: string[] + filter?: Record + include?: IncludeOptions + limit: number + offset: number + orderBy?: string + search?: string + segmentId?: string + }): string { + const cleanParams = Object.fromEntries( + Object.entries({ + countOnly: params.countOnly, + fields: params.fields?.sort(), + filter: params.filter, + include: params.include, + limit: params.limit, + offset: params.offset, + orderBy: params.orderBy, + search: params.search, + segmentId: params.segmentId, + }).filter(([, value]) => value !== null && value !== undefined), + ) + + const hash = createHash('md5').update(JSON.stringify(cleanParams)).digest('hex') + return `organizations_advanced_${hash}` + } + + async get(cacheKey: string): Promise | null> { + try { + const cachedResult = await this.cache.get(cacheKey) + if (cachedResult) { + return JSON.parse(cachedResult) + } + return null + } catch (error) { + log.warn('Error retrieving from cache', { error }) + return null + } + } + + async set( + cacheKey: string, + result: PageData, + ttlSeconds: number, + ): Promise { + try { + await this.cache.set(cacheKey, JSON.stringify(result), ttlSeconds) + } catch (error) { + log.warn('Error saving to cache', { error }) + } + } + + async getCount(cacheKey: string): Promise { + try { + const cachedCount = await this.countCache.get(cacheKey) + return cachedCount ? parseInt(cachedCount, 10) : null + } catch (error) { + log.warn('Error retrieving count from cache', { error }) + return null + } + } + + async setCount(cacheKey: string, count: number, ttlSeconds: number): Promise { + try { + await this.countCache.set(cacheKey, count.toString(), ttlSeconds) + } catch (error) { + log.warn('Error saving count to cache', { error }) + } + } +} diff --git a/backend/src/database/repositories/priorityLevelContextRepository.ts b/backend/src/database/repositories/priorityLevelContextRepository.ts new file mode 100644 index 0000000000..79cc4e1784 --- /dev/null +++ b/backend/src/database/repositories/priorityLevelContextRepository.ts @@ -0,0 +1,32 @@ +import { QueryTypes } from 'sequelize' + +import { IQueuePriorityCalculationContext } from '@crowd/types' + +import { IRepositoryOptions } from './IRepositoryOptions' +import SequelizeRepository from './sequelizeRepository' + +export class PriorityLevelContextRepository { + public constructor(private readonly options: IRepositoryOptions) {} + + public async loadPriorityLevelContext( + tenantId: string, + ): Promise { + const seq = SequelizeRepository.getSequelize(this.options) + + const results = await seq.query( + `select plan, "priorityLevel" as "dbPriority" from tenants where id = :tenantId`, + { + replacements: { + tenantId, + }, + type: QueryTypes.SELECT, + }, + ) + + if (results.length === 1) { + return results[0] as IQueuePriorityCalculationContext + } + + throw new Error(`Tenant not found: ${tenantId}!`) + } +} diff --git a/backend/src/database/repositories/recurringEmailsHistoryRepository.ts b/backend/src/database/repositories/recurringEmailsHistoryRepository.ts deleted file mode 100644 index 25f2d86771..0000000000 --- a/backend/src/database/repositories/recurringEmailsHistoryRepository.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { generateUUIDv1 as uuid } from '@crowd/common' -import { QueryTypes } from 'sequelize' -import { - RecurringEmailsHistoryData, - RecurringEmailType, -} from '../../types/recurringEmailsHistoryTypes' -import { IRepositoryOptions } from './IRepositoryOptions' -import { RepositoryBase } from './repositoryBase' - -class RecurringEmailsHistoryRepository extends RepositoryBase< - RecurringEmailsHistoryData, - string, - RecurringEmailsHistoryData, - unknown, - unknown -> { - public constructor(options: IRepositoryOptions) { - super(options, true) - } - - /** - * Inserts recurring emails receipt history. - * @param data recurring emails historical data - * @param options - * @returns - */ - async create(data: RecurringEmailsHistoryData): Promise { - const historyInserted = await this.options.database.sequelize.query( - `INSERT INTO "recurringEmailsHistory" ("id", "type", "tenantId", "weekOfYear", "emailSentAt", "emailSentTo") - VALUES - (:id, :type, :tenantId, :weekOfYear, :emailSentAt, ARRAY[:emailSentTo]) - RETURNING "id" - `, - { - replacements: { - id: uuid(), - type: data.type, - tenantId: data.tenantId, - weekOfYear: data.weekOfYear || null, - emailSentAt: data.emailSentAt, - emailSentTo: data.emailSentTo, - }, - type: QueryTypes.INSERT, - }, - ) - - const emailHistory = await this.findById(historyInserted[0][0].id) - return emailHistory - } - - /** - * Finds a historical entry given tenantId and weekOfYear - * Returns null if not found. - * @param tenantId - * @param weekOfYear - * @param options - * @returns - */ - async findByWeekOfYear( - tenantId: string, - weekOfYear: string, - type: RecurringEmailType, - ): Promise { - const records = await this.options.database.sequelize.query( - `SELECT * - FROM "recurringEmailsHistory" - WHERE "tenantId" = :tenantId - AND "weekOfYear" = :weekOfYear - and "type" = :type; - `, - { - replacements: { - tenantId, - weekOfYear, - type, - }, - type: QueryTypes.SELECT, - }, - ) - - if (records.length === 0) { - return null - } - - return records[0] - } - - /** - * Finds a historical entry by id. - * Returns null if not found - * @param id - * @param options - * @returns - */ - async findById(id: string): Promise { - const records = await this.options.database.sequelize.query( - `SELECT * - FROM "recurringEmailsHistory" - WHERE id = :id; - `, - { - replacements: { - id, - }, - type: QueryTypes.SELECT, - }, - ) - - if (records.length === 0) { - return null - } - - return records[0] - } -} - -export default RecurringEmailsHistoryRepository diff --git a/backend/src/database/repositories/reportRepository.ts b/backend/src/database/repositories/reportRepository.ts deleted file mode 100644 index f62c2d003d..0000000000 --- a/backend/src/database/repositories/reportRepository.ts +++ /dev/null @@ -1,363 +0,0 @@ -import lodash from 'lodash' -import Sequelize from 'sequelize' -import SequelizeRepository from './sequelizeRepository' -import AuditLogRepository from './auditLogRepository' -import SequelizeFilterUtils from '../utils/sequelizeFilterUtils' -import Error404 from '../../errors/Error404' -import { IRepositoryOptions } from './IRepositoryOptions' -import { QueryOutput } from './filters/queryTypes' -import QueryParser from './filters/queryParser' - -const { Op } = Sequelize - -class ReportRepository { - static async create(data, options: IRepositoryOptions) { - const currentUser = SequelizeRepository.getCurrentUser(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - const transaction = SequelizeRepository.getTransaction(options) - - const segmentId = data.noSegment - ? null - : SequelizeRepository.getStrictlySingleActiveSegment(options).id - - const record = await options.database.report.create( - { - ...lodash.pick(data, ['name', 'public', 'importHash', 'isTemplate', 'viewedBy']), - - tenantId: tenant.id, - segmentId, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { - transaction, - }, - ) - - await record.setWidgets(data.widgets || [], { - transaction, - }) - - await this._createAuditLog(AuditLogRepository.CREATE, record, data, options) - - return this.findById(record.id, options) - } - - static async update(id, data, options: IRepositoryOptions) { - const currentUser = SequelizeRepository.getCurrentUser(options) - - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - let record = await options.database.report.findOne({ - where: { - id, - tenantId: currentTenant.id, - ...ReportRepository.prepareSegmentFilter(options), - }, - transaction, - }) - - if (!record) { - throw new Error404() - } - - record = await record.update( - { - ...lodash.pick(data, ['name', 'public', 'importHash', 'isTemplate', 'viewedBy']), - - updatedById: currentUser.id, - }, - { - transaction, - }, - ) - - if (data.widgets) { - await record.setWidgets(data.widgets || [], { - transaction, - }) - } - - await this._createAuditLog(AuditLogRepository.UPDATE, record, data, options) - - return this.findById(record.id, options) - } - - static async destroy(id, options: IRepositoryOptions, force = false) { - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const record = await options.database.report.findOne({ - where: { - id, - tenantId: currentTenant.id, - ...ReportRepository.prepareSegmentFilter(options), - }, - transaction, - }) - - if (!record) { - throw new Error404() - } - - await record.destroy({ - transaction, - force, - }) - - await this._createAuditLog(AuditLogRepository.DELETE, record, record, options) - } - - static async findById(id, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const include = [] - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - const record = await options.database.report.findOne({ - where: { - id, - tenantId: currentTenant.id, - ...ReportRepository.prepareSegmentFilter(options), - }, - include, - transaction, - }) - - if (!record) { - throw new Error404() - } - - return this._populateRelations(record, options) - } - - static async filterIdInTenant(id, options: IRepositoryOptions) { - return lodash.get(await this.filterIdsInTenant([id], options), '[0]', null) - } - - static async filterIdsInTenant(ids, options: IRepositoryOptions) { - if (!ids || !ids.length) { - return [] - } - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const where = { - id: { - [Op.in]: ids, - }, - tenantId: currentTenant.id, - } - - const records = await options.database.report.findAll({ - attributes: ['id'], - where, - }) - - return records.map((record) => record.id) - } - - static async count(filter, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - return options.database.report.count({ - where: { - ...filter, - tenantId: tenant.id, - ...ReportRepository.prepareSegmentFilter(options), - }, - transaction, - }) - } - - static async findAndCountAll( - { filter = {} as any, advancedFilter = null as any, limit = 0, offset = 0, orderBy = '' }, - options: IRepositoryOptions, - ) { - const include = [] - - // If the advanced filter is empty, we construct it from the query parameter filter - if (!advancedFilter) { - advancedFilter = { and: [] } - - if (filter.id) { - advancedFilter.and.push({ - id: filter.id, - }) - } - - if (filter.name) { - advancedFilter.and.push({ - name: { - textContains: filter.name, - }, - }) - } - - if (filter.public !== undefined) { - advancedFilter.and.push({ - public: filter.public, - }) - } - - if (filter.isTemplate !== undefined) { - advancedFilter.and.push({ - isTemplate: filter.isTemplate, - }) - } - - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange - - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - createdAt: { - gte: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - createdAt: { - lte: end, - }, - }) - } - } - } - - const parser = new QueryParser( - { - segmentsNullable: true, - }, - options, - ) - - const parsed: QueryOutput = parser.parse({ - filter: advancedFilter, - orderBy: orderBy || ['createdAt_DESC'], - limit, - offset, - }) - - let { - rows, - count, // eslint-disable-line prefer-const - } = await options.database.report.findAndCountAll({ - ...(parsed.where ? { where: parsed.where } : {}), - ...(parsed.having ? { having: parsed.having } : {}), - order: parsed.order, - limit: parsed.limit, - offset: parsed.offset, - include, - transaction: SequelizeRepository.getTransaction(options), - }) - - rows = await this._populateRelationsForRows(rows, options) - - return { rows, count, limit: parsed.limit, offset: parsed.offset } - } - - static async findAllAutocomplete(query, limit, options: IRepositoryOptions) { - const tenant = SequelizeRepository.getCurrentTenant(options) - - const whereAnd: Array = [ - { - tenantId: tenant.id, - }, - { - ...ReportRepository.prepareSegmentFilter(options), - }, - ] - - if (query) { - whereAnd.push({ - [Op.or]: [{ id: SequelizeFilterUtils.uuid(query) }], - }) - } - - const where = { [Op.and]: whereAnd } - - const records = await options.database.report.findAll({ - attributes: ['id', 'id'], - where, - limit: limit ? Number(limit) : undefined, - order: [['id', 'ASC']], - }) - - return records.map((record) => ({ - id: record.id, - label: record.id, - })) - } - - static async _createAuditLog(action, record, data, options: IRepositoryOptions) { - let values = {} - - if (data) { - values = { - ...record.get({ plain: true }), - widgetsIds: data.widgets, - } - } - - await AuditLogRepository.log( - { - entityName: 'report', - entityId: record.id, - action, - values, - }, - options, - ) - } - - static async _populateRelationsForRows(rows, options: IRepositoryOptions) { - if (!rows) { - return rows - } - - return Promise.all(rows.map((record) => this._populateRelations(record, options))) - } - - static async _populateRelations(record, options: IRepositoryOptions) { - if (!record) { - return record - } - - const output = record.get({ plain: true }) - - const transaction = SequelizeRepository.getTransaction(options) - - output.widgets = await record.getWidgets({ - transaction, - }) - - return output - } - - private static prepareSegmentFilter(options: IRepositoryOptions) { - return { - [Op.or]: [ - { - segmentId: SequelizeRepository.getSegmentIds(options), - }, - { - segmentId: { - [Op.is]: null, - }, - }, - ], - } - } -} - -export default ReportRepository diff --git a/backend/src/database/repositories/repositoryBase.ts b/backend/src/database/repositories/repositoryBase.ts index 6af1818868..0b5649fe09 100644 --- a/backend/src/database/repositories/repositoryBase.ts +++ b/backend/src/database/repositories/repositoryBase.ts @@ -1,8 +1,15 @@ /* eslint-disable class-methods-use-this,@typescript-eslint/no-unused-vars */ import { Sequelize } from 'sequelize' + +import { + QueryExecutor, + SequelizeQueryExecutor, + TransactionalSequelizeQueryExecutor, + optionsQx, +} from '@crowd/data-access-layer/src/queryExecutor' +import { PageData, SearchCriteria } from '@crowd/types' + import { IRepositoryOptions } from './IRepositoryOptions' -import { PageData, SearchCriteria } from '../../types/common' -import AuditLogRepository from './auditLogRepository' import SequelizeRepository from './sequelizeRepository' export abstract class RepositoryBase< @@ -33,6 +40,10 @@ export abstract class RepositoryBase< return SequelizeRepository.getSequelize(this.options) } + protected get queryExecutor(): QueryExecutor { + return optionsQx(this.options) + } + protected get database(): any { return this.options.database } @@ -72,33 +83,6 @@ export abstract class RepositoryBase< return page.rows } - protected async createAuditLog( - entity: string, - action: string, - record: any, - data: any, - ): Promise { - if (this.log) { - let values = {} - - if (data) { - values = { - ...record.get({ plain: true }), - } - } - - await AuditLogRepository.log( - { - entityName: entity, - entityId: record.id, - action, - values, - }, - this.options, - ) - } - } - protected async populateRelationsForRows(rows: any[]): Promise { return Promise.all(rows.map((r) => this.populateRelations(r))) } diff --git a/backend/src/database/repositories/segmentRepository.ts b/backend/src/database/repositories/segmentRepository.ts index e4b78f7a1f..eff0535d39 100644 --- a/backend/src/database/repositories/segmentRepository.ts +++ b/backend/src/database/repositories/segmentRepository.ts @@ -1,25 +1,38 @@ import lodash from 'lodash' -import { v4 as uuid } from 'uuid' import { QueryTypes } from 'sequelize' -import { DEFAULT_ACTIVITY_TYPE_SETTINGS } from '@crowd/integrations' -import { ActivityTypeSettings } from '@crowd/types' -import { IRepositoryOptions } from './IRepositoryOptions' -import { RepositoryBase } from './repositoryBase' +import { v4 as uuid } from 'uuid' + +import { Error404 } from '@crowd/common' +import { + buildSegmentActivityTypes, + findSegmentById, + getMappedWithSegmentName, + getSegmentActivityTypes, + hasMappedRepos, + isSegmentProject, + isSegmentProjectGroup, + populateSegmentRelations, +} from '@crowd/data-access-layer/src/segments' import { + ActivityTypeSettings, + PageData, + PlatformType, + QueryData, SegmentCreateData, SegmentData, SegmentLevel, SegmentProjectGroupNestedData, SegmentProjectNestedData, - SegmentRawData, SegmentStatus, SegmentUpdateChildrenPartialData, SegmentUpdateData, -} from '../../types/segmentTypes' -import { PageData, QueryData } from '../../types/common' -import Error404 from '../../errors/Error404' +} from '@crowd/types' + import removeFieldsFromObject from '../../utils/getObjectWithoutKey' + +import { IRepositoryOptions } from './IRepositoryOptions' import IntegrationRepository from './integrationRepository' +import { RepositoryBase } from './repositoryBase' import SequelizeRepository from './sequelizeRepository' class SegmentRepository extends RepositoryBase< @@ -41,15 +54,16 @@ class SegmentRepository extends RepositoryBase< async create(data: SegmentCreateData): Promise { const transaction = this.transaction - const segmentInsertResult = await this.options.database.sequelize.query( - `INSERT INTO "segments" ("id", "url", "name", "slug", "parentSlug", "grandparentSlug", "status", "parentName", "sourceId", "sourceParentId", "tenantId", "grandparentName") + const id = uuid() + + await this.options.database.sequelize.query( + `INSERT INTO "segments" ("id", "url", "name", "slug", "parentSlug", "grandparentSlug", "status", "parentName", "sourceId", "sourceParentId", "tenantId", "grandparentName", "parentId", "grandparentId", "isLF") VALUES - (:id, :url, :name, :slug, :parentSlug, :grandparentSlug, :status, :parentName, :sourceId, :sourceParentId, :tenantId, :grandparentName) - RETURNING "id" + (:id, :url, :name, :slug, :parentSlug, :grandparentSlug, :status, :parentName, :sourceId, :sourceParentId, :tenantId, :grandparentName, :parentId, :grandparentId, :isLF) `, { replacements: { - id: uuid(), + id, url: data.url || null, name: data.name, parentName: data.parentName || null, @@ -61,16 +75,23 @@ class SegmentRepository extends RepositoryBase< sourceId: data.sourceId || null, sourceParentId: data.sourceParentId || null, tenantId: this.options.currentTenant.id, + parentId: data.parentId || null, + grandparentId: data.grandparentId || null, + isLF: data.isLF ?? true, }, type: QueryTypes.INSERT, transaction, }, ) - const segment = await this.findById(segmentInsertResult[0][0].id) + const segment = await this.findById(id) return segment } + public async findById(id: string): Promise { + return findSegmentById(this.queryExecutor, id) + } + /** * Updates: * parent slugs of children => parentSlug, grandparentSlug @@ -83,7 +104,7 @@ class SegmentRepository extends RepositoryBase< segment: SegmentData, data: SegmentUpdateChildrenPartialData, ): Promise { - if (SegmentRepository.isProjectGroup(segment)) { + if (isSegmentProjectGroup(segment)) { // update projects await this.updateBulk( (segment as SegmentProjectGroupNestedData).projects.map((p) => p.id), @@ -102,13 +123,14 @@ class SegmentRepository extends RepositoryBase< grandparentSlug: data.slug, grandparentName: data.name, }) - } else if (SegmentRepository.isProject(segment)) { + } else if (isSegmentProject(segment)) { // update subprojects await this.updateBulk( (segment as SegmentProjectNestedData).subprojects.map((sp) => sp.id), { parentName: data.name, parentSlug: data.slug, + isLF: data.isLF, }, ) } @@ -120,19 +142,23 @@ class SegmentRepository extends RepositoryBase< const transaction = this.transaction // strip arbitrary fields + const nullishValues = [undefined, null, '', NaN] const updateFields = Object.keys(data).filter( (key) => - data[key] && + !nullishValues.includes(data[key]) && [ 'name', 'slug', 'parentSlug', 'grandparentSlug', + 'parentId', + 'grandparentId', 'status', 'parentName', 'sourceId', 'sourceParentId', 'grandparentName', + 'isLF', ].includes(key), ) @@ -183,6 +209,7 @@ class SegmentRepository extends RepositoryBase< 'sourceId', 'sourceParentId', 'customActivityTypes', + 'isLF', ].includes(key), ) @@ -268,7 +295,11 @@ class SegmentRepository extends RepositoryBase< }, {}) } - async fetchTenantActivityChannels() { + async fetchTenantActivityChannels(segmentIds: string[]) { + if (segmentIds.length === 0) { + return {} + } + const transaction = this.transaction const records = await this.options.database.sequelize.query( @@ -278,11 +309,13 @@ class SegmentRepository extends RepositoryBase< json_agg(DISTINCT "channel") AS "channels" FROM "segmentActivityChannels" WHERE "tenantId" = :tenantId + and "segmentId" in (:segmentIds) GROUP BY "platform"; `, { replacements: { tenantId: this.options.currentTenant.id, + segmentIds, }, type: QueryTypes.SELECT, transaction, @@ -295,68 +328,51 @@ class SegmentRepository extends RepositoryBase< }, {}) } - async getChildrenOfProjectGroups(segment: SegmentData) { + async findBySlug(slug: string, level: SegmentLevel) { const transaction = this.transaction - const records = await this.options.database.sequelize.query( - ` - SELECT * - FROM segments s - WHERE (s."grandparentSlug" = :slug OR - (s."parentSlug" = :slug AND s."grandparentSlug" IS NULL)) - AND s."tenantId" = :tenantId - ORDER BY "grandparentSlug" DESC, "parentSlug" DESC, slug DESC; - `, - { - replacements: { - slug: segment.slug, - tenantId: this.options.currentTenant.id, - }, - type: QueryTypes.SELECT, - transaction, - }, - ) + let findBySlugQuery = `SELECT * FROM segments WHERE slug = :slug AND "tenantId" = :tenantId` - return records - } + if (level === SegmentLevel.SUB_PROJECT) { + findBySlugQuery += ` and "parentSlug" is not null and "grandparentSlug" is not null` + } else if (level === SegmentLevel.PROJECT) { + findBySlugQuery += ` and "parentSlug" is not null and "grandparentSlug" is null` + } else if (level === SegmentLevel.PROJECT_GROUP) { + findBySlugQuery += ` and "parentSlug" is null and "grandparentSlug" is null` + } - async getChildrenOfProjects(segment: SegmentData) { - const records = await this.options.database.sequelize.query( - ` - select * from segments s - where s."parentSlug" = :slug - AND s."grandparentSlug" = :parentSlug - and s."tenantId" = :tenantId; - `, - { - replacements: { - slug: segment.slug, - parentSlug: segment.parentSlug, - tenantId: this.options.currentTenant.id, - }, - type: QueryTypes.SELECT, + const records = await this.options.database.sequelize.query(findBySlugQuery, { + replacements: { + slug, + tenantId: this.options.currentTenant.id, }, - ) + type: QueryTypes.SELECT, + transaction, + }) - return records + if (records.length === 0) { + return null + } + + return this.findById(records[0].id) } - async findBySlug(slug: string, level: SegmentLevel) { + async findByName(name: string, level: SegmentLevel) { const transaction = this.transaction - let findBySlugQuery = `SELECT * FROM segments WHERE slug = :slug AND "tenantId" = :tenantId` + let findByNameQuery = `SELECT * FROM segments WHERE name = :name AND "tenantId" = :tenantId` if (level === SegmentLevel.SUB_PROJECT) { - findBySlugQuery += ` and "parentSlug" is not null and "grandparentSlug" is not null` + findByNameQuery += ` and "parentSlug" is not null and "grandparentSlug" is not null` } else if (level === SegmentLevel.PROJECT) { - findBySlugQuery += ` and "parentSlug" is not null and "grandparentSlug" is null` + findByNameQuery += ` and "parentSlug" is not null and "grandparentSlug" is null` } else if (level === SegmentLevel.PROJECT_GROUP) { - findBySlugQuery += ` and "parentSlug" is null and "grandparentSlug" is null` + findByNameQuery += ` and "parentSlug" is null and "grandparentSlug" is null` } - const records = await this.options.database.sequelize.query(findBySlugQuery, { + const records = await this.options.database.sequelize.query(findByNameQuery, { replacements: { - slug, + name, tenantId: this.options.currentTenant.id, }, type: QueryTypes.SELECT, @@ -396,89 +412,27 @@ class SegmentRepository extends RepositoryBase< }, ) - return records.map((sr) => SegmentRepository.populateRelations(sr)) - } - - static populateRelations(record: SegmentRawData): SegmentData { - const segmentData: SegmentData = { - ...record, - activityTypes: null, - } - - if (SegmentRepository.isSubproject(record)) { - segmentData.activityTypes = SegmentRepository.buildActivityTypes(record) - } - - return segmentData + return records.map((sr) => populateSegmentRelations(sr)) } - async findById(id: string): Promise { - const transaction = this.transaction - + async findByIds(ids: string[]) { const records = await this.options.database.sequelize.query( ` SELECT - s.* + s.* FROM segments s - WHERE s.id = :id - AND s."tenantId" = :tenantId - GROUP BY s.id; + WHERE s."id" IN (:ids); `, { replacements: { - id, - tenantId: this.options.currentTenant.id, + ids, }, type: QueryTypes.SELECT, - transaction, + raw: true, }, ) - if (records.length === 0) { - return null - } - - const record = records[0] - - if (SegmentRepository.isProjectGroup(record)) { - // find projects - // TODO: Check sorting - parent should come first - const children = await this.getChildrenOfProjectGroups(record) - - const projects = children.reduce((acc, child) => { - if (SegmentRepository.isProject(child)) { - acc.push(child) - } else if (SegmentRepository.isSubproject(child)) { - // find project index - const projectIndex = acc.findIndex((project) => project.slug === child.parentSlug) - if (!acc[projectIndex].subprojects) { - acc[projectIndex].subprojects = [child] - } else { - acc[projectIndex].subprojects.push(child) - } - } - return acc - }, []) - - record.projects = projects - } else if (SegmentRepository.isProject(record)) { - const children = await this.getChildrenOfProjects(record) - record.subprojects = children - } - - return SegmentRepository.populateRelations(record) - } - - static isProjectGroup(segment: SegmentData | SegmentRawData): boolean { - return segment.slug && segment.parentSlug === null && segment.grandparentSlug === null - } - - static isProject(segment: SegmentData | SegmentRawData): boolean { - return segment.slug && segment.parentSlug && segment.grandparentSlug === null - } - - static isSubproject(segment: SegmentData | SegmentRawData): boolean { - return segment.slug != null && segment.parentSlug != null && segment.grandparentSlug != null + return records } /** @@ -487,6 +441,14 @@ class SegmentRepository extends RepositoryBase< */ async queryProjectGroups(criteria: QueryData): Promise> { let searchQuery = 'WHERE 1=1' + let segmentsSearchQuery = '' + + const replacements = { + tenantId: this.currentTenant.id, + name: `%${criteria.filter?.name}%`, + status: criteria.filter?.status, + adminSegments: null, + } if (criteria.filter?.status) { searchQuery += `AND s.status = :status` @@ -496,6 +458,15 @@ class SegmentRepository extends RepositoryBase< searchQuery += `AND s.name ilike :name` } + if (criteria.filter?.adminOnly) { + const adminSegments = this.options.currentUser.tenants.flatMap((t) => t.adminSegments) + if (adminSegments.length === 0) { + return { count: 0, rows: [], limit: criteria.limit, offset: criteria.offset } + } + segmentsSearchQuery += `AND sp.id IN (:adminSegments)` + replacements.adminSegments = adminSegments + } + const projectGroups = await this.options.database.sequelize.query( ` WITH @@ -531,6 +502,7 @@ class SegmentRepository extends RepositoryBase< AND sp."tenantId" = f."tenantId" WHERE f."parentSlug" IS NULL AND f."tenantId" = :tenantId + ${segmentsSearchQuery} GROUP BY f."id", p.id ) SELECT @@ -551,11 +523,7 @@ class SegmentRepository extends RepositoryBase< ${this.getPaginationString(criteria)}; `, { - replacements: { - tenantId: this.currentTenant.id, - name: `%${criteria.filter?.name}%`, - status: criteria.filter?.status, - }, + replacements, type: QueryTypes.SELECT, }, ) @@ -584,21 +552,28 @@ class SegmentRepository extends RepositoryBase< const projects = await this.options.database.sequelize.query( ` - SELECT + SELECT s.*, COUNT(DISTINCT sp.id) AS subproject_count, - jsonb_agg(jsonb_build_object('id', sp.id, 'name', sp.name, 'status', sp.status)) as subprojects, + jsonb_agg(jsonb_build_object( + 'id', sp.id, + 'name', sp.name, + 'status', sp.status, + 'insightsProjectName', ip.name, + 'insightsProjectId', ip.id + )) as subprojects, count(*) over () as "totalCount" FROM segments s JOIN segments sp ON sp."parentSlug" = s."slug" and sp."grandparentSlug" is not null AND sp."tenantId" = s."tenantId" - WHERE + LEFT JOIN "insightsProjects" ip ON ip."segmentId" = sp.id + WHERE s."grandparentSlug" IS NULL and s."parentSlug" is not null and s."tenantId" = :tenantId ${searchQuery} GROUP BY s."id" - ORDER BY s."name" + ORDER BY s."updatedAt" DESC ${this.getPaginationString(criteria)}; `, { @@ -614,17 +589,50 @@ class SegmentRepository extends RepositoryBase< const subprojects = projects.map((p) => p.subprojects).flat() const integrationsBySegments = await this.queryIntegrationsForSubprojects(subprojects) + const qx = SequelizeRepository.getQueryExecutor(this.options) + const githubPlatforms = [PlatformType.GITHUB, PlatformType.GITHUB_NANGO] + const mappedGithubReposBySegments = ( + await Promise.all( + subprojects.map(async (s) => ({ + segmentId: s.id, + hasMappedRepo: await hasMappedRepos(qx, s.id, githubPlatforms), + })), + ) + ).reduce((acc, { segmentId, hasMappedRepo }) => { + if (hasMappedRepo) { + acc[segmentId] = true + } + return acc + }, {}) const count = projects.length > 0 ? Number.parseInt(projects[0].totalCount, 10) : 0 const rows = projects.map((i) => removeFieldsFromObject(i, 'totalCount')) // assign integrations to subprojects - rows.forEach((row) => { - row.subprojects.forEach((subproject) => { - subproject.integrations = integrationsBySegments[subproject.id] || [] - }) - }) + await Promise.all( + rows.map(async (row) => { + await Promise.all( + row.subprojects.map(async (subproject) => { + const integrations = integrationsBySegments[subproject.id] || [] + const githubIntegration = integrations.find((i) => i.platform === 'github') + + if (githubIntegration) { + githubIntegration.type = 'primary' + } else if (mappedGithubReposBySegments[subproject.id]) { + integrations.push({ + platform: 'github', + segmentId: subproject.id, + type: 'mapped', + mappedWith: await getMappedWithSegmentName(qx, subproject.id, githubPlatforms), + }) + } + + subproject.integrations = integrations + }), + ) + }), + ) return { count, rows, limit: criteria.limit, offset: criteria.offset } } @@ -672,6 +680,7 @@ class SegmentRepository extends RepositoryBase< status: criteria.filter?.status, parent_slug: `${criteria.filter?.parentSlug}`, grandparent_slug: `${criteria.filter?.grandparentSlug}`, + ids: criteria.filter?.ids, }, type: QueryTypes.SELECT, }, @@ -681,7 +690,66 @@ class SegmentRepository extends RepositoryBase< return { count: 1, - rows: rows.map((sr) => SegmentRepository.populateRelations(sr)), + rows: rows.map((sr) => populateSegmentRelations(sr)), + limit: criteria.limit, + offset: criteria.offset, + } + } + + async querySubprojectsLite(criteria: QueryData): Promise> { + let searchQuery = '' + + if (criteria.filter?.status) { + searchQuery += ` AND s.status = :status` + } + + if (criteria.filter?.name) { + searchQuery += ` AND s.name ilike :name` + } + + if (criteria.filter?.parentSlug) { + searchQuery += ` AND s."parentSlug" = :parent_slug ` + } + + if (criteria.filter?.grandparentSlug) { + searchQuery += ` AND s."grandparentSlug" = :grandparent_slug ` + } + + const subprojects = await this.options.database.sequelize.query( + ` + SELECT + s.id, + s.name, + s.url, + s.slug, + s.description, + COUNT(*) OVER () AS "totalCount" + FROM segments s + WHERE s."grandparentSlug" IS NOT NULL + AND s."parentSlug" IS NOT NULL + AND s."tenantId" = :tenantId + ${searchQuery} + ORDER BY s.name + ${this.getPaginationString(criteria)}; + `, + { + replacements: { + tenantId: this.currentTenant.id, + name: `%${criteria.filter?.name}%`, + status: criteria.filter?.status, + parent_slug: `${criteria.filter?.parentSlug}`, + grandparent_slug: `${criteria.filter?.grandparentSlug}`, + ids: criteria.filter?.ids, + }, + type: QueryTypes.SELECT, + }, + ) + + const rows = subprojects.map((i) => removeFieldsFromObject(i, 'totalCount')) + const count = subprojects.length > 0 ? +subprojects[0].totalCount : 0 + return { + count, + rows, limit: criteria.limit, offset: criteria.offset, } @@ -711,28 +779,8 @@ class SegmentRepository extends RepositoryBase< return lodash.groupBy(integrations, 'segmentId') } - /** - * Builds activity types object with both default and custom activity types - * @param record - * @returns - */ - static buildActivityTypes(record: SegmentRawData): ActivityTypeSettings { - const activityTypes = {} as ActivityTypeSettings - - activityTypes.default = lodash.cloneDeep(DEFAULT_ACTIVITY_TYPE_SETTINGS) - activityTypes.custom = {} - - const customActivityTypes = record.customActivityTypes || {} - - if (Object.keys(customActivityTypes).length > 0) { - activityTypes.custom = customActivityTypes - } - - return activityTypes - } - static getActivityTypes(options: IRepositoryOptions): ActivityTypeSettings { - return options.currentSegments.reduce((acc, s) => lodash.merge(acc, s.activityTypes), {}) + return getSegmentActivityTypes(options.currentSegments) } static async fetchTenantActivityTypes(options: IRepositoryOptions) { @@ -757,7 +805,7 @@ class SegmentRepository extends RepositoryBase< }, ) - return SegmentRepository.buildActivityTypes(record) + return buildSegmentActivityTypes(record) } static activityTypeExists(platform: string, key: string, options: IRepositoryOptions): boolean { @@ -772,6 +820,34 @@ class SegmentRepository extends RepositoryBase< return false } + + async findBySourceIds(sourceIds: string[]) { + const transaction = SequelizeRepository.getTransaction(this.options) + const seq = SequelizeRepository.getSequelize(this.options) + + if (!sourceIds || !sourceIds.length) { + return [] + } + + const segments = await seq.query( + ` + SELECT + DISTINCT UNNEST(ARRAY[s.id, s1.id, s2.id]) AS id + FROM segments s + JOIN segments s1 ON s1."parentSlug" = s.slug + JOIN segments s2 ON s2."parentSlug" = s1.slug + WHERE s."tenantId" = :tenantId + AND s."sourceId" IN (:sourceIds) + `, + { + replacements: { sourceIds, tenantId: this.options.currentTenant.id }, + type: QueryTypes.SELECT, + transaction, + }, + ) + + return segments.map((i: any) => i.id) + } } export default SegmentRepository diff --git a/backend/src/database/repositories/sequelizeRepository.ts b/backend/src/database/repositories/sequelizeRepository.ts index 0624dea30a..f8cbe2d122 100644 --- a/backend/src/database/repositories/sequelizeRepository.ts +++ b/backend/src/database/repositories/sequelizeRepository.ts @@ -1,13 +1,30 @@ import lodash from 'lodash' -import { Sequelize, UniqueConstraintError } from 'sequelize' +import { Sequelize, Transaction, UniqueConstraintError } from 'sequelize' + +import { Error400 } from '@crowd/common' +import { DbConnection, getDbConnection } from '@crowd/data-access-layer/src/database' +import { + QueryExecutor, + SequelizeQueryExecutor, + TransactionalSequelizeQueryExecutor, +} from '@crowd/data-access-layer/src/queryExecutor' import { getServiceLogger } from '@crowd/logging' +import { getOpensearchClient } from '@crowd/opensearch' import { getRedisClient } from '@crowd/redis' -import { IS_TEST_ENV, REDIS_CONFIG } from '../../conf' -import Error400 from '../../errors/Error400' +import { Client as TemporalClient, getTemporalClient } from '@crowd/temporal' +import { SegmentData } from '@crowd/types' + +import { + IS_TEST_ENV, + OPENSEARCH_CONFIG, + PRODUCT_DB_CONFIG, + REDIS_CONFIG, + TEMPORAL_CONFIG, +} from '../../conf' +import { IServiceOptions } from '../../services/IServiceOptions' import { databaseInit } from '../databaseConnection' + import { IRepositoryOptions } from './IRepositoryOptions' -import { SegmentData } from '../../types/segmentTypes' -import { IServiceOptions } from '../../services/IServiceOptions' /** * Abstracts some basic Sequelize operations. @@ -30,6 +47,18 @@ export default class SequelizeRepository { tenant?, segments?, ): Promise { + let temporal: TemporalClient | undefined + if (TEMPORAL_CONFIG.serverUrl) { + temporal = await getTemporalClient(TEMPORAL_CONFIG) + } + + let productDb: DbConnection | undefined + if (PRODUCT_DB_CONFIG) { + productDb = await getDbConnection(PRODUCT_DB_CONFIG) + } + + const opensearch = await getOpensearchClient(OPENSEARCH_CONFIG) + return { log: getServiceLogger(), database: await databaseInit(), @@ -39,6 +68,9 @@ export default class SequelizeRepository { bypassPermissionValidation: true, language: 'en', redis: await getRedisClient(REDIS_CONFIG, true), + temporal, + productDb, + opensearch, } } @@ -95,6 +127,18 @@ export default class SequelizeRepository { return options.database.sequelize.transaction() } + static async withTx(options: IRepositoryOptions, fn: (tx: Transaction) => Promise) { + const tx = await this.createTransaction(options) + try { + const result = await fn(tx) + await this.commitTransaction(tx) + return result + } catch (error) { + await this.rollbackTransaction(tx) + throw error + } + } + /** * Creates a transactional repository options instance */ @@ -152,6 +196,14 @@ export default class SequelizeRepository { return options.database.sequelize as Sequelize } + static getQueryExecutor(options: IRepositoryOptions): QueryExecutor { + const seq = this.getSequelize(options) + const transaction = this.getTransaction(options) + return transaction + ? new TransactionalSequelizeQueryExecutor(seq, transaction) + : new SequelizeQueryExecutor(seq) + } + static getSegmentIds(options: IRepositoryOptions): string[] { return options.currentSegments.map((s) => s.id) } diff --git a/backend/src/database/repositories/settingsRepository.ts b/backend/src/database/repositories/settingsRepository.ts index 402ebdd8d0..65465cf060 100644 --- a/backend/src/database/repositories/settingsRepository.ts +++ b/backend/src/database/repositories/settingsRepository.ts @@ -1,9 +1,10 @@ import _get from 'lodash/get' -import SequelizeRepository from './sequelizeRepository' -import AuditLogRepository from './auditLogRepository' -import { IRepositoryOptions } from './IRepositoryOptions' + import SegmentService from '../../services/segmentService' +import { IRepositoryOptions } from './IRepositoryOptions' +import SequelizeRepository from './sequelizeRepository' + export default class SettingsRepository { static async findOrCreateDefault(defaults, options: IRepositoryOptions) { const currentUser = SequelizeRepository.getCurrentUser(options) @@ -54,16 +55,6 @@ export default class SettingsRepository { transaction, }) - await AuditLogRepository.log( - { - entityName: 'settings', - entityId: settings.id, - action: AuditLogRepository.UPDATE, - values: data, - }, - options, - ) - return this._populateRelations(settings, options) } @@ -83,7 +74,7 @@ export default class SettingsRepository { return record } - const activityTypes = await SegmentService.getTenantActivityTypes(options.currentSegments) + const activityTypes = SegmentService.getTenantActivityTypes(options.currentSegments) const settings = record.get({ plain: true }) diff --git a/backend/src/database/repositories/tagRepository.ts b/backend/src/database/repositories/tagRepository.ts deleted file mode 100644 index 5f54ac8d01..0000000000 --- a/backend/src/database/repositories/tagRepository.ts +++ /dev/null @@ -1,364 +0,0 @@ -import lodash from 'lodash' -import Sequelize from 'sequelize' -import SequelizeRepository from './sequelizeRepository' -import AuditLogRepository from './auditLogRepository' -import Error404 from '../../errors/Error404' -import { IRepositoryOptions } from './IRepositoryOptions' -import QueryParser from './filters/queryParser' -import { QueryOutput } from './filters/queryTypes' -import SequelizeFilterUtils from '../utils/sequelizeFilterUtils' - -const { Op } = Sequelize - -class TagRepository { - static async create(data, options: IRepositoryOptions) { - const currentUser = SequelizeRepository.getCurrentUser(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - const transaction = SequelizeRepository.getTransaction(options) - - const record = await options.database.tag.create( - { - ...lodash.pick(data, ['name', 'importHash']), - - tenantId: tenant.id, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { - transaction, - }, - ) - - await record.setMembers(data.members || [], { - transaction, - }) - - await this._createAuditLog(AuditLogRepository.CREATE, record, data, options) - - return this.findById(record.id, options) - } - - static async update(id, data, options: IRepositoryOptions) { - const currentUser = SequelizeRepository.getCurrentUser(options) - - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - let record = await options.database.tag.findOne({ - where: { - id, - tenantId: currentTenant.id, - }, - transaction, - }) - - if (!record) { - throw new Error404() - } - - record = await record.update( - { - ...lodash.pick(data, ['name', 'importHash']), - - updatedById: currentUser.id, - }, - { - transaction, - }, - ) - - await record.setMembers(data.members || [], { - transaction, - }) - - await this._createAuditLog(AuditLogRepository.UPDATE, record, data, options) - - return this.findById(record.id, options) - } - - static async destroyBulk(ids, options: IRepositoryOptions, force = false) { - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - await options.database.tag.destroy({ - where: { - id: ids, - tenantId: currentTenant.id, - }, - force, - transaction, - }) - } - - static async destroy(id, options: IRepositoryOptions, force = false) { - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const record = await options.database.tag.findOne({ - where: { - id, - tenantId: currentTenant.id, - }, - transaction, - }) - - if (!record) { - throw new Error404() - } - - await record.destroy({ - transaction, - force, - }) - - await this._createAuditLog(AuditLogRepository.DELETE, record, record, options) - } - - static async findById(id, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const include = [] - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const record = await options.database.tag.findOne({ - where: { - id, - tenantId: currentTenant.id, - }, - include, - transaction, - }) - - if (!record) { - throw new Error404() - } - - return this._populateRelations(record, options) - } - - static async filterIdInTenant(id, options: IRepositoryOptions) { - return lodash.get(await this.filterIdsInTenant([id], options), '[0]', null) - } - - static async filterIdsInTenant(ids, options: IRepositoryOptions) { - if (!ids || !ids.length) { - return [] - } - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const transaction = SequelizeRepository.getTransaction(options) - - const where = { - id: { - [Op.in]: ids, - }, - tenantId: currentTenant.id, - } - - const records = await options.database.tag.findAll({ - attributes: ['id'], - where, - transaction, - }) - - return records.map((record) => record.id) - } - - static async count(filter, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - return options.database.tag.count({ - where: { - ...filter, - tenantId: tenant.id, - }, - transaction, - }) - } - - static async findAndCountAll( - { filter = {} as any, advancedFilter = null as any, limit = 0, offset = 0, orderBy = '' }, - options: IRepositoryOptions, - ) { - const include = [] - - if (!advancedFilter) { - advancedFilter = { and: [] } - } - - if (filter) { - if (filter.id) { - advancedFilter.and.push({ - id: filter.id, - }) - } - if (filter.ids) { - advancedFilter.and.push({ - or: filter.ids.map((id) => ({ - id, - })), - }) - } - - if (filter.name) { - advancedFilter.and.push({ - name: { - textContains: filter.name, - }, - }) - } - - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange - - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - createdAt: { - gte: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - createdAt: { - lte: end, - }, - }) - } - } - } - - const parser = new QueryParser( - { - manyToMany: { - members: { - table: 'tags', - relationTable: { - name: 'memberTags', - from: 'tagId', - to: 'memberId', - }, - }, - }, - withSegments: false, - }, - options, - ) - const parsed: QueryOutput = parser.parse({ - filter: advancedFilter, - orderBy: orderBy || ['createdAt_DESC'], - limit, - offset, - }) - - let { - rows, - count, // eslint-disable-line prefer-const - } = await options.database.tag.findAndCountAll({ - ...(parsed.where ? { where: parsed.where } : {}), - ...(parsed.having ? { having: parsed.having } : {}), - order: parsed.order, - limit: parsed.limit, - offset: parsed.offset, - include, - transaction: SequelizeRepository.getTransaction(options), - }) - - rows = await this._populateRelationsForRows(rows, options) - - return { rows, count, limit: parsed.limit, offset: parsed.offset } - } - - static async findAllAutocomplete(query, limit, options: IRepositoryOptions) { - const tenant = SequelizeRepository.getCurrentTenant(options) - - const whereAnd: Array = [ - { - tenantId: tenant.id, - }, - ] - - if (query) { - whereAnd.push({ - [Op.or]: [ - { id: SequelizeFilterUtils.uuid(query) }, - { - [Op.and]: SequelizeFilterUtils.ilikeIncludes('tag', 'name', query), - }, - ], - }) - } - - const where = { [Op.and]: whereAnd } - - const records = await options.database.tag.findAll({ - attributes: ['id', 'name'], - where, - limit: limit ? Number(limit) : undefined, - order: [['name', 'ASC']], - }) - - return records.map((record) => ({ - id: record.id, - label: record.name, - })) - } - - static async _createAuditLog(action, record, data, options: IRepositoryOptions) { - let values = {} - - if (data) { - values = { - ...record.get({ plain: true }), - memberIds: data.members, - } - } - - await AuditLogRepository.log( - { - entityName: 'tag', - entityId: record.id, - action, - values, - }, - options, - ) - } - - static async _populateRelationsForRows(rows, options: IRepositoryOptions) { - if (!rows) { - return rows - } - - return Promise.all(rows.map((record) => this._populateRelations(record, options))) - } - - static async _populateRelations(record, options: IRepositoryOptions) { - if (!record) { - return record - } - - const output = record.get({ plain: true }) - - const transaction = SequelizeRepository.getTransaction(options) - - output.members = await record.getMembers({ - transaction, - joinTableAttributes: [], - }) - - return output - } -} - -export default TagRepository diff --git a/backend/src/database/repositories/taskRepository.ts b/backend/src/database/repositories/taskRepository.ts deleted file mode 100644 index 6c689790ac..0000000000 --- a/backend/src/database/repositories/taskRepository.ts +++ /dev/null @@ -1,504 +0,0 @@ -import sanitizeHtml from 'sanitize-html' -import lodash from 'lodash' -import Sequelize from 'sequelize' -import SequelizeRepository from './sequelizeRepository' -import AuditLogRepository from './auditLogRepository' -import SequelizeFilterUtils from '../utils/sequelizeFilterUtils' -import Error404 from '../../errors/Error404' -import { IRepositoryOptions } from './IRepositoryOptions' -import QueryParser from './filters/queryParser' -import { QueryOutput } from './filters/queryTypes' - -const { Op } = Sequelize - -class TaskRepository { - static async create(data, options: IRepositoryOptions) { - const currentUser = SequelizeRepository.getCurrentUser(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - const transaction = SequelizeRepository.getTransaction(options) - - const segment = SequelizeRepository.getStrictlySingleActiveSegment(options) - - if (data.body) { - data.body = sanitizeHtml(data.body).trim() - } - - const record = await options.database.task.create( - { - ...lodash.pick(data, ['name', 'body', 'type', 'status', 'dueDate', 'importHash']), - - tenantId: tenant.id, - segmentId: segment.id, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { - transaction, - }, - ) - - await record.setMembers(data.members || [], { - transaction, - }) - await record.setActivities(data.activities || [], { - transaction, - }) - - await record.setAssignees(data.assignees || [], { - transaction, - }) - - await this._createAuditLog(AuditLogRepository.CREATE, record, data, options) - - return this.findById(record.id, options) - } - - static async createSuggestedTasks(options: IRepositoryOptions) { - const fs = require('fs') - const path = require('path') - - const suggestedTasks = JSON.parse( - fs.readFileSync(path.resolve(__dirname, '../initializers/suggested-tasks.json'), 'utf8'), - ) - - for (const suggestedTask of suggestedTasks) { - await TaskRepository.create({ ...suggestedTask, type: 'suggested' }, options) - } - } - - static async updateBulk(ids, data, options: IRepositoryOptions) { - const currentUser = SequelizeRepository.getCurrentUser(options) - - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const records = await options.database.task.update( - { ...data, updatedById: currentUser.id }, - { - where: { - id: ids, - tenantId: currentTenant.id, - segmentId: SequelizeRepository.getSegmentIds(options), - }, - transaction, - }, - ) - - return { rowsUpdated: records[0] } - } - - static async update(id, data, options: IRepositoryOptions) { - const currentUser = SequelizeRepository.getCurrentUser(options) - - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - let record = await options.database.task.findOne({ - where: { - id, - tenantId: currentTenant.id, - segmentId: SequelizeRepository.getSegmentIds(options), - }, - transaction, - }) - - if (!record) { - throw new Error404() - } - - if (data.body) { - data.body = sanitizeHtml(data.body).trim() - } - - record = await record.update( - { - ...lodash.pick(data, ['name', 'body', 'status', 'type', 'dueDate', 'importHash']), - updatedById: currentUser.id, - }, - { - transaction, - }, - ) - - if (data.members) { - await record.setMembers(data.members, { - transaction, - }) - } - - if (data.activities) { - await record.setActivities(data.activities, { - transaction, - }) - } - - if (data.assignees) { - await record.setAssignees(data.assignees, { - transaction, - }) - } - - await this._createAuditLog(AuditLogRepository.UPDATE, record, data, options) - - return this.findById(record.id, options) - } - - static async destroy(id, options: IRepositoryOptions, force = false) { - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const record = await options.database.task.findOne({ - where: { - id, - tenantId: currentTenant.id, - segmentId: SequelizeRepository.getSegmentIds(options), - }, - transaction, - }) - - if (!record) { - throw new Error404() - } - - await record.destroy({ - transaction, - force, - }) - - await this._createAuditLog(AuditLogRepository.DELETE, record, record, options) - } - - static async findById(id, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const include = [] - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const record = await options.database.task.findOne({ - where: { - id, - tenantId: currentTenant.id, - segmentId: SequelizeRepository.getSegmentIds(options), - }, - include, - transaction, - }) - - if (!record) { - throw new Error404() - } - - return this._populateRelations(record, options) - } - - static async filterIdInTenant(id, options: IRepositoryOptions) { - return lodash.get(await this.filterIdsInTenant([id], options), '[0]', null) - } - - static async filterIdsInTenant(ids, options: IRepositoryOptions) { - if (!ids || !ids.length) { - return [] - } - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const where = { - id: { - [Op.in]: ids, - }, - tenantId: currentTenant.id, - } - - const records = await options.database.task.findAll({ - attributes: ['id'], - where, - }) - - return records.map((record) => record.id) - } - - static async count(filter, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - return options.database.task.count({ - where: { - ...filter, - tenantId: tenant.id, - segmentId: SequelizeRepository.getSegmentIds(options), - }, - transaction, - }) - } - - static async findAndCountAll( - { filter = {} as any, advancedFilter = null as any, limit = 0, offset = 0, orderBy = '' }, - options: IRepositoryOptions, - ) { - const include = [] - - // If the advanced filter is empty, we construct it from the query parameter filter - if (!advancedFilter) { - advancedFilter = { and: [] } - - if (filter.id) { - advancedFilter.and.push({ - id: filter.id, - }) - } - - if (filter.name) { - advancedFilter.and.push({ - name: { - textContains: filter.name, - }, - }) - } - - if (filter.body) { - advancedFilter.and.push({ - body: { - textContains: filter.body, - }, - }) - } - - if (filter.type) { - advancedFilter.and.push({ - type: { - textContains: filter.type, - }, - }) - } - - if (filter.status) { - advancedFilter.and.push({ - status: filter.status, - }) - } - - if (filter.dueDateRange) { - const [start, end] = filter.dueDateRange - - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - dueDate: { - gte: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - dueDate: { - lte: end, - }, - }) - } - } - - if (filter.assignees) { - advancedFilter.and.push({ - assignees: filter.assignees, - }) - } - - if (filter.members) { - advancedFilter.and.push({ - members: filter.members, - }) - } - - if (filter.activities) { - advancedFilter.and.push({ - activities: filter.activities, - }) - } - - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange - - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - createdAt: { - gte: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - createdAt: { - lte: end, - }, - }) - } - } - } - - const parser = new QueryParser( - { - manyToMany: { - members: { - table: 'tasks', - model: 'task', - relationTable: { - name: 'memberTasks', - from: 'taskId', - to: 'memberId', - }, - }, - assignees: { - table: 'tasks', - model: 'task', - relationTable: { - name: 'taskAssignees', - from: 'taskId', - to: 'userId', - }, - }, - activities: { - table: 'tasks', - model: 'task', - relationTable: { - name: 'activityTasks', - from: 'taskId', - to: 'activityId', - }, - }, - }, - }, - options, - ) - - const parsed: QueryOutput = parser.parse({ - filter: advancedFilter, - orderBy: orderBy || ['createdAt_DESC'], - limit, - offset, - }) - - let { - rows, - count, // eslint-disable-line prefer-const - } = await options.database.task.findAndCountAll({ - ...(parsed.where ? { where: parsed.where } : {}), - ...(parsed.having ? { having: parsed.having } : {}), - order: parsed.order, - limit: parsed.limit, - offset: parsed.offset, - include, - transaction: SequelizeRepository.getTransaction(options), - }) - - rows = await this._populateRelationsForRows(rows, options) - - return { rows, count, limit: parsed.limit, offset: parsed.offset } - } - - static async findAllAutocomplete(query, limit, options: IRepositoryOptions) { - const tenant = SequelizeRepository.getCurrentTenant(options) - - const whereAnd: Array = [ - { - tenantId: tenant.id, - }, - { - segmentId: SequelizeRepository.getSegmentIds(options), - }, - ] - - if (query) { - whereAnd.push({ - [Op.or]: [ - { id: SequelizeFilterUtils.uuid(query) }, - { - [Op.and]: SequelizeFilterUtils.ilikeIncludes('task', 'name', query), - }, - ], - }) - } - - const where = { [Op.and]: whereAnd } - - const records = await options.database.task.findAll({ - attributes: ['id', 'name'], - where, - limit: limit ? Number(limit) : undefined, - order: [['name', 'ASC']], - }) - - return records.map((record) => ({ - id: record.id, - label: record.name, - })) - } - - static async _createAuditLog(action, record, data, options: IRepositoryOptions) { - let values = {} - - if (data) { - values = { - ...record.get({ plain: true }), - memberIds: data.members, - } - } - - await AuditLogRepository.log( - { - entityName: 'task', - entityId: record.id, - action, - values, - }, - options, - ) - } - - static async _populateRelationsForRows(rows, options: IRepositoryOptions) { - if (!rows) { - return rows - } - - return Promise.all(rows.map((record) => this._populateRelations(record, options))) - } - - static async _populateRelations(record, options: IRepositoryOptions) { - if (!record) { - return record - } - - const output = record.get({ plain: true }) - - const transaction = SequelizeRepository.getTransaction(options) - - output.members = await record.getMembers({ - transaction, - joinTableAttributes: [], - }) - - output.activities = await record.getActivities({ - transaction, - joinTableAttributes: [], - }) - - output.assignees = ( - await record.getAssignees({ - transaction, - joinTableAttributes: [], - raw: true, - }) - ).map((a) => ({ id: a.id, avatarUrl: null, fullName: a.fullName, email: a.email })) - - return output - } -} - -export default TaskRepository diff --git a/backend/src/database/repositories/tenantRepository.ts b/backend/src/database/repositories/tenantRepository.ts index 3b2b720165..634ac5f7c4 100644 --- a/backend/src/database/repositories/tenantRepository.ts +++ b/backend/src/database/repositories/tenantRepository.ts @@ -1,44 +1,19 @@ import lodash from 'lodash' import Sequelize, { QueryTypes } from 'sequelize' -import { getCleanString } from '@crowd/common' -import { Edition } from '@crowd/types' -import SequelizeRepository from './sequelizeRepository' -import AuditLogRepository from './auditLogRepository' + +import { Error400, Error404, getCleanString } from '@crowd/common' + import SequelizeFilterUtils from '../utils/sequelizeFilterUtils' -import Error404 from '../../errors/Error404' -import Error400 from '../../errors/Error400' import { isUserInTenant } from '../utils/userTenantUtils' + import { IRepositoryOptions } from './IRepositoryOptions' -import SegmentRepository from './segmentRepository' -import Plans from '../../security/plans' -import { API_CONFIG } from '../../conf' +import SequelizeRepository from './sequelizeRepository' const { Op } = Sequelize const forbiddenTenantUrls = ['www'] class TenantRepository { - static async getPayingTenantIds(options: IRepositoryOptions): Promise<({ id: string } & {})[]> { - const database = SequelizeRepository.getSequelize(options) - const plans = Plans.values - const transaction = SequelizeRepository.getTransaction(options) - - const query = ` - SELECT "id" - FROM "tenants" - WHERE tenants."plan" IN (:growth) - OR (tenants."isTrialPlan" is true AND tenants."plan" = :growth) - ; - ` - return database.query(query, { - type: QueryTypes.SELECT, - transaction, - replacements: { - growth: plans.growth, - }, - }) - } - static async create(data, options: IRepositoryOptions) { const currentUser = SequelizeRepository.getCurrentUser(options) @@ -73,7 +48,6 @@ class TenantRepository { 'integrationsRequired', 'importHash', ]), - plan: API_CONFIG.edition === Edition.LFX ? Plans.values.enterprise : Plans.values.essential, createdById: currentUser.id, updatedById: currentUser.id, }, @@ -82,11 +56,6 @@ class TenantRepository { }, ) - await this._createAuditLog(AuditLogRepository.CREATE, record, data, { - ...options, - currentTenant: record, - }) - return this.findById(record.id, { ...options, }) @@ -184,62 +153,6 @@ class TenantRepository { }, ) - await this._createAuditLog(AuditLogRepository.UPDATE, record, data, options) - - return this.findById(record.id, options) - } - - static async updatePlanUser(id, planStripeCustomerId, planUserId, options: IRepositoryOptions) { - const currentUser = SequelizeRepository.getCurrentUser(options) - - const transaction = SequelizeRepository.getTransaction(options) - - let record = await options.database.tenant.findByPk(id, { - transaction, - }) - - const data = { - planStripeCustomerId, - planUserId, - updatedById: currentUser.id, - } - - record = await record.update(data, { - transaction, - }) - - await this._createAuditLog(AuditLogRepository.UPDATE, record, data, options) - - return this.findById(record.id, options) - } - - static async updatePlanStatus( - planStripeCustomerId, - plan, - planStatus, - options: IRepositoryOptions, - ) { - const transaction = SequelizeRepository.getTransaction(options) - - let record = await options.database.tenant.findOne({ - where: { - planStripeCustomerId, - }, - transaction, - }) - - const data = { - plan, - planStatus, - updatedById: null, - } - - record = await record.update(data, { - transaction, - }) - - await this._createAuditLog(AuditLogRepository.UPDATE, record, data, options) - return this.findById(record.id, options) } @@ -259,14 +172,12 @@ class TenantRepository { await record.destroy({ transaction, }) - - await this._createAuditLog(AuditLogRepository.DELETE, record, record, options) } static async findById(id, options: IRepositoryOptions) { const transaction = SequelizeRepository.getTransaction(options) - const include = ['settings', 'conversationSettings'] + const include = ['settings'] const record = await options.database.tenant.findByPk(id, { include, @@ -274,11 +185,6 @@ class TenantRepository { }) if (record && record.settings && record.settings[0] && record.settings[0].dataValues) { - record.settings[0].dataValues.activityTypes = - await SegmentRepository.fetchTenantActivityTypes({ - ...options, - currentTenant: record, - }) record.settings[0].dataValues.slackWebHook = !!record.settings[0].dataValues.slackWebHook } @@ -288,7 +194,7 @@ class TenantRepository { static async findByUrl(url, options: IRepositoryOptions) { const transaction = SequelizeRepository.getTransaction(options) - const include = ['settings', 'conversationSettings'] + const include = ['settings'] const record = await options.database.tenant.findOne({ where: { url }, @@ -432,26 +338,6 @@ class TenantRepository { })) } - static async _createAuditLog(action, record, data, options: IRepositoryOptions) { - let values = {} - - if (data) { - values = { - ...record.get({ plain: true }), - } - } - - await AuditLogRepository.log( - { - entityName: 'tenant', - entityId: record.id, - action, - values, - }, - options, - ) - } - /** * Get current tenant * @param options Repository options @@ -461,13 +347,14 @@ class TenantRepository { return SequelizeRepository.getCurrentTenant(options) } - static async getAvailablePlatforms(id, options: IRepositoryOptions) { + static async getAvailablePlatforms(options: IRepositoryOptions) { const query = ` - select distinct platform from "memberIdentities" where "tenantId" = :tenantId + SELECT platform + FROM "memberIdentities" + WHERE "deletedAt" is null + GROUP BY 1 ` - const parameters: any = { - tenantId: id, - } + const parameters: any = {} const platforms = await options.database.sequelize.query(query, { replacements: parameters, diff --git a/backend/src/database/repositories/tenantUserRepository.ts b/backend/src/database/repositories/tenantUserRepository.ts index 7f11beced2..db0986e00f 100644 --- a/backend/src/database/repositories/tenantUserRepository.ts +++ b/backend/src/database/repositories/tenantUserRepository.ts @@ -1,7 +1,11 @@ import crypto from 'crypto' -import SequelizeRepository from './sequelizeRepository' -import AuditLogRepository from './auditLogRepository' +import lodash from 'lodash' + +import Roles from '../../security/roles' + import { IRepositoryOptions } from './IRepositoryOptions' +import SegmentRepository from './segmentRepository' +import SequelizeRepository from './sequelizeRepository' export default class TenantUserRepository { static async findByTenant(tenantId: string, options: IRepositoryOptions): Promise { @@ -45,10 +49,36 @@ export default class TenantUserRepository { return record } - static async create(tenant, user, roles, options: IRepositoryOptions) { - roles = roles || [] + static async convertRoles(roles: string[], options: IRepositoryOptions) { + const segmentRepository = new SegmentRepository(options) + + const adminSegments = [] + roles = lodash.uniq( + roles.map((role) => { + if (role.startsWith(`${Roles.values.admin}:`)) { + adminSegments.push(role.split(':')[1].trim()) + return Roles.values.projectAdmin + } + return role + }), + ) + const adminSegmentIds = await segmentRepository.findBySourceIds(adminSegments) + + return { + roles, + adminSegments: adminSegmentIds, + } + } + + static async create(tenant, user, rawRoles, options: IRepositoryOptions) { + rawRoles = rawRoles || [] const transaction = SequelizeRepository.getTransaction(options) + const { roles, adminSegments } = await this.convertRoles(rawRoles, { + currentTenant: tenant, + ...options, + }) + const status = selectStatus('active', roles) await options.database.tenantUser.create( @@ -57,62 +87,26 @@ export default class TenantUserRepository { userId: user.id, status, roles, + adminSegments, }, { transaction }, ) - - await AuditLogRepository.log( - { - entityName: 'user', - entityId: user.id, - action: AuditLogRepository.CREATE, - values: { - email: user.email, - status, - roles, - }, - }, - options, - ) } static async destroy(tenantId, id, options: IRepositoryOptions) { const transaction = SequelizeRepository.getTransaction(options) - const user = await options.database.user.findByPk(id, { - transaction, - }) - const tenantUser = await this.findByTenantAndUser(tenantId, id, options) await tenantUser.destroy({ transaction }) - - await AuditLogRepository.log( - { - entityName: 'user', - entityId: user.id, - action: AuditLogRepository.DELETE, - values: { - email: user.email, - }, - }, - options, - ) } static async updateRoles(tenantId, id, roles, options, isInvited = false) { const transaction = SequelizeRepository.getTransaction(options) - const user = await options.database.user.findByPk(id, { - transaction, - }) - let tenantUser = await this.findByTenantAndUser(tenantId, id, options) - let isCreation = false - if (!tenantUser) { - isCreation = true tenantUser = await options.database.tenantUser.create( { tenantId, @@ -145,20 +139,6 @@ export default class TenantUserRepository { transaction, }) - await AuditLogRepository.log( - { - entityName: 'user', - entityId: user.id, - action: isCreation ? AuditLogRepository.CREATE : AuditLogRepository.UPDATE, - values: { - email: user.email, - status: tenantUser.status, - roles: newRoles, - }, - }, - options, - ) - return tenantUser } @@ -255,21 +235,21 @@ export default class TenantUserRepository { }, { where: { id: currentUser.id }, transaction }, ) + } + + static async replaceRoles(tenantUserId, rawRoles, options: IRepositoryOptions) { + const transaction = SequelizeRepository.getTransaction(options) - const auditLogRoles = existingTenantUser ? existingTenantUser.roles : invitationTenantUser.roles + const { roles, adminSegments } = await TenantUserRepository.convertRoles(rawRoles, options) - await AuditLogRepository.log( + await options.database.tenantUser.update( + { roles, adminSegments, status: 'active', invitationToken: null }, { - entityName: 'user', - entityId: currentUser.id, - action: AuditLogRepository.UPDATE, - values: { - email: currentUser.email, - roles: auditLogRoles, - status: selectStatus('active', auditLogRoles), + where: { + id: tenantUserId, }, + transaction, }, - options, ) } } diff --git a/backend/src/database/repositories/types/automationTypes.ts b/backend/src/database/repositories/types/automationTypes.ts deleted file mode 100644 index a066d8d98d..0000000000 --- a/backend/src/database/repositories/types/automationTypes.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { AutomationSyncTrigger } from '@crowd/types' -import { - AutomationExecutionState, - AutomationSettings, - AutomationState, - AutomationTrigger, - AutomationType, -} from '../../../types/automationTypes' - -export interface DbAutomationInsertData { - name: string - type: AutomationType - trigger: AutomationTrigger | AutomationSyncTrigger - settings: AutomationSettings - state: AutomationState -} - -export interface DbAutomationUpdateData { - name: string - trigger: AutomationTrigger - settings: AutomationSettings - state: AutomationState -} - -export interface DbAutomationExecutionInsertData { - automationId: string - type: AutomationType - tenantId: string - trigger: AutomationTrigger | AutomationSyncTrigger - state: AutomationExecutionState - error: any | null - executedAt: Date - eventId: string - payload: any -} diff --git a/backend/src/database/repositories/types/memberAttributeSettingsTypes.ts b/backend/src/database/repositories/types/memberAttributeSettingsTypes.ts index 9274bbc39f..a0fec6a072 100644 --- a/backend/src/database/repositories/types/memberAttributeSettingsTypes.ts +++ b/backend/src/database/repositories/types/memberAttributeSettingsTypes.ts @@ -1,4 +1,5 @@ import { MemberAttributeType } from '@crowd/types' + import { AttributeData } from '../../attributes/attribute' export interface MemberAttributeSettingsCreateData { diff --git a/backend/src/database/repositories/types/memberTypes.ts b/backend/src/database/repositories/types/memberTypes.ts index 73c8efeca3..466443d42c 100644 --- a/backend/src/database/repositories/types/memberTypes.ts +++ b/backend/src/database/repositories/types/memberTypes.ts @@ -1,3 +1,5 @@ +import { MemberIdentityType } from '@crowd/types' + export interface IActiveMemberData { id: string displayName: string @@ -13,23 +15,17 @@ export interface IActiveMemberFilter { isBot?: boolean isTeamMember?: boolean isOrganization?: boolean - activityIsContribution?: boolean activityTimestampFrom: string activityTimestampTo: string } -export interface IMemberIdentity { - platform: string - username: string - integrationId?: string - sourceId?: string - createdAt: string -} +export type BasicMemberIdentity = { value: string; type: MemberIdentityType } -export const mapSingleUsernameToIdentity = (usernameOrIdentity: any): any => { +export const mapSingleUsernameToIdentity = (usernameOrIdentity: any): BasicMemberIdentity => { if (typeof usernameOrIdentity === 'string') { return { - username: usernameOrIdentity, + value: usernameOrIdentity, + type: MemberIdentityType.USERNAME, } } @@ -40,7 +36,9 @@ export const mapSingleUsernameToIdentity = (usernameOrIdentity: any): any => { throw new Error(`Unknown username type: ${typeof usernameOrIdentity}: ${usernameOrIdentity}`) } -export const mapUsernameToIdentities = (username: any, platform?: string): any => { +export type UsernameIdentities = { [key: string]: BasicMemberIdentity[] } + +export const mapUsernameToIdentities = (username: any, platform?: string): UsernameIdentities => { const mapped = {} if (typeof username === 'string') { @@ -54,7 +52,7 @@ export const mapUsernameToIdentities = (username: any, platform?: string): any = const data = username[platform] if (Array.isArray(data)) { - const identities = [] + const identities: BasicMemberIdentity[] = [] for (const entry of data) { identities.push(mapSingleUsernameToIdentity(entry)) } diff --git a/backend/src/database/repositories/types/organizationTypes.ts b/backend/src/database/repositories/types/organizationTypes.ts new file mode 100644 index 0000000000..b3df938f36 --- /dev/null +++ b/backend/src/database/repositories/types/organizationTypes.ts @@ -0,0 +1,13 @@ +export interface IActiveOrganizationData { + id: string + displayName: string + activityCount: number + activeDaysCount: number +} + +export interface IActiveOrganizationFilter { + platforms?: string[] + isTeamOrganization?: boolean + activityTimestampFrom: string + activityTimestampTo: string +} diff --git a/backend/src/database/repositories/userRepository.ts b/backend/src/database/repositories/userRepository.ts index 7f1f7a63c1..40e73a38a6 100644 --- a/backend/src/database/repositories/userRepository.ts +++ b/backend/src/database/repositories/userRepository.ts @@ -1,13 +1,15 @@ import crypto from 'crypto' -import Sequelize from 'sequelize' import lodash from 'lodash' -import SequelizeRepository from './sequelizeRepository' -import AuditLogRepository from './auditLogRepository' +import Sequelize from 'sequelize' + +import { Error404 } from '@crowd/common' + +import SequelizeArrayUtils from '../utils/sequelizeArrayUtils' import SequelizeFilterUtils from '../utils/sequelizeFilterUtils' -import Error404 from '../../errors/Error404' import { isUserInTenant } from '../utils/userTenantUtils' + import { IRepositoryOptions } from './IRepositoryOptions' -import SequelizeArrayUtils from '../utils/sequelizeArrayUtils' +import SequelizeRepository from './sequelizeRepository' const { Op } = Sequelize @@ -75,19 +77,6 @@ export default class UserRepository { { transaction }, ) - await AuditLogRepository.log( - { - entityName: 'user', - entityId: user.id, - action: AuditLogRepository.CREATE, - values: { - ...user.get({ plain: true }), - avatars: data.avatars, - }, - }, - options, - ) - return this.findById(user.id, { ...options, bypassPermissionValidation: true, @@ -110,18 +99,6 @@ export default class UserRepository { ) delete user.password - await AuditLogRepository.log( - { - entityName: 'user', - entityId: user.id, - action: AuditLogRepository.CREATE, - values: { - ...user.get({ plain: true }), - avatars: data.avatars, - }, - }, - options, - ) return this.findById(user.id, { ...options, @@ -149,19 +126,6 @@ export default class UserRepository { { transaction }, ) - await AuditLogRepository.log( - { - entityName: 'user', - entityId: user.id, - action: AuditLogRepository.UPDATE, - values: { - ...user.get({ plain: true }), - avatars: data.avatars, - }, - }, - options, - ) - return this.findById(user.id, options) } @@ -190,18 +154,6 @@ export default class UserRepository { await user.update(data, { transaction }) - await AuditLogRepository.log( - { - entityName: 'user', - entityId: user.id, - action: AuditLogRepository.UPDATE, - values: { - id, - }, - }, - options, - ) - return this.findById(user.id, { ...options, bypassPermissionValidation: true, @@ -230,20 +182,6 @@ export default class UserRepository { { transaction }, ) - await AuditLogRepository.log( - { - entityName: 'user', - entityId: user.id, - action: AuditLogRepository.UPDATE, - values: { - id: user.id, - emailVerificationToken, - emailVerificationTokenExpiresAt, - }, - }, - options, - ) - return emailVerificationToken } @@ -269,20 +207,6 @@ export default class UserRepository { { transaction }, ) - await AuditLogRepository.log( - { - entityName: 'user', - entityId: user.id, - action: AuditLogRepository.UPDATE, - values: { - id: user.id, - passwordResetToken, - passwordResetTokenExpiresAt, - }, - }, - options, - ) - return passwordResetToken } @@ -308,20 +232,6 @@ export default class UserRepository { { transaction }, ) - await AuditLogRepository.log( - { - entityName: 'user', - entityId: user.id, - action: AuditLogRepository.UPDATE, - values: { - ...user.get({ plain: true }), - avatars: data.avatars, - roles: data.roles, - }, - }, - options, - ) - return this.findById(user.id, options) } @@ -338,6 +248,19 @@ export default class UserRepository { return this._populateRelations(record, options) } + static async findByProviderId(providerId, options: IRepositoryOptions) { + const transaction = SequelizeRepository.getTransaction(options) + + const record = await options.database.user.findOne({ + where: { + [Op.and]: SequelizeFilterUtils.ilikeExact('user', 'providerId', providerId), + }, + transaction, + }) + + return this._populateRelations(record, options) + } + static async findByEmailWithoutAvatar(email, options: IRepositoryOptions) { const transaction = SequelizeRepository.getTransaction(options) @@ -368,6 +291,7 @@ export default class UserRepository { as: 'tenants', where: { tenantId: currentTenant.id, + status: 'active', }, }) } @@ -394,6 +318,15 @@ export default class UserRepository { whereAnd.push(SequelizeFilterUtils.ilikeIncludes('user', 'email', filter.email)) } + if (filter.query) { + whereAnd.push({ + [Op.or]: [ + SequelizeFilterUtils.ilikeIncludes('user', 'fullName', filter.query), + SequelizeFilterUtils.ilikeIncludes('user', 'email', filter.query), + ], + }) + } + if (filter.role) { const innerWhereAnd: Array = [] @@ -458,7 +391,7 @@ export default class UserRepository { rows = await this._populateRelationsForRows(rows, options) - rows = this._mapUserForTenantForRows(rows, currentTenant) + rows = await this._mapUserForTenantForRows(rows, currentTenant, options) return { rows, count, limit: false, offset: 0 } } @@ -505,7 +438,7 @@ export default class UserRepository { order: [['fullName', 'ASC']], }) - users = this._mapUserForTenantForRows(users, currentTenant) + users = await this._mapUserForTenantForRows(users, currentTenant, options) const buildText = (user) => { if (!user.fullName) { @@ -518,13 +451,14 @@ export default class UserRepository { return users.map((user) => ({ id: user.id, label: buildText(user), + email: user.email, })) } static async findById(id, options: IRepositoryOptions) { const transaction = SequelizeRepository.getTransaction(options) - let record: any = await options.database.sequelize.query( + const records: any[] = await options.database.sequelize.query( ` SELECT "id", @@ -540,22 +474,32 @@ export default class UserRepository { mapToModel: true, }, ) - record = record[0] + if (records.length !== 1) { + throw new Error404() + } + + let record = records[0] record = await this._populateRelations(record, options, { where: { status: 'active', }, }) + record = { ...record, ...record.json, } delete record.json - if (!record) { - throw new Error404() - } + // Remove sensitive fields + delete record.password + delete record.emailVerificationToken + delete record.emailVerificationTokenExpiresAt + delete record.providerId + delete record.passwordResetToken + delete record.passwordResetTokenExpiresAt + delete record.jwtTokenInvalidBefore const currentTenant = SequelizeRepository.getCurrentTenant(options) @@ -564,7 +508,7 @@ export default class UserRepository { throw new Error404() } - record = this._mapUserForTenant(record, currentTenant) + record = await this._mapUserForTenant(record, currentTenant, options) } return record @@ -640,19 +584,6 @@ export default class UserRepository { { transaction }, ) - await AuditLogRepository.log( - { - entityName: 'user', - entityId: user.id, - action: AuditLogRepository.UPDATE, - values: { - id, - emailVerified: true, - }, - }, - options, - ) - return true } @@ -710,17 +641,6 @@ export default class UserRepository { }) delete user.password - await AuditLogRepository.log( - { - entityName: 'user', - entityId: user.id, - action: AuditLogRepository.CREATE, - values: { - ...user.get({ plain: true }), - }, - }, - options, - ) return this.findById(user.id, { ...options, @@ -740,42 +660,6 @@ export default class UserRepository { return lodash.pick(userOrUsers, ['id', 'firstName', 'lastName', 'email']) } - static async filterIdInTenant(id, options: IRepositoryOptions) { - return lodash.get(await this.filterIdsInTenant([id], options), '[0]', null) - } - - static async filterIdsInTenant(ids, options: IRepositoryOptions) { - if (!ids || !ids.length) { - return [] - } - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const where = { - id: { - [Op.in]: ids, - }, - } - - const include = [ - { - model: options.database.tenantUser, - as: 'tenants', - where: { - tenantId: currentTenant.id, - }, - }, - ] - - const records = await options.database.user.findAll({ - attributes: ['id'], - where, - include, - }) - - return records.map((record) => record.id) - } - static async _populateRelationsForRows(rows, options: IRepositoryOptions) { if (!rows) { return rows @@ -810,18 +694,22 @@ export default class UserRepository { /** * Maps the users data to show only the current tenant related info */ - static _mapUserForTenantForRows(rows, tenant) { + static async _mapUserForTenantForRows(rows, tenant, options: IRepositoryOptions) { if (!rows) { return rows } - - return rows.map((record) => this._mapUserForTenant(record, tenant)) + const segments = await options.database.segment.findAll({ + where: { tenantId: tenant.id }, + }) + return Promise.all( + rows.map((record) => this._mapUserForTenant(record, tenant, options, segments)), + ) } /** * Maps the user data to show only the current tenant related info */ - static _mapUserForTenant(user, tenant) { + static async _mapUserForTenant(user, tenant, options: IRepositoryOptions, segments?) { if (!user || !user.tenants) { return user } @@ -835,6 +723,12 @@ export default class UserRepository { const status = tenantUser ? tenantUser.status : null const roles = tenantUser ? tenantUser.roles : [] + const adminSegments = tenantUser ? tenantUser.adminSegments : [] + + let adminSegmentsWithNames = adminSegments + if (adminSegments?.length > 0 && segments) { + adminSegmentsWithNames = segments.filter((segment) => adminSegments.includes(segment.id)) + } // If the user is only invited, // tenant members can only see its email @@ -848,6 +742,7 @@ export default class UserRepository { roles, status, invitationToken: tenantUser?.invitationToken, + adminSegments: adminSegmentsWithNames, } } } diff --git a/backend/src/database/repositories/widgetRepository.ts b/backend/src/database/repositories/widgetRepository.ts deleted file mode 100644 index 7ddb2ff03c..0000000000 --- a/backend/src/database/repositories/widgetRepository.ts +++ /dev/null @@ -1,358 +0,0 @@ -import lodash from 'lodash' -import Sequelize from 'sequelize' -import SequelizeRepository from './sequelizeRepository' -import AuditLogRepository from './auditLogRepository' -import SequelizeFilterUtils from '../utils/sequelizeFilterUtils' -import Error404 from '../../errors/Error404' -import { IRepositoryOptions } from './IRepositoryOptions' -import QueryParser from './filters/queryParser' -import { QueryOutput } from './filters/queryTypes' - -const { Op } = Sequelize - -class WidgetRepository { - static async create(data, options: IRepositoryOptions) { - const currentUser = SequelizeRepository.getCurrentUser(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - const transaction = SequelizeRepository.getTransaction(options) - - const segment = SequelizeRepository.getStrictlySingleActiveSegment(options) - - const record = await options.database.widget.create( - { - ...lodash.pick(data, ['cache', 'importHash', 'settings', 'title', 'type']), - reportId: data.report || null, - tenantId: tenant.id, - segmentId: segment.id, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { - transaction, - }, - ) - - await this._createAuditLog(AuditLogRepository.CREATE, record, data, options) - - return this.findById(record.id, options) - } - - /** - * Find a widget by type - * @param type Type of widget to find - * @param options DB options - * @returns Widget object - */ - static async findByType(type, options: IRepositoryOptions) { - const widgets = await this.findAndCountAll({ filter: { type } }, options) - - if (widgets.count === 0) { - throw new Error404() - } - - return widgets.rows[0] - } - - static async update(id, data, options: IRepositoryOptions) { - const currentUser = SequelizeRepository.getCurrentUser(options) - - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - let record = await options.database.widget.findOne({ - where: { - id, - tenantId: currentTenant.id, - segmentId: SequelizeRepository.getSegmentIds(options), - }, - transaction, - }) - - if (!record) { - throw new Error404() - } - - record = await record.update( - { - ...lodash.pick(data, ['cache', 'importHash', 'settings', 'title', 'type']), - reportId: data.report || null, - updatedById: currentUser.id, - }, - { - transaction, - }, - ) - - await this._createAuditLog(AuditLogRepository.UPDATE, record, data, options) - - return this.findById(record.id, options) - } - - static async destroy(id, options: IRepositoryOptions, force = false) { - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const record = await options.database.widget.findOne({ - where: { - id, - tenantId: currentTenant.id, - segmentId: SequelizeRepository.getSegmentIds(options), - }, - transaction, - }) - - if (!record) { - throw new Error404() - } - - await record.destroy({ - transaction, - force, - }) - - await this._createAuditLog(AuditLogRepository.DELETE, record, record, options) - } - - static async findById(id, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const include = [ - { - model: options.database.report, - as: 'report', - }, - ] - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const record = await options.database.widget.findOne({ - where: { - id, - tenantId: currentTenant.id, - segmentId: SequelizeRepository.getSegmentIds(options), - }, - include, - transaction, - }) - - if (!record) { - throw new Error404() - } - - return this._populateRelations(record) - } - - static async filterIdInTenant(id, options: IRepositoryOptions) { - return lodash.get(await this.filterIdsInTenant([id], options), '[0]', null) - } - - static async filterIdsInTenant(ids, options: IRepositoryOptions) { - if (!ids || !ids.length) { - return [] - } - const transaction = SequelizeRepository.getTransaction(options) - - const currentTenant = SequelizeRepository.getCurrentTenant(options) - - const where = { - id: { - [Op.in]: ids, - }, - tenantId: currentTenant.id, - } - - const records = await options.database.widget.findAll({ - attributes: ['id'], - where, - transaction, - }) - - return records.map((record) => record.id) - } - - static async count(filter, options: IRepositoryOptions) { - const transaction = SequelizeRepository.getTransaction(options) - - const tenant = SequelizeRepository.getCurrentTenant(options) - - return options.database.widget.count({ - where: { - ...filter, - tenantId: tenant.id, - segmentId: SequelizeRepository.getSegmentIds(options), - }, - transaction, - }) - } - - static async findAndCountAll( - { filter = {} as any, advancedFilter = null as any, limit = 0, offset = 0, orderBy = '' }, - options: IRepositoryOptions, - ) { - const include = [ - { - model: options.database.report, - as: 'report', - }, - ] - - // If the advanced filter is empty, we construct it from the query parameter filter - if (!advancedFilter) { - advancedFilter = { and: [] } - - if (filter.id) { - advancedFilter.and.push({ - id: filter.id, - }) - } - - if (filter.type) { - advancedFilter.and.push({ - type: { textContains: filter.type }, - }) - } - - if (filter.title) { - advancedFilter.and.push({ - title: { - textContains: filter.title, - }, - }) - } - - if (filter.report) { - advancedFilter.and.push({ - reportId: filter.report, - }) - } - - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange - - if (start !== undefined && start !== null && start !== '') { - advancedFilter.and.push({ - createdAt: { - gte: start, - }, - }) - } - - if (end !== undefined && end !== null && end !== '') { - advancedFilter.and.push({ - createdAt: { - lte: end, - }, - }) - } - } - } - - const parser = new QueryParser({}, options) - - const parsed: QueryOutput = parser.parse({ - filter: advancedFilter, - orderBy: orderBy || ['createdAt_DESC'], - limit, - offset, - }) - - let { - rows, - count, // eslint-disable-line prefer-const - } = await options.database.widget.findAndCountAll({ - ...(parsed.where ? { where: parsed.where } : {}), - ...(parsed.having ? { having: parsed.having } : {}), - order: parsed.order, - limit: parsed.limit, - offset: parsed.offset, - include, - transaction: SequelizeRepository.getTransaction(options), - }) - - rows = await this._populateRelationsForRows(rows) - - return { rows, count, limit: parsed.limit, offset: parsed.offset } - } - - static async findAllAutocomplete(query, limit, options: IRepositoryOptions) { - const tenant = SequelizeRepository.getCurrentTenant(options) - - const whereAnd: Array = [ - { - tenantId: tenant.id, - }, - { - segmentId: SequelizeRepository.getSegmentIds(options), - }, - ] - - if (query) { - whereAnd.push({ - [Op.or]: [ - { id: SequelizeFilterUtils.uuid(query) }, - { - [Op.and]: SequelizeFilterUtils.ilikeIncludes('widget', 'type', query), - }, - ], - }) - } - - const where = { [Op.and]: whereAnd } - - const records = await options.database.widget.findAll({ - attributes: ['id', 'type'], - where, - limit: limit ? Number(limit) : undefined, - order: [['type', 'ASC']], - }) - - return records.map((record) => ({ - id: record.id, - label: record.type, - })) - } - - static async _createAuditLog(action, record, data, options: IRepositoryOptions) { - let values = {} - - if (data) { - values = { - ...record.get({ plain: true }), - } - } - - await AuditLogRepository.log( - { - entityName: 'widget', - entityId: record.id, - action, - values, - }, - options, - ) - } - - static async _populateRelationsForRows(rows) { - if (!rows) { - return rows - } - - return Promise.all(rows.map((record) => this._populateRelations(record))) - } - - static async _populateRelations(record) { - if (!record) { - return record - } - - const output = record.get({ plain: true }) - - return output - } -} - -export default WidgetRepository diff --git a/backend/src/database/username_to_value_identities.sql b/backend/src/database/username_to_value_identities.sql new file mode 100644 index 0000000000..89d57f7be0 --- /dev/null +++ b/backend/src/database/username_to_value_identities.sql @@ -0,0 +1,170 @@ +-- move emails to identities +do +$$ + declare + member_row members%rowtype; + email text; + begin + for member_row in select * from members where cardinality("oldEmails") > 0 + loop + foreach email in array member_row."oldEmails" + loop + if email is not null then + if member_row."lastEnriched" is not null then + if member_row."lastEnriched" > '2024-01-01 00:01:00 +00:00'::timestamptz then + -- if it was enriched after January 1st emails that were set are reliable + raise notice 'inserting enriched member (%) identity email "%" that is verified', member_row.id, email; + insert into "memberIdentities"("memberId", platform, value, type, "tenantId", verified) + values (member_row.id, 'integration_or_enrichment', email, 'email', member_row."tenantId", true) + on conflict do nothing; + else + -- if member was enriched before January 1st emails that were set are not reliable + raise notice 'inserting enriched member (%) identity email "%" that is not verified', member_row.id, email; + insert into "memberIdentities"("memberId", platform, value, type, "tenantId", verified) + values (member_row.id, 'integration_or_enrichment', email, 'email', member_row."tenantId", false) + on conflict do nothing; + end if; + else + -- member is not enriched -> emails came from integrations and are verified + raise notice 'inserting member (%) identity email "%" that is verified', member_row.id, email; + insert into "memberIdentities"("memberId", platform, value, type, "tenantId", verified) + values (member_row.id, 'integration', email, 'email', member_row."tenantId", true) + on conflict do nothing; + end if; + end if; + end loop; + end loop; + end; +$$; + +-- move weak identities to unverified identities +do +$$ + declare + member_row members%rowtype; + identity jsonb; + begin + for member_row in select * from members + loop + for identity in select jsonb_array_elements(member_row."oldWeakIdentities") + loop + raise notice 'member %, platform %, username %', member_row.id, (identity ->> 'platform'), (identity ->> 'username'); + insert into "memberIdentities"("memberId", platform, value, type, "tenantId", verified) + values (member_row.id, (identity ->> 'platform'), (identity ->> 'username'), 'username', member_row."tenantId", false) + on conflict do nothing; + end loop; + end loop; + end; +$$; + +-- fix mergeActions +do +$$ + declare + rec record; + updated_jsonb jsonb; + identity jsonb; + email text; + begin + for rec in select "id", "unmergeBackup" + from "mergeActions" + where type = 'member' + and "unmergeBackup" is not null + loop + -- Initialize the updated JSONB structure + updated_jsonb := rec."unmergeBackup"; + -- clean emails, username and identities + select jsonb_set(updated_jsonb, array ['primary'], (updated_jsonb -> 'primary') - 'emails') + into updated_jsonb; + select jsonb_set(updated_jsonb, array ['primary'], (updated_jsonb -> 'primary') - 'username') + into updated_jsonb; + select jsonb_set(updated_jsonb, array ['primary', 'identities'], jsonb_build_array()) + into updated_jsonb; + select jsonb_set(updated_jsonb, array ['secondary'], (updated_jsonb -> 'secondary') - 'emails') + into updated_jsonb; + select jsonb_set(updated_jsonb, array ['secondary'], (updated_jsonb -> 'secondary') - 'username') + into updated_jsonb; + select jsonb_set(updated_jsonb, array ['secondary', 'identities'], jsonb_build_array()) + into updated_jsonb; + + -- now move identities with a new format into the update_jsonb identities + for identity in select jsonb_array_elements(rec."unmergeBackup" -> 'primary' -> 'identities') + loop + updated_jsonb := jsonb_set( + updated_jsonb, + array ['primary', 'identities'], + (updated_jsonb -> 'primary' -> 'identities') || jsonb_build_object( + 'memberId', identity -> 'memberId', + 'platform', identity -> 'platform', + 'sourceId', identity -> 'sourceId', + 'tenantId', identity -> 'tenantId', + 'value', identity -> 'username', + 'type', 'username', + 'verified', true, + 'createdAt', identity -> 'createdAt', + 'updatedAt', identity -> 'updatedAt', + 'integrationId', identity -> 'integrationId') + ); + end loop; + + for identity in select jsonb_array_elements(rec."unmergeBackup" -> 'secondary' -> 'identities') + loop + updated_jsonb := jsonb_set( + updated_jsonb, + array ['secondary', 'identities'], + (updated_jsonb -> 'secondary' -> 'identities') || jsonb_build_object( + 'memberId', identity -> 'memberId', + 'platform', identity -> 'platform', + 'sourceId', identity -> 'sourceId', + 'tenantId', identity -> 'tenantId', + 'value', identity -> 'username', + 'type', 'username', + 'verified', true, + 'createdAt', identity -> 'createdAt', + 'updatedAt', identity -> 'updatedAt', + 'integrationId', identity -> 'integrationId') + ); + end loop; + + -- now also move emails as unverified identities + for email in select jsonb_array_elements_text(rec."unmergeBackup" -> 'primary' -> 'emails') + loop + updated_jsonb := jsonb_set( + updated_jsonb, + array ['primary', 'identities'], + (updated_jsonb -> 'primary' -> 'identities') || jsonb_build_object( + 'memberId', (rec."unmergeBackup" -> 'primary') ->> 'id', + 'platform', 'unknown', + 'sourceId', null, + 'tenantId', (rec."unmergeBackup" -> 'primary') ->> 'tenantId', + 'value', email, + 'type', 'email', + 'verified', false, + 'createdAt', to_jsonb(now()), + 'updatedAt', to_jsonb(now()), + 'integrationId', null) + ); + end loop; + for email in select jsonb_array_elements_text(rec."unmergeBackup" -> 'secondary' -> 'emails') + loop + updated_jsonb := jsonb_set( + updated_jsonb, + array ['secondary', 'identities'], + (updated_jsonb -> 'secondary' -> 'identities') || jsonb_build_object( + 'memberId', (rec."unmergeBackup" -> 'secondary') ->> 'id', + 'platform', 'unknown', + 'sourceId', null, + 'tenantId', (rec."unmergeBackup" -> 'secondary') ->> 'tenantId', + 'value', email, + 'type', 'email', + 'verified', false, + 'createdAt', to_jsonb(now()), + 'updatedAt', to_jsonb(now()), + 'integrationId', null) + ); + end loop; + + update "mergeActions" set "unmergeBackup" = updated_jsonb where id = rec.id; + end loop; + end +$$; \ No newline at end of file diff --git a/backend/src/database/utils/__tests__/getUserContext.test.ts b/backend/src/database/utils/__tests__/getUserContext.test.ts deleted file mode 100644 index fb94c3bf6a..0000000000 --- a/backend/src/database/utils/__tests__/getUserContext.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import getUserContext from '../getUserContext' -import SequelizeTestUtils from '../sequelizeTestUtils' - -const db = null - -describe('Get user context tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('Get user context tests', () => { - it('Should get the user context for an existing tenant', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const tenantId = mockIRepositoryOptions.currentTenant.dataValues.id - const userContext = await getUserContext(tenantId) - expect(userContext.currentTenant.dataValues.id).toBe(tenantId) - expect(userContext.currentUser).toBeDefined() - }) - }) -}) diff --git a/backend/src/database/utils/segmentTestUtils.ts b/backend/src/database/utils/segmentTestUtils.ts deleted file mode 100644 index 55dc0702a8..0000000000 --- a/backend/src/database/utils/segmentTestUtils.ts +++ /dev/null @@ -1,12 +0,0 @@ -import SegmentRepository from '../repositories/segmentRepository' - -export async function populateSegments(options) { - const repository = new SegmentRepository(options) - options.currentSegments = await Promise.all( - options.currentSegments.map(async (segment) => repository.findById(segment.id)), - ) -} - -export function switchSegments(options, segments) { - options.currentSegments = segments -} diff --git a/backend/src/database/utils/sequelizeArrayUtils.ts b/backend/src/database/utils/sequelizeArrayUtils.ts index 9cddf7d092..d4a42033cb 100644 --- a/backend/src/database/utils/sequelizeArrayUtils.ts +++ b/backend/src/database/utils/sequelizeArrayUtils.ts @@ -1,4 +1,5 @@ import Sequelize, { DataTypes } from 'sequelize' + import { DB_CONFIG } from '../../conf' export default class SequelizeArrayUtils { diff --git a/backend/src/database/utils/sequelizeFilterUtils.ts b/backend/src/database/utils/sequelizeFilterUtils.ts index f577c98071..24b54e3ad0 100644 --- a/backend/src/database/utils/sequelizeFilterUtils.ts +++ b/backend/src/database/utils/sequelizeFilterUtils.ts @@ -1,7 +1,8 @@ -import validator from 'validator' -import { generateUUIDv4 as uuid } from '@crowd/common' import Sequelize from 'sequelize' import { Col } from 'sequelize/types/utils' +import validator from 'validator' + +import { generateUUIDv4 as uuid } from '@crowd/common' /** * Utilities to use on Sequelize queries. diff --git a/backend/src/database/utils/sequelizeTestUtils.ts b/backend/src/database/utils/sequelizeTestUtils.ts deleted file mode 100644 index b9165b422b..0000000000 --- a/backend/src/database/utils/sequelizeTestUtils.ts +++ /dev/null @@ -1,245 +0,0 @@ -import moment from 'moment' -import jwt from 'jsonwebtoken' -import bcrypt from 'bcrypt' -import { getServiceLogger } from '@crowd/logging' -import { getRedisClient } from '@crowd/redis' -import { databaseInit } from '../databaseConnection' -import { IRepositoryOptions } from '../repositories/IRepositoryOptions' -import { IServiceOptions } from '../../services/IServiceOptions' -import Roles from '../../security/roles' -import UserRepository from '../repositories/userRepository' -import TenantRepository from '../repositories/tenantRepository' -import Plans from '../../security/plans' -import { API_CONFIG, REDIS_CONFIG } from '../../conf' -import SettingsRepository from '../repositories/settingsRepository' -import { SegmentStatus } from '../../types/segmentTypes' - -export default class SequelizeTestUtils { - static async wipeDatabase(db) { - db = await this.getDatabase(db) - await db.sequelize.query(` - truncate table - tenants, - integrations, - activities, - members, - automations, - "automationExecutions", - conversations, - notes, - reports, - organizations, - "organizationCaches", - settings, - tags, - tasks, - users, - files, - microservices, - "eagleEyeContents", - "eagleEyeActions", - "auditLogs", - "memberEnrichmentCache" - cascade; - `) - } - - static async refreshMaterializedViews(db) { - db = await this.getDatabase(db) - await db.sequelize.query( - 'refresh materialized view concurrently "memberActivityAggregatesMVs";', - ) - } - - static async getDatabase(db?) { - if (!db) { - db = await databaseInit() - } - return db - } - - static async getTestIServiceOptions(db, plan = Plans.values.essential, tenantName?, tenantUrl?) { - db = await this.getDatabase(db) - - const randomTenant = - tenantName && tenantUrl - ? this.getTenant(tenantName, tenantUrl, plan) - : this.getRandomTestTenant(plan) - - const randomUser = await this.getRandomUser() - - let tenant = await db.tenant.create(randomTenant) - const segment = ( - await db.segment.create({ - url: tenant.url, - name: tenant.name, - parentName: tenant.name, - grandparentName: tenant.name, - slug: 'default', - parentSlug: 'default', - grandparentSlug: 'default', - status: SegmentStatus.ACTIVE, - sourceId: null, - sourceParentId: null, - tenantId: tenant.id, - }) - ).get({ plain: true }) - - let user = await db.user.create(randomUser) - - await db.tenantUser.create({ - roles: [Roles.values.admin], - status: 'active', - tenantId: tenant.id, - userId: user.id, - }) - - await SettingsRepository.findOrCreateDefault({}, { - language: 'en', - currentUser: user, - currentTenant: tenant, - currentSegments: [segment], - database: db, - } as IRepositoryOptions) - - tenant = await TenantRepository.findById(tenant.id, { - database: db, - } as IRepositoryOptions) - - user = await UserRepository.findById(user.id, { - database: db, - currentTenant: tenant, - bypassPermissionValidation: true, - } as IRepositoryOptions) - - const log = getServiceLogger() - - const redis = await getRedisClient(REDIS_CONFIG, true) - - return { - language: 'en', - currentUser: user, - currentTenant: tenant, - currentSegments: [segment], - database: db, - log, - redis, - } as IServiceOptions - } - - static async getTestIRepositoryOptions(db) { - db = await this.getDatabase(db) - - const randomTenant = this.getRandomTestTenant() - const randomUser = await this.getRandomUser() - - let tenant = await db.tenant.create(randomTenant) - const segment = ( - await db.segment.create({ - url: tenant.url, - name: tenant.name, - parentName: tenant.name, - grandparentName: tenant.name, - slug: 'default', - parentSlug: 'default', - grandparentSlug: 'default', - status: SegmentStatus.ACTIVE, - description: null, - sourceId: null, - sourceParentId: null, - tenantId: tenant.id, - }) - ).get({ plain: true }) - const user = await db.user.create(randomUser) - await db.tenantUser.create({ - roles: ['admin'], - status: 'active', - tenantId: tenant.id, - userId: user.id, - }) - - await SettingsRepository.findOrCreateDefault({}, { - language: 'en', - currentUser: user, - currentTenant: tenant, - currentSegments: [segment], - database: db, - } as IRepositoryOptions) - - tenant = await TenantRepository.findById(tenant.id, { - database: db, - } as IRepositoryOptions) - - const log = getServiceLogger() - const redis = await getRedisClient(REDIS_CONFIG, true) - - return { - language: 'en', - currentUser: user, - currentTenant: tenant, - currentSegments: [segment], - database: db, - bypassPermissionValidation: true, - log, - redis, - } as IRepositoryOptions - } - - static getRandomTestTenant(plan = Plans.values.essential) { - return this.getTenant(this.getRandomString('test-tenant'), this.getRandomString('url#'), plan) - } - - static getTenant(name, url, plan = Plans.values.essential) { - return { - name, - url, - plan, - } - } - - static async getRandomUser() { - return { - email: this.getRandomString('test-user-', '@crowd.dev'), - password: await bcrypt.hash('12345', 12), - emailVerified: true, - } - } - - static getUserToken(mockIRepositoryOptions) { - const userId = mockIRepositoryOptions.currentUser.id - return jwt.sign({ id: userId }, API_CONFIG.jwtSecret, { - expiresIn: API_CONFIG.jwtExpiresIn, - }) - } - - static getRandomString(prefix = '', suffix = '') { - const randomTestSuffix = Math.trunc(Math.random() * 50000 + 1) - - return `${prefix}${randomTestSuffix}${suffix}` - } - - static getNowWithoutTime() { - return moment.utc().format('YYYY-MM-DD') - } - - static async closeConnection(db) { - db = await this.getDatabase(db) - db.sequelize.close() - } - - static objectWithoutKey(object, key) { - let objectWithoutKeys - if (typeof key === 'string') { - const { [key]: _, ...otherKeys } = object - objectWithoutKeys = otherKeys - } else if (Array.isArray(key)) { - objectWithoutKeys = key.reduce((acc, i) => { - const { [i]: _, ...otherKeys } = acc - acc = otherKeys - return acc - }, object) - } - - return objectWithoutKeys - } -} diff --git a/backend/src/db/packagesDb.ts b/backend/src/db/packagesDb.ts new file mode 100644 index 0000000000..48ad1d5821 --- /dev/null +++ b/backend/src/db/packagesDb.ts @@ -0,0 +1,23 @@ +import { getDbConnection } from '@crowd/data-access-layer/src/database' +import { QueryExecutor, pgpQx } from '@crowd/data-access-layer/src/queryExecutor' + +import { PACKAGES_DB_CONFIG } from '@/conf' + +let _init: Promise | undefined + +export function getPackagesQx(): Promise { + if (!_init) { + if (!PACKAGES_DB_CONFIG) { + throw new Error( + 'Packages DB is not configured — set CROWD_PACKAGES_DB_* environment variables', + ) + } + _init = getDbConnection(PACKAGES_DB_CONFIG) + .then(pgpQx) + .catch((err) => { + _init = undefined + throw err + }) + } + return _init +} diff --git a/backend/src/documentation/openapi.json b/backend/src/documentation/openapi.json deleted file mode 100644 index 2cfcf5bbdb..0000000000 --- a/backend/src/documentation/openapi.json +++ /dev/null @@ -1,5455 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "crowd.dev API", - "version": "1.0.5", - "description": "crowd.dev API\n", - "contact": { "email": "joan@crowd.dev" }, - "license": { "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" }, - "x-github": "https://github.com/crowdHQ" - }, - "servers": [{ "url": "https://app.crowd.dev/api" }], - "paths": { - "/tenant/{tenantId}/activity/with-member": { - "post": { - "summary": "Create or update an activity with a member", - "tags": ["Activities"], - "security": [{ "Bearer": [] }], - "description": "Create or update an activity with a member\nActivity existence is checked by sourceId and tenantId\nMember existence is checked by platform and username", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ActivityUpsertWithMemberInput" } - } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Activity" }, - "examples": { "Activity": { "$ref": "#/components/examples/ActivityUpsert" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/activity": { - "post": { - "summary": "Create or update an activity", - "tags": ["Activities"], - "security": [{ "Bearer": [] }], - "description": "Create or update an activity. Existence is checked by sourceId and tenantId", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/ActivityUpsertInput" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Activity" }, - "examples": { "Activity": { "$ref": "#/components/examples/ActivityUpsert" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/activity/{id}": { - "delete": { - "summary": "Delete an activity", - "tags": ["Activities"], - "security": [{ "Bearer": [] }], - "description": "Delete a activity given an ID", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the activity", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { "description": "Ok" }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "Find an activity", - "tags": ["Activities"], - "security": [{ "Bearer": [] }], - "description": "Find a single activity by ID", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the activity", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ActivityResponse" }, - "examples": { "Activity": { "$ref": "#/components/examples/ActivityFind" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "put": { - "summary": "Update an activity", - "tags": ["Activities"], - "security": [{ "Bearer": [] }], - "description": "Update an activity given an ID.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the activity", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/ActivityUpsertInput" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Activity" }, - "examples": { "Activity": { "$ref": "#/components/examples/ActivityFind" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/activity/query": { - "post": { - "summary": "Query activities", - "tags": ["Activities"], - "security": [{ "Bearer": [] }], - "description": "Query activities. It accepts filters, sorting options and pagination.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/ActivityQuery" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ActivityList" }, - "examples": { "Activity": { "$ref": "#/components/examples/ActivityList" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/automation": { - "post": { - "summary": "Create an automation", - "tags": ["Automations"], - "security": [{ "Bearer": [] }], - "description": "Create a new automation for the tenant.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/AutomationCreateInput" } - } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Automation" }, - "examples": { "Automation": { "$ref": "#/components/examples/Automation" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "List automations", - "tags": ["Automations"], - "security": [{ "Bearer": [] }], - "description": "Get all existing automation data for tenant.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "filter[type]", - "in": "query", - "description": "Filter by type of automation", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[trigger]", - "in": "query", - "description": "Filter by trigger type of automation", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[state]", - "in": "query", - "description": "Filter by state of automation", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "offset", - "in": "query", - "description": "Skip the first n results. Default 0.", - "required": false, - "schema": { "type": "number" } - }, - { - "name": "limit", - "in": "query", - "description": "Limit the number of results. Default 50.", - "required": false, - "schema": { "type": "number" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/AutomationPage" }, - "examples": { "AutomationPage": { "$ref": "#/components/examples/AutomationPage" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/automation/{automationId}": { - "delete": { - "summary": "Destroy an automation", - "tags": ["Automations"], - "security": [{ "Bearer": [] }], - "description": "Destroys an existing automation in the tenant.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "automationId", - "in": "path", - "description": "Automation ID that you want to update", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "204": { "description": "Ok - No content" }, - "401": { "description": "Unauthorized" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "Find an automation", - "tags": ["Automations"], - "security": [{ "Bearer": [] }], - "description": "Get an existing automation data in the tenant.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "automationId", - "in": "path", - "description": "Automation ID that you want to find", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Automation" }, - "examples": { "Automation": { "$ref": "#/components/examples/Automation" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "429": { "description": "Too many requests" } - } - }, - "put": { - "summary": "Update an automation", - "tags": ["Automations"], - "security": [{ "Bearer": [] }], - "description": "Updates an existing automation in the tenant.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "automationId", - "in": "path", - "description": "Automation ID that you want to update", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/AutomationUpdateInput" } - } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Automation" }, - "examples": { "Automation": { "$ref": "#/components/examples/Automation" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/automation/{automationId}/executions": { - "get": { - "summary": "Get automation history", - "tags": ["Automations"], - "security": [{ "Bearer": [] }], - "description": "Get all automation execution history for tenant and automation", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "automationId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "offset", - "in": "query", - "description": "How many elements from the beginning would you like to skip", - "required": false, - "schema": { "type": "integer", "default": 0 } - }, - { - "name": "limit", - "in": "query", - "description": "How many elements would you like to fetch", - "required": false, - "schema": { "type": "integer", "default": 10 } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/AutomationExecutionPage" }, - "examples": { - "AutomationExecutionPage": { - "$ref": "#/components/examples/AutomationExecutionPage" - } - } - } - } - }, - "401": { "description": "Unauthorized" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/conversation": { - "post": { - "summary": "Create a conversation", - "tags": ["Conversations"], - "security": [{ "Bearer": [] }], - "description": "Create a conversation.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/ConversationNoId" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Conversation" }, - "examples": { "Conversation": { "$ref": "#/components/examples/Conversation" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "List conversations", - "tags": ["Conversations"], - "security": [{ "Bearer": [] }], - "description": "Get a list of conversations with filtering, sorting and offsetting.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "filter[title]", - "in": "query", - "description": "Filter by the title of the conversation.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[slug]", - "in": "query", - "description": "Filter by the slug of the conversation.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[published]", - "in": "query", - "description": "Filter by whether it is published or not.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[platform]", - "in": "query", - "description": "Filter by the platform of the conversation.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[channel]", - "in": "query", - "description": "Filter by the channel of the conversation.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[activitiesCountRange]", - "in": "query", - "description": "activitiesCount lower bound. If you want a range, send this parameter twice with [min] and [max]. If you send it once it will be interpreted as a lower bound.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[createdAtRange]", - "in": "query", - "description": "Send this parameter twice with [min] and [max].", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "orderBy", - "in": "query", - "description": "Sort the results. Default timestamp_DESC.", - "required": false, - "schema": { "$ref": "#/components/schemas/ConversationSort" } - }, - { - "name": "offset", - "in": "query", - "description": "Skip the first n results. Default 0.", - "required": false, - "schema": { "type": "number" } - }, - { - "name": "limit", - "in": "query", - "description": "Limit the number of results. Default 50.", - "required": false, - "schema": { "type": "number" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ConversationList" }, - "examples": { - "Conversations": { "$ref": "#/components/examples/ConversationList" } - } - } - } - }, - "401": { "description": "Unauthorized" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/conversation/{id}": { - "delete": { - "summary": "Delete a conversation", - "tags": ["Conversations"], - "security": [{ "Bearer": [] }], - "description": "Delete a conversation.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the conversation", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { "description": "Ok" }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "Find a conversation", - "tags": ["Conversations"], - "security": [{ "Bearer": [] }], - "description": "Find a conversation by ID.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID.", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the conversation.", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Conversation" }, - "examples": { "Conversation": { "$ref": "#/components/examples/Conversation" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "put": { - "summary": "Update an conversation", - "tags": ["Conversations"], - "security": [{ "Bearer": [] }], - "description": "Update a conversation given an ID.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the conversation", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/ConversationNoId" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Conversation" }, - "examples": { "Conversation": { "$ref": "#/components/examples/Conversation" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/member/active": { - "get": { - "summary": "List active members", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "List active members. It accepts filters, sorting options and pagination.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "filter[platforms]", - "in": "query", - "description": "Filter by activity platforms (comma separated list without spaces)", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[isTeamMember]", - "in": "query", - "description": "If true we will return just team members, if false we will return just non-team members, if undefined we will return both.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[isBot]", - "in": "query", - "description": "If true we will return just members who are bots, if false we will return just non-bot members, if undefined we will return both.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[activityTimestampFrom]", - "in": "query", - "description": "Filter by activity timestamp from (required)", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[activityTimestampTo]", - "in": "query", - "description": "Filter by activity timestamp to (required)", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "orderBy", - "in": "query", - "description": "How to sort results. Available values: activityCount_DESC, activityCount_ASC, activeDaysCount_DESC, activeDaysCount_ASC (default activityCount_DESC)", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "offset", - "in": "query", - "description": "Skip the first n results. Default 0.", - "required": false, - "schema": { "type": "number" } - }, - { - "name": "limit", - "in": "query", - "description": "Limit the number of results. Default 20.", - "required": false, - "schema": { "type": "number" } - } - ], - "responses": { - "200": { "description": "Ok" }, - "401": { "description": "Unauthorized" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/member": { - "post": { - "summary": "Create or update a member", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Create or update a member. Existence is checked by platform and username.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/MemberUpsertInput" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Member" }, - "examples": { "Member": { "$ref": "#/components/examples/MemberUpsert" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/member/{id}": { - "delete": { - "summary": "Delete a member", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Delete a member given an ID", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the member", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { "description": "Ok" }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "Find a member", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Find a single member by ID.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the member", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/MemberResponse" }, - "examples": { "Member": { "$ref": "#/components/examples/MemberFind" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "put": { - "summary": "Update a member", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Update a member", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the member", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/MemberUpsertInput" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Member" }, - "examples": { "Member": { "$ref": "#/components/examples/MemberUpsert" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/member/export": { - "post": { - "summary": "Export members as CSV", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Export members. It accepts filters, sorting options and pagination.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/MemberQuery" } } - } - }, - "responses": { - "200": { "description": "Ok" }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/member/query": { - "post": { - "summary": "Query members", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Query members. It accepts filters, sorting options and pagination.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/MemberQuery" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/MemberList" }, - "examples": { "Member": { "$ref": "#/components/examples/MemberList" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/note": { - "post": { - "summary": "Create a note", - "tags": ["Notes"], - "security": [{ "Bearer": [] }], - "description": "Create a note", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/NoteNoId" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Note" }, - "examples": { "Note": { "$ref": "#/components/examples/Note" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/note/{id}": { - "delete": { - "summary": "Delete a note", - "tags": ["Notes"], - "security": [{ "Bearer": [] }], - "description": "Delete a note.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the note", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { "description": "Ok" }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "Find a note", - "tags": ["Notes"], - "security": [{ "Bearer": [] }], - "description": "Find a note by ID.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID.", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the note.", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/NoteResponse" }, - "examples": { "Note": { "$ref": "#/components/examples/Note" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "put": { - "summary": "Update a note", - "tags": ["Notes"], - "security": [{ "Bearer": [] }], - "description": "Update a note", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the note", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/NoteInput" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Note" }, - "examples": { "Note": { "$ref": "#/components/examples/Note" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/note/query": { - "post": { - "summary": "Query notes", - "tags": ["Notes"], - "security": [{ "Bearer": [] }], - "description": "Query notes. It accepts filters, sorting options and pagination.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/NoteQuery" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/NoteList" }, - "examples": { "Note": { "$ref": "#/components/examples/NoteList" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/organization": { - "post": { - "summary": "Create a organization", - "tags": ["Organizations"], - "security": [{ "Bearer": [] }], - "description": "Create a organization", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/OrganizationInput" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Organization" }, - "examples": { - "Organization": { "$ref": "#/components/examples/OrganizationCreate" } - } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/organization/{id}": { - "delete": { - "summary": "Delete a organization", - "tags": ["Organizations"], - "security": [{ "Bearer": [] }], - "description": "Delete a organization.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the organization", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { "description": "Ok" }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "Find an organization", - "tags": ["Organizations"], - "security": [{ "Bearer": [] }], - "description": "Find an organization by ID.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the organization", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/OrganizationResponse" }, - "examples": { "Organization": { "$ref": "#/components/examples/Organization" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "put": { - "summary": "Update an organization", - "tags": ["Organizations"], - "security": [{ "Bearer": [] }], - "description": "Update a organization", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the organization", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/OrganizationInput" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Organization" }, - "examples": { - "Organization": { "$ref": "#/components/examples/OrganizationCreate" } - } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/organization/query": { - "post": { - "summary": "Query organizations", - "tags": ["Organizations"], - "security": [{ "Bearer": [] }], - "description": "Query organizations. It accepts filters, sorting options and pagination.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/OrganizationQuery" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/OrganizationList" }, - "examples": { "Organization": { "$ref": "#/components/examples/OrganizationList" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/settings/members/attributes": { - "post": { - "summary": "Attribute settings: create", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Create a members' attribute setting", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/MemberAttributeSettingsCreateInput" } - } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/MemberAttributeSettings" }, - "examples": { - "MemberAttributeSettings": { - "$ref": "#/components/examples/MemberAttributeSettings" - } - } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "delete": { - "summary": "Attribute settings: delete", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Delete a members' attribute setting", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "query", - "description": "Id to destroy", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { "description": "Ok" }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "Attributes settings: list", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Get a list of members' attribute settings", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "filter[label]", - "in": "query", - "description": "Filter by label of member attribute settings", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[name]", - "in": "query", - "description": "Filter by name of member attribute settings", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[type]", - "in": "query", - "description": "Filter by type of member attribute settings", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[canDelete]", - "in": "query", - "description": "Filter by canDelete: \"true\" or \"false\"", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[show]", - "in": "query", - "description": "Filter by show: \"true\" or \"false\"", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[createdAtRange]", - "in": "query", - "description": "CreatedAt lower bound. If you want a range, send this parameter twice with [min] and [max]. If you send it once it will be interpreted as a lower bound.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "orderBy", - "in": "query", - "description": "Sort the results. Default createdAt_DESC.", - "required": false, - "schema": { "$ref": "#/components/schemas/MemberAttributeSettingsSort" } - }, - { - "name": "offset", - "in": "query", - "description": "Skip the first n results. Default 0.", - "required": false, - "schema": { "type": "number" } - }, - { - "name": "limit", - "in": "query", - "description": "Limit the number of results. Default 50.", - "required": false, - "schema": { "type": "number" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/MemberAttributeSettingsList" }, - "examples": { - "MemberAttributeSettings": { - "$ref": "#/components/examples/MemberAttributeSettingsList" - } - } - } - } - }, - "401": { "description": "Unauthorized" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/settings/members/attributes/{id}": { - "get": { - "summary": "Attributes settings: find", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Find a single members' attribute setting by ID", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the member attribute's settings", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/MemberAttributeSettings" }, - "examples": { - "MemberAttributeSettings": { - "$ref": "#/components/examples/MemberAttributeSettings" - } - } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "put": { - "summary": "Attribute settings: update", - "tags": ["Members"], - "security": [{ "Bearer": [] }], - "description": "Update a members' attribute setting", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the member attribute settings", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/MemberAttributeSettingsUpdateInput" } - } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/MemberAttributeSettings" }, - "examples": { - "MemberAttributeSettings": { - "$ref": "#/components/examples/MemberAttributeSettings" - } - } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/tag": { - "post": { - "summary": "Create a tag", - "tags": ["Tags"], - "security": [{ "Bearer": [] }], - "description": "Create a tag", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/TagNoId" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Tag" }, - "examples": { "Tag": { "$ref": "#/components/examples/Tag" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "List tags", - "tags": ["Tags"], - "security": [{ "Bearer": [] }], - "description": "Get a list of tags with filtering, sorting and offsetting.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "filter[name]", - "in": "query", - "description": "Filter by the name of the tag.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "filter[createdAtRange]", - "in": "query", - "description": "Created at lower bound. If you want a range, send this parameter twice with [min] and [max]. If you send it once it will be interpreted as a lower bound.", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "orderBy", - "in": "query", - "description": "Sort the results. Default timestamp_DESC.", - "required": false, - "schema": { "$ref": "#/components/schemas/TagSort" } - }, - { - "name": "offset", - "in": "query", - "description": "Skip the first n results. Default 0.", - "required": false, - "schema": { "type": "number" } - }, - { - "name": "limit", - "in": "query", - "description": "Limit the number of results. Default 50.", - "required": false, - "schema": { "type": "number" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/TagList" }, - "examples": { "Tags": { "$ref": "#/components/examples/TagList" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/tag/{id}": { - "delete": { - "summary": "Delete a tag", - "tags": ["Tags"], - "security": [{ "Bearer": [] }], - "description": "Delete a tag.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the tag", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { "description": "Ok" }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "Find a tag", - "tags": ["Tags"], - "security": [{ "Bearer": [] }], - "description": "Find a tag by ID", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the tag", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Tag" }, - "examples": { "Tag": { "$ref": "#/components/examples/Tag" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "put": { - "summary": "Update an tag", - "tags": ["Tags"], - "security": [{ "Bearer": [] }], - "description": "Update a tag", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the tag", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/TagNoId" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Tag" }, - "examples": { "Tag": { "$ref": "#/components/examples/Tag" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/task/batch": { - "post": { - "summary": "Make batch operations on tasks", - "tags": ["Tasks"], - "security": [{ "Bearer": [] }], - "description": "Make batch operations on tasks", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/TaskBatchInput" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/TaskFindAndUpdateAll" }, - "examples": { "Task": { "$ref": "#/components/examples/TaskFindAndUpdateAll" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/task": { - "post": { - "summary": "Create a task", - "tags": ["Tasks"], - "security": [{ "Bearer": [] }], - "description": "Create a task", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/TaskInput" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Task" }, - "examples": { "Task": { "$ref": "#/components/examples/Task" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/task/{id}": { - "delete": { - "summary": "Delete a task", - "tags": ["Tasks"], - "security": [{ "Bearer": [] }], - "description": "Delete a task.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the task", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { "description": "Ok" }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "get": { - "summary": "Find a task", - "tags": ["Tasks"], - "security": [{ "Bearer": [] }], - "description": "Find a task by ID", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the task", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/TaskResponse" }, - "examples": { "Task": { "$ref": "#/components/examples/Task" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - }, - "put": { - "summary": "Update an task", - "tags": ["Tasks"], - "security": [{ "Bearer": [] }], - "description": "Update a task", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "id", - "in": "path", - "description": "The ID of the task", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/TaskInput" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Task" }, - "examples": { "Task": { "$ref": "#/components/examples/Task" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - }, - "/tenant/{tenantId}/task/query": { - "post": { - "summary": "Query tasks", - "tags": ["Tasks"], - "security": [{ "Bearer": [] }], - "description": "Query tasks. It accepts filters, sorting options and pagination.", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "Your workspace/tenant ID", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { "schema": { "$ref": "#/components/schemas/TaskQuery" } } - } - }, - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/TaskList" }, - "examples": { "Task": { "$ref": "#/components/examples/TaskList" } } - } - } - }, - "401": { "description": "Unauthorized" }, - "404": { "description": "Not found" }, - "429": { "description": "Too many requests" } - } - } - } - }, - "components": { - "securitySchemes": { "Bearer": { "type": "http", "scheme": "bearer" } }, - "schemas": { - "MemberType": { "type": "string", "enum": ["member"] }, - "MemberScore": { "type": "integer", "minimum": -1, "maximum": 10 }, - "MemberSort": { - "type": "string", - "enum": [ - "activitiesCount_ASC", - "activitiesCount_DESC", - "score_ASC", - "score_ASC", - "joinedAt_ASC", - "joinedAt_DESC", - "createdAt_ASC", - "createdAt_DESC", - "organisation_ASC", - "organisation_DESC", - "location_ASC", - "location_DESC" - ] - }, - "ActivitySort": { - "type": "string", - "enum": [ - "timestamp_DESC", - "timestamp_ASC", - "createdAt_DESC", - "createdAt_ASC", - "score_DESC", - "score_ASC", - "type_DESC", - "type_ASC", - "platform_DESC", - "platform_ASC", - "createdBy_DESC", - "createdBy_ASC" - ] - }, - "ConversationSort": { - "type": "string", - "enum": [ - "createdAt_DESC", - "createdAt_ASC", - "activityCount_DESC", - "activityCount_ASC", - "platform_DESC", - "platform_ASC", - "channel_DESC", - "channel_ASC", - "createdBy_DESC", - "createdBy_ASC" - ] - }, - "TagSort": { - "type": "string", - "enum": ["name_ASC", "name_DESC", "createdAt_DESC", "createdAt_ASC"] - }, - "MemberAttributeSettingsSort": { - "type": "string", - "enum": [ - "label_ASC", - "label_DESC", - "type_ASC", - "type_DESC", - "createdAt_DESC", - "createdAt_ASC" - ] - }, - "ActivityRelationsInput": { - "description": "Relations of an activity.", - "type": "object", - "properties": { - "tasks": { - "description": "Tasks associated with the activity", - "type": "array", - "items": { "$ref": "#/components/schemas/TaskNoId" } - } - } - }, - "ActivityUpsertInput": { - "required": ["memberId"], - "description": "An activity performed by a member of your community. The member is sent as an ID.", - "allOf": [ - { "$ref": "#/components/schemas/ActivityNoId" }, - { "$ref": "#/components/schemas/ActivityRelationsInput" } - ], - "properties": { - "memberId": { "description": "The ID of the member that performed the activity" } - } - }, - "ActivityUpsertWithMemberInput": { - "type": "object", - "description": "An activity performed by a member of your community. The member is sent as a whole object.", - "allOf": [ - { "$ref": "#/components/schemas/ActivityNoId" }, - { "$ref": "#/components/schemas/ActivityRelationsInput" } - ], - "properties": { "member": { "$ref": "#/components/schemas/MemberNoId" } } - }, - "ActivityNoId": { - "description": "An activity performed by a member of your community.", - "type": "object", - "required": ["type", "platform", "timestamp", "sourceId"], - "properties": { - "type": { "description": "Type of activity", "type": "string" }, - "timestamp": { - "description": "Date and time when the activity took place", - "type": "string", - "format": "date-time" - }, - "platform": { - "description": "Platform on which the activity took place", - "type": "string" - }, - "title": { "description": "Title of the activity", "type": "string" }, - "body": { "description": "Body of the activity", "type": "string" }, - "channel": { "description": "Channel of the activity", "type": "string" }, - "sentiment": { - "description": "Sentiment of the activity", - "type": "object", - "properties": { - "sentiment": { - "description": "Default sentiment score.
Computed by mapping (positive - negative) from 0 to 100", - "type": "number", - "minimum": 0, - "maximum": 100 - }, - "label": { - "description": "Sentiment label", - "type": "string", - "enum": ["positive", "negative", "neutral", "mixed"] - }, - "positive": { - "description": "Positive sentiment score", - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "negative": { - "description": "Negative sentiment score", - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "neutral": { - "description": "Neutral sentiment score", - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "mixed": { - "description": "Mixed sentiment score. Mixed contains both positive and negative sentiments", - "type": "number", - "minimum": 0, - "maximum": 1 - } - } - }, - "sourceId": { - "description": "The id of the activity in the platform (e.g. the id of the message in Discord)", - "type": "string" - }, - "sourceParentId": { - "description": "The id of the parent activity in the platform (e.g. the id of the parent message in Discord)", - "type": "string" - }, - "parentId": { - "description": "Id of the parent activity, if the activity has a parent", - "type": "string", - "format": "uuid" - }, - "score": { "description": "Score associated with the activity", "type": "number" }, - "isContribution": { - "description": "Whether the activity was a contribution", - "type": "boolean" - }, - "attributes": { - "description": "Extra attributes of the activity", - "type": "object", - "additionalProperties": true - }, - "createdAt": { - "description": "Date the activity was created", - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "description": "Date the activity was last updated", - "type": "string", - "format": "date-time" - } - }, - "xml": { "name": "ActivityNoId" } - }, - "FilterType": { - "type": "object", - "additionalProperties": { - "oneOf": [{ "type": "string" }, { "$ref": "#/components/schemas/FilterType" }] - } - }, - "ActivityQuery": { - "description": "All the parameters you can use to query activitys.", - "properties": { - "filter": { - "description": "Filter. Please refer to filter docs.", - "type": "string", - "format": "blob" - }, - "orderBy": { - "type": "string", - "enum": [ - "activitiesCount_DESC", - "score_ASC", - "score_ASC", - "joinedAt_ASC", - "joinedAt_DESC", - "createdAt_ASC", - "createdAt_DESC", - "organisation_ASC", - "organisation_DESC", - "location_ASC", - "location_DESC" - ] - }, - "limit": { - "description": "Limit the number of records returned. Default is 10.", - "type": "integer", - "minimum": 1, - "maximum": 200, - "default": 10 - }, - "offset": { - "description": "Offset the number of records returned. Default is 0.", - "type": "integer", - "minimum": 0, - "default": 0 - } - } - }, - "Activity": { - "type": "object", - "allOf": [{ "$ref": "#/components/schemas/ActivityNoId" }], - "properties": { "id": { "description": "The unique identifier for an activity." } } - }, - "ActivityRelationsResponse": { - "description": "Relations of an activity.", - "type": "object", - "properties": { - "member": { - "description": "Member that performed the activity", - "$ref": "#/components/schemas/Member" - }, - "tasks": { - "description": "Tasks associated with the activity.", - "type": "array", - "items": { "$ref": "#/components/schemas/Task" } - } - } - }, - "ActivityResponse": { - "description": "An activity performed by a member.", - "type": "object", - "allOf": [ - { "$ref": "#/components/schemas/Activity" }, - { "$ref": "#/components/schemas/ActivityRelationsResponse" } - ] - }, - "ActivityList": { - "description": "List and count of activities.", - "type": "object", - "properties": { - "rows": { - "description": "List of activities", - "type": "array", - "items": { "$ref": "#/components/schemas/ActivityResponse" } - }, - "count": { "description": "Count", "type": "integer" }, - "limit": { "description": "Limit of records returned", "type": "integer" }, - "offset": { "description": "Offset, for pagination", "type": "integer" } - }, - "xml": { "name": "ActivitiesList" } - }, - "AutomationCreateInput": { - "type": "object", - "description": "Data to create a new automation.", - "required": ["type", "trigger", "settings"], - "properties": { - "type": { "$ref": "#/components/schemas/AutomationType" }, - "trigger": { "$ref": "#/components/schemas/AutomationTrigger" }, - "settings": { "$ref": "#/components/schemas/AutomationSettings" } - } - }, - "AutomationUpdateInput": { - "type": "object", - "description": "Data to update an existing automation.", - "required": ["trigger", "settings", "state"], - "properties": { - "trigger": { "$ref": "#/components/schemas/AutomationTrigger" }, - "settings": { "$ref": "#/components/schemas/AutomationSettings" }, - "state": { "$ref": "#/components/schemas/AutomationState" } - } - }, - "AutomationType": { "description": "Automation type", "type": "string", "enum": ["webhook"] }, - "AutomationState": { - "description": "Automation state", - "type": "string", - "enum": ["active", "disabled"] - }, - "AutomationTrigger": { - "description": "What will trigger an automation", - "type": "string", - "enum": ["new_activity", "new_member"] - }, - "AutomationExecutionState": { - "description": "What was the state of the automation execution", - "type": "string", - "enum": ["success", "error"] - }, - "WebhookAutomationSettings": { - "description": "Settings used by automation with type webhook", - "type": "object", - "required": ["url"], - "properties": { - "url": { "description": "URL to POST webhook data to", "type": "string", "format": "uri" } - } - }, - "NewActivityAutomationSettings": { - "description": "Settings used by automation that is triggered by new activities", - "type": "object", - "required": ["types", "platforms", "keywords", "teamMemberActivities"], - "properties": { - "types": { - "description": "If activity type matches any of these we should trigger this automation", - "type": "array", - "items": { "type": "string" } - }, - "platforms": { - "description": "If activity came from any of these platforms we should trigger this automation", - "type": "array", - "items": { "type": "string" } - }, - "keywords": { - "description": "If activity content contains any of these keywords we should trigger this automation", - "type": "array", - "items": { "type": "string" } - }, - "teamMemberActivities": { - "description": "If activity came from any of our team members - should we trigger automation or not?", - "type": "boolean" - } - } - }, - "AutomationSettings": { - "description": "Settings based on automation type and trigger - you need to provide union object of both automation type based settings and trigger based settings", - "type": "object", - "anyOf": [ - { "$ref": "#/components/schemas/WebhookAutomationSettings" }, - { "$ref": "#/components/schemas/NewActivityAutomationSettings" } - ] - }, - "Automation": { - "type": "object", - "required": ["id", "type", "tenantId", "trigger", "settings", "state", "createdAt"], - "properties": { - "id": { "description": "Automation unique ID", "type": "string", "format": "uuid" }, - "type": { "$ref": "#/components/schemas/AutomationType" }, - "tenantId": { - "description": "Automation tenant unique ID", - "type": "string", - "format": "uuid" - }, - "trigger": { "$ref": "#/components/schemas/AutomationTrigger" }, - "settings": { "$ref": "#/components/schemas/AutomationSettings" }, - "state": { "$ref": "#/components/schemas/AutomationState" }, - "createdAt": { - "description": "When was automation created", - "type": "string", - "format": "date-time" - }, - "lastExecutionAt": { - "description": "When was automation last executed", - "type": "string", - "format": "date-time" - }, - "lastExecutionState": { - "description": "State of the last automation execution", - "$ref": "#/components/schemas/AutomationExecutionState" - }, - "lastExecutionError": { - "description": "Error information if last automation execution failed", - "type": "object" - } - } - }, - "AutomationPage": { - "type": "object", - "required": ["rows", "count", "offset", "limit"], - "properties": { - "rows": { - "description": "Array of automations that were fetched", - "type": "array", - "items": { "$ref": "#/components/schemas/Automation" } - }, - "count": { "description": "How many total automations there are", "type": "integer" }, - "offset": { - "description": "What offset was used when preparing this response", - "type": "integer" - }, - "limit": { - "description": "What limit was used when preparing this response", - "type": "integer" - } - } - }, - "AutomationExecution": { - "type": "object", - "required": ["id", "automationId", "state", "executedAt", "eventId", "payload"], - "properties": { - "id": { - "description": "Automation execution unique ID", - "type": "string", - "format": "uuid" - }, - "automationId": { - "description": "Automation unique ID", - "type": "string", - "format": "uuid" - }, - "state": { - "description": "Automation execution state", - "$ref": "#/components/schemas/AutomationExecutionState" - }, - "error": { - "description": "If execution was not successful this object will contain error information", - "type": "object" - }, - "executedAt": { - "description": "Automation execution timestamp", - "type": "string", - "format": "date-time" - }, - "eventId": { - "description": "Unique ID of the event that triggered this automation execution.", - "type": "string" - }, - "payload": { - "description": "Payload that was sent when this execution was processed", - "type": "object" - } - } - }, - "AutomationExecutionPage": { - "type": "object", - "required": ["rows", "count", "offset", "limit"], - "properties": { - "rows": { - "description": "Automation Execution List", - "type": "array", - "items": { "$ref": "#/components/schemas/AutomationExecution" } - }, - "count": { "description": "How many items are there in total", "type": "integer" }, - "offset": { - "description": "What offset was used when preparing this response", - "type": "integer" - }, - "limit": { - "description": "What limit was used when preparing this response", - "type": "integer" - } - } - }, - "ConversationNoId": { - "type": "object", - "required": ["platform", "slug", "tenantId"], - "description": "A conversation is a group of activities. Some attributes, like slug, are mostly used in public pages.", - "properties": { - "title": { "description": "Title of the conversation", "type": "string" }, - "slug": { "description": "Unique slug of the conversation", "type": "string" }, - "published": { - "description": "Whether the conversation is publicaly visible from open pages.", - "type": "boolean", - "default": false - }, - "conversationStarter": { - "description": "The conversation starter activity", - "type": "object", - "additionalProperties": { "$ref": "#/components/schemas/Activity" } - }, - "memberCount": { - "description": "Number of participating members in the conversation.", - "type": "integer" - }, - "lastActive": { - "description": "Last activity time in the conversation", - "type": "string", - "format": "date-time" - }, - "createdAt": { - "description": "Date the conversation was created", - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "description": "Date the conversation was last updated", - "type": "string", - "format": "date-time" - }, - "tenantId": { - "description": "Your workspace/tenant id", - "type": "string", - "format": "uuid" - } - }, - "xml": { "name": "Conversation" } - }, - "Conversation": { - "allOf": [{ "$ref": "#/components/schemas/ConversationNoId" }], - "properties": { - "id": { "description": "Unique identifier of the conversation", "type": "string" }, - "activities": { - "description": "List of IDs of the activities in the conversation", - "type": "array", - "items": { "type": "string" } - } - } - }, - "ConversationList": { - "type": "object", - "properties": { - "rows": { "type": "array", "items": { "$ref": "#/components/schemas/Conversation" } }, - "count": { "description": "Count", "type": "integer" }, - "limit": { "description": "Limit of records returned", "type": "integer" }, - "offset": { "description": "Offset, for pagination", "type": "integer" } - } - }, - "MemberPlatformHelper": { - "type": "object", - "required": ["platform"], - "properties": { - "platform": { - "type": "string", - "description": "Platform for which to check member existence." - } - } - }, - "MemberOrganizations": { - "type": "object", - "properties": { - "organizations": { - "description": "Organizations associated with the member. Each element in the array is the name of the organization, or an organization object. If the organization does not exist, it will be created.", - "type": "array", - "items": { "$ref": "#/components/schemas/OrganizationNoId" } - } - } - }, - "MemberInputRelations": { - "type": "object", - "properties": { - "tags": { - "description": "Tags associated with the member. Each element in the array is the ID of the tag.", - "type": "array", - "items": { "type": "string" } - }, - "tasks": { - "description": "Tasks associated with the member. Each element in the array is the ID of the task.", - "type": "array", - "items": { "type": "string" } - }, - "notes": { - "description": "Notes associated with the member. Each element in the array is the ID of the note.", - "type": "array", - "items": { "type": "string" } - }, - "activities": { - "description": "Activities associated with the member. Each element in the array is the ID of the activity.", - "type": "array", - "items": { "type": "string" } - } - } - }, - "MemberUpsertInput": { - "allOf": [ - { "$ref": "#/components/schemas/MemberPlatformHelper" }, - { "$ref": "#/components/schemas/MemberNoId" }, - { "$ref": "#/components/schemas/MemberOrganizations" }, - { "$ref": "#/components/schemas/MemberInputRelations" } - ] - }, - "MemberNoId": { - "description": "A member of your community.", - "type": "object", - "required": ["username"], - "properties": { - "username": { - "description": "Usernames of the member in each platform. Exactly one for each platform in which the member is active.
Example: ```{ github: 'iamgilfoyle', discord: 'gilfoyle '}```", - "type": "object", - "additionalProperties": true - }, - "displayName": { "description": "UI friendly name of the member", "type": "string" }, - "email": { "description": "Email address of the member", "type": "string" }, - "joinedAt": { - "description": "Date of joining the community", - "type": "string", - "format": "date-time" - }, - "activeOn": { - "description": "List of platforms the member is active on.", - "type": "array", - "items": { "type": "string" } - }, - "identities": { - "description": "List of platforms the member has identities in.", - "type": "array", - "items": { "type": "string" } - }, - "activityCount": { "description": "Number of activities member has.", "type": "integer" }, - "averageSentiment": { - "description": "Averge sentiment of member's activities.", - "type": "number" - }, - "numberOfOpenSourceContributions": { - "description": "Number of open source contributions by the member.", - "type": "integer" - }, - "lastActivity": { - "description": "The last activity of a member", - "type": "object", - "additionalProperties": { "$ref": "#/components/schemas/Activity" } - }, - "score": { - "description": "Engagement score of the member. From 0 to 10. Set -1 for not yet calculated.", - "type": "number" - }, - "reach": { - "description": "Reach of the member in each platform. At most one for each platform in which the member is active.
Example: ```{ github: 10, twitter: 250, total: 260 }```", - "type": "object", - "properties": { - "total": { "description": "Sum of all the platform reaches.", "type": "number" } - }, - "additionalProperties": true - }, - "attributes": { - "description": "Attributes associated to the member. Each attribute must be an object with it's value for each platform, and a default.
For example: ```{\"location\": {\"github\": \"San Francisco\", \"twitter\": \"California\", \"default\": \"San Francisco\"}}```", - "type": "object", - "additionalProperties": { "$ref": "#/components/schemas/MemberAttribute" } - }, - "createdAt": { - "description": "Date the member was created", - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "description": "Date the member was last updated", - "type": "string", - "format": "date-time" - } - }, - "xml": { "name": "Member" } - }, - "MemberAttribute": { - "description": "A key for each platform.
- ```default``` is the value that will be displayed by default in the app
- ```custom``` is the value that will be displayed if the user has set a custom value for the attribute", - "type": "object", - "properties": { - "default": { - "description": "Default value for the attribute. This is set automatically according to crowd.dev rules.", - "type": "string" - }, - "custom": { - "description": "Custom value for the attribute. This is optionally set by the user. It will always be picked as the default when sent.", - "type": "string" - } - }, - "additionalProperties": true - }, - "MemberQuery": { - "description": "All the parameters you can use to query members.", - "properties": { - "filter": { - "description": "Filter. Please refer to filter docs.", - "type": "string", - "format": "blob" - }, - "orderBy": { - "type": "string", - "enum": [ - "activityCount_ASC", - "activityCount_DESC", - "score_ASC", - "score_DESC", - "joinedAt_ASC", - "joinedAt_DESC", - "createdAt_ASC", - "createdAt_DESC", - "organisation_ASC", - "organisation_DESC", - "location_ASC", - "location_DESC", - "numberOfOpenSourceContributions_ASC", - "numberOfOpenSourceContributions_DESC" - ] - }, - "limit": { - "description": "Limit the number of records returned. Default is 10.", - "type": "integer", - "minimum": 1, - "maximum": 200, - "default": 10 - }, - "offset": { - "description": "Offset the number of records returned. Default is 0.", - "type": "integer", - "minimum": 0, - "default": 0 - } - } - }, - "Member": { - "type": "object", - "allOf": [{ "$ref": "#/components/schemas/MemberNoId" }], - "properties": { - "id": { "description": "The unique identifier for a member of your community." }, - "activityCount": { - "description": "Number of activities performed by the member.", - "type": "integer" - }, - "lastActivity": { - "description": "Timestamp, type and platform of the last activity performed by the member.", - "type": "object", - "properties": { - "type": { "description": "Type of the last activity", "type": "string" }, - "timestamp": { - "description": "Date and time of the last activity", - "type": "string", - "format": "date-time" - }, - "platform": { "description": "Platform of the last activity", "type": "string" } - } - }, - "averageSentiment": { - "description": "Average sentiment of the member. From 0 to 100.", - "type": "number" - }, - "numberOfOpenSourceContributions": { - "description": "Number of open source contributions by the member.", - "type": "integer" - } - } - }, - "MemberRelationsResponse": { - "description": "Relations of a member.", - "type": "object", - "properties": { - "tags": { - "description": "Tags associated with the member.", - "type": "array", - "items": { "$ref": "#/components/schemas/Tag" } - }, - "notes": { - "description": "Notes associated with the member.", - "type": "array", - "items": { "$ref": "#/components/schemas/Note" } - }, - "tasks": { - "description": "Tasks associated with the member.", - "type": "array", - "items": { "$ref": "#/components/schemas/Task" } - }, - "organizations": { - "description": "Organizations associated with the member.", - "type": "array", - "items": { "$ref": "#/components/schemas/Organization" } - } - } - }, - "MemberResponse": { - "description": "A member of your community.", - "type": "object", - "allOf": [ - { "$ref": "#/components/schemas/Member" }, - { "$ref": "#/components/schemas/MemberRelationsResponse" } - ] - }, - "MemberList": { - "description": "List and count of members.", - "type": "object", - "properties": { - "rows": { - "description": "List of members", - "type": "array", - "items": { "$ref": "#/components/schemas/MemberResponse" } - }, - "count": { "description": "Count", "type": "integer" }, - "limit": { "description": "Limit of records returned", "type": "integer" }, - "offset": { "description": "Offset, for pagination", "type": "integer" } - }, - "xml": { "name": "MembersList" } - }, - "NoteInputRelations": { - "type": "object", - "properties": { - "members": { - "description": "Members associated with the note. Each element in the array is the ID of the member.", - "type": "array", - "items": { "type": "string" } - } - } - }, - "NoteInput": { - "allOf": [ - { "$ref": "#/components/schemas/NoteNoId" }, - { "$ref": "#/components/schemas/NoteInputRelations" } - ] - }, - "NoteNoId": { - "description": "A created note.", - "type": "object", - "properties": { - "body": { "description": "The body of the note.", "type": "string", "format": "blob" }, - "createdAt": { - "description": "Date the note was created.", - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "description": "Date the note was last updated.", - "type": "string", - "format": "date-time" - } - }, - "xml": { "name": "Note" } - }, - "NoteQuery": { - "description": "All the parameters you can use to query notes.", - "properties": { - "filter": { - "description": "Filter. Please refer to filter docs.", - "type": "string", - "format": "blob" - }, - "orderBy": { "type": "string", "enum": ["createdAt_ASC", "createdAt_DESC"] }, - "limit": { - "description": "Limit the number of records returned. Default is 10.", - "type": "integer", - "minimum": 1, - "maximum": 200, - "default": 10 - }, - "offset": { - "description": "Offset the number of records returned. Default is 0.", - "type": "integer", - "minimum": 0, - "default": 0 - } - } - }, - "Note": { - "type": "object", - "allOf": [{ "$ref": "#/components/schemas/NoteNoId" }], - "properties": { - "id": { "description": "The ID of the note." }, - "body": { "description": "The body of the note.", "type": "string", "format": "blob" } - } - }, - "NoteRelationsResponse": { - "description": "Relations of a note.", - "type": "object", - "properties": { - "members": { - "description": "Members associated with the note.", - "type": "array", - "items": { "$ref": "#/components/schemas/Member" } - } - } - }, - "NoteResponse": { - "description": "A note of your community.", - "type": "object", - "allOf": [ - { "$ref": "#/components/schemas/Note" }, - { "$ref": "#/components/schemas/NoteRelationsResponse" } - ] - }, - "NoteList": { - "description": "List and count of notes.", - "type": "object", - "properties": { - "rows": { - "description": "List of notes", - "type": "array", - "items": { "$ref": "#/components/schemas/NoteResponse" } - }, - "count": { "description": "Count", "type": "integer" }, - "limit": { "description": "Limit of records returned", "type": "integer" }, - "offset": { "description": "Offset, for pagination", "type": "integer" } - }, - "xml": { "name": "NotesList" } - }, - "OrganizationInputRelations": { - "type": "object", - "properties": { - "members": { - "description": "Members associated with the organization. Each element in the array is the ID of the member.", - "type": "array", - "items": { "type": "string", "format": "uuid" } - } - } - }, - "OrganizationInput": { - "allOf": [ - { "$ref": "#/components/schemas/OrganizationNoId" }, - { "$ref": "#/components/schemas/OrganizationInputRelations" } - ] - }, - "OrganizationNoId": { - "description": "A created organization.", - "type": "object", - "required": ["name"], - "properties": { - "name": { "description": "The name of the organization.", "type": "string" }, - "url": { "description": "The URL of the organization.", "type": "string" }, - "description": { - "description": "A short description of the organization.", - "type": "string", - "format": "blob" - }, - "logo": { "description": "A URL for logo of the organization.", "type": "string" }, - "emails": { - "description": "The emails for contacting the organization.", - "type": "array", - "items": { "type": "string" } - }, - "phoneNumbers": { - "description": "The phone numbers for contacting for the organization.", - "type": "array", - "items": { "type": "string" } - }, - "tags": { - "description": "Tags associated with the organization.", - "type": "array", - "items": { "type": "string" } - }, - "twitter": { - "description": "Twitter information for the organization.", - "type": "object", - "properties": { - "handle": { - "description": "The Twitter handle for the organization.", - "type": "string" - }, - "id": { "description": "The Twitter ID for the organization.", "type": "string" }, - "bio": { "description": "The Twitter bio for the organization.", "type": "string" }, - "followers": { - "description": "The number of followers on Twitter.", - "type": "integer" - }, - "location": { - "description": "The Twitter location for the organization.", - "type": "string" - }, - "site": { - "description": "The website linked to the organization's Twitter profile.", - "type": "string" - }, - "avatar": { - "description": "The URL for the organization's Twitter avatar.", - "type": "string" - } - } - }, - "employees": { - "description": "The number of employees of the organization.", - "type": "integer" - }, - "revenueRange": { - "description": "The estimated revenue range of the organization.", - "type": "object", - "properties": { - "min": { - "description": "The minimum estimated revenue of the organization.", - "type": "integer" - }, - "max": { - "description": "The maximum estimated revenue of the organization.", - "type": "integer" - } - } - }, - "linkedin": { - "description": "LinkedIn information for the organization.", - "type": "object", - "properties": { - "handle": { - "description": "The LinkedIn handle for the organization.", - "type": "string" - } - } - }, - "crunchbase": { - "description": "Crunchbase information for the organization.", - "type": "object", - "properties": { - "handle": { - "description": "The Crunchbase handle for the organization.", - "type": "string" - } - } - }, - "activeOn": { - "description": "List of platforms the organization members are active on.", - "type": "array", - "items": { "type": "string" } - }, - "identities": { - "description": "List of platforms the organization members have identities in.", - "type": "array", - "items": { "type": "string" } - }, - "memberCount": { - "description": "Number of members organization has.", - "type": "integer" - }, - "createdAt": { - "description": "Date the organization was created.", - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "description": "Date the organization was last updated.", - "type": "string", - "format": "date-time" - } - }, - "xml": { "name": "Organization" } - }, - "OrganizationQuery": { - "description": "All the parameters you can use to query organizations.", - "properties": { - "filter": { - "description": "Filter. Please refer to filter docs.", - "type": "string", - "format": "blob" - }, - "orderBy": { - "type": "string", - "enum": [ - "createdAt_ASC", - "createdAt_DESC", - "memberCount_ASC", - "memberCount_DESC", - "activityCount_ASC", - "activityCount_DESC", - "joinedAt_ASC", - "joinedAt_DESC", - "lastActive_ASC", - "lastActive_DESC" - ] - }, - "limit": { - "description": "Limit the number of records returned. Default is 10.", - "type": "integer", - "minimum": 1, - "maximum": 200, - "default": 10 - }, - "offset": { - "description": "Offset the number of records returned. Default is 0.", - "type": "integer", - "minimum": 0, - "default": 0 - } - } - }, - "Organization": { - "type": "object", - "allOf": [{ "$ref": "#/components/schemas/OrganizationNoId" }], - "properties": { - "id": { "description": "The ID of the organization." }, - "body": { - "description": "The body of the organization.", - "type": "string", - "format": "blob" - } - } - }, - "OrganizationRelationsResponse": { - "description": "Relations of a organization.", - "type": "object", - "properties": { - "members": { - "description": "Members associated with the organization.", - "type": "array", - "items": { "$ref": "#/components/schemas/Member" } - }, - "activeOn": { - "description": "The platforms where the organization is active.", - "type": "array", - "items": { "type": "string" } - }, - "identities": { - "description": "The list of identities of the members in the organization.", - "type": "array", - "items": { "type": "string" } - }, - "lastActive": { - "description": "The last time the organization was active.", - "type": "string", - "format": "date-time" - }, - "joinedAt": { - "description": "The date the first member from the organization joined the community.", - "type": "string", - "format": "date-time" - } - } - }, - "OrganizationResponse": { - "description": "A organization of your community.", - "type": "object", - "allOf": [ - { "$ref": "#/components/schemas/Organization" }, - { "$ref": "#/components/schemas/OrganizationRelationsResponse" } - ] - }, - "OrganizationList": { - "description": "List and count of organizations.", - "type": "object", - "properties": { - "rows": { - "description": "List of organizations", - "type": "array", - "items": { "$ref": "#/components/schemas/OrganizationResponse" } - }, - "count": { "description": "Count", "type": "integer" }, - "limit": { "description": "Limit of records returned", "type": "integer" }, - "offset": { "description": "Offset, for pagination", "type": "integer" } - }, - "xml": { "name": "OrganizationsList" } - }, - "TagNoId": { - "description": "A tag associated with a member.", - "type": "object", - "required": ["name", "tenantId"], - "properties": { - "name": { "description": "The name of the tag", "type": "string" }, - "createdAt": { - "description": "Date the tag was created", - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "description": "Date the tag was last updated", - "type": "string", - "format": "date-time" - }, - "tenantId": { - "description": "Your workspace/tenant id", - "type": "string", - "format": "uuid" - } - }, - "xml": { "name": "Tag" } - }, - "Tag": { - "type": "object", - "allOf": [{ "$ref": "#/components/schemas/TagNoId" }], - "properties": { "id": { "description": "The unique identifier for a tag." } } - }, - "TagList": { - "description": "List and count of tags.", - "type": "object", - "properties": { - "rows": { - "description": "List of tags", - "type": "array", - "items": { "$ref": "#/components/schemas/Tag" } - }, - "count": { "description": "Count", "type": "integer" }, - "limit": { "description": "Limit of records returned", "type": "integer" }, - "offset": { "description": "Offset, for pagination", "type": "integer" } - }, - "xml": { "name": "TagsList" } - }, - "TaskInputRelations": { - "type": "object", - "properties": { - "members": { - "description": "Members associated with the task. Each element in the array is the ID of the member.", - "type": "array", - "items": { "type": "string", "format": "uuid" } - }, - "activities": { - "description": "Activities associated with the task. Each element in the array is the ID of the activity.", - "type": "array", - "items": { "type": "string", "format": "uuid" } - }, - "assignees": { - "description": "Users assigned with the task. Each element in the array is the ID of the user.", - "type": "string", - "format": "uuid", - "default": null - } - } - }, - "TaskInput": { - "allOf": [ - { "$ref": "#/components/schemas/TaskNoId" }, - { "$ref": "#/components/schemas/TaskInputRelations" } - ] - }, - "TaskBatchInput": { - "type": "object", - "properties": { - "operation": { - "description": "Batch operation name.", - "type": "string", - "enum": ["findAndUpdateAll"] - }, - "payload": { - "type": "object", - "description": "Payload to send to the batch operation", - "properties": { - "filter": { - "description": "Filter to select the task entities. Please refer to filter docs.", - "type": "string", - "format": "blob" - }, - "update": { - "description": "key value object with desired updated fields.", - "type": "object" - } - } - } - } - }, - "TaskNoId": { - "description": "A created task.", - "type": "object", - "properties": { - "name": { "description": "The name of the task.", "type": "string" }, - "body": { "description": "The body of the task.", "type": "string", "format": "blob" }, - "status": { - "description": "The status of the task.", - "type": "string", - "enum": ["in-progress", "done"], - "default": null - }, - "createdAt": { - "description": "Date the task was created.", - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "description": "Date the task was last updated.", - "type": "string", - "format": "date-time" - } - }, - "xml": { "name": "Task" } - }, - "TaskQuery": { - "description": "All the parameters you can use to query tasks.", - "properties": { - "filter": { - "description": "Filter. Please refer to filter docs.", - "type": "string", - "format": "blob" - }, - "orderBy": { "type": "string", "enum": ["createdAt_ASC", "createdAt_DESC"] }, - "limit": { - "description": "Limit the number of records returned. Default is 10.", - "type": "integer", - "minimum": 1, - "maximum": 200, - "default": 10 - }, - "offset": { - "description": "Offset the number of records returned. Default is 0.", - "type": "integer", - "minimum": 0, - "default": 0 - } - } - }, - "Task": { - "type": "object", - "allOf": [{ "$ref": "#/components/schemas/TaskNoId" }], - "properties": { - "id": { "description": "The ID of the task." }, - "body": { "description": "The body of the task.", "type": "string", "format": "blob" } - } - }, - "TaskRelationsResponse": { - "description": "Relations of a task.", - "type": "object", - "properties": { - "members": { - "description": "Members associated with the task.", - "type": "array", - "items": { "$ref": "#/components/schemas/Member" } - }, - "activities": { - "description": "Activities associated with the task.", - "type": "array", - "items": { "$ref": "#/components/schemas/Activity" } - }, - "assignedTo": { - "description": "The workspace member assigned to the task.", - "$ref": "#/components/schemas/Member" - } - } - }, - "TaskResponse": { - "description": "A task of your community.", - "type": "object", - "allOf": [ - { "$ref": "#/components/schemas/Task" }, - { "$ref": "#/components/schemas/TaskRelationsResponse" } - ] - }, - "TaskList": { - "description": "List and count of tasks.", - "type": "object", - "properties": { - "rows": { - "description": "List of tasks", - "type": "array", - "items": { "$ref": "#/components/schemas/TaskResponse" } - }, - "count": { "description": "Count", "type": "integer" }, - "limit": { "description": "Limit of records returned", "type": "integer" }, - "offset": { "description": "Offset, for pagination", "type": "integer" } - }, - "xml": { "name": "TasksList" } - }, - "TaskFindAndUpdateAll": { - "description": "Returns number of tasks updated", - "type": "object", - "properties": { - "rowsUpdated": { "description": "Number of tasks updated", "type": "integer" } - } - }, - "MemberAttributeSettingsCreateInput": { - "description": "A member attribute.", - "allOf": [{ "$ref": "#/components/schemas/MemberAttributeSettingsNoId" }] - }, - "MemberAttributeSettingsUpdateInput": { - "description": "A member attribute.", - "properties": { - "label": { - "description": "Human-friendly name of the attribute. Label is unique in workspaces.", - "type": "string" - }, - "show": { - "description": "Whether to show the member attribute in the web app or not.", - "type": "boolean", - "default": true - } - } - }, - "MemberAttributeSettingsNoId": { - "type": "object", - "required": ["label", "type"], - "description": "A member attribute that can be created dynamically.", - "properties": { - "label": { - "description": "Human-friendly name of the attribute. Label is unique in workspaces.", - "type": "string" - }, - "name": { - "description": "Camel-case code friendly name of the attribute. If ommited, name will be generated from the label. Name is unique in workspaces.", - "type": "string" - }, - "type": { - "description": "Type of the attribute's value", - "type": "string", - "enum": ["boolean", "number", "email", "string", "url", "date"] - }, - "canDelete": { - "description": "If set to false, member attribute can not be deleted in future requests.", - "type": "boolean", - "default": false - }, - "show": { - "description": "Whether to show the member attribute in the web app or not.", - "type": "boolean", - "default": true - }, - "createdAt": { - "description": "Date the member attribute was created.", - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "description": "Date the member attribute was last updated.", - "type": "string", - "format": "date-time" - } - }, - "xml": { "name": "MemberAttributeSettings" } - }, - "MemberAttributeSettings": { - "type": "object", - "allOf": [{ "$ref": "#/components/schemas/MemberAttributeSettingsNoId" }], - "properties": { "id": { "description": "The attribute settings ID." } } - }, - "MemberAttributeSettingsList": { - "description": "List and count member attribute settings.", - "type": "object", - "properties": { - "rows": { - "description": "List of member attribute settings", - "type": "array", - "items": { "$ref": "#/components/schemas/MemberAttributeSettings" } - }, - "count": { "description": "Count", "type": "integer" }, - "limit": { "description": "Limit of records returned", "type": "integer" }, - "offset": { "description": "Offset, for pagination", "type": "integer" } - }, - "xml": { "name": "MemberAttributeSettingsList" } - } - }, - "examples": { - "ActivityUpsert": { - "value": { - "id": "782b426d-adc8-4fb4-a4ee-ab0bb07ffca0", - "type": "message", - "timestamp": "2020-05-27T15:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "1234", - "sourceParentId": null, - "attributes": { "reactions": 43 }, - "channel": "dev", - "body": "It's not magic. It's talend and sweat.", - "title": null, - "url": "discord.gg/1234", - "sentiment": { - "label": "negative", - "mixed": 1.1410574428737164, - "neutral": 11.00325882434845, - "negative": 85.99738478660583, - "positive": 1.8582981079816818, - "sentiment": 2 - }, - "importHash": null, - "createdAt": "2022-10-03T15:18:11.294Z", - "updatedAt": "2022-10-03T15:21:49.402Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": null, - "parentId": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": -1, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-03T15:17:27.073Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - "parent": null, - "tasks": [] - } - }, - "ActivityFind": { - "value": { - "id": "462ddc6b-5672-43b2-9018-4e3fd7332228", - "type": "pull_request-closed", - "timestamp": "2021-07-27T20:20:30.000Z", - "platform": "github", - "isContribution": true, - "score": 10, - "sourceId": "gh_1", - "sourceParentId": null, - "attributes": {}, - "channel": "piedpiper", - "body": "Last one to finish the code sprint! But I will have fewer bugs than Gilfoyle.", - "title": "Code sprint over!", - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "positive", - "mixed": 0.7594161201268435, - "neutral": 39.13898766040802, - "negative": 12.336093187332153, - "positive": 47.76550233364105, - "sentiment": 79 - }, - "createdAt": "2022-10-03T15:36:43.775Z", - "updatedAt": "2022-10-03T15:39:38.199Z", - "deletedAt": null, - "memberId": "2effc566-1932-44f3-a821-2d692933a953", - "conversationId": "291af008-7717-457e-9242-f5c507c8987b", - "parentId": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "2effc566-1932-44f3-a821-2d692933a953", - "username": { "github": "dinesh", "twitter": "dinesh.chugtai" }, - "attributes": { - "bio": { - "github": "Lead developer at Pied Piper", - "default": "Pakistani Denzel. Tesla and gold chain owner.", - "twitter": "Pakistani Denzel. Tesla and gold chain owner." - }, - "url": { - "github": "https://github.com/dinesh", - "default": "https://t.co/d", - "twitter": "https://t.co/d" - }, - "location": { - "custom": "Silicon Valley", - "github": "Palo alto", - "default": "Silicon Valley" - } - }, - "displayName": "Dinesh", - "email": "dinesh@piedpiper.io", - "score": 9, - "joinedAt": "2022-10-03T15:30:55.672Z", - "importHash": null, - "reach": { "total": 100, "github": 60, "twitter": 40 }, - "createdAt": "2022-10-03T15:30:55.679Z", - "updatedAt": "2022-10-03T15:30:55.679Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - "parent": null, - "tasks": [] - } - }, - "ActivityFind2": { - "value": { - "id": "73aa13b7-1ef9-4987-a273-e560edff94ca", - "type": "pull_request-comment", - "timestamp": "2021-07-27T20:22:30.000Z", - "platform": "github", - "isContribution": true, - "score": 3, - "sourceId": "gh_2", - "sourceParentId": "gh_1", - "attributes": {}, - "channel": "piedpiper", - "body": "I will never underestimate my talents again.", - "title": null, - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "positive", - "mixed": 14.308956265449524, - "neutral": 14.437079429626465, - "negative": 9.826807677745819, - "positive": 61.42715811729431, - "sentiment": 86 - }, - "importHash": null, - "createdAt": "2022-10-03T15:38:05.847Z", - "updatedAt": "2022-10-03T15:46:34.610Z", - "deletedAt": null, - "memberId": "2effc566-1932-44f3-a821-2d692933a953", - "conversationId": "291af008-7717-457e-9242-f5c507c8987b", - "parentId": "462ddc6b-5672-43b2-9018-4e3fd7332228", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "2effc566-1932-44f3-a821-2d692933a953", - "username": { "github": "dinesh", "twitter": "dinesh.chugtai" }, - "attributes": { - "bio": { - "github": "Lead developer at Pied Piper", - "default": "Pakistani Denzel. Tesla and gold chain owner.", - "twitter": "Pakistani Denzel. Tesla and gold chain owner." - }, - "url": { - "github": "https://github.com/dinesh", - "default": "https://t.co/d", - "twitter": "https://t.co/d" - }, - "location": { - "custom": "Silicon Valley", - "github": "Palo alto", - "default": "Silicon Valley" - } - }, - "displayName": "Dinesh", - "email": "dinesh@piedpiper.io", - "score": -1, - "joinedAt": "2022-10-03T15:30:55.672Z", - "importHash": null, - "reach": { "total": 100, "github": 60, "twitter": 40 }, - "createdAt": "2022-10-03T15:30:55.679Z", - "updatedAt": "2022-10-03T15:30:55.679Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - "parent": { - "id": "462ddc6b-5672-43b2-9018-4e3fd7332228", - "type": "pull_request-closed", - "timestamp": "2021-07-27T20:22:30.000Z", - "platform": "github", - "isContribution": true, - "score": 10, - "sourceId": "gh_1", - "sourceParentId": null, - "attributes": {}, - "channel": "piedpiper", - "body": "Last one to finish the code sprint! But I will have less bugs than Gilfoyle.", - "title": "Code sprint over!", - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "positive", - "mixed": 0.7594161201268435, - "neutral": 39.13898766040802, - "negative": 12.336093187332153, - "positive": 47.76550233364105, - "sentiment": 79 - }, - "importHash": null, - "createdAt": "2022-10-03T15:36:43.775Z", - "updatedAt": "2022-10-03T15:39:38.199Z", - "deletedAt": null, - "memberId": "2effc566-1932-44f3-a821-2d692933a953", - "conversationId": "291af008-7717-457e-9242-f5c507c8987b", - "parentId": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - "tasks": [] - } - }, - "ActivityFind3": { - "value": { - "id": "2dcbe40e-36e0-4929-ab21-a30467fd9a65", - "type": "pull_request-comment", - "timestamp": "2021-07-27T20:23:30.000Z", - "platform": "github", - "isContribution": true, - "score": 3, - "sourceId": "gh_3", - "sourceParentId": "gh_1", - "attributes": {}, - "channel": "piedpiper", - "body": "Don't worry. I will continue to do it for you.", - "title": null, - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "positive", - "mixed": 2.9098065569996834, - "neutral": 25.578168034553528, - "negative": 2.241993509232998, - "positive": 69.27002668380737, - "sentiment": 97 - }, - "importHash": null, - "createdAt": "2022-10-03T15:47:20.151Z", - "updatedAt": "2022-10-03T15:47:20.220Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "291af008-7717-457e-9242-f5c507c8987b", - "parentId": "462ddc6b-5672-43b2-9018-4e3fd7332228", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": -1, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-03T15:17:27.073Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - "parent": { - "id": "462ddc6b-5672-43b2-9018-4e3fd7332228", - "type": "pull_request-closed", - "timestamp": "2021-07-27T20:22:30.000Z", - "platform": "github", - "isContribution": true, - "score": 10, - "sourceId": "gh_1", - "sourceParentId": null, - "attributes": {}, - "channel": "piedpiper", - "body": "Last one to finish the code sprint! But I will have less bugs than Gilfoyle.", - "title": "Code sprint over!", - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "positive", - "mixed": 0.7594161201268435, - "neutral": 39.13898766040802, - "negative": 12.336093187332153, - "positive": 47.76550233364105, - "sentiment": 79 - }, - "importHash": null, - "createdAt": "2022-10-03T15:36:43.775Z", - "updatedAt": "2022-10-03T15:39:38.199Z", - "deletedAt": null, - "memberId": "2effc566-1932-44f3-a821-2d692933a953", - "conversationId": "291af008-7717-457e-9242-f5c507c8987b", - "parentId": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - "tasks": [] - } - }, - "ActivityList": { - "value": { - "rows": [ - { "$ref": "#/components/examples/ActivityFind" }, - { "$ref": "#/components/examples/ActivityFind2" }, - { "$ref": "#/components/examples/ActivityFind3" } - ], - "count": 3, - "limit": 10, - "offset": 0 - } - }, - "Automation": { - "value": { - "id": "b3297f3b-6924-4e92-80e7-ef2e0d87a120", - "type": "webhook", - "tenantId": "a3297f3b-6924-4e92-80e7-ef2e0d87a120", - "trigger": "new_activity", - "settings": { "url": "https://webhook.url/new_activities" }, - "createdAt": "2022-03-29T09:22:31.989Z" - } - }, - "AutomationPage": { - "value": { - "count": 1, - "offset": 0, - "limit": 10, - "rows": [ - { - "id": "b3297f3b-6924-4e92-80e7-ef2e0d87a120", - "type": "webhook", - "tenantId": "a3297f3b-6924-4e92-80e7-ef2e0d87a120", - "trigger": "new_activity", - "settings": { "url": "https://webhook.url/new_activities" }, - "createdAt": "2022-03-29T09:22:31.989Z" - } - ] - } - }, - "AutomationExecutionPage": { - "value": { - "count": 1, - "offset": 0, - "limit": 10, - "rows": [ - { - "id": "b3297f3b-6924-4e92-80e7-ef2e0d87a120", - "automationId": "a3297f3b-6924-4e92-80e7-ef2e0d87a120", - "state": "success", - "executedAt": "2022-03-29T09:22:31.989Z", - "eventId": "a3297f3b-6924-4e92-80e7-ef2e0d87a121", - "payload": [ - { - "id": "a3297f3b-6924-4e92-80e7-ef2e0d87a121", - "type": "comment", - "timestamp": "2022-03-29T09:22:31.989Z", - "platform": "twitter" - } - ] - } - ] - } - }, - "Conversation": { - "value": { - "id": "24bdea79-3125-4950-bb38-07fa4a555012", - "title": "Best of dinesh and Gilfoyle", - "slug": "best-of-dinesh-and-gilfoyle", - "published": true, - "createdAt": "2022-10-05T12:21:53.271Z", - "updatedAt": "2022-10-05T12:21:53.271Z", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "activities": [ - { - "id": "89a136ed-336d-4586-8842-790775465212", - "type": "message", - "timestamp": "2020-06-27T14:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "d42", - "sourceParentId": null, - "attributes": {}, - "channel": "piedpiper", - "body": "Sooner or later Gilfoyle's servers are going to fail and then it's all done", - "title": null, - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "negative", - "mixed": 3.6482997238636017, - "neutral": 19.5749893784523, - "negative": 75.36468505859375, - "positive": 1.4120269566774368, - "sentiment": 2 - }, - "importHash": null, - "createdAt": "2022-10-05T12:09:44.414Z", - "updatedAt": "2022-10-05T12:21:53.279Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "24bdea79-3125-4950-bb38-07fa4a555012", - "parentId": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - }, - { - "id": "c39dc046-da1d-4a25-8624-6b78aad00f30", - "type": "message", - "timestamp": "2020-06-27T15:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "2345", - "sourceParentId": "1234", - "attributes": { "reactions": 68 }, - "channel": "dev", - "body": "My servers could handle 10x the traffic, if they weren't busy apologizing for your sh*t codebase.", - "title": null, - "url": "discord.gg/2345", - "sentiment": { - "label": "negative", - "mixed": 5.963129922747612, - "neutral": 20.673033595085144, - "negative": 69.99874711036682, - "positive": 3.365083411335945, - "sentiment": 5 - }, - "importHash": null, - "createdAt": "2022-10-03T15:19:30.415Z", - "updatedAt": "2022-10-05T12:21:53.279Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "24bdea79-3125-4950-bb38-07fa4a555012", - "parentId": "782b426d-adc8-4fb4-a4ee-ab0bb07ffca0", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - } - ], - "conversationStarter": { - "id": "89a136ed-336d-4586-8842-790775465212", - "type": "message", - "timestamp": "2020-06-27T14:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "d42", - "sourceParentId": null, - "attributes": {}, - "channel": "piedpiper", - "body": "Sooner or later Gilfoyle's servers are going to fail and then it's all done", - "title": null, - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "negative", - "mixed": 3.6482997238636017, - "neutral": 19.5749893784523, - "negative": 75.36468505859375, - "positive": 1.4120269566774368, - "sentiment": 2 - }, - "importHash": null, - "createdAt": "2022-10-05T12:09:44.414Z", - "updatedAt": "2022-10-05T12:21:53.279Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "24bdea79-3125-4950-bb38-07fa4a555012", - "parentId": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - }, - "activityCount": 2, - "memberCount": 2, - "platform": "discord", - "channel": "piedpiper", - "lastActive": "2020-06-27T15:13:30.000Z" - } - }, - "ConversationList": { - "value": { - "rows": [ - { - "id": "291af008-7717-457e-9242-f5c507c8987b", - "title": "Code sprint over!", - "slug": "code-sprint-over", - "published": false, - "createdAt": "2022-10-03T15:38:05.900Z", - "updatedAt": "2022-10-03T15:38:05.900Z", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "platform": "github", - "activityCount": 3, - "lastActive": "2021-07-27T20:23:30.000Z", - "conversationStarter": { - "id": "89a136ed-336d-4586-8842-790775465212", - "type": "message", - "timestamp": "2020-06-27T14:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "d42", - "sourceParentId": null, - "attributes": {}, - "channel": "piedpiper", - "body": "Sooner or later Gilfoyle's servers are going to fail and then it's all done", - "title": null, - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "negative", - "mixed": 3.6482997238636017, - "neutral": 19.5749893784523, - "negative": 75.36468505859375, - "positive": 1.4120269566774368, - "sentiment": 2 - }, - "importHash": null, - "createdAt": "2022-10-05T12:09:44.414Z", - "updatedAt": "2022-10-05T12:21:53.279Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "24bdea79-3125-4950-bb38-07fa4a555012", - "parentId": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - }, - "lastReplies": [ - { - "id": "c39dc046-da1d-4a25-8624-6b78aad00f30", - "type": "message", - "timestamp": "2020-06-27T15:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "2345", - "sourceParentId": "1234", - "attributes": { "reactions": 68 }, - "channel": "dev", - "body": "My servers could handle 10x the traffic, if they weren't busy apologizing for your sh*t codebase.", - "title": null, - "url": "discord.gg/2345", - "sentiment": { - "label": "negative", - "mixed": 5.963129922747612, - "neutral": 20.673033595085144, - "negative": 69.99874711036682, - "positive": 3.365083411335945, - "sentiment": 5 - }, - "importHash": null, - "createdAt": "2022-10-03T15:19:30.415Z", - "updatedAt": "2022-10-05T12:21:53.279Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "24bdea79-3125-4950-bb38-07fa4a555012", - "parentId": "782b426d-adc8-4fb4-a4ee-ab0bb07ffca0", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - } - ], - "memberCount": 2, - "channel": null - }, - { - "id": "24bdea79-3125-4950-bb38-07fa4a555012", - "title": "Best of dinesh and Gilfoyle", - "slug": "best-of-dinesh-and-gilfoyle", - "published": true, - "createdAt": "2022-10-05T12:21:53.271Z", - "updatedAt": "2022-10-05T12:21:53.271Z", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "platform": "discord", - "activityCount": 1, - "lastActive": "2020-06-29T15:13:30.000Z", - "conversationStarter": { - "id": "89a136ed-336d-4586-8842-790775465212", - "type": "message", - "timestamp": "2020-05-27T14:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "d42", - "sourceParentId": null, - "attributes": {}, - "channel": "piedpiper", - "body": "Best of Dinesh and gilfoyle", - "title": null, - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "negative", - "mixed": 1.6482997238636017, - "neutral": 12.5749893784523, - "negative": 62.36468505859375, - "positive": 1.4120269566774368, - "sentiment": 2 - }, - "importHash": null, - "createdAt": "2022-10-05T12:09:44.414Z", - "updatedAt": "2022-10-05T12:21:53.279Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "24bdea79-3125-4950-bb38-07fa4a555012", - "parentId": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - }, - "lastReplies": [ - { - "id": "c39dc046-da1d-4a25-8624-6b78aad00f30", - "type": "message", - "timestamp": "2020-06-29T15:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "2345", - "sourceParentId": "1234", - "attributes": { "reactions": 68 }, - "channel": "dev", - "body": "A very last reply to the conversation.", - "title": null, - "url": "discord.gg/2345", - "sentiment": { - "label": "negative", - "mixed": 5.963129922747612, - "neutral": 20.673033595085144, - "negative": 69.99874711036682, - "positive": 3.365083411335945, - "sentiment": 5 - }, - "importHash": null, - "createdAt": "2022-10-03T15:19:30.415Z", - "updatedAt": "2022-10-05T12:21:53.279Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "24bdea79-3125-4950-bb38-07fa4a555012", - "parentId": "782b426d-adc8-4fb4-a4ee-ab0bb07ffca0", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "member": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - } - ], - "memberCount": 2, - "channel": "dev" - } - ], - "count": 2, - "limit": 10, - "offset": 0 - } - }, - "MemberUpsert": { - "value": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": -1, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-03T15:17:27.073Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - }, - "MemberFind": { - "value": { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "activities": [ - { - "id": "2dcbe40e-36e0-4929-ab21-a30467fd9a65", - "type": "pull_request-comment", - "timestamp": "2021-07-27T20:23:30.000Z", - "platform": "github", - "isContribution": true, - "score": 3, - "sourceId": "gh_3", - "sourceParentId": "gh_1", - "attributes": {}, - "channel": "piedpiper", - "body": "Don't worry. I will continue to do it for you.", - "title": null, - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "positive", - "mixed": 2.9098065569996834, - "neutral": 25.578168034553528, - "negative": 2.241993509232998, - "positive": 69.27002668380737, - "sentiment": 97 - }, - "importHash": null, - "createdAt": "2022-10-03T15:47:20.151Z", - "updatedAt": "2022-10-03T15:47:20.220Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "291af008-7717-457e-9242-f5c507c8987b", - "parentId": "462ddc6b-5672-43b2-9018-4e3fd7332228", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - { - "id": "c39dc046-da1d-4a25-8624-6b78aad00f30", - "type": "message", - "timestamp": "2020-06-27T15:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "2345", - "sourceParentId": "1234", - "attributes": { "reactions": 68 }, - "channel": "dev", - "body": "My servers could handle 10x the traffic, if they weren't busy apologizing for your sh*t codebase.", - "title": null, - "url": "discord.gg/2345", - "sentiment": { - "label": "negative", - "mixed": 5.963129922747612, - "neutral": 20.673033595085144, - "negative": 69.99874711036682, - "positive": 3.365083411335945, - "sentiment": 5 - }, - "importHash": null, - "createdAt": "2022-10-03T15:19:30.415Z", - "updatedAt": "2022-10-03T15:26:02.599Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": null, - "parentId": "782b426d-adc8-4fb4-a4ee-ab0bb07ffca0", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - { - "id": "782b426d-adc8-4fb4-a4ee-ab0bb07ffca0", - "type": "message", - "timestamp": "2020-05-27T15:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "1234", - "sourceParentId": null, - "attributes": { "reactions": 43 }, - "channel": "dev", - "body": "It's not magic. It's talend and sweat.", - "title": null, - "url": "discord.gg/1234", - "sentiment": { - "label": "negative", - "mixed": 1.1410574428737164, - "neutral": 11.00325882434845, - "negative": 85.99738478660583, - "positive": 1.8582981079816818, - "sentiment": 2 - }, - "importHash": null, - "createdAt": "2022-10-03T15:18:11.294Z", - "updatedAt": "2022-10-03T15:21:49.402Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": null, - "parentId": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ], - "lastActivity": { - "id": "2dcbe40e-36e0-4929-ab21-a30467fd9a65", - "type": "pull_request-comment", - "timestamp": "2021-07-27T20:23:30.000Z", - "platform": "github", - "isContribution": true, - "score": 3, - "sourceId": "gh_3", - "sourceParentId": "gh_1", - "attributes": {}, - "channel": "piedpiper", - "body": "Don't worry. I will continue to do it for you.", - "title": null, - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "positive", - "mixed": 2.9098065569996834, - "neutral": 25.578168034553528, - "negative": 2.241993509232998, - "positive": 69.27002668380737, - "sentiment": 97 - }, - "importHash": null, - "createdAt": "2022-10-03T15:47:20.151Z", - "updatedAt": "2022-10-03T15:47:20.220Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "291af008-7717-457e-9242-f5c507c8987b", - "parentId": "462ddc6b-5672-43b2-9018-4e3fd7332228", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - "lastActive": "2021-07-27T20:23:30.000Z", - "activityCount": 3, - "averageSentiment": 34.67, - "tags": [ - { - "id": "38807625-6302-47b5-9f35-58566ddec83b", - "name": "developer", - "importHash": null, - "createdAt": "2022-10-05T11:41:20.162Z", - "updatedAt": "2022-10-05T11:41:20.162Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - { - "id": "dca36c33-38cd-4e68-8ba8-515167e00971", - "name": "attended-hooli-con", - "importHash": null, - "createdAt": "2022-10-05T11:42:17.414Z", - "updatedAt": "2022-10-05T11:42:17.414Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ], - "organizations": [ - { - "id": "31bff99a-2eac-49f5-b015-cba95aa6e530", - "name": "Pied Piper", - "url": "https://piedpiper.io", - "description": "The new internet", - "emails": ["richard@piedpiper.io", "hello@piedpiper.io"], - "phoneNumbers": null, - "logo": null, - "tags": ["new-internet", "making-the-world-a-better-place", "not-like-hooli"], - "twitter": { - "bio": "The internet we deserve", - "handle": "PiedPiper", - "location": "The valley", - "followers": 5000, - "following": 20 - }, - "linkedin": { "handle": "company/PiedPiper" }, - "crunchbase": { "handle": "company/PiedPiper" }, - "employees": 50, - "revenueRange": { "max": 50, "min": 10 }, - "importHash": null, - "createdAt": "2022-10-03T16:15:21.812Z", - "updatedAt": "2022-10-03T16:15:21.812Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ], - "tasks": [], - "notes": [], - "noMerge": [], - "toMerge": [] - } - }, - "MemberList": { - "value": { - "rows": [ - { - "id": "2effc566-1932-44f3-a821-2d692933a953", - "username": { "github": "dinesh", "twitter": "dinesh.chugtai" }, - "attributes": { - "bio": { - "github": "Lead developer at Pied Piper", - "default": "Pakistani Denzel. Tesla and gold chain owner.", - "twitter": "Pakistani Denzel. Tesla and gold chain owner." - }, - "url": { - "github": "https://github.com/dinesh", - "default": "https://t.co/d", - "twitter": "https://t.co/d" - }, - "location": { - "custom": "Silicon Valley", - "github": "Palo alto", - "default": "Silicon Valley" - } - }, - "displayName": "Dinesh", - "email": "dinesh@piedpiper.io", - "score": 9, - "joinedAt": "2022-10-03T15:30:55.672Z", - "importHash": null, - "reach": { "total": 100, "github": 60, "twitter": 40 }, - "createdAt": "2022-10-03T15:30:55.679Z", - "updatedAt": "2022-10-05T11:39:58.095Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "identities": ["github", "twitter"], - "activeOn": ["github"], - "activityCount": "2", - "lastActive": "2021-07-27T20:22:30.000Z", - "averageSentiment": "82.50", - "numberOfOpenSourceContributions": 10, - "noMerge": [], - "toMerge": [], - "lastActivity": { - "id": "73aa13b7-1ef9-4987-a273-e560edff94ca", - "type": "pull_request-comment", - "timestamp": "2021-07-27T20:22:30.000Z", - "platform": "github", - "isContribution": true, - "score": 3, - "sourceId": "gh_2", - "sourceParentId": "gh_1", - "attributes": {}, - "channel": "piedpiper", - "body": "I will never underestimate my talents again.", - "title": null, - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "positive", - "mixed": 14.308956265449524, - "neutral": 14.437079429626465, - "negative": 9.826807677745819, - "positive": 61.42715811729431, - "sentiment": 86 - }, - "importHash": null, - "createdAt": "2022-10-03T15:38:05.847Z", - "updatedAt": "2022-10-03T15:46:34.610Z", - "deletedAt": null, - "memberId": "2effc566-1932-44f3-a821-2d692933a953", - "conversationId": "291af008-7717-457e-9242-f5c507c8987b", - "parentId": "462ddc6b-5672-43b2-9018-4e3fd7332228", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - "organizations": [ - { - "id": "31bff99a-2eac-49f5-b015-cba95aa6e530", - "name": "Pied Piper", - "url": "https://piedpiper.io", - "description": "The new internet", - "emails": ["richard@piedpiper.io", "hello@piedpiper.io"], - "phoneNumbers": null, - "logo": null, - "tags": ["new-internet", "making-the-world-a-better-place", "not-like-hooli"], - "twitter": { - "bio": "The internet we deserve", - "handle": "PiedPiper", - "location": "The valley", - "followers": 5000, - "following": 20 - }, - "linkedin": { "handle": "company/PiedPiper" }, - "crunchbase": { "handle": "company/PiedPiper" }, - "employees": 50, - "revenueRange": { "max": 50, "min": 10 }, - "importHash": null, - "createdAt": "2022-10-03T16:15:21.812Z", - "updatedAt": "2022-10-03T16:15:21.812Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ], - "tags": [ - { - "id": "38807625-6302-47b5-9f35-58566ddec83b", - "name": "developer", - "importHash": null, - "createdAt": "2022-10-05T11:41:20.162Z", - "updatedAt": "2022-10-05T11:41:20.162Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ] - }, - { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "activityCount": "3", - "lastActive": "2021-07-27T20:23:30.000Z", - "averageSentiment": "34.67", - "numberOfOpenSourceContributions": 5, - "noMerge": [], - "toMerge": [], - "lastActivity": { - "id": "2dcbe40e-36e0-4929-ab21-a30467fd9a65", - "type": "pull_request-comment", - "timestamp": "2021-07-27T20:23:30.000Z", - "platform": "github", - "isContribution": true, - "score": 3, - "sourceId": "gh_3", - "sourceParentId": "gh_1", - "attributes": {}, - "channel": "piedpiper", - "body": "Don't worry. I will continue to do it for you.", - "title": null, - "url": "github.com/piedpiper/piedpier", - "sentiment": { - "label": "positive", - "mixed": 2.9098065569996834, - "neutral": 25.578168034553528, - "negative": 2.241993509232998, - "positive": 69.27002668380737, - "sentiment": 97 - }, - "importHash": null, - "createdAt": "2022-10-03T15:47:20.151Z", - "updatedAt": "2022-10-03T15:47:20.220Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": "291af008-7717-457e-9242-f5c507c8987b", - "parentId": "462ddc6b-5672-43b2-9018-4e3fd7332228", - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - "organizations": [ - { - "id": "31bff99a-2eac-49f5-b015-cba95aa6e530", - "name": "Pied Piper", - "url": "https://piedpiper.io", - "description": "The new internet", - "emails": ["richard@piedpiper.io", "hello@piedpiper.io"], - "phoneNumbers": null, - "logo": null, - "tags": ["new-internet", "making-the-world-a-better-place", "not-like-hooli"], - "twitter": { - "bio": "The internet we deserve", - "handle": "PiedPiper", - "location": "The valley", - "followers": 5000, - "following": 20 - }, - "linkedin": { "handle": "company/PiedPiper" }, - "crunchbase": { "handle": "company/PiedPiper" }, - "employees": 50, - "revenueRange": { "max": 50, "min": 10 }, - "importHash": null, - "createdAt": "2022-10-03T16:15:21.812Z", - "updatedAt": "2022-10-03T16:15:21.812Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ], - "tags": [ - { - "id": "38807625-6302-47b5-9f35-58566ddec83b", - "name": "developer", - "importHash": null, - "createdAt": "2022-10-05T11:41:20.162Z", - "updatedAt": "2022-10-05T11:41:20.162Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - { - "id": "dca36c33-38cd-4e68-8ba8-515167e00971", - "name": "attended-hooli-con", - "importHash": null, - "createdAt": "2022-10-05T11:42:17.414Z", - "updatedAt": "2022-10-05T11:42:17.414Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ] - } - ], - "count": 2, - "offset": 0, - "limit": 10 - } - }, - "Note2": { - "value": { - "id": "39c850f6-fb96-4d16-8e8c-cd7072e33925", - "body": "Refused to have a user feedback call", - "importHash": null, - "createdAt": "2022-10-03T16:00:57.867Z", - "updatedAt": "2022-10-03T16:00:57.867Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "members": [ - { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": -1, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-03T15:17:27.073Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ] - } - }, - "Note": { - "value": { - "id": "196c07da-14e0-419e-bd9a-5f15c721a694", - "body": "Likes frunks", - "importHash": null, - "createdAt": "2022-10-05T11:58:30.977Z", - "updatedAt": "2022-10-05T11:58:30.977Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "members": [ - { - "id": "2effc566-1932-44f3-a821-2d692933a953", - "username": { "github": "dinesh", "twitter": "dinesh.chugtai" }, - "attributes": { - "bio": { - "github": "Lead developer at Pied Piper", - "default": "Pakistani Denzel. Tesla and gold chain owner.", - "twitter": "Pakistani Denzel. Tesla and gold chain owner." - }, - "url": { - "github": "https://github.com/dinesh", - "default": "https://t.co/d", - "twitter": "https://t.co/d" - }, - "location": { - "custom": "Silicon Valley", - "github": "Palo alto", - "default": "Silicon Valley" - } - }, - "displayName": "Dinesh", - "email": "dinesh@piedpiper.io", - "score": 9, - "joinedAt": "2022-10-03T15:30:55.672Z", - "importHash": null, - "reach": { "total": 100, "github": 60, "twitter": 40 }, - "createdAt": "2022-10-03T15:30:55.679Z", - "updatedAt": "2022-10-05T11:39:58.095Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ] - } - }, - "NoteList": { - "value": { - "rows": [ - { "$ref": "#/components/examples/Note" }, - { "$ref": "#/components/examples/Note2" } - ], - "count": 2, - "limit": 10, - "offset": 0 - } - }, - "OrganizationCreate": { - "value": { - "id": "31bff99a-2eac-49f5-b015-cba95aa6e530", - "name": "Pied Piper", - "url": "https://piedpiper.io", - "description": "The new internet", - "emails": ["richard@piedpiper.io", "hello@piedpiper.io"], - "phoneNumbers": null, - "logo": null, - "tags": ["new-internet", "making-the-world-a-better-place", "not-like-hooli"], - "twitter": { - "bio": "The internet we deserve", - "handle": "PiedPiper", - "location": "The valley", - "followers": 5000, - "following": 20 - }, - "linkedin": { "handle": "company/PiedPiper" }, - "crunchbase": { "handle": "company/PiedPiper" }, - "employees": 50, - "revenueRange": { "max": 50, "min": 10 }, - "importHash": null, - "createdAt": "2022-10-03T16:15:21.812Z", - "updatedAt": "2022-10-03T16:15:21.812Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "memberCount": 2, - "activityCount": 4 - } - }, - "Organization": { - "value": { - "id": "31bff99a-2eac-49f5-b015-cba95aa6e530", - "name": "Pied Piper", - "url": "https://piedpiper.io", - "description": "The new internet", - "emails": ["richard@piedpiper.io", "hello@piedpiper.io"], - "phoneNumbers": null, - "logo": null, - "tags": ["new-internet", "making-the-world-a-better-place", "not-like-hooli"], - "identities": ["github", "twitter"], - "activeOn": ["github"], - "lastActive": "2022-10-03T16:15:21.812Z", - "joinedAt": "2022-05-03T11:16:32.812Z", - "twitter": { - "bio": "The internet we deserve", - "handle": "PiedPiper", - "location": "The valley", - "followers": 5000, - "following": 20 - }, - "linkedin": { "handle": "company/PiedPiper" }, - "crunchbase": { "handle": "company/PiedPiper" }, - "employees": 50, - "revenueRange": { "max": 50, "min": 10 }, - "importHash": null, - "createdAt": "2022-10-03T16:15:21.812Z", - "updatedAt": "2022-10-03T16:15:21.812Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "memberCount": 2, - "activityCount": 4 - } - }, - "Organization2": { - "value": { - "id": "65257687-0bfa-498e-8b2f-53559f41522b", - "name": "Hooli", - "url": "https://hooli.xyz", - "description": "Hooli is an international corporation founded by Gavin Belson and Peter Gregory", - "emails": ["gavin@hooli.xyz"], - "phoneNumbers": null, - "logo": null, - "tags": ["hooli", "tethics", "not-google"], - "identities": ["devto", "github", "twitter"], - "activeOn": ["devto"], - "lastActive": "2022-10-04", - "joinedAt": "2020-01-30", - "twitter": { - "bio": "Hooli is an international corporation founded by Gavin Belson and Peter Gregory", - "handle": "hooli", - "location": "Menlo Park", - "followers": 500000, - "following": 0 - }, - "linkedin": { "handle": "company/Hooli" }, - "crunchbase": { "handle": "company/Hooli" }, - "employees": 4000, - "revenueRange": { "max": 500, "min": 100 }, - "importHash": null, - "createdAt": "2022-10-05T12:03:11.228Z", - "updatedAt": "2022-10-05T12:03:11.228Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "memberCount": 0, - "activityCount": 0 - } - }, - "OrganizationList": { - "value": { - "rows": [ - { "$ref": "#/components/examples/Organization" }, - { "$ref": "#/components/examples/Organization2" } - ], - "count": 2, - "limit": 10, - "offset": 0 - } - }, - "Tag": { - "value": { - "id": "dca36c33-38cd-4e68-8ba8-515167e00971", - "name": "attended-hooli-con", - "importHash": null, - "createdAt": "2022-10-05T11:42:17.414Z", - "updatedAt": "2022-10-05T11:42:17.414Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "members": [ - { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ] - } - }, - "Tag2": { - "value": { - "id": "38807625-6302-47b5-9f35-58566ddec83b", - "name": "developer", - "createdAt": "2022-10-05T11:41:20.162Z", - "updatedAt": "2022-10-05T11:41:20.162Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "members": [ - { - "id": "2effc566-1932-44f3-a821-2d692933a953", - "username": { "github": "dinesh", "twitter": "dinesh.chugtai" }, - "attributes": { - "bio": { - "github": "Lead developer at Pied Piper", - "default": "Pakistani Denzel. Tesla and gold chain owner.", - "twitter": "Pakistani Denzel. Tesla and gold chain owner." - }, - "url": { - "github": "https://github.com/dinesh", - "default": "https://t.co/d", - "twitter": "https://t.co/d" - }, - "location": { - "custom": "Silicon Valley", - "github": "Palo alto", - "default": "Silicon Valley" - } - }, - "displayName": "Dinesh", - "email": "dinesh@piedpiper.io", - "score": 9, - "joinedAt": "2022-10-03T15:30:55.672Z", - "reach": { "total": 100, "github": 60, "twitter": 40 }, - "createdAt": "2022-10-03T15:30:55.679Z", - "updatedAt": "2022-10-05T11:39:58.095Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - }, - { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": 8, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-05T11:40:32.560Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ] - } - }, - "TagList": { - "value": { - "rows": [ - { "$ref": "#/components/examples/Tag" }, - { "$ref": "#/components/examples/Tag2" } - ], - "count": 2, - "limit": 10, - "offset": 0 - } - }, - "Task": { - "value": { - "id": "8a127785-f11d-4102-804d-5b79ccddd4cc", - "name": "Ask for tips on building a new Anton", - "body": null, - "status": null, - "dueDate": "2022-05-27T15:13:30.000Z", - "importHash": null, - "createdAt": "2022-10-03T16:00:18.701Z", - "updatedAt": "2022-10-03T16:00:18.701Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "assignedToId": null, - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "members": [ - { - "id": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "username": { "github": "gilfoyle", "twitter": "gilfoyle" }, - "attributes": { - "bio": { - "github": "Systems engineer at Pied Piper", - "default": "It's not magic. It's talent and sweat", - "twitter": "It's not magic. It's talent and sweat" - }, - "url": { - "github": "https://github.com/gilfoyle", - "default": "https://t.co/g", - "twitter": "https://t.co/g" - }, - "location": { - "custom": "Erlich's house", - "github": "Palo alto", - "default": "Erlich's house" - } - }, - "displayName": "Gilfoyle", - "email": "gilfoyle@piedpiper.io", - "score": -1, - "joinedAt": "2022-10-03T15:17:03.540Z", - "importHash": null, - "reach": { "total": 10000, "github": 5000, "twitter": 5000 }, - "createdAt": "2022-10-03T15:17:03.547Z", - "updatedAt": "2022-10-03T15:17:27.073Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ], - "activities": [] - } - }, - "Task2": { - "value": { - "id": "ef22fb05-a41b-472e-9917-a4d10d19fcc6", - "name": "Ask if we can use as quote", - "body": null, - "status": null, - "dueDate": "2022-08-27T00:00:00.000Z", - "importHash": null, - "createdAt": "2022-10-05T11:55:55.606Z", - "updatedAt": "2022-10-05T11:55:55.606Z", - "deletedAt": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "assignedToId": null, - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "members": [], - "activities": [ - { - "id": "782b426d-adc8-4fb4-a4ee-ab0bb07ffca0", - "type": "message", - "timestamp": "2020-05-27T15:13:30.000Z", - "platform": "discord", - "isContribution": true, - "score": 1, - "sourceId": "1234", - "sourceParentId": null, - "attributes": { "reactions": 43 }, - "channel": "dev", - "body": "It's not magic. It's talend and sweat.", - "title": null, - "url": "discord.gg/1234", - "sentiment": { - "label": "negative", - "mixed": 1.1410574428737164, - "neutral": 11.00325882434845, - "negative": 85.99738478660583, - "positive": 1.8582981079816818, - "sentiment": 2 - }, - "importHash": null, - "createdAt": "2022-10-03T15:18:11.294Z", - "updatedAt": "2022-10-03T15:21:49.402Z", - "deletedAt": null, - "memberId": "ab7a9fe9-4576-46b1-a710-8b8eaeff87a5", - "conversationId": null, - "parentId": null, - "tenantId": "8642a2bd-965e-4acd-be8c-dfedc83ef0af", - "createdById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75", - "updatedById": "debc3c7f-4c5d-4bec-9130-17bb0aea8b75" - } - ] - } - }, - "TaskList": { - "value": { - "rows": [ - { "$ref": "#/components/examples/Task" }, - { "$ref": "#/components/examples/Task2" } - ], - "count": 2, - "limit": 10, - "offset": 0 - } - }, - "TaskFindAndUpdateAll": { "value": { "rowsUpdated": 5 } }, - "MemberAttributeSettings": { - "value": { - "id": "9eaedce9-1f3a-4a75-adc8-e475cbc47553'", - "type": "string", - "canDelete": false, - "show": true, - "label": "Url", - "name": "url", - "createdAt": "2022-09-07", - "updatedAt": "2022-09-07", - "tenantId": "fcd5b9cc-144b-4687-8fd9-34818f35e70d" - } - }, - "MemberAttributeSettings2": { - "value": { - "id": "13bb9e12-c371-44ad-8806-0678c2f53dd1", - "type": "boolean", - "canDelete": false, - "show": true, - "label": "is Hireable", - "name": "isHireable", - "createdAt": "2022-09-07", - "updatedAt": "2022-09-07", - "tenantId": "fcd5b9cc-144b-4687-8fd9-34818f35e70d" - } - }, - "MemberAttributeSettingsList": { - "value": { - "rows": [ - { "$ref": "#/components/examples/MemberAttributeSettings" }, - { "$ref": "#/components/examples/MemberAttributeSettings2" } - ], - "count": 2 - } - } - } - }, - "tags": [ - { "name": "Members", "description": "Everything about members" }, - { "name": "Member Attributes", "description": "Settings for member's attributes" }, - { "name": "Activities", "description": "Everything about activities" }, - { "name": "Organizations", "description": "Everything about organizations" }, - { "name": "Conversations", "description": "Everything about conversations" }, - { "name": "Tags", "description": "Everything about tags" }, - { "name": "Automations", "description": "Everything about automations" }, - { "name": "Notes", "description": "Everything about notes" } - ] -} diff --git a/backend/src/errors/Error400.ts b/backend/src/errors/Error400.ts deleted file mode 100644 index 6753085049..0000000000 --- a/backend/src/errors/Error400.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { i18n, i18nExists } from '../i18n' - -export default class Error400 extends Error { - code: Number - - constructor(language?, messageCode?, ...args) { - let message - - if (messageCode && i18nExists(language, messageCode)) { - message = i18n(language, messageCode, ...args) - } - - message = message || i18n(language, 'errors.validation.message') - - super(message) - this.code = 400 - } -} diff --git a/backend/src/errors/Error401.ts b/backend/src/errors/Error401.ts deleted file mode 100644 index 9acea1b306..0000000000 --- a/backend/src/errors/Error401.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { i18n, i18nExists } from '../i18n' - -export default class Error401 extends Error { - code: Number - - constructor(language?, messageCode?, ...args) { - let message - - if (messageCode && i18nExists(language, messageCode)) { - message = i18n(language, messageCode, ...args) - } - - message = message || i18n(language, 'errors.validation.message') - - super(message) - this.code = 401 - } -} diff --git a/backend/src/errors/Error403.ts b/backend/src/errors/Error403.ts deleted file mode 100644 index aaa2e4824d..0000000000 --- a/backend/src/errors/Error403.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { i18n, i18nExists } from '../i18n' - -export default class Error403 extends Error { - code: Number - - constructor(language?, messageCode?, params?) { - let message - - if (messageCode && i18nExists(language, messageCode)) { - message = i18n(language, messageCode) - } - - message = message || i18n(language, 'errors.forbidden.message') - - if (params && params.integration && params.scopes) { - if (typeof params.scopes === 'string') { - params.scopes = [params.scopes.join(', ')] - } - message = message.replace('{integration}', params.integration) - message = message.replace('{scopes}', params.scopes) - } - - super(message) - this.code = 403 - } -} diff --git a/backend/src/errors/Error404.ts b/backend/src/errors/Error404.ts deleted file mode 100644 index 2769f24c3f..0000000000 --- a/backend/src/errors/Error404.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { i18n, i18nExists } from '../i18n' - -export default class Error404 extends Error { - code: Number - - constructor(language?, messageCode?) { - let message - - if (messageCode && i18nExists(language, messageCode)) { - message = i18n(language, messageCode) - } - - message = message || i18n(language, 'errors.notFound.message') - - super(message) - this.code = 404 - } -} diff --git a/backend/src/errors/Error405.ts b/backend/src/errors/Error405.ts deleted file mode 100644 index da824007ee..0000000000 --- a/backend/src/errors/Error405.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { i18n, i18nExists } from '../i18n' - -export default class Error405 extends Error { - code: Number - - constructor(language?, messageCode?) { - let message - - if (messageCode && i18nExists(language, messageCode)) { - message = i18n(language, messageCode) - } - - message = message || i18n(language, 'errors.notFound.message') - - super(message) - this.code = 405 - } -} diff --git a/backend/src/errors/Error409.ts b/backend/src/errors/Error409.ts deleted file mode 100644 index 6c93378d66..0000000000 --- a/backend/src/errors/Error409.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { i18n, i18nExists } from '../i18n' - -export default class Error409 extends Error { - code: Number - - constructor(language?, messageCode?) { - let message - - if (messageCode && i18nExists(language, messageCode)) { - message = i18n(language, messageCode) - } - - message = message || i18n(language, 'errors.notFound.message') - - super(message) - this.code = 409 - } -} diff --git a/backend/src/errors/Error500.ts b/backend/src/errors/Error500.ts deleted file mode 100644 index a9c2c02c36..0000000000 --- a/backend/src/errors/Error500.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default class Error500 extends Error { - code: Number - - constructor(message?) { - message = message || 'Internal server error' - super(message) - this.code = 500 - } -} diff --git a/backend/src/errors/Error542.ts b/backend/src/errors/Error542.ts deleted file mode 100644 index d9d60c2cce..0000000000 --- a/backend/src/errors/Error542.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default class Error542 extends Error { - code: Number - - constructor(message?) { - message = message || 'Internal server error' - super(message) - this.code = 542 - } -} diff --git a/backend/src/feature-flags/getFeatureFlagTenantContext.ts b/backend/src/feature-flags/getFeatureFlagTenantContext.ts deleted file mode 100644 index b6f0f10ccc..0000000000 --- a/backend/src/feature-flags/getFeatureFlagTenantContext.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { RedisCache, RedisClient } from '@crowd/redis' -import { Logger } from '@crowd/logging' -import { getSecondsTillEndOfMonth } from '../utils/timing' -import AutomationRepository from '../database/repositories/automationRepository' -import { FeatureFlagRedisKey } from '../types/common' - -export default async function getFeatureFlagTenantContext( - tenant: any, - database: any, - redis: RedisClient, - log: Logger, -) { - const automationCount = await AutomationRepository.countAllActive(database, tenant.id) - const csvExportCountCache = new RedisCache(FeatureFlagRedisKey.CSV_EXPORT_COUNT, redis, log) - const memberEnrichmentCountCache = new RedisCache( - FeatureFlagRedisKey.MEMBER_ENRICHMENT_COUNT, - redis, - log, - ) - - let csvExportCount = await csvExportCountCache.get(tenant.id) - let memberEnrichmentCount = await memberEnrichmentCountCache.get(tenant.id) - - const secondsRemainingUntilEndOfMonth = getSecondsTillEndOfMonth() - - if (!csvExportCount) { - await csvExportCountCache.set(tenant.id, '0', secondsRemainingUntilEndOfMonth) - csvExportCount = '0' - } - - if (!memberEnrichmentCount) { - await memberEnrichmentCountCache.set(tenant.id, '0', secondsRemainingUntilEndOfMonth) - memberEnrichmentCount = '0' - } - - return { - tenantId: tenant.id, - plan: tenant.plan, - automationCount: automationCount.toString(), - csvExportCount, - memberEnrichmentCount, - } -} diff --git a/backend/src/feature-flags/isFeatureEnabled.ts b/backend/src/feature-flags/isFeatureEnabled.ts deleted file mode 100644 index 7b076161c3..0000000000 --- a/backend/src/feature-flags/isFeatureEnabled.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Unleash } from 'unleash-client' -import { Edition } from '@crowd/types' -import { API_CONFIG } from '../conf' -import { FeatureFlag } from '../types/common' -import getFeatureFlagTenantContext from './getFeatureFlagTenantContext' -import Plans from '../security/plans' - -export const PLAN_LIMITS = { - [Plans.values.essential]: { - [FeatureFlag.AUTOMATIONS]: 2, - [FeatureFlag.CSV_EXPORT]: 2, - }, - [Plans.values.growth]: { - [FeatureFlag.AUTOMATIONS]: 10, - [FeatureFlag.CSV_EXPORT]: 10, - [FeatureFlag.MEMBER_ENRICHMENT]: 1000, - [FeatureFlag.ORGANIZATION_ENRICHMENT]: 200, - }, - [Plans.values.scale]: { - [FeatureFlag.AUTOMATIONS]: 20, - [FeatureFlag.CSV_EXPORT]: 20, - [FeatureFlag.MEMBER_ENRICHMENT]: Infinity, - [FeatureFlag.ORGANIZATION_ENRICHMENT]: Infinity, - }, - [Plans.values.enterprise]: { - [FeatureFlag.AUTOMATIONS]: Infinity, - [FeatureFlag.CSV_EXPORT]: Infinity, - [FeatureFlag.MEMBER_ENRICHMENT]: Infinity, - [FeatureFlag.ORGANIZATION_ENRICHMENT]: Infinity, - }, -} - -export default async (featureFlag: FeatureFlag, req: any): Promise => { - if (featureFlag === FeatureFlag.SEGMENTS) { - return API_CONFIG.edition === Edition.LFX - } - - if ([Edition.COMMUNITY, Edition.LFX].includes(API_CONFIG.edition as Edition)) { - return true - } - - const context = await getFeatureFlagTenantContext( - req.currentTenant, - req.database, - req.redis, - req.log, - ) - - const unleash: Unleash = req.unleash - - const enabled = unleash.isEnabled(featureFlag, context) - - return enabled -} diff --git a/backend/src/i18n/index.ts b/backend/src/i18n/index.ts deleted file mode 100644 index 6b306379fb..0000000000 --- a/backend/src/i18n/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -import _get from 'lodash/get' -import en from './en' -import ptBR from './pt-BR' -import es from './es' - -/** - * Object with the languages available. - */ -const languages = { - en, - 'pt-BR': ptBR, - es, -} - -/** - * Replaces the parameters of a message with the args. - */ -function format(message, args) { - if (!message) { - return null - } - - return message.replace(/{(\d+)}/g, (match, number) => - typeof args[number] !== 'undefined' ? args[number] : match, - ) -} - -/** - * Checks if the key exists on the language. - */ -export const i18nExists = (languageCode, key) => { - const dictionary = languages[languageCode] || languages.en - const message = _get(dictionary, key) - return Boolean(message) -} - -/** - * Returns the translation based on the key. - */ -export const i18n = (languageCode, key, ...args) => { - const dictionary = languages[languageCode] || languages.en - const message = _get(dictionary, key) - - if (!message) { - return key - } - - return format(message, args) -} diff --git a/backend/src/jsons/dashboard-widgets.json b/backend/src/jsons/dashboard-widgets.json deleted file mode 100644 index 1a1753cf60..0000000000 --- a/backend/src/jsons/dashboard-widgets.json +++ /dev/null @@ -1,13 +0,0 @@ -[ - "inactive-members", - "time-to-first-interaction", - "number-activities", - "number-members", - "number-activities-graph", - "number-members-graph", - "benchmark", - "latest-activities", - "newest-members", - "integrations", - "builder" -] diff --git a/backend/src/jsons/default-report.json b/backend/src/jsons/default-report.json deleted file mode 100644 index 843abd243b..0000000000 --- a/backend/src/jsons/default-report.json +++ /dev/null @@ -1,208 +0,0 @@ -{ - "name": "Default Report", - "public": false, - "widgets": [ - { - "title": "Total activities, All time", - "type": "cubejs", - "settings": { - "chartType": "number", - "query": { - "measures": ["Activities.count"], - "timeDimensions": [ - { - "dimension": "Activities.date" - } - ], - "limit": 10000 - }, - "layout": { - "x": 0, - "y": 0, - "w": 6, - "h": 6 - } - } - }, - { - "title": "Total members, All time", - "type": "cubejs", - "settings": { - "chartType": "number", - "query": { - "measures": ["Members.count"], - "timeDimensions": [ - { - "dimension": "Members.joinedAt" - } - ], - "limit": 10000, - "order": { - "Members.joinedAt": "asc" - } - }, - "layout": { - "x": 6, - "y": 0, - "w": 6, - "h": 6 - } - } - }, - { - "title": "New Activities", - "type": "cubejs", - "settings": { - "chartType": "area", - "query": { - "measures": ["Activities.count"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "granularity": "day", - "dateRange": "Last 30 days" - } - ], - "limit": 10000 - }, - "layout": { - "x": 0, - "y": 6, - "w": 6, - "h": 18 - } - } - }, - { - "title": "New Members", - "type": "cubejs", - "settings": { - "chartType": "bar", - "query": { - "measures": ["Members.count"], - "timeDimensions": [ - { - "dimension": "Members.joinedAt", - "granularity": "day", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Members.joinedAt": "asc" - } - }, - "layout": { - "x": 6, - "y": 6, - "w": 6, - "h": 18 - } - } - }, - { - "title": "New Activities (by Platform)", - "type": "cubejs", - "settings": { - "chartType": "pie", - "query": { - "measures": ["Activities.count"], - "dimensions": ["Activities.platform"], - "timeDimensions": [ - { - "dimension": "Activities.date", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Activities.count": "desc" - } - }, - "layout": { - "x": 0, - "y": 24, - "w": 6, - "h": 18 - } - } - }, - { - "title": "New Members (by Platform)", - "type": "cubejs", - "settings": { - "chartType": "pie", - "query": { - "measures": ["Members.count"], - "dimensions": ["Activities.platform"], - "timeDimensions": [ - { - "dimension": "Members.joinedAt", - "dateRange": "Last 30 days" - } - ], - "limit": 10000, - "order": { - "Members.count": "desc" - } - }, - "layout": { - "x": 6, - "y": 24, - "w": 6, - "h": 18 - } - } - }, - { - "title": "Members by Engagement Level", - "type": "cubejs", - "settings": { - "chartType": "bar", - "query": { - "measures": ["Members.count"], - "dimensions": ["Members.score"], - "timeDimensions": [ - { - "dimension": "Members.joinedAt" - } - ], - "limit": 10000, - "order": [["Members.count", "desc"]] - }, - "layout": { - "x": 6, - "y": 42, - "w": 6, - "h": 18 - } - } - }, - { - "title": "Activities by Type", - "type": "cubejs", - "settings": { - "chartType": "pie", - "query": { - "measures": ["Activities.count"], - "dimensions": ["Activities.type"], - "timeDimensions": [ - { - "dimension": "Activities.date" - } - ], - "limit": 10000, - "order": { - "Activities.count": "desc" - } - }, - "layout": { - "x": 0, - "y": 42, - "w": 6, - "h": 23 - } - } - } - ] -} diff --git a/backend/src/middlewares/authMiddleware.ts b/backend/src/middlewares/authMiddleware.ts index 4e65bcefe0..d5034b3380 100644 --- a/backend/src/middlewares/authMiddleware.ts +++ b/backend/src/middlewares/authMiddleware.ts @@ -1,4 +1,5 @@ -import Error401 from '../errors/Error401' +import { Error401 } from '@crowd/common' + import AuthService from '../services/auth/authService' /** @@ -34,11 +35,11 @@ export async function authMiddleware(req, res, next) { try { const currentUser: any = await AuthService.findByToken(idToken, req) - req.currentUser = currentUser next() } catch (error) { + req.log.error(error, 'Error while finding user') await req.responseHandler.error(req, res, new Error401()) } } diff --git a/backend/src/middlewares/databaseMiddleware.ts b/backend/src/middlewares/databaseMiddleware.ts index bbbc2cd62c..dbac78732c 100644 --- a/backend/src/middlewares/databaseMiddleware.ts +++ b/backend/src/middlewares/databaseMiddleware.ts @@ -1,4 +1,5 @@ import { getServiceLogger } from '@crowd/logging' + import { databaseInit } from '../database/databaseConnection' const log = getServiceLogger() diff --git a/backend/src/middlewares/featureFlagMiddleware.ts b/backend/src/middlewares/featureFlagMiddleware.ts deleted file mode 100644 index 0efcf3239e..0000000000 --- a/backend/src/middlewares/featureFlagMiddleware.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Error403 from '../errors/Error403' -import isFeatureEnabled from '../feature-flags/isFeatureEnabled' -import { FeatureFlag } from '../types/common' - -export function featureFlagMiddleware(featureFlag: FeatureFlag, errorMessage: string) { - return async (req, res, next) => { - if (!(await isFeatureEnabled(featureFlag, req))) { - await req.responseHandler.error(req, res, new Error403(req.language, errorMessage)) - return - } - next() - } -} diff --git a/backend/src/middlewares/passportStrategyMiddleware.ts b/backend/src/middlewares/passportStrategyMiddleware.ts index 3df0fd792f..02979cd955 100644 --- a/backend/src/middlewares/passportStrategyMiddleware.ts +++ b/backend/src/middlewares/passportStrategyMiddleware.ts @@ -1,9 +1,11 @@ -import { getServiceLogger } from '@crowd/logging' import passport from 'passport' -import { GOOGLE_CONFIG, SLACK_CONFIG, GITHUB_CONFIG } from '../conf' + +import { getServiceLogger } from '@crowd/logging' + +import { GITHUB_CONFIG, GOOGLE_CONFIG, SLACK_CONFIG } from '../conf' +import { getGithubStrategy } from '../services/auth/passportStrategies/githubStrategy' import { getGoogleStrategy } from '../services/auth/passportStrategies/googleStrategy' import { getSlackStrategy } from '../services/auth/passportStrategies/slackStrategy' -import { getGithubStrategy } from '../services/auth/passportStrategies/githubStrategy' const log = getServiceLogger() diff --git a/backend/src/middlewares/productDbMiddleware.ts b/backend/src/middlewares/productDbMiddleware.ts new file mode 100644 index 0000000000..b9e92b6d18 --- /dev/null +++ b/backend/src/middlewares/productDbMiddleware.ts @@ -0,0 +1,8 @@ +import { DbConnection } from '@crowd/data-access-layer/src/database' + +export function productDatabaseMiddleware(db: DbConnection) { + return async (req, res, next) => { + req.productDb = db + next() + } +} diff --git a/backend/src/middlewares/segmentMiddleware.ts b/backend/src/middlewares/segmentMiddleware.ts index 47d52748c9..cd2f5c5c4d 100644 --- a/backend/src/middlewares/segmentMiddleware.ts +++ b/backend/src/middlewares/segmentMiddleware.ts @@ -1,38 +1,42 @@ +import { NextFunction, Request, Response } from 'express' + +import { IRepositoryOptions } from '../database/repositories/IRepositoryOptions' import SegmentRepository from '../database/repositories/segmentRepository' -import isFeatureEnabled from '../feature-flags/isFeatureEnabled' -import { FeatureFlag } from '../types/common' -import { SegmentData } from '../types/segmentTypes' -export async function segmentMiddleware(req, res, next) { +/** Resolves segment(s) from the request and sets `req.currentSegments` for downstream handlers. */ +export async function segmentMiddleware(req: Request, _res: Response, next: NextFunction) { try { - let segments: SegmentData[] = [] - const segmentRepository = new SegmentRepository(req) - - if (!(await isFeatureEnabled(FeatureFlag.SEGMENTS, req))) { - // return default segment - const segments = await segmentRepository.querySubprojects({ limit: 1, offset: 0 }) - req.currentSegments = segments.rows - next() - return - } + const options = req as unknown as IRepositoryOptions + const segmentRepository = new SegmentRepository(options) + + const querySegments = toStringArray(req.query.segments) + const bodySegments = toStringArray((req.body as Record)?.segments) - if (req.query.segments) { - // for get requests, segments will be in query - segments = await segmentRepository.findInIds(req.query.segments) - } else if (req.body.segments) { - // for post and put requests, segments will be in body - segments = await segmentRepository.findInIds(req.body.segments) + const segmentIds = querySegments.length > 0 ? querySegments : bodySegments + + if (segmentIds.length > 0) { + options.currentSegments = await segmentRepository.findInIds(segmentIds) } else { - const segments = await segmentRepository.querySubprojects({ limit: 1, offset: 0 }) - req.currentSegments = segments.rows - next() - return + const { rows } = await segmentRepository.querySubprojects({ limit: 1, offset: 0 }) + options.currentSegments = rows } - req.currentSegments = segments.filter((s) => SegmentRepository.isSubproject(s)) - next() } catch (error) { next(error) } } + +/** + * Safely extracts a string[] from an unknown query/body value. + */ +function toStringArray(value: unknown): string[] { + if (value === undefined || value === null) return [] + + const items = Array.isArray(value) ? value : [value] + + return items + .filter((item): item is string => typeof item === 'string') + .map((item) => item.trim()) + .filter(Boolean) +} diff --git a/backend/src/middlewares/tenantMiddleware.ts b/backend/src/middlewares/tenantMiddleware.ts index a95a7745e9..b8f57404fc 100644 --- a/backend/src/middlewares/tenantMiddleware.ts +++ b/backend/src/middlewares/tenantMiddleware.ts @@ -1,8 +1,11 @@ +import { DEFAULT_TENANT_ID } from '@crowd/common' + import TenantService from '../services/tenantService' -export async function tenantMiddleware(req, res, next, value) { +export async function tenantMiddleware(req, res, next) { try { - const tenant = await new TenantService(req).findById(value) + const tenantId = DEFAULT_TENANT_ID + const tenant = await new TenantService(req).findById(tenantId) req.currentTenant = tenant next() } catch (error) { diff --git a/backend/src/osspckgs/Dockerfile.flyway b/backend/src/osspckgs/Dockerfile.flyway new file mode 100644 index 0000000000..1615fac033 --- /dev/null +++ b/backend/src/osspckgs/Dockerfile.flyway @@ -0,0 +1,17 @@ +FROM flyway/flyway:7.8.1-alpine + +USER root + +# Install envsubst from gettext used for templating. +RUN apk update \ + && apk add --no-cache gettext + +USER flyway + +COPY ./flyway_migrate.sh /migrate.sh + +# Override default `flyway` entrypoint. +ENTRYPOINT ["/migrate.sh"] + +# Copy migrations. +COPY ./migrations /tmp/migrations diff --git a/backend/src/osspckgs/flyway_migrate.sh b/backend/src/osspckgs/flyway_migrate.sh new file mode 100755 index 0000000000..b2a4582979 --- /dev/null +++ b/backend/src/osspckgs/flyway_migrate.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -e +echo "Migrating jdbc:postgresql://${PGHOST}:${PGPORT}/${PGDATABASE}" + +flyway \ + -locations="filesystem:/tmp/migrations" \ + -url="jdbc:postgresql://${PGHOST}:${PGPORT}/${PGDATABASE}" \ + -user="$PGUSER" \ + -password="$PGPASSWORD" \ + -connectRetries=60 \ + -outOfOrder=true \ + -mixed=true \ + -placeholderReplacement=false \ + -schemas=public \ + -X \ + migrate diff --git a/backend/src/osspckgs/migrations/V1779710880__initial_schema.sql b/backend/src/osspckgs/migrations/V1779710880__initial_schema.sql new file mode 100644 index 0000000000..d3b713c162 --- /dev/null +++ b/backend/src/osspckgs/migrations/V1779710880__initial_schema.sql @@ -0,0 +1,977 @@ +-- Staging schema — used by gcsParquetToStaging activity for temporary unlogged tables. +CREATE SCHEMA IF NOT EXISTS staging; + +-- ============================================================ +-- DOMAIN 1: UNIVERSE (Tier 3 → Tier 2 ranking input) +-- ============================================================ +CREATE TABLE packages_universe ( + id bigserial PRIMARY KEY, + purl text UNIQUE, + ecosystem text NOT NULL, + namespace text, + name text NOT NULL, + -- Cached latest 30-day window count. Written by the same weekly ranking worker that upserts rows into + -- the downloads_last_30d table (keyed by purl/end_date). This column is the denormalized latest value + -- used directly by rank_packages_universe() to avoid a join; the downloads_last_30d table holds the + -- full rolling-window timeline. + downloads_last_30d bigint, + dependent_packages_count int, + dependent_repos_count int, + criticality_score numeric(10, 4), + rank_in_ecosystem int, + is_critical bool NOT NULL DEFAULT FALSE, + -- Renamed from last_ranked_at (original pckgs.md spec) to last_rank_pass_at to make the + -- every-pass update semantic explicit: updated unconditionally on each ranking run, + -- not only when the rank changes. Mirrors the same column added to packages. + last_rank_pass_at timestamptz +); + +CREATE INDEX ON packages_universe (ecosystem, rank_in_ecosystem); + +CREATE INDEX ON packages_universe (is_critical) +WHERE + is_critical; + +-- ============================================================ +-- DOMAIN 2: TIER 2 PACKAGE DATA +-- ============================================================ +CREATE TABLE packages ( + id bigserial PRIMARY KEY, + purl text UNIQUE NOT NULL, + ecosystem text NOT NULL, + namespace text, + name text NOT NULL, + registry_url text, + status text, -- 'active' | 'deprecated' | 'unpublished' | 'yanked' + description text, + homepage text, + declared_repository_url text, + repository_url text, + licenses text[], -- SPDX normalized + licenses_raw text, + keywords text[], + -- npm-specific (NULL for other ecosystems) + dist_tags_latest text, + dist_tags_next text, + dist_tags_beta text, + -- Aggregates (refreshed each ingestion run) + versions_count int, + latest_version text, + first_release_at timestamptz, + latest_release_at timestamptz, + dependent_packages_count int, + dependent_repos_count int, + -- has_critical_vulnerability: TRUE iff latest_version is inside an active + -- affected range of a critical advisory (CVSS >= 7.0) OR a MAL-* malicious- + -- package advisory matches the package. Maintained by the deriveCriticalFlag + -- activity in packages_worker/src/osv/. See ADR-0001 §`has_critical_vulnerability` + -- semantics for the option-b + MAL- override rationale. + has_critical_vulnerability bool NOT NULL DEFAULT FALSE, + criticality_score numeric(10, 4), + -- is_critical and last_rank_pass_at are not in the original pckgs.md spec; added so + -- the packages table can answer "is this package critical?" without joining packages_universe, + -- which is an ephemeral ranking workspace and gets truncated on every weekly pass. + is_critical bool NOT NULL DEFAULT FALSE, + -- Set on every ranking pass (not just when rank changes) so queries can detect stale rows + -- via last_rank_pass_at < NOW() - INTERVAL '8 days'. + last_rank_pass_at timestamptz, + ingestion_source text, + last_synced_at timestamptz NOT NULL DEFAULT NOW() +); + +CREATE UNIQUE INDEX ON packages (ecosystem, COALESCE(namespace, ''), name); + +CREATE INDEX ON packages (is_critical) WHERE is_critical; + +CREATE INDEX ON packages (ecosystem, name); + +CREATE INDEX ON packages USING gin (keywords); + +-- Partial index on has_critical_vulnerability TRUE rows only — that's the bucket +-- the security overlay query needs ("list all packages with a known critical +-- vuln"). The FALSE rows dominate the table and don't need an index. +CREATE INDEX ON packages (has_critical_vulnerability) +WHERE + has_critical_vulnerability; + +CREATE INDEX ON packages (criticality_score DESC) +WHERE + criticality_score IS NOT NULL; + +CREATE TABLE package_name_history ( + id bigserial PRIMARY KEY, + package_id bigint NOT NULL REFERENCES packages (id), + old_name text NOT NULL, + new_name text NOT NULL, + changed_at timestamptz NOT NULL DEFAULT NOW() +); + +CREATE INDEX ON package_name_history (package_id); + +CREATE TABLE package_funding_links ( + id bigserial PRIMARY KEY, + package_id bigint NOT NULL REFERENCES packages (id), + type TEXT, -- 'github' | 'patreon' | 'opencollective' | 'individual' | 'other' + url text NOT NULL, + UNIQUE (package_id, url) +); + +-- ============================================================ +-- VERSIONS — PARTITION BY HASH(package_id) +-- Hot query: WHERE package_id = X (all versions of a package). +-- 32 buckets → ~2.8M rows each at 90M total. +-- ============================================================ +CREATE TABLE versions ( + id bigserial, + package_id bigint NOT NULL REFERENCES packages (id), + ecosystem text NOT NULL, + number text NOT NULL, + published_at timestamptz, + -- Nullable: deps.dev PackageVersions does not expose is_latest; set by the npm/maven enricher + -- workers that have authoritative latest-version data. NULL = unknown (not yet enriched). + is_latest bool, + -- Nullable for same reason: yanked status comes from registry-specific workers, not deps.dev. + is_yanked bool, + is_prerelease bool NOT NULL DEFAULT FALSE, + -- Denormalized from packages for fast deps merge resolution. + -- Allows resolving (ecosystem, namespace, name, number) → version_id in one index lookup. + namespace text, + name text NOT NULL, + licenses text[], -- SPDX array, deterministically sorted; can differ per version + download_count bigint, -- per-version where available (npm, crates) + last_synced_at timestamptz NOT NULL DEFAULT NOW(), + PRIMARY KEY (id, package_id), + UNIQUE (package_id, number) +) +PARTITION BY HASH (package_id); + +CREATE TABLE versions_p0 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 0); + +CREATE TABLE versions_p1 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 1); + +CREATE TABLE versions_p2 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 2); + +CREATE TABLE versions_p3 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 3); + +CREATE TABLE versions_p4 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 4); + +CREATE TABLE versions_p5 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 5); + +CREATE TABLE versions_p6 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 6); + +CREATE TABLE versions_p7 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 7); + +CREATE TABLE versions_p8 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 8); + +CREATE TABLE versions_p9 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 9); + +CREATE TABLE versions_p10 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 10); + +CREATE TABLE versions_p11 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 11); + +CREATE TABLE versions_p12 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 12); + +CREATE TABLE versions_p13 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 13); + +CREATE TABLE versions_p14 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 14); + +CREATE TABLE versions_p15 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 15); + +CREATE TABLE versions_p16 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 16); + +CREATE TABLE versions_p17 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 17); + +CREATE TABLE versions_p18 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 18); + +CREATE TABLE versions_p19 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 19); + +CREATE TABLE versions_p20 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 20); + +CREATE TABLE versions_p21 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 21); + +CREATE TABLE versions_p22 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 22); + +CREATE TABLE versions_p23 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 23); + +CREATE TABLE versions_p24 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 24); + +CREATE TABLE versions_p25 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 25); + +CREATE TABLE versions_p26 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 26); + +CREATE TABLE versions_p27 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 27); + +CREATE TABLE versions_p28 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 28); + +CREATE TABLE versions_p29 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 29); + +CREATE TABLE versions_p30 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 30); + +CREATE TABLE versions_p31 PARTITION OF versions +FOR VALUES WITH (MODULUS 32, REMAINDER 31); + +CREATE INDEX ON versions (published_at DESC); + +CREATE INDEX ON versions (package_id) +WHERE + is_latest; + +CREATE INDEX ON versions (ecosystem, COALESCE(namespace, ''), name, number); + +-- ============================================================ +-- PACKAGE DEPENDENCIES — PARTITION BY HASH(depends_on_id) +-- +-- Terminology: +-- downstream = packages that depend ON a given package (its consumers) +-- upstream = packages that a given package depends ON (its suppliers) +-- +-- Security alerting hot query: "vulnerability in X — who is at risk?" +-- → WHERE depends_on_id = X (find all downstream consumers of X) +-- → lands in one partition → fast +-- 64 buckets → ~18M rows each at 1.15B total. +-- +-- Upstream query: "what does version X depend on?" +-- → WHERE version_id = X (scatters across all 64 partitions) +-- → slower by design; use the index on (version_id, depends_on_id, dependency_kind) +-- +-- package_id: no standalone FK; satisfies composite FK to +-- partitioned versions(id, package_id). +-- depends_on_id: FK to packages; also second component of +-- composite FK for resolved depends_on_version_id. +-- ============================================================ +CREATE TABLE package_dependencies ( + id bigserial, + package_id bigint NOT NULL, + version_id bigint NOT NULL, + depends_on_id bigint NOT NULL REFERENCES packages (id), + depends_on_version_id bigint, -- resolved version; NULL if unknown + version_constraint text, -- declared constraint e.g. '^1.2.3' + dependency_kind text NOT NULL, -- 'direct' | 'dev' | 'peer' + is_optional bool NOT NULL DEFAULT FALSE, + PRIMARY KEY (id, depends_on_id), + UNIQUE (version_id, depends_on_id, dependency_kind), + FOREIGN KEY (version_id, package_id) REFERENCES versions (id, package_id), + FOREIGN KEY (depends_on_version_id, depends_on_id) REFERENCES versions (id, package_id) +) +PARTITION BY HASH (depends_on_id); + +CREATE TABLE package_dependencies_p0 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 0); + +CREATE TABLE package_dependencies_p1 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 1); + +CREATE TABLE package_dependencies_p2 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 2); + +CREATE TABLE package_dependencies_p3 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 3); + +CREATE TABLE package_dependencies_p4 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 4); + +CREATE TABLE package_dependencies_p5 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 5); + +CREATE TABLE package_dependencies_p6 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 6); + +CREATE TABLE package_dependencies_p7 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 7); + +CREATE TABLE package_dependencies_p8 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 8); + +CREATE TABLE package_dependencies_p9 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 9); + +CREATE TABLE package_dependencies_p10 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 10); + +CREATE TABLE package_dependencies_p11 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 11); + +CREATE TABLE package_dependencies_p12 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 12); + +CREATE TABLE package_dependencies_p13 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 13); + +CREATE TABLE package_dependencies_p14 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 14); + +CREATE TABLE package_dependencies_p15 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 15); + +CREATE TABLE package_dependencies_p16 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 16); + +CREATE TABLE package_dependencies_p17 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 17); + +CREATE TABLE package_dependencies_p18 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 18); + +CREATE TABLE package_dependencies_p19 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 19); + +CREATE TABLE package_dependencies_p20 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 20); + +CREATE TABLE package_dependencies_p21 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 21); + +CREATE TABLE package_dependencies_p22 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 22); + +CREATE TABLE package_dependencies_p23 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 23); + +CREATE TABLE package_dependencies_p24 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 24); + +CREATE TABLE package_dependencies_p25 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 25); + +CREATE TABLE package_dependencies_p26 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 26); + +CREATE TABLE package_dependencies_p27 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 27); + +CREATE TABLE package_dependencies_p28 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 28); + +CREATE TABLE package_dependencies_p29 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 29); + +CREATE TABLE package_dependencies_p30 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 30); + +CREATE TABLE package_dependencies_p31 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 31); + +CREATE TABLE package_dependencies_p32 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 32); + +CREATE TABLE package_dependencies_p33 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 33); + +CREATE TABLE package_dependencies_p34 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 34); + +CREATE TABLE package_dependencies_p35 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 35); + +CREATE TABLE package_dependencies_p36 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 36); + +CREATE TABLE package_dependencies_p37 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 37); + +CREATE TABLE package_dependencies_p38 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 38); + +CREATE TABLE package_dependencies_p39 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 39); + +CREATE TABLE package_dependencies_p40 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 40); + +CREATE TABLE package_dependencies_p41 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 41); + +CREATE TABLE package_dependencies_p42 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 42); + +CREATE TABLE package_dependencies_p43 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 43); + +CREATE TABLE package_dependencies_p44 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 44); + +CREATE TABLE package_dependencies_p45 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 45); + +CREATE TABLE package_dependencies_p46 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 46); + +CREATE TABLE package_dependencies_p47 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 47); + +CREATE TABLE package_dependencies_p48 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 48); + +CREATE TABLE package_dependencies_p49 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 49); + +CREATE TABLE package_dependencies_p50 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 50); + +CREATE TABLE package_dependencies_p51 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 51); + +CREATE TABLE package_dependencies_p52 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 52); + +CREATE TABLE package_dependencies_p53 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 53); + +CREATE TABLE package_dependencies_p54 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 54); + +CREATE TABLE package_dependencies_p55 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 55); + +CREATE TABLE package_dependencies_p56 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 56); + +CREATE TABLE package_dependencies_p57 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 57); + +CREATE TABLE package_dependencies_p58 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 58); + +CREATE TABLE package_dependencies_p59 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 59); + +CREATE TABLE package_dependencies_p60 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 60); + +CREATE TABLE package_dependencies_p61 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 61); + +CREATE TABLE package_dependencies_p62 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 62); + +CREATE TABLE package_dependencies_p63 PARTITION OF package_dependencies +FOR VALUES WITH (MODULUS 64, REMAINDER 63); + +-- upstream: version-specific lookup within the single partition +CREATE INDEX ON package_dependencies (depends_on_id, depends_on_version_id); + +-- downstream: version_id queries scatter across 64 partitions but use this index +CREATE INDEX ON package_dependencies (version_id); + +-- ============================================================ +-- DOMAIN 3: REPOSITORIES +-- ============================================================ +CREATE TABLE repos ( + id bigserial PRIMARY KEY, + url text UNIQUE NOT NULL, + host text, -- 'github' | 'gitlab' | 'bitbucket' | 'other' + owner TEXT, + name text, + description text, + primary_language text, + topics text[], + stars int, + forks int, + watchers int, + open_issues int, + last_commit_at timestamptz, + -- Nullable: deps.dev ProjectsLatest does not expose archived/disabled/is_fork. + -- These are populated by the GitHub API enricher worker. NULL = not yet enriched. + archived bool, + disabled bool, + is_fork bool, + -- DEFAULT NOW() added: fallback when upstream source does not provide a creation timestamp. + created_at timestamptz DEFAULT NOW(), + homepage text, + -- raw_project_type/raw_project_name preserve deps.dev's original project identity (e.g. "GITLAB", + -- "github.com/owner/repo") so self-hosted GitLab instances can be detected later without backfill. + -- canonicalRepoUrl() uses these to build the canonical url; they remain queryable for debugging. + raw_project_type text, + raw_project_name text, + -- Scorecard aggregate; per-check detail in repo_scorecard_checks + scorecard_score numeric(3, 1), + scorecard_last_run_at timestamptz, + -- Nullable with no default: multiple enrichers (deps.dev, GitHub worker, Scorecard) each write + -- different columns at different times. NOT NULL DEFAULT would stamp a "synced" timestamp on + -- first insert even when most columns are still NULL, making freshness checks misleading. + last_synced_at timestamptz +); + +CREATE INDEX ON repos (host, OWNER, name); + +CREATE INDEX ON repos (stars DESC); + +CREATE INDEX ON repos (scorecard_score) +WHERE + scorecard_score IS NOT NULL; + +-- OpenSSF Scorecard per-check detail (~18 named checks) +CREATE TABLE repo_scorecard_checks ( + id bigserial PRIMARY KEY, + repo_id bigint NOT NULL REFERENCES repos (id), + check_name text NOT NULL, -- 'Binary-Artifacts' | 'Branch-Protection' | ... + score numeric(3, 1), + reason text, + UNIQUE (repo_id, check_name) +); + +-- Docker images published by a repo (one-to-many) +CREATE TABLE repo_docker ( + id bigserial PRIMARY KEY, + repo_id bigint REFERENCES repos (id), -- nullable: image may predate repo link + image_name text NOT NULL, + pulls bigint, + stars int, + last_synced_at timestamptz NOT NULL DEFAULT NOW() +); + +CREATE UNIQUE INDEX ON repo_docker (image_name); + +CREATE INDEX ON repo_docker (repo_id) +WHERE + repo_id IS NOT NULL; + +-- Package → repo provenance (monorepos publish N packages from one repo) +CREATE TABLE package_repos ( + id bigserial PRIMARY KEY, + package_id bigint NOT NULL REFERENCES packages (id), + repo_id bigint NOT NULL REFERENCES repos (id), + -- source values TBD pending alignment on data provider (e.g. deps.dev) + source text NOT NULL, -- 'declared' | 'deps_dev' | 'heuristic' | 'manual' + confidence numeric(3, 2) NOT NULL CHECK (confidence BETWEEN 0.00 AND 1.00), + verified_at timestamptz NOT NULL DEFAULT NOW(), + UNIQUE (package_id, repo_id) +); + +CREATE INDEX ON package_repos (repo_id); + +-- ============================================================ +-- DOMAIN 4: SECURITY (OSV-shaped) +-- One advisory → many affected packages → many version ranges. +-- Mirrors OSV schema: a single advisory can affect N packages +-- across different ecosystems (e.g. a vuln in a shared C lib). +-- ============================================================ +CREATE TABLE advisories ( + id bigserial PRIMARY KEY, + osv_id text UNIQUE NOT NULL, -- SourceID from deps.dev BQ (GHSA-xxx, CVE-xxx, OSV-xxx, etc.) + source text, -- 'GHSA' | 'OSV' | 'NVD' | 'NSWG' etc. (BQ: Source) + source_url text, -- upstream advisory URL (BQ: SourceURL) + aliases text[], -- CVE-XXXX, GHSA-... + severity text, -- 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' + cvss numeric(3, 1), + -- Provenance of the cvss value above. Lets downstream consumers distinguish + -- a real vendor-supplied vector from a synthesized qualitative fallback. + -- See ADR-0001 §CVSS scoring strategy. Allowed values: + -- 'osv_cvss_v3' numeric score from a CVSS_V3 vector + -- 'osv_cvss_v4' reserved; v4 numeric scoring deferred + -- 'osv_qualitative_fallback' synthesized from database_specific.severity + -- 'osv_malicious_package' MAL-* id with no CVSS vector + -- Extensible to 'ghsa' | 'nvd' as additional sources come online. + cvss_source text, + -- >= 7.0 intentional: treat HIGH + CRITICAL both as actionable + is_critical bool GENERATED ALWAYS AS (cvss >= 7.0) STORED, + summary text, + details text, + published_at timestamptz, + modified_at timestamptz -- NULL for BQ-sourced rows; tracked in-house on re-sync +); + +-- osv_id index omitted: UNIQUE constraint above already creates one. +CREATE INDEX ON advisories (is_critical) +WHERE + is_critical; + +-- Advisory → package mapping. One advisory can affect many packages. +-- package_id is NULL when the package exists in OSV but not yet in our DB. +CREATE TABLE advisory_packages ( + id bigserial PRIMARY KEY, + advisory_id bigint NOT NULL REFERENCES advisories (id), + package_id bigint REFERENCES packages (id), + ecosystem text NOT NULL, + package_name text NOT NULL, + UNIQUE (advisory_id, ecosystem, package_name) +); + +CREATE INDEX ON advisory_packages (ecosystem, package_name); + +CREATE INDEX ON advisory_packages (package_id) +WHERE + package_id IS NOT NULL; + +-- Drives the resolveMissingPackageIds catch-up UPDATE in deriveCriticalFlag: +-- the query filters WHERE package_id IS NULL and joins on (ecosystem, +-- package_name). The non-partial (ecosystem, package_name) index above is +-- usable here too (the planner just adds a Filter on package_id IS NULL), but +-- as the table grows the vast majority of rows have package_id IS NOT NULL, +-- so the non-partial scan ends up filtering out most of what it reads. This +-- partial index only contains the still-unresolved rows, keeping it tiny +-- regardless of total table size and making the daily catch-up O(unresolved) +-- instead of O(total). +CREATE INDEX ON advisory_packages (ecosystem, package_name) +WHERE + package_id IS NULL; + +-- Version ranges affected by an advisory per package. Populated by the OSV +-- ingest worker (packages_worker/src/osv) using introduced_version / +-- fixed_version / last_affected. range_raw / unaffected_raw are reserved +-- for the deps.dev BQ ingest worker (future): that worker writes the raw +-- range strings without parsing into structured boundaries. The OSV upsert +-- path only deletes rows where range_raw / unaffected_raw are both NULL, +-- so deps.dev rows are not clobbered when OSV re-syncs. +-- COALESCE prevents silent duplicates when introduced_version is NULL. +CREATE TABLE advisory_affected_ranges ( + id bigserial PRIMARY KEY, + advisory_package_id bigint NOT NULL REFERENCES advisory_packages (id), + introduced_version text, -- NULL = unknown start + fixed_version text, -- NULL = no fix yet + last_affected text, -- NULL = no known upper bound + range_raw text, -- raw AffectedVersions string from deps.dev BQ + unaffected_raw text -- raw UnaffectedVersions string from deps.dev BQ +); + +-- Full-tuple uniqueness so two ranges sharing introduced_version but differing +-- in fixed_version or last_affected (cross-distro patches, partial fixes in a +-- single advisory) both survive insertion. The narrower (advisory_package_id, +-- introduced_version) form silently collapsed those cases to one row, dropping +-- the wider range and under-reporting vulnerable windows in the derive step. +-- See ADR-0001 §`advisory_affected_ranges` uniqueness scope. +CREATE UNIQUE INDEX ON advisory_affected_ranges ( + advisory_package_id, + COALESCE(introduced_version, ''), + COALESCE(fixed_version, ''), + COALESCE(last_affected, '') +); + +-- advisory_package_id prefix lookups are served by the UNIQUE index on +-- (advisory_package_id, introduced_version, fixed_version) — no separate index needed. + +-- ============================================================ +-- MAINTAINERS +-- ============================================================ +CREATE TABLE maintainers ( + id bigserial PRIMARY KEY, + ecosystem text NOT NULL, + username text NOT NULL, + display_name text, + url text, + email_hash text, -- SHA-256; never raw email (GDPR) + github_login text, + UNIQUE (ecosystem, username) +); + +CREATE INDEX ON maintainers (github_login) +WHERE + github_login IS NOT NULL; + +CREATE TABLE package_maintainers ( + id bigserial PRIMARY KEY, + package_id bigint NOT NULL REFERENCES packages (id), + maintainer_id bigint NOT NULL REFERENCES maintainers (id), + role TEXT, -- 'author' | 'maintainer' + UNIQUE (package_id, maintainer_id) +); + +-- ============================================================ +-- DOWNLOADS +-- +-- Two tables track download volume at different tiers and granularities: +-- +-- downloads_daily (tier 2 — packages) +-- Source of truth for daily download counts. One row per package per day. +-- No denormalized rollup on the packages table — consumers SUM over this +-- table when they need a window (e.g. last 30 days). +-- +-- downloads_last_30d (tier 3 — packages_universe) +-- Rolling 30-day download timeline keyed by purl. Each row represents one +-- 30-day window (start_date..end_date). Keyed by purl so rows survive the +-- weekly truncation of packages_universe. The latest window's count is also +-- cached in packages_universe.downloads_last_30d for fast access by the +-- criticality-ranking function (no join needed). +-- +-- ============================================================ +-- DOWNLOADS DAILY (tier 2 — packages, daily granularity) +-- +-- Partitioned by month via pg_partman. pg_partman MUST be enabled in OCI +-- config before this migration runs: +-- OCI Console → Database → Configuration → Extensions → enable pg_partman +-- +-- After enabling, run the setup below (once, outside Flyway or in a +-- separate migration) to register pg_partman and create initial partitions: +-- +-- CREATE EXTENSION IF NOT EXISTS pg_partman SCHEMA partman; +-- +-- SELECT partman.create_parent( +-- p_parent_table => 'public.downloads_daily', +-- p_control => 'date', +-- p_interval => '1 month', +-- p_premake => 3 -- pre-creates 3 future monthly partitions +-- ); +-- +-- -- pg_cron job to maintain partitions (also needs pg_cron enabled in OCI): +-- SELECT cron.schedule('partman-maintain', '0 1 * * *', +-- $$CALL partman.run_maintenance_proc()$$); +-- +-- Without this setup, inserts into downloads_daily will fail with +-- "no partition found for row". The table structure below is correct; +-- only the partition management setup is deferred. +-- +-- PK includes date because Postgres requires the partition key to be +-- part of the primary key on range-partitioned tables. +-- ============================================================ +CREATE TABLE downloads_daily ( + id bigserial, + package_id bigint NOT NULL REFERENCES packages (id), + date date NOT NULL, + count bigint NOT NULL, + PRIMARY KEY (id, date), + UNIQUE (package_id, date) +) +PARTITION BY RANGE (date); + +-- ============================================================ +-- DOWNLOADS LAST 30D (tier 3 — packages_universe, rolling 30-day granularity) +-- +-- Historical timeline of rolling 30-day download counts, keyed by purl. +-- Each row captures one window: downloads from start_date to end_date (inclusive). +-- Keyed by purl (not packages_universe.id) so rows survive the weekly +-- truncation of packages_universe. The latest window is also written +-- to packages_universe.downloads_last_30d column for fast access by the ranking function. +-- +-- Writers should upsert: INSERT ... ON CONFLICT (purl, end_date) DO UPDATE SET count = EXCLUDED.count, start_date = EXCLUDED.start_date +-- PK includes end_date because Postgres requires the partition key to be +-- part of the primary key on range-partitioned tables. +-- +-- Partitioned by month via pg_partman. pg_partman MUST be enabled in OCI +-- config before this migration runs: +-- OCI Console → Database → Configuration → Extensions → enable pg_partman +-- +-- After enabling, run the setup below (once, outside Flyway or in a +-- separate migration) to register pg_partman and create initial partitions: +-- +-- CREATE EXTENSION IF NOT EXISTS pg_partman SCHEMA partman; +-- +-- SELECT partman.create_parent( +-- p_parent_table => 'public.downloads_last_30d', +-- p_control => 'end_date', +-- p_interval => '1 month', +-- p_premake => 3 -- pre-creates 3 future monthly partitions +-- ); +-- +-- -- pg_cron job to maintain partitions (also needs pg_cron enabled in OCI): +-- SELECT cron.schedule('partman-maintain-30d', '0 2 * * *', +-- $$CALL partman.run_maintenance_proc()$$); +-- +-- Without this setup, inserts into downloads_last_30d will fail with +-- "no partition found for row". The table structure below is correct; +-- only the partition management setup is deferred. +-- +-- ============================================================ +CREATE TABLE downloads_last_30d ( + id bigserial, + purl text NOT NULL, + start_date date NOT NULL, + end_date date NOT NULL, + count bigint NOT NULL, + PRIMARY KEY (id, end_date), + UNIQUE (purl, end_date) +) +PARTITION BY RANGE (end_date); + +CREATE INDEX ON downloads_last_30d (purl, end_date DESC); + +-- ============================================================ +-- AUDIT — per-purl field-change log +-- +-- Append-only log of field-level changes. Each row records the set of +-- 'table.column' tokens that were actually mutated for a given purl during one +-- worker write pass. Rows with no real changes are not written (caller skips +-- the insert when changed_fields is empty). +-- ============================================================ +CREATE TABLE audit_field_changes ( + id bigserial PRIMARY KEY, + worker text NOT NULL, + purl text NOT NULL, + logged_at timestamptz NOT NULL DEFAULT NOW(), + changed_fields text[] NOT NULL +); + +CREATE INDEX ON audit_field_changes (purl, logged_at DESC); + +CREATE INDEX ON audit_field_changes (worker, logged_at DESC); + +CREATE INDEX ON audit_field_changes USING gin (changed_fields); + +-- ============================================================ +-- CRITICALITY RANKING FUNCTION +-- ============================================================ +CREATE OR REPLACE FUNCTION rank_packages_universe( + weight_downloads numeric, + weight_dependent_repos numeric, + weight_dependent_packages numeric, + log_smoothing numeric, + critical_top_n_by_ecosystem jsonb +) +RETURNS TABLE (scored_rows int, ranked_rows int, propagated_rows int) +LANGUAGE plpgsql +AS $$ +DECLARE + n_scored int; + n_ranked int; + n_propagated int; +BEGIN + -- Step 1: recompute scores; only touch rows whose score changed. + -- log_smoothing is added before LN() to avoid LN(0) on zero-count rows + -- and to compress the gap between small and large values (e.g. LN(1)=0 + -- vs LN(2)≈0.69 gives a gentler floor than LN(0)=-∞). Typically 1.0. + -- + -- Until the npm-registry / Maven downloads enricher runs, downloads_last_30d + -- is NULL on every row. weight_downloads contributes 0 to the score; + -- ranking effectively reduces to: + -- LN(1 + dependent_repos_count) * weight_dependent_repos + -- + LN(1 + dependent_packages_count) * weight_dependent_packages + -- + -- last_rank_pass_at is set at INSERT time in rankPackagesUniverse activity (TRUNCATE + INSERT + -- before each call), so no separate full-table UPDATE needed here. + WITH new_scores AS ( + SELECT + id, + ( LN(log_smoothing + COALESCE(downloads_last_30d, 0)) * weight_downloads + + LN(log_smoothing + COALESCE(dependent_repos_count, 0)) * weight_dependent_repos + + LN(log_smoothing + COALESCE(dependent_packages_count, 0)) * weight_dependent_packages + )::numeric(10, 4) AS new_score + FROM packages_universe + ) + UPDATE packages_universe pu + SET criticality_score = ns.new_score + FROM new_scores ns + WHERE pu.id = ns.id + AND pu.criticality_score IS DISTINCT FROM ns.new_score; + + GET DIAGNOSTICS n_scored = ROW_COUNT; + + -- Step 2: rank within ecosystem; flag is_critical via JSONB lookup. + -- Only purl-having rows are ranked (null purls can't propagate to packages). + -- Tie-break by id keeps ranks deterministic across runs so IS DISTINCT FROM + -- doesn't no-op-write equal-score rows on every call. + WITH ranked AS ( + SELECT + id, + ecosystem, + ROW_NUMBER() OVER ( + PARTITION BY ecosystem + ORDER BY criticality_score DESC NULLS LAST, id + ) AS r + FROM packages_universe + WHERE purl IS NOT NULL + ), + with_flag AS ( + SELECT + id, + r, + COALESCE( + r <= (critical_top_n_by_ecosystem ->> ecosystem)::int, + FALSE + ) AS new_is_critical + FROM ranked + ) + UPDATE packages_universe pu + SET rank_in_ecosystem = wf.r, + is_critical = wf.new_is_critical + FROM with_flag wf + WHERE pu.id = wf.id + AND ( pu.rank_in_ecosystem IS DISTINCT FROM wf.r + OR pu.is_critical IS DISTINCT FROM wf.new_is_critical ); + + GET DIAGNOSTICS n_ranked = ROW_COUNT; + + -- Step 3: propagate criticality_score onto Tier-2 packages rows. + UPDATE packages p + SET criticality_score = pu.criticality_score + FROM packages_universe pu + WHERE p.purl = pu.purl + AND p.criticality_score IS DISTINCT FROM pu.criticality_score; + + GET DIAGNOSTICS n_propagated = ROW_COUNT; + + RETURN QUERY SELECT n_scored, n_ranked, n_propagated; +END; +$$; + +-- ============================================================ +-- INGEST JOB TRACKING +-- Tracks each BQ → GCS → Postgres ingest run per job_kind. +-- snapshot_at = SnapshotAt date used as watermark for incremental diff. +-- ============================================================ +CREATE TABLE osspckgs_ingest_jobs ( + id bigserial PRIMARY KEY, + job_kind text NOT NULL CHECK (job_kind IN ( + 'packages', 'versions', 'package_dependencies', + 'repos', 'package_repos', + 'advisories', 'advisory_packages', + 'dependent_counts' + )), + status text NOT NULL CHECK (status IN ( + 'pending', 'exporting', 'exported', + 'loading', 'merging', 'done', 'failed', 'cleaned' + )), + sync_mode text NOT NULL DEFAULT 'incremental' + CHECK (sync_mode IN ('full', 'incremental')), + snapshot_at date, -- committed watermark: promoted from provisional unconditionally on 'done' (including 0-row quiet windows) + provisional_snapshot_at date, -- set at job creation; promoted to snapshot_at when job reaches 'done' + gcs_prefix text, -- gs://bucket/packages/2026-05-26T00-00-00Z/ + row_count_bq bigint, + row_count_staging bigint, -- rows loaded into staging table from GCS parquet files + row_count_pg bigint, -- total rows inserted into final table(s) after merge + table_row_counts jsonb, -- per-table inserted row counts, e.g. {"packages": 5000000} + bq_bytes_billed bigint, -- totalBytesProcessed from BQ (cost metric, not GCS export size) + bq_job_id text, -- GCP BigQuery job ID (project:location.jobId) + bq_stats jsonb, -- full BQ job statistics: bytesProcessed, bytesBilled, slotMs, cacheHit, etc. + bq_cost_usd numeric(12, 8) GENERATED ALWAYS AS ( + ROUND(COALESCE(bq_bytes_billed, 0)::numeric / 1000000000000.0 * 5.0, 8) + ) STORED, -- estimated BQ cost at $5/TB on-demand pricing + export_name text, -- named export group (e.g. "cargo-may-2026") for --export-name bootstrap + error_message text, + started_at timestamptz NOT NULL DEFAULT NOW(), + finished_at timestamptz, + cleaned_at timestamptz +); + +CREATE INDEX ON osspckgs_ingest_jobs (job_kind, started_at DESC); + +CREATE INDEX ON osspckgs_ingest_jobs (status) +WHERE status NOT IN ('done', 'cleaned'); + +CREATE INDEX ON osspckgs_ingest_jobs (job_kind, snapshot_at DESC) +WHERE status = 'done'; + +CREATE INDEX ON osspckgs_ingest_jobs (bq_job_id) +WHERE bq_job_id IS NOT NULL; + +CREATE INDEX ON osspckgs_ingest_jobs (job_kind, export_name) +WHERE export_name IS NOT NULL; diff --git a/backend/src/osspckgs/migrations/V1780231200__npm_worker.sql b/backend/src/osspckgs/migrations/V1780231200__npm_worker.sql new file mode 100644 index 0000000000..ea00673874 --- /dev/null +++ b/backend/src/osspckgs/migrations/V1780231200__npm_worker.sql @@ -0,0 +1,97 @@ +-- npm worker supporting tables and partition management for download tracking. + +ALTER TABLE maintainers DROP COLUMN IF EXISTS email_hash; +ALTER TABLE maintainers ADD COLUMN IF NOT EXISTS email text; + +CREATE TABLE npm_worker_state ( + name text PRIMARY KEY, + value text NOT NULL, + updated_at timestamptz NOT NULL DEFAULT now() +); + +CREATE TABLE npm_package_state ( + purl text PRIMARY KEY, + metadata_first_scanned_at timestamptz NOT NULL DEFAULT now(), + metadata_last_run_at timestamptz, + metadata_run_result jsonb, -- { status, attempts, httpStatus?, errorKind?, message? } + daily_downloads_last_processed_at timestamptz, + daily_downloads_run_result jsonb -- { status, httpStatus?, errorKind?, message? } +); + +CREATE TABLE npm_package_universe_state ( + purl text PRIMARY KEY, + downloads_30d_last_run_at timestamptz, -- breadth watermark: latest 30d window refreshed + downloads_30d_history_backfilled_at timestamptz, -- depth watermark: NULL until full older history filled + downloads_30d_run_result jsonb -- { status, httpStatus?, errorKind?, message? } +); +CREATE INDEX ON npm_package_universe_state (downloads_30d_last_run_at); +CREATE INDEX ON npm_package_universe_state (downloads_30d_history_backfilled_at); + +-- ============================================================ +-- pg_partman setup for downloads_daily (monthly partitions) +-- ============================================================ +CREATE SCHEMA IF NOT EXISTS partman; +CREATE EXTENSION IF NOT EXISTS pg_partman SCHEMA partman; + +SELECT partman.create_parent( + p_parent_table => 'public.downloads_daily', + p_control => 'date', + p_interval => '1 month', + p_premake => 12 +); + +-- Create all historical monthly partitions (2015-01 through last month). +DO $$ +DECLARE + m date; +BEGIN + FOR m IN + SELECT d::date + FROM generate_series( + '2015-01-01'::date, + (date_trunc('month', now()) - interval '1 month')::date, + '1 month'::interval + ) AS d + LOOP + EXECUTE format( + 'CREATE TABLE IF NOT EXISTS %I PARTITION OF downloads_daily FOR VALUES FROM (%L) TO (%L)', + 'downloads_daily_p' || to_char(m, 'YYYYMMDD'), + m, + (m + interval '1 month')::date + ); + END LOOP; +END +$$; + +-- ============================================================ +-- pg_partman setup for downloads_last_30d (yearly partitions) +-- ============================================================ +SELECT partman.create_parent( + p_parent_table => 'public.downloads_last_30d', + p_control => 'end_date', + p_interval => '1 year', + p_premake => 3 +); + +-- Create all historical yearly partitions (2015 through last year). +DO $$ +DECLARE + y date; +BEGIN + FOR y IN + SELECT d::date + FROM generate_series( + '2015-01-01'::date, + (date_trunc('year', now()) - interval '1 year')::date, + '1 year'::interval + ) AS d + LOOP + EXECUTE format( + 'CREATE TABLE IF NOT EXISTS %I PARTITION OF downloads_last_30d FOR VALUES FROM (%L) TO (%L)', + 'downloads_last_30d_p' || to_char(y, 'YYYYMMDD'), + y, + (y + interval '1 year')::date + ); + END LOOP; +END +$$; diff --git a/backend/src/osspckgs/migrations/V1780394591__packages_universe_graph_signals.sql b/backend/src/osspckgs/migrations/V1780394591__packages_universe_graph_signals.sql new file mode 100644 index 0000000000..450056a18c --- /dev/null +++ b/backend/src/osspckgs/migrations/V1780394591__packages_universe_graph_signals.sql @@ -0,0 +1,100 @@ +-- Graph-derived signals for criticality scoring (ADR-0001 §Criticality scoring methodology). +-- Populated by the criticality worker; NULL until first pass. + +ALTER TABLE packages_universe + ADD COLUMN IF NOT EXISTS transitive_dependent_count bigint, + ADD COLUMN IF NOT EXISTS centrality_score numeric(10, 8); + +-- Split dependent_packages_count into direct (MinimumDepth=1) vs transitive (MinimumDepth>1). + +ALTER TABLE packages + RENAME COLUMN dependent_packages_count TO dependent_count; + +ALTER TABLE packages + ADD COLUMN IF NOT EXISTS transitive_dependent_count bigint; + +ALTER TABLE packages_universe + RENAME COLUMN dependent_packages_count TO dependent_count; + +-- The renamed column holds old all-dependent counts (direct + transitive combined). +-- NULL them out so criticality scoring uses 0 for this signal until dependent_counts +-- is re-ingested with the split values; inflated scores are worse than missing ones. +UPDATE packages SET dependent_count = NULL; +UPDATE packages_universe SET dependent_count = NULL; + +-- rank_packages_universe() stores its body as text — renaming the column above does NOT +-- update the function. Replace it here so it references dependent_count, not the old name. +CREATE OR REPLACE FUNCTION rank_packages_universe( + weight_downloads numeric, + weight_dependent_repos numeric, + weight_dependent_packages numeric, + log_smoothing numeric, + critical_top_n_by_ecosystem jsonb +) +RETURNS TABLE (scored_rows int, ranked_rows int, propagated_rows int) +LANGUAGE plpgsql +AS $$ +DECLARE + n_scored int; + n_ranked int; + n_propagated int; +BEGIN + WITH new_scores AS ( + SELECT + id, + ( LN(log_smoothing + COALESCE(downloads_last_30d, 0)) * weight_downloads + + LN(log_smoothing + COALESCE(dependent_repos_count, 0)) * weight_dependent_repos + + LN(log_smoothing + COALESCE(dependent_count, 0)) * weight_dependent_packages + )::numeric(10, 4) AS new_score + FROM packages_universe + ) + UPDATE packages_universe pu + SET criticality_score = ns.new_score + FROM new_scores ns + WHERE pu.id = ns.id + AND pu.criticality_score IS DISTINCT FROM ns.new_score; + + GET DIAGNOSTICS n_scored = ROW_COUNT; + + WITH ranked AS ( + SELECT + id, + ecosystem, + ROW_NUMBER() OVER ( + PARTITION BY ecosystem + ORDER BY criticality_score DESC NULLS LAST, id + ) AS r + FROM packages_universe + WHERE purl IS NOT NULL + ), + with_flag AS ( + SELECT + id, + r, + COALESCE( + r <= (critical_top_n_by_ecosystem ->> ecosystem)::int, + FALSE + ) AS new_is_critical + FROM ranked + ) + UPDATE packages_universe pu + SET rank_in_ecosystem = wf.r, + is_critical = wf.new_is_critical + FROM with_flag wf + WHERE pu.id = wf.id + AND ( pu.rank_in_ecosystem IS DISTINCT FROM wf.r + OR pu.is_critical IS DISTINCT FROM wf.new_is_critical ); + + GET DIAGNOSTICS n_ranked = ROW_COUNT; + + UPDATE packages p + SET criticality_score = pu.criticality_score + FROM packages_universe pu + WHERE p.purl = pu.purl + AND p.criticality_score IS DISTINCT FROM pu.criticality_score; + + GET DIAGNOSTICS n_propagated = ROW_COUNT; + + RETURN QUERY SELECT n_scored, n_ranked, n_propagated; +END; +$$; diff --git a/backend/src/osspckgs/migrations/V1780496100__add_skip_enrichment_to_repos.sql b/backend/src/osspckgs/migrations/V1780496100__add_skip_enrichment_to_repos.sql new file mode 100644 index 0000000000..6282525aed --- /dev/null +++ b/backend/src/osspckgs/migrations/V1780496100__add_skip_enrichment_to_repos.sql @@ -0,0 +1 @@ +ALTER TABLE repos ADD COLUMN IF NOT EXISTS skip_enrichment boolean NOT NULL DEFAULT false; diff --git a/backend/src/osspckgs/migrations/V1780589351__package_criticality_spotlight.sql b/backend/src/osspckgs/migrations/V1780589351__package_criticality_spotlight.sql new file mode 100644 index 0000000000..fd83d47261 --- /dev/null +++ b/backend/src/osspckgs/migrations/V1780589351__package_criticality_spotlight.sql @@ -0,0 +1,24 @@ +-- Manual override table for criticality scoring (ADR-0001 §Spotlight overrides). +-- Packages listed here are forced is_critical = TRUE regardless of computed score. +-- Applied after ranking inside rank_packages_universe() so overrides survive +-- every automated re-rank pass. +-- +-- rationale, added_by, added_at are required — the table must stay auditable. +-- namespace is nullable: cargo crates have no namespace, Maven artifacts do. +-- The UNIQUE key uses COALESCE so (ecosystem, NULL namespace, name) is enforced correctly. + +CREATE TABLE package_criticality_spotlight ( + id bigserial PRIMARY KEY, + ecosystem text NOT NULL, + namespace text, + name text NOT NULL, + rationale text NOT NULL, + added_by text NOT NULL, + added_at timestamptz NOT NULL DEFAULT NOW() +); + +-- Functional unique index: COALESCE treats NULL namespace as '' so that +-- (cargo, NULL, tokio) and (cargo, NULL, serde) are unique but a duplicate +-- (cargo, NULL, tokio) entry is rejected. +CREATE UNIQUE INDEX ON package_criticality_spotlight + (ecosystem, COALESCE(namespace, ''), name); diff --git a/backend/src/osspckgs/migrations/V1780589607__rank_packages_universe_v2.sql b/backend/src/osspckgs/migrations/V1780589607__rank_packages_universe_v2.sql new file mode 100644 index 0000000000..64d3277db7 --- /dev/null +++ b/backend/src/osspckgs/migrations/V1780589607__rank_packages_universe_v2.sql @@ -0,0 +1,132 @@ +-- Renames criticality_score → impact on both packages_universe and packages, +-- and installs rank_packages_universe() with the updated formula. +-- +-- Formula (ADR-0001 §Criticality scoring methodology): +-- impact = w_downloads * pct_rank( LOG(1 + downloads_last_30d) ) within ecosystem +-- + w_dep_pkgs * pct_rank( LOG(1 + dependent_count) ) within ecosystem +-- + w_transitive * pct_rank( LOG(1 + transitive_dependent_count) ) within ecosystem +-- +-- Default weights: 0.25 / 0.25 / 0.50 (sum to 1.0). +-- All weights and the top-N budget are call-time parameters — tunable without +-- schema or code changes. +-- +-- Steps inside the function: +-- 1. Score — compute impact via weighted PERCENT_RANK() +-- 2. Rank — ROW_NUMBER() per ecosystem, flag top-N as is_critical +-- 2.5 Spotlight — force is_critical = TRUE for rows in package_criticality_spotlight +-- 3. Propagate — copy impact + is_critical onto the packages table + +ALTER TABLE packages_universe + RENAME COLUMN criticality_score TO impact; + +ALTER TABLE packages + RENAME COLUMN criticality_score TO impact; + +CREATE OR REPLACE FUNCTION rank_packages_universe( + weight_downloads numeric DEFAULT 0.25, + weight_dependent_packages numeric DEFAULT 0.25, + weight_transitive numeric DEFAULT 0.50, + critical_top_n_by_ecosystem jsonb DEFAULT '{ + "npm": 210000, + "pypi": 140000, + "maven": 120000, + "nuget": 70000, + "packagist": 56000, + "go": 42000, + "cargo": 28000, + "rubygems": 21000, + "docker": 13000 + }'::jsonb +) +RETURNS TABLE(scored_rows int, ranked_rows int, propagated_rows int) +LANGUAGE plpgsql AS $$ +DECLARE + n_scored int; + n_ranked int; + n_propagated int; +BEGIN + -- ── Step 1: score ────────────────────────────────────────────────────────── + -- last_rank_pass_at updated unconditionally on every pass (schema requirement). + WITH percentile_scores AS ( + SELECT + id, + ( + weight_downloads * PERCENT_RANK() OVER ( + PARTITION BY ecosystem ORDER BY LOG(1 + COALESCE(downloads_last_30d, 0))) + + + weight_dependent_packages * PERCENT_RANK() OVER ( + PARTITION BY ecosystem ORDER BY LOG(1 + COALESCE(dependent_count, 0))) + + + weight_transitive * PERCENT_RANK() OVER ( + PARTITION BY ecosystem ORDER BY LOG(1 + COALESCE(transitive_dependent_count, 0))) + )::numeric(10, 4) AS new_impact + FROM packages_universe + ) + UPDATE packages_universe pu + SET impact = ps.new_impact, + last_rank_pass_at = NOW() + FROM percentile_scores ps + WHERE pu.id = ps.id; + + GET DIAGNOSTICS n_scored = ROW_COUNT; + + -- ── Step 2: rank + flag ──────────────────────────────────────────────────── + WITH ranked AS ( + SELECT + id, ecosystem, + ROW_NUMBER() OVER ( + PARTITION BY ecosystem + ORDER BY impact DESC NULLS LAST, id + ) AS r + FROM packages_universe + WHERE purl IS NOT NULL + ), + flagged AS ( + SELECT + id, r, + COALESCE( + r <= (critical_top_n_by_ecosystem ->> ecosystem)::int, + FALSE + ) AS new_is_critical + FROM ranked + ) + UPDATE packages_universe pu + SET rank_in_ecosystem = f.r, + is_critical = f.new_is_critical + FROM flagged f + WHERE pu.id = f.id + AND ( + pu.rank_in_ecosystem IS DISTINCT FROM f.r + OR pu.is_critical IS DISTINCT FROM f.new_is_critical + ); + + GET DIAGNOSTICS n_ranked = ROW_COUNT; + + -- ── Step 2.5: apply spotlight overrides ─────────────────────────────────── + -- Force is_critical = TRUE for any row in package_criticality_spotlight, + -- regardless of computed score or rank. Runs after Step 2 so overrides + -- survive every automated re-rank pass. + -- IS NOT DISTINCT FROM handles the NULL namespace case (e.g. cargo crates). + UPDATE packages_universe pu + SET is_critical = TRUE + FROM package_criticality_spotlight s + WHERE pu.ecosystem = s.ecosystem + AND (pu.namespace IS NOT DISTINCT FROM s.namespace) + AND pu.name = s.name + AND pu.is_critical = FALSE; + + -- ── Step 3: propagate to packages ───────────────────────────────────────── + -- last_rank_pass_at updated unconditionally on every pass (schema requirement). + UPDATE packages p + SET impact = pu.impact, + is_critical = pu.is_critical, + last_rank_pass_at = NOW() + FROM packages_universe pu + WHERE p.purl = pu.purl + AND p.ecosystem = pu.ecosystem; + + GET DIAGNOSTICS n_propagated = ROW_COUNT; + + RETURN QUERY SELECT n_scored, n_ranked, n_propagated; +END; +$$; diff --git a/backend/src/osspckgs/migrations/V1780600000__add_created_updated_at.sql b/backend/src/osspckgs/migrations/V1780600000__add_created_updated_at.sql new file mode 100644 index 0000000000..68c10700d7 --- /dev/null +++ b/backend/src/osspckgs/migrations/V1780600000__add_created_updated_at.sql @@ -0,0 +1,79 @@ +-- Add created_at / updated_at to all packages-db tables for Tinybird sync watermarking. +-- +-- Excluded tables and why: +-- package_name_history — event log; has changed_at +-- audit_field_changes — audit log; has logged_at +-- osspckgs_ingest_jobs — job tracking; has started_at / finished_at +-- npm_worker_state — state machine; has updated_at +-- npm_package_state — watermark columns serve the same purpose +-- npm_package_universe_state — watermark columns serve the same purpose +-- package_criticality_spotlight — override config; has added_at +-- +-- repos: created_at already exists (stores the GitHub repository creation date). +-- last_synced_at is nullable (not set until the GitHub enricher runs), so updated_at +-- is added as a non-nullable watermark for Tinybird sync. last_synced_at is kept as +-- a separate enrichment-freshness signal. +-- +-- packages, versions, repo_docker: last_synced_at already serves as updated_at. +-- Only created_at is added to avoid duplicate semantics. +-- +-- package_repos: verified_at already serves as updated_at. Only created_at is added. +-- +-- Partitioned tables (versions, package_dependencies, downloads_daily, downloads_last_30d): +-- PostgreSQL 12+ propagates new columns to all child partitions automatically — +-- no per-partition ALTER needed. + +ALTER TABLE repos + ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT NOW(); + +ALTER TABLE packages + ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT NOW(); + +ALTER TABLE package_funding_links + ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT NOW(); + +ALTER TABLE versions + ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT NOW(); + +ALTER TABLE package_dependencies + ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT NOW(); + +ALTER TABLE repo_scorecard_checks + ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT NOW(); + +ALTER TABLE repo_docker + ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT NOW(); + +ALTER TABLE package_repos + ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT NOW(); + +ALTER TABLE advisories + ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT NOW(); + +ALTER TABLE advisory_packages + ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT NOW(); + +ALTER TABLE advisory_affected_ranges + ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT NOW(); + +ALTER TABLE maintainers + ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT NOW(); + +ALTER TABLE package_maintainers + ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT NOW(); + +ALTER TABLE downloads_daily + ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT NOW(); + +ALTER TABLE downloads_last_30d + ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT NOW(), + ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT NOW(); diff --git a/backend/src/osspckgs/migrations/V1780926783__DropPackagesUniverse.sql b/backend/src/osspckgs/migrations/V1780926783__DropPackagesUniverse.sql new file mode 100644 index 0000000000..f877c83ddd --- /dev/null +++ b/backend/src/osspckgs/migrations/V1780926783__DropPackagesUniverse.sql @@ -0,0 +1,170 @@ +-- Retire packages_universe (Tier 3 workspace). All signals are migrated onto +-- packages, rank_packages_universe() is replaced by rank_packages() which operates on packages directly, +-- and the table is dropped. +-- +-- Columns migrated from packages_universe → packages: +-- downloads_last_30d bigint (npm 30-day window cache for ranking) +-- centrality_score numeric(10,8) (PageRank, stored for future formula use) +-- rank_in_ecosystem int (computed by rank_packages) +-- +-- rank_packages_universe() → rank_packages() changes: +-- - Operates on packages directly; no more TRUNCATE/INSERT workspace. +-- - Scope limited to ecosystems present in critical_top_n_by_ecosystem JSONB +-- (dynamic — add an ecosystem to the JSONB to include it in ranking). +-- - "Propagate to packages" step removed (packages IS the target now). +-- - Return column propagated_rows removed (no propagation step). + +-- ── 1. Add missing columns to packages ──────────────────────────────────────── + +ALTER TABLE packages + ADD COLUMN IF NOT EXISTS downloads_last_30d bigint, + ADD COLUMN IF NOT EXISTS centrality_score numeric(10, 8), + ADD COLUMN IF NOT EXISTS rank_in_ecosystem int; + +-- ── 2. Back-fill from packages_universe ─────────────────────────────────────── + +UPDATE packages p + SET downloads_last_30d = pu.downloads_last_30d, + centrality_score = pu.centrality_score, + rank_in_ecosystem = pu.rank_in_ecosystem + FROM packages_universe pu + WHERE p.purl = pu.purl + AND ( + p.downloads_last_30d IS DISTINCT FROM pu.downloads_last_30d + OR p.centrality_score IS DISTINCT FROM pu.centrality_score + OR p.rank_in_ecosystem IS DISTINCT FROM pu.rank_in_ecosystem + ); + +-- ── 3. Replace rank_packages_universe() ─────────────────────────────────────── +-- Two overloads exist in the schema: +-- V1779710880 created (numeric, numeric, numeric, numeric, jsonb) — 5 params +-- V1780589607 CREATE OR REPLACE'd with (numeric, numeric, numeric, jsonb) — different +-- signature, so it added a second overload rather than replacing the first. +-- Both must be dropped; only one new 4-param version is created. + +DROP FUNCTION IF EXISTS rank_packages_universe(numeric, numeric, numeric, numeric, jsonb); +DROP FUNCTION IF EXISTS rank_packages_universe(numeric, numeric, numeric, jsonb); + +-- Usage: +-- -- with defaults (weights 0.25/0.25/0.50, built-in top-N budget) +-- SELECT * FROM rank_packages(); +-- +-- -- with custom weights and/or a different top-N budget +-- SELECT * FROM rank_packages( +-- 0.20, 0.30, 0.50, +-- '{"npm": 400000, "maven": 200000, "cargo": 75000}'::jsonb +-- ); + +-- rank_packages() — score, rank, and flag packages in one pass. +-- +-- Formula: +-- impact = w_downloads * pct_rank( LOG(1 + downloads_last_30d) ) within ecosystem +-- + w_dep_pkgs * pct_rank( LOG(1 + dependent_count) ) within ecosystem +-- + w_transitive * pct_rank( LOG(1 + transitive_dependent_count) ) within ecosystem +-- +-- Steps: +-- 1. Score — compute impact via weighted PERCENT_RANK() (scoped to JSONB ecosystems) +-- 2. Rank — ROW_NUMBER() per ecosystem, flag top-N as is_critical (scoped to JSONB ecosystems) +-- 2.5 Spotlight — force is_critical = TRUE for rows in package_criticality_spotlight +-- 3. Stamp — unconditionally set last_rank_pass_at on all scored rows (schema contract) +-- +-- All weights and the top-N budget are call-time parameters. +-- ROW_NUMBER() (not RANK()) keeps each ecosystem's critical set exactly at top-N. +-- Only ecosystems present as keys in critical_top_n_by_ecosystem are scored/ranked; +-- packages from other ecosystems are not touched. + +CREATE OR REPLACE FUNCTION rank_packages( + weight_downloads numeric DEFAULT 0.25, + weight_dependent_packages numeric DEFAULT 0.25, + weight_transitive numeric DEFAULT 0.50, + critical_top_n_by_ecosystem jsonb DEFAULT '{"npm":400000,"go":100000,"maven":200000,"pypi":100000,"nuget":50000,"cargo":75000}'::jsonb +) +RETURNS TABLE(scored_rows int, ranked_rows int) +LANGUAGE plpgsql AS $$ +DECLARE + n_scored int; + n_ranked int; +BEGIN + -- ── Step 1: score ────────────────────────────────────────────────────────── + WITH percentile_scores AS ( + SELECT + id, + ( + weight_downloads * PERCENT_RANK() OVER ( + PARTITION BY ecosystem ORDER BY LOG(1 + COALESCE(downloads_last_30d, 0))) + + + weight_dependent_packages * PERCENT_RANK() OVER ( + PARTITION BY ecosystem ORDER BY LOG(1 + COALESCE(dependent_count, 0))) + + + weight_transitive * PERCENT_RANK() OVER ( + PARTITION BY ecosystem ORDER BY LOG(1 + COALESCE(transitive_dependent_count, 0))) + )::numeric(10, 4) AS new_impact + FROM packages + WHERE ecosystem IN (SELECT jsonb_object_keys(critical_top_n_by_ecosystem)) + ) + UPDATE packages p + SET impact = ps.new_impact + FROM percentile_scores ps + WHERE p.id = ps.id + AND p.impact IS DISTINCT FROM ps.new_impact; + + GET DIAGNOSTICS n_scored = ROW_COUNT; + + -- ── Step 2: rank + flag ──────────────────────────────────────────────────── + WITH ranked AS ( + SELECT + id, ecosystem, + ROW_NUMBER() OVER ( + PARTITION BY ecosystem + ORDER BY impact DESC NULLS LAST, id + ) AS r + FROM packages + WHERE purl IS NOT NULL + AND ecosystem IN (SELECT jsonb_object_keys(critical_top_n_by_ecosystem)) + ), + flagged AS ( + SELECT + id, r, + COALESCE( + r <= (critical_top_n_by_ecosystem ->> ecosystem)::int, + FALSE + ) AS new_is_critical + FROM ranked + ) + UPDATE packages p + SET rank_in_ecosystem = f.r, + is_critical = f.new_is_critical + FROM flagged f + WHERE p.id = f.id + AND ( + p.rank_in_ecosystem IS DISTINCT FROM f.r + OR p.is_critical IS DISTINCT FROM f.new_is_critical + ); + + GET DIAGNOSTICS n_ranked = ROW_COUNT; + + -- ── Step 2.5: spotlight overrides ───────────────────────────────────────── + UPDATE packages p + SET is_critical = TRUE + FROM package_criticality_spotlight s + WHERE p.ecosystem = s.ecosystem + AND (p.namespace IS NOT DISTINCT FROM s.namespace) + AND p.name = s.name + AND p.is_critical = FALSE; + + -- ── Step 3: stamp last_rank_pass_at unconditionally ─────────────────────── + -- Schema contract: must be updated on every pass (not only when scores change) + -- so stale-detection queries (last_rank_pass_at < NOW() - INTERVAL '8 days') work. + UPDATE packages + SET last_rank_pass_at = NOW() + WHERE ecosystem IN (SELECT jsonb_object_keys(critical_top_n_by_ecosystem)); + + RETURN QUERY SELECT n_scored, n_ranked; +END; +$$; + +-- ── 4. Drop packages_universe ───────────────────────────────────────────────── +-- No FK constraints reference this table (npm_package_universe_state and +-- downloads_last_30d use purl text, not FK). + +DROP TABLE packages_universe; diff --git a/backend/src/osspckgs/migrations/V1780928852__dockerhub_sync.sql b/backend/src/osspckgs/migrations/V1780928852__dockerhub_sync.sql new file mode 100644 index 0000000000..d17ecd7fc8 --- /dev/null +++ b/backend/src/osspckgs/migrations/V1780928852__dockerhub_sync.sql @@ -0,0 +1,61 @@ +-- dockerhub-sync (CM-1213) +-- +-- Adds discovery/refresh bookkeeping for the dockerhub-sync worker +-- (services/apps/packages_worker/src/dockerhub) and a daily snapshot table +-- for Docker Hub lifetime pull counts. + +-- Last time dockerhub-sync probed this repo for a published Docker image +-- (Dockerfile detection + Hub candidate lookup). NULL = never checked. +-- Separate from repos.last_synced_at because discovery cadence (weeks) +-- differs from light-metadata refresh cadence (daily). +ALTER TABLE repos + ADD COLUMN IF NOT EXISTS docker_checked_at timestamptz; + +-- Partial index for the discovery backlog query: pages repos that have never +-- been probed for a Docker image. Once docker_checked_at is set the row drops +-- out of the index, so this stays small even as the repos table grows. +CREATE INDEX IF NOT EXISTS repos_docker_pending_idx ON repos (id) +WHERE + host = 'github' AND docker_checked_at IS NULL; + +-- Supports the refresh query (WHERE last_synced_at < NOW() - interval). +CREATE INDEX IF NOT EXISTS repo_docker_stale_idx ON repo_docker (last_synced_at); + +-- ============================================================ +-- REPO DOCKER PULLS DAILY +-- One row per image per day storing the *lifetime* pull_count as returned +-- by hub.docker.com/v2/repositories/. Docker Hub does not expose +-- per-day download counts, so daily deltas are derived at query time: +-- pulls_total - LAG(pulls_total) OVER (PARTITION BY image_name ORDER BY date) +-- Keyed by image_name (matches repo_docker UNIQUE) so rows survive a +-- repo_docker re-discovery without an FK cascade. +-- +-- Partitioned monthly via pg_partman (extension + schema already created in +-- V1780231200__npm_worker.sql). +-- ============================================================ +CREATE TABLE IF NOT EXISTS repo_docker_pulls_daily ( + image_name text NOT NULL, + date date NOT NULL, + pulls_total bigint NOT NULL, + PRIMARY KEY (image_name, date) +) +PARTITION BY RANGE (date); + +-- Guard so this migration is idempotent against environments where the +-- table was already registered manually (e.g. local dev that applied the +-- earlier in-place schema edit). +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM partman.part_config + WHERE parent_table = 'public.repo_docker_pulls_daily' + ) THEN + PERFORM partman.create_parent( + p_parent_table => 'public.repo_docker_pulls_daily', + p_control => 'date', + p_interval => '1 month', + p_premake => 3 + ); + END IF; +END +$$; diff --git a/backend/src/osspckgs/migrations/V1780996561__repo_activity_snapshot.sql b/backend/src/osspckgs/migrations/V1780996561__repo_activity_snapshot.sql new file mode 100644 index 0000000000..96549d7f2d --- /dev/null +++ b/backend/src/osspckgs/migrations/V1780996561__repo_activity_snapshot.sql @@ -0,0 +1,30 @@ +ALTER TABLE repos ADD COLUMN IF NOT EXISTS security_policy_enabled boolean; +ALTER TABLE repos ADD COLUMN IF NOT EXISTS security_file_enabled boolean; +ALTER TABLE repos ADD COLUMN IF NOT EXISTS snapshot_at timestamptz; + +CREATE TABLE IF NOT EXISTS repo_activity_snapshot ( + repo_id bigint PRIMARY KEY REFERENCES repos(id) ON DELETE CASCADE, + snapshot_at timestamptz NOT NULL, + window_months int NOT NULL DEFAULT 12, + -- commit activity + commits_last_12m int, + commits_last_6m int, + commits_prior_6m int, + -- PR health + prs_opened_last_12m int, + prs_merged_last_12m int, + prs_closed_unmerged_12m int, + pr_median_time_to_merge_hours int, + pr_median_time_to_first_response_hours int, + -- issue health + issues_opened_last_12m int, + issues_closed_last_12m int, + issues_opened_last_6m int, + issues_opened_prior_6m int, + issues_open_now int, + issue_median_time_to_close_hours int, + issue_median_time_to_first_response_hours int +); + +CREATE INDEX IF NOT EXISTS repo_activity_snapshot_snapshot_at_idx + ON repo_activity_snapshot (snapshot_at); diff --git a/backend/src/osspckgs/migrations/V1781009234__sequin_publication_and_rank_packages_sync.sql b/backend/src/osspckgs/migrations/V1781009234__sequin_publication_and_rank_packages_sync.sql new file mode 100644 index 0000000000..c6bb0c0c91 --- /dev/null +++ b/backend/src/osspckgs/migrations/V1781009234__sequin_publication_and_rank_packages_sync.sql @@ -0,0 +1,165 @@ +-- Wire packages-db into the Sequin → Kafka → Tinybird pipeline. +-- +-- Two related changes bundled here because both serve the same goal — making +-- packages-db row changes replicate cleanly into Tinybird: +-- +-- 1. Publication + REPLICA IDENTITY FULL on the 11 tables the Tinybird +-- datasources read from. publish_via_partition_root collapses the +-- versions (32) / package_dependencies (64) partition leaves into a +-- single logical topic each. REPLICA IDENTITY on a partitioned root +-- does not cascade, so every leaf is set explicitly via pg_inherits. +-- +-- 2. rank_packages() bumps last_synced_at on every UPDATE that touches a +-- DS-exported field (impact, is_critical, last_rank_pass_at). +-- last_synced_at is the Tinybird ENGINE_VER for the packages datasource; +-- without this bump, ReplacingMergeTree may keep an older row when +-- criticality changes without any other write path touching the row. + +-- ─── 1. Sequin publication ────────────────────────────────────────────────── + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_publication WHERE pubname = 'sequin_pub' + ) THEN + CREATE PUBLICATION sequin_pub + FOR TABLE + packages, + versions, + package_dependencies, + package_maintainers, + package_repos, + maintainers, + repos, + repo_scorecard_checks, + advisories, + advisory_packages, + advisory_affected_ranges + WITH (publish_via_partition_root = true); + END IF; +END$$; + +ALTER TABLE public.packages REPLICA IDENTITY FULL; +ALTER TABLE public.versions REPLICA IDENTITY FULL; +ALTER TABLE public.package_dependencies REPLICA IDENTITY FULL; +ALTER TABLE public.package_maintainers REPLICA IDENTITY FULL; +ALTER TABLE public.package_repos REPLICA IDENTITY FULL; +ALTER TABLE public.maintainers REPLICA IDENTITY FULL; +ALTER TABLE public.repos REPLICA IDENTITY FULL; +ALTER TABLE public.repo_scorecard_checks REPLICA IDENTITY FULL; +ALTER TABLE public.advisories REPLICA IDENTITY FULL; +ALTER TABLE public.advisory_packages REPLICA IDENTITY FULL; +ALTER TABLE public.advisory_affected_ranges REPLICA IDENTITY FULL; + +-- versions (32) and package_dependencies (64) are hash-partitioned. REPLICA +-- IDENTITY on the partitioned root does not cascade; set it on every leaf. +DO $$ +DECLARE + parent_table text; + partition_oid regclass; +BEGIN + FOREACH parent_table IN ARRAY ARRAY['public.versions', 'public.package_dependencies'] + LOOP + FOR partition_oid IN + SELECT inhrelid::regclass + FROM pg_inherits + WHERE inhparent = parent_table::regclass + LOOP + EXECUTE format('ALTER TABLE %s REPLICA IDENTITY FULL', partition_oid); + END LOOP; + END LOOP; +END$$; + +-- ─── 2. rank_packages() bumps last_synced_at ──────────────────────────────── + +CREATE OR REPLACE FUNCTION rank_packages( + weight_downloads numeric DEFAULT 0.25, + weight_dependent_packages numeric DEFAULT 0.25, + weight_transitive numeric DEFAULT 0.50, + critical_top_n_by_ecosystem jsonb DEFAULT '{"npm":400000,"go":100000,"maven":200000,"pypi":100000,"nuget":50000,"cargo":75000}'::jsonb +) +RETURNS TABLE(scored_rows int, ranked_rows int) +LANGUAGE plpgsql AS $$ +DECLARE + n_scored int; + n_ranked int; +BEGIN + -- Step 1: score + WITH percentile_scores AS ( + SELECT + id, + ( + weight_downloads * PERCENT_RANK() OVER ( + PARTITION BY ecosystem ORDER BY LOG(1 + COALESCE(downloads_last_30d, 0))) + + + weight_dependent_packages * PERCENT_RANK() OVER ( + PARTITION BY ecosystem ORDER BY LOG(1 + COALESCE(dependent_count, 0))) + + + weight_transitive * PERCENT_RANK() OVER ( + PARTITION BY ecosystem ORDER BY LOG(1 + COALESCE(transitive_dependent_count, 0))) + )::numeric(10, 4) AS new_impact + FROM packages + WHERE ecosystem IN (SELECT jsonb_object_keys(critical_top_n_by_ecosystem)) + ) + UPDATE packages p + SET impact = ps.new_impact, + last_synced_at = NOW() + FROM percentile_scores ps + WHERE p.id = ps.id + AND p.impact IS DISTINCT FROM ps.new_impact; + + GET DIAGNOSTICS n_scored = ROW_COUNT; + + -- Step 2: rank + flag + WITH ranked AS ( + SELECT + id, ecosystem, + ROW_NUMBER() OVER ( + PARTITION BY ecosystem + ORDER BY impact DESC NULLS LAST, id + ) AS r + FROM packages + WHERE purl IS NOT NULL + AND ecosystem IN (SELECT jsonb_object_keys(critical_top_n_by_ecosystem)) + ), + flagged AS ( + SELECT + id, r, + COALESCE( + r <= (critical_top_n_by_ecosystem ->> ecosystem)::int, + FALSE + ) AS new_is_critical + FROM ranked + ) + UPDATE packages p + SET rank_in_ecosystem = f.r, + is_critical = f.new_is_critical, + last_synced_at = NOW() + FROM flagged f + WHERE p.id = f.id + AND ( + p.rank_in_ecosystem IS DISTINCT FROM f.r + OR p.is_critical IS DISTINCT FROM f.new_is_critical + ); + + GET DIAGNOSTICS n_ranked = ROW_COUNT; + + -- Step 2.5: spotlight overrides + UPDATE packages p + SET is_critical = TRUE, + last_synced_at = NOW() + FROM package_criticality_spotlight s + WHERE p.ecosystem = s.ecosystem + AND (p.namespace IS NOT DISTINCT FROM s.namespace) + AND p.name = s.name + AND p.is_critical = FALSE; + + -- Step 3: stamp last_rank_pass_at unconditionally + UPDATE packages + SET last_rank_pass_at = NOW(), + last_synced_at = NOW() + WHERE ecosystem IN (SELECT jsonb_object_keys(critical_top_n_by_ecosystem)); + + RETURN QUERY SELECT n_scored, n_ranked; +END; +$$; diff --git a/backend/src/osspckgs/migrations/V1781020800__merge_30d_into_npm_package_state.sql b/backend/src/osspckgs/migrations/V1781020800__merge_30d_into_npm_package_state.sql new file mode 100644 index 0000000000..9f65fa2599 --- /dev/null +++ b/backend/src/osspckgs/migrations/V1781020800__merge_30d_into_npm_package_state.sql @@ -0,0 +1,25 @@ +-- ── 1. Add the 30d watermark columns to npm_package_state ────────────────────── +ALTER TABLE npm_package_state + ADD COLUMN IF NOT EXISTS downloads_30d_last_run_at timestamptz, -- breadth watermark: latest 30d window refreshed + ADD COLUMN IF NOT EXISTS downloads_30d_history_backfilled_at timestamptz, -- depth watermark: NULL until full older history filled + ADD COLUMN IF NOT EXISTS downloads_30d_run_result jsonb; -- { status, httpStatus?, errorKind?, message? } + +-- Recreate the two indexes the old table had — both due-selection queries +-- filter/order on these columns. +CREATE INDEX IF NOT EXISTS npm_package_state_downloads_30d_last_run_at_idx + ON npm_package_state (downloads_30d_last_run_at); +CREATE INDEX IF NOT EXISTS npm_package_state_downloads_30d_history_backfilled_at_idx + ON npm_package_state (downloads_30d_history_backfilled_at); + +-- ── 2. Migrate existing rows ─────────────────────────────────────────────────── +INSERT INTO npm_package_state + (purl, downloads_30d_last_run_at, downloads_30d_history_backfilled_at, downloads_30d_run_result) +SELECT purl, downloads_30d_last_run_at, downloads_30d_history_backfilled_at, downloads_30d_run_result + FROM npm_package_universe_state +ON CONFLICT (purl) DO UPDATE SET + downloads_30d_last_run_at = EXCLUDED.downloads_30d_last_run_at, + downloads_30d_history_backfilled_at = EXCLUDED.downloads_30d_history_backfilled_at, + downloads_30d_run_result = EXCLUDED.downloads_30d_run_result; + +-- ── 3. Drop the retired table ────────────────────────────────────────────────── +DROP TABLE npm_package_universe_state; diff --git a/backend/src/osspckgs/migrations/V1781074345__add-scorecard-job-kinds.sql b/backend/src/osspckgs/migrations/V1781074345__add-scorecard-job-kinds.sql new file mode 100644 index 0000000000..d77a13722b --- /dev/null +++ b/backend/src/osspckgs/migrations/V1781074345__add-scorecard-job-kinds.sql @@ -0,0 +1,11 @@ +-- Extend osspckgs_ingest_jobs.job_kind CHECK constraint to include scorecard kinds. +-- Required for ingestScorecard workflow (CM-1227). +ALTER TABLE osspckgs_ingest_jobs + DROP CONSTRAINT osspckgs_ingest_jobs_job_kind_check, + ADD CONSTRAINT osspckgs_ingest_jobs_job_kind_check CHECK (job_kind IN ( + 'packages', 'versions', 'package_dependencies', + 'repos', 'package_repos', + 'advisories', 'advisory_packages', + 'dependent_counts', + 'scorecard_repos', 'scorecard_checks' + )); diff --git a/backend/src/osspckgs/migrations/V1781094067__stewardship-tables.sql b/backend/src/osspckgs/migrations/V1781094067__stewardship-tables.sql new file mode 100644 index 0000000000..4236dc172a --- /dev/null +++ b/backend/src/osspckgs/migrations/V1781094067__stewardship-tables.sql @@ -0,0 +1,123 @@ +-- Stewardship tables for the OSSPREY Self Serve program (v1). +-- In v1: only `stewardships` is populated (one unassigned row per critical package). +-- All other tables are schema-only — empty until v2 write flows land. + +CREATE TABLE IF NOT EXISTS stewardships ( + id BIGSERIAL PRIMARY KEY, + package_id BIGINT NOT NULL REFERENCES packages(id), + status TEXT NOT NULL, -- 'unassigned'|'open'|'assessing'|'active'|'needs_attention'|'escalated'|'blocked'|'inactive' + origin TEXT NOT NULL, -- 'auto_imported'|'self_claimed'|'assigned'|'opened_for_claim' + version INT NOT NULL DEFAULT 1, + opened_at TIMESTAMPTZ, + last_status_at TIMESTAMPTZ, + inactive_reason TEXT, -- 'quarterly_cadence_missed'|'stepped_down'|'no_longer_critical' + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (package_id) +); + +CREATE INDEX IF NOT EXISTS stewardships_status_idx + ON stewardships (status); +CREATE INDEX IF NOT EXISTS stewardships_last_status_at_active_idx + ON stewardships (last_status_at) WHERE status = 'active'; + +-- Many-to-many stewards. Empty in v1; soft-delete preserves historical membership. +CREATE TABLE IF NOT EXISTS stewardship_stewards ( + id BIGSERIAL PRIMARY KEY, + stewardship_id BIGINT NOT NULL REFERENCES stewardships(id), + user_id TEXT NOT NULL, + role TEXT NOT NULL, -- 'lead'|'co_steward' + assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + assigned_by TEXT, + deleted_at TIMESTAMPTZ +); + +CREATE UNIQUE INDEX IF NOT EXISTS stewardship_stewards_active_unique + ON stewardship_stewards (stewardship_id, user_id) + WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS stewardship_stewards_user_id_active_idx + ON stewardship_stewards (user_id) WHERE deleted_at IS NULL; + +-- Append-only audit log. Empty in v1. +CREATE TABLE IF NOT EXISTS stewardship_activity ( + id BIGSERIAL PRIMARY KEY, + stewardship_id BIGINT NOT NULL REFERENCES stewardships(id), + actor_user_id TEXT, -- NULL for system events + actor_type TEXT NOT NULL, -- 'user'|'system' + activity_type TEXT NOT NULL, -- 'state_changed'|'assessment_completed'|'assessment_flagged'| + -- 'remediation_logged'|'status_update'|'escalation'| + -- 'escalation_resolved'|'blocker_added'|'blocker_resolved'| + -- 'steward_added'|'steward_removed' + content TEXT, + metadata JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS stewardship_activity_stewardship_id_created_at_idx + ON stewardship_activity (stewardship_id, created_at DESC); + +-- One current assessment per stewardship; historical ones preserved via superseded_at. +CREATE TABLE IF NOT EXISTS stewardship_assessments ( + id BIGSERIAL PRIMARY KEY, + stewardship_id BIGINT NOT NULL REFERENCES stewardships(id), + posture TEXT, + summary TEXT, + security_contact TEXT, + disclosure_preference TEXT, + tier_0_ready BOOL NOT NULL DEFAULT FALSE, + monitoring_plan TEXT, + draft BOOL NOT NULL DEFAULT TRUE, + completed_at TIMESTAMPTZ, + completed_by TEXT, + reviewed BOOL NOT NULL DEFAULT FALSE, + reviewed_at TIMESTAMPTZ, + reviewed_by TEXT, + flagged BOOL NOT NULL DEFAULT FALSE, + flag_note TEXT, + superseded_at TIMESTAMPTZ, + superseded_by_id BIGINT REFERENCES stewardship_assessments(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -- tracks mutations: reviewed, flagged, superseded_at +); + +CREATE INDEX IF NOT EXISTS stewardship_assessments_stewardship_id_superseded_at_idx + ON stewardship_assessments (stewardship_id, superseded_at); +CREATE UNIQUE INDEX IF NOT EXISTS stewardship_assessments_one_current + ON stewardship_assessments (stewardship_id) + WHERE superseded_at IS NULL; + +-- Per-dimension findings. assessment_id links a finding to the assessment that produced it. +CREATE TABLE IF NOT EXISTS stewardship_findings ( + id BIGSERIAL PRIMARY KEY, + stewardship_id BIGINT NOT NULL REFERENCES stewardships(id), + assessment_id BIGINT REFERENCES stewardship_assessments(id), -- NULL until assessment flow lands in v2 + dimension TEXT NOT NULL, -- 'maintainer_health'|'security_posture'|'vulnerability_exposure'| + -- 'dependency_risk'|'supply_chain_integrity'|'release_health' + severity TEXT NOT NULL, -- 'critical'|'high'|'medium'|'low'|'informational' + finding TEXT NOT NULL, + evidence TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS stewardship_findings_stewardship_id_idx + ON stewardship_findings (stewardship_id); +CREATE INDEX IF NOT EXISTS stewardship_findings_dimension_severity_idx + ON stewardship_findings (dimension, severity); + +-- Concrete remediation actions. Empty in v1. +CREATE TABLE IF NOT EXISTS stewardship_remediation_actions ( + id BIGSERIAL PRIMARY KEY, + stewardship_id BIGINT NOT NULL REFERENCES stewardships(id), + finding_id BIGINT REFERENCES stewardship_findings(id), + action TEXT NOT NULL, + status TEXT NOT NULL, -- 'pending'|'in_progress'|'done'|'blocked'|'abandoned' + url TEXT, + notes TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + completed_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS stewardship_remediation_actions_stewardship_id_status_idx + ON stewardship_remediation_actions (stewardship_id, status); diff --git a/backend/src/osspckgs/migrations/V1781180529__branch_protection_fields.sql b/backend/src/osspckgs/migrations/V1781180529__branch_protection_fields.sql new file mode 100644 index 0000000000..6b3142f012 --- /dev/null +++ b/backend/src/osspckgs/migrations/V1781180529__branch_protection_fields.sql @@ -0,0 +1,4 @@ +ALTER TABLE repos ADD COLUMN IF NOT EXISTS branch_protection_enabled boolean; +ALTER TABLE repos ADD COLUMN IF NOT EXISTS branch_protection_required_reviews int; +ALTER TABLE repos ADD COLUMN IF NOT EXISTS branch_protection_requires_status_checks boolean; +ALTER TABLE repos ADD COLUMN IF NOT EXISTS branch_protection_allows_force_push boolean; diff --git a/backend/src/osspckgs/migrations/V1781262276__rank_packages_cumulative_coverage.sql b/backend/src/osspckgs/migrations/V1781262276__rank_packages_cumulative_coverage.sql new file mode 100644 index 0000000000..89340c6aff --- /dev/null +++ b/backend/src/osspckgs/migrations/V1781262276__rank_packages_cumulative_coverage.sql @@ -0,0 +1,116 @@ +-- Two related changes bundled here: +-- +-- 1. package_criticality_spotlight: replace text-based (ecosystem, namespace, name) +-- matching with a package_id FK so the spotlight join uses an indexed integer key. +-- +-- 2. rank_packages(): replace the weighted PERCENT_RANK formula and arbitrary +-- per-ecosystem top-N caps with cumulative-coverage scoring. +-- The smallest set of packages that together account for coverage_cutoff (default 90%) +-- of each signal is critical. impact = average of (1 − cumulative_coverage) across +-- available signals. is_critical = true if in that set for any signal. + +-- ── 1. package_criticality_spotlight schema change ─────────────────────────── + +DROP INDEX IF EXISTS package_criticality_spotlight_ecosystem_coalesce_name_idx; + +ALTER TABLE package_criticality_spotlight + ADD COLUMN IF NOT EXISTS package_id bigint NOT NULL REFERENCES packages(id), + DROP COLUMN IF EXISTS name, + DROP COLUMN IF EXISTS namespace; + +CREATE UNIQUE INDEX IF NOT EXISTS package_criticality_spotlight_package_id_idx + ON package_criticality_spotlight (package_id); + +-- ── 2. rank_packages() ─────────────────────────────────────────────────────── + +DROP FUNCTION IF EXISTS rank_packages(numeric, numeric, numeric, jsonb); + +CREATE OR REPLACE FUNCTION rank_packages( + coverage_cutoff numeric DEFAULT 0.90, + ecosystems text[] DEFAULT NULL +) +RETURNS TABLE(processed_rows int) +LANGUAGE plpgsql AS $$ +DECLARE + processed_count int; + effective_ecosystems text[]; +BEGIN + IF ecosystems IS NULL THEN + SELECT ARRAY_AGG(DISTINCT ecosystem) + INTO effective_ecosystems + FROM packages; + ELSE + effective_ecosystems := ecosystems; + END IF; + + WITH base AS ( + SELECT + id, + ecosystem, + COALESCE(downloads_last_30d, 0) AS downloads, + COALESCE(dependent_count, 0) AS direct_dependents, + COALESCE(transitive_dependent_count, 0) AS transitive_dependents, + SUM(COALESCE(downloads_last_30d, 0)) OVER (PARTITION BY ecosystem) AS ecosystem_total_downloads, + SUM(COALESCE(dependent_count, 0)) OVER (PARTITION BY ecosystem) AS ecosystem_total_direct_dependents, + SUM(COALESCE(transitive_dependent_count, 0)) OVER (PARTITION BY ecosystem) AS ecosystem_total_transitive_dependents + FROM packages + WHERE ecosystem = ANY(effective_ecosystems) + ), + walked AS ( + -- One row per (package × signal). Signals with a zero ecosystem total are + -- excluded so they don't factor into the average (e.g. downloads for maven). + -- cumulative_share_exclusive for the top-ranked package equals 0 by arithmetic + -- (sum of rows above it is 0), so the top package is always inside the critical set. + SELECT + id, + ecosystem, + SUM(signal_value) OVER coverage_window / ecosystem_signal_total::numeric AS cumulative_share_inclusive, + (SUM(signal_value) OVER coverage_window - signal_value) / ecosystem_signal_total::numeric AS cumulative_share_exclusive + FROM base + CROSS JOIN LATERAL (VALUES + ('downloads', downloads, ecosystem_total_downloads), + ('direct_dependents', direct_dependents, ecosystem_total_direct_dependents), + ('transitive_dependents', transitive_dependents, ecosystem_total_transitive_dependents) + ) AS signal(signal_name, signal_value, ecosystem_signal_total) + WHERE ecosystem_signal_total > 0 + WINDOW coverage_window AS ( + PARTITION BY ecosystem, signal_name + ORDER BY signal_value DESC, id + ROWS UNBOUNDED PRECEDING + ) + ), + combined AS ( + SELECT + id, + ecosystem, + AVG(1.0 - cumulative_share_inclusive)::numeric(10, 4) AS new_impact, + BOOL_OR(cumulative_share_exclusive < coverage_cutoff) AS new_is_critical + FROM walked + GROUP BY id, ecosystem + ), + final AS ( + SELECT + combined.id, + combined.new_impact, + combined.new_is_critical OR (spotlight.package_id IS NOT NULL) AS new_is_critical, + ROW_NUMBER() OVER ( + PARTITION BY combined.ecosystem + ORDER BY combined.new_impact DESC NULLS LAST, combined.id + ) AS new_rank_in_ecosystem + FROM combined + LEFT JOIN package_criticality_spotlight spotlight ON spotlight.package_id = combined.id + ) + UPDATE packages p + SET impact = final.new_impact, + is_critical = final.new_is_critical, + rank_in_ecosystem = final.new_rank_in_ecosystem, + last_rank_pass_at = NOW(), + last_synced_at = NOW() + FROM final + WHERE p.id = final.id; + + GET DIAGNOSTICS processed_count = ROW_COUNT; + + RETURN QUERY SELECT processed_count; +END; +$$; diff --git a/backend/src/osspckgs/migrations/V1781300000__stewardship-status-path-note.sql b/backend/src/osspckgs/migrations/V1781300000__stewardship-status-path-note.sql new file mode 100644 index 0000000000..081df73dc9 --- /dev/null +++ b/backend/src/osspckgs/migrations/V1781300000__stewardship-status-path-note.sql @@ -0,0 +1,3 @@ +ALTER TABLE stewardships + ADD COLUMN IF NOT EXISTS resolution_path TEXT, + ADD COLUMN IF NOT EXISTS status_note TEXT; diff --git a/backend/src/osspckgs/migrations/V1781514035__ranking-job-kind.sql b/backend/src/osspckgs/migrations/V1781514035__ranking-job-kind.sql new file mode 100644 index 0000000000..8cfa62ab07 --- /dev/null +++ b/backend/src/osspckgs/migrations/V1781514035__ranking-job-kind.sql @@ -0,0 +1,5 @@ +-- Drop CHECK constraints on job_kind and sync_mode so new kinds can be added +-- without a migration every time. +ALTER TABLE osspckgs_ingest_jobs + DROP CONSTRAINT osspckgs_ingest_jobs_job_kind_check, + DROP CONSTRAINT osspckgs_ingest_jobs_sync_mode_check; diff --git a/backend/src/osspckgs/migrations/V1781539311__packages_tables_sequin_updates.sql b/backend/src/osspckgs/migrations/V1781539311__packages_tables_sequin_updates.sql new file mode 100644 index 0000000000..d84c330567 --- /dev/null +++ b/backend/src/osspckgs/migrations/V1781539311__packages_tables_sequin_updates.sql @@ -0,0 +1,30 @@ +-- ─── 1. package_dependencies (created_at, id) index ───────────────────────── +CREATE INDEX IF NOT EXISTS package_dependencies_created_at_id_idx + ON package_dependencies (created_at, id); + +-- ─── 2. repo_activity_snapshot replication ────────────────────────────────── +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM pg_publication WHERE pubname = 'sequin_pub' + ) AND NOT EXISTS ( + SELECT 1 FROM pg_publication_tables + WHERE pubname = 'sequin_pub' + AND schemaname = 'public' + AND tablename = 'repo_activity_snapshot' + ) THEN + ALTER PUBLICATION sequin_pub ADD TABLE repo_activity_snapshot; + END IF; +END$$; + +ALTER TABLE public.repo_activity_snapshot REPLICA IDENTITY FULL; + +-- ─── 3. packages enriched health/lifecycle fields (synced back from Tinybird) ─── +ALTER TABLE packages + ADD COLUMN IF NOT EXISTS lifecycle_label text, + ADD COLUMN IF NOT EXISTS health_score smallint, + ADD COLUMN IF NOT EXISTS health_label text, + ADD COLUMN IF NOT EXISTS maintainer_health_score smallint, + ADD COLUMN IF NOT EXISTS security_supply_chain_score smallint, + ADD COLUMN IF NOT EXISTS development_activity_score smallint, + ADD COLUMN IF NOT EXISTS signal_coverage_health jsonb; diff --git a/backend/src/osspckgs/migrations/V1781600000__stewards-table.sql b/backend/src/osspckgs/migrations/V1781600000__stewards-table.sql new file mode 100644 index 0000000000..c1af7c1749 --- /dev/null +++ b/backend/src/osspckgs/migrations/V1781600000__stewards-table.sql @@ -0,0 +1,6 @@ +CREATE TABLE stewards ( + user_id TEXT PRIMARY KEY, + username TEXT NOT NULL, + display_name TEXT NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); diff --git a/backend/src/osspckgs/migrations/V1781700000__stewardship-activity-actor-display.sql b/backend/src/osspckgs/migrations/V1781700000__stewardship-activity-actor-display.sql new file mode 100644 index 0000000000..2dca278f6f --- /dev/null +++ b/backend/src/osspckgs/migrations/V1781700000__stewardship-activity-actor-display.sql @@ -0,0 +1,4 @@ +ALTER TABLE stewardship_activity + ADD COLUMN IF NOT EXISTS actor_username TEXT, + ADD COLUMN IF NOT EXISTS actor_display_name TEXT, + ADD COLUMN IF NOT EXISTS actor_avatar_url TEXT; diff --git a/backend/src/product/Dockerfile.flyway b/backend/src/product/Dockerfile.flyway new file mode 100644 index 0000000000..096615d4f5 --- /dev/null +++ b/backend/src/product/Dockerfile.flyway @@ -0,0 +1,17 @@ +FROM flyway/flyway:7.8.1-alpine + +USER root + +# Install envsubst from gettext used for templating. +RUN apk update \ + && apk add --no-cache gettext + +USER flyway + +COPY ./flyway_migrate.sh /migrate.sh + +# Override default `flyway` entrypoint. +ENTRYPOINT ["/migrate.sh"] + +# Copy migrations. +COPY ./migrations /tmp/migrations \ No newline at end of file diff --git a/backend/src/product/flyway_migrate.sh b/backend/src/product/flyway_migrate.sh new file mode 100755 index 0000000000..b2a4582979 --- /dev/null +++ b/backend/src/product/flyway_migrate.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -e +echo "Migrating jdbc:postgresql://${PGHOST}:${PGPORT}/${PGDATABASE}" + +flyway \ + -locations="filesystem:/tmp/migrations" \ + -url="jdbc:postgresql://${PGHOST}:${PGPORT}/${PGDATABASE}" \ + -user="$PGUSER" \ + -password="$PGPASSWORD" \ + -connectRetries=60 \ + -outOfOrder=true \ + -mixed=true \ + -placeholderReplacement=false \ + -schemas=public \ + -X \ + migrate diff --git a/backend/src/product/migrations/V1716389909__init.sql b/backend/src/product/migrations/V1716389909__init.sql new file mode 100644 index 0000000000..9fa7e18266 --- /dev/null +++ b/backend/src/product/migrations/V1716389909__init.sql @@ -0,0 +1,20 @@ +create table public.sessions ( + id uuid not null primary key, + "userId" uuid not null, + "userEmail" text not null, + "startTime" timestamp with time zone default now() not null, + "endTime" timestamp with time zone, + "ipAddress" text, + country text +); + +create table public.events ( + id uuid not null primary key, + "sessionId" uuid not null references public.sessions(id) on delete cascade, + type text not null, + key text not null, + properties jsonb default '{}'::jsonb, + "createdAt" timestamp with time zone default now() not null, + "userId" uuid not null, + "userEmail" text not null +); \ No newline at end of file diff --git a/backend/src/security/permissions.ts b/backend/src/security/permissions.ts index f701f4ab02..af448431cc 100644 --- a/backend/src/security/permissions.ts +++ b/backend/src/security/permissions.ts @@ -1,1184 +1,320 @@ import Roles from './roles' -import Plans from './plans' import Storage from './storage' const storage = Storage.values const roles = Roles.values -const plans = Plans.values class Permissions { static get values() { return { tenantEdit: { id: 'tenantEdit', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], }, tenantDestroy: { id: 'tenantDestroy', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - planEdit: { - id: 'planEdit', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - planRead: { - id: 'planRead', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - userEdit: { - id: 'userEdit', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - userDestroy: { - id: 'userDestroy', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - userCreate: { - id: 'userCreate', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - userImport: { - id: 'userImport', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], }, userRead: { id: 'userRead', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], }, userAutocomplete: { id: 'userAutocomplete', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], }, auditLogRead: { id: 'auditLogRead', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, settingsRead: { id: 'settingsRead', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], allowedStorage: [storage.settingsBackgroundImages, storage.settingsLogos], }, settingsEdit: { id: 'settingsEdit', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [storage.settingsBackgroundImages, storage.settingsLogos], }, memberAttributesRead: { id: 'memberAttributesRead', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], allowedStorage: [], }, memberAttributesEdit: { id: 'memberAttributesEdit', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, memberAttributesDestroy: { id: 'memberAttributesDestroy', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, memberAttributesCreate: { id: 'memberAttributesCreate', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, memberImport: { id: 'memberImport', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], }, memberCreate: { id: 'memberCreate', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, memberEdit: { id: 'memberEdit', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, memberDestroy: { id: 'memberDestroy', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, memberRead: { id: 'memberRead', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, memberAutocomplete: { id: 'memberAutocomplete', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, activityImport: { id: 'activityImport', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], }, activityCreate: { id: 'activityCreate', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, activityEdit: { id: 'activityEdit', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, activityDestroy: { id: 'activityDestroy', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, activityRead: { id: 'activityRead', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, activityAutocomplete: { id: 'activityAutocomplete', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - automationCreate: { - id: 'automationCreate', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - automationUpdate: { - id: 'automationUpdate', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - automationDestroy: { - id: 'automationDestroy', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - automationRead: { - id: 'automationRead', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, tagImport: { id: 'tagImport', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], }, tagCreate: { id: 'tagCreate', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, tagEdit: { id: 'tagEdit', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, tagDestroy: { id: 'tagDestroy', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, tagRead: { id: 'tagRead', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, tagAutocomplete: { id: 'tagAutocomplete', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, organizationImport: { id: 'organizationImport', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], }, organizationCreate: { id: 'organizationCreate', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, organizationEdit: { id: 'organizationEdit', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, organizationDestroy: { id: 'organizationDestroy', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, organizationRead: { id: 'organizationRead', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, organizationAutocomplete: { id: 'organizationAutocomplete', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - widgetImport: { - id: 'widgetImport', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - widgetCreate: { - id: 'widgetCreate', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - allowedStorage: [], - }, - widgetEdit: { - id: 'widgetEdit', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - allowedStorage: [], - }, - widgetDestroy: { - id: 'widgetDestroy', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - allowedStorage: [], - }, - widgetRead: { - id: 'widgetRead', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - widgetAutocomplete: { - id: 'widgetAutocomplete', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - reportImport: { - id: 'reportImport', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - reportCreate: { - id: 'reportCreate', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - allowedStorage: [], - }, - reportEdit: { - id: 'reportEdit', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - allowedStorage: [], - }, - reportDestroy: { - id: 'reportDestroy', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - allowedStorage: [], - }, - reportRead: { - id: 'reportRead', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - reportAutocomplete: { - id: 'reportAutocomplete', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - integrationImport: { - id: 'integrationImport', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - integrationControlLimit: { - id: 'integrationControlLimit', - allowedRoles: [], - allowedPlans: [], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, integrationCreate: { id: 'integrationCreate', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, integrationEdit: { id: 'integrationEdit', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, integrationDestroy: { id: 'integrationDestroy', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, integrationRead: { id: 'integrationRead', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, integrationAutocomplete: { id: 'integrationAutocomplete', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - microserviceImport: { - id: 'microserviceImport', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - microserviceCreate: { - id: 'microserviceCreate', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - allowedStorage: [], - }, - microserviceEdit: { - id: 'microserviceEdit', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - allowedStorage: [], - }, - microserviceDestroy: { - id: 'microserviceDestroy', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - allowedStorage: [], - }, - microserviceRead: { - id: 'microserviceRead', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - microserviceAutocomplete: { - id: 'microserviceAutocomplete', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - microserviceVariantFree: { - id: 'microserviceVariantFree', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - microserviceVariantPremium: { - id: 'microserviceVariantPremium', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [plans.growth, plans.eagleEye, plans.enterprise, plans.scale], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, conversationCreate: { id: 'conversationCreate', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, conversationEdit: { id: 'conversationEdit', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, conversationDestroy: { id: 'conversationDestroy', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, conversationRead: { id: 'conversationRead', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, eagleEyeActionCreate: { id: 'eagleEyeActionCreate', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.growth, - plans.essential, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, eagleEyeActionDestroy: { id: 'eagleEyeActionDestroy', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.growth, - plans.essential, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, eagleEyeContentCreate: { id: 'eagleEyeContentCreate', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.growth, - plans.essential, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, eagleEyeContentRead: { id: 'eagleEyeContentRead', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.growth, - plans.essential, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, eagleEyeContentSearch: { id: 'eagleEyeContentSearch', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.growth, - plans.essential, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, eagleEyeContentEdit: { id: 'eagleEyeContentEdit', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.growth, - plans.essential, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - taskImport: { - id: 'taskImport', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - taskCreate: { - id: 'taskCreate', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - allowedStorage: [], - }, - taskEdit: { - id: 'taskEdit', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - allowedStorage: [], - }, - taskDestroy: { - id: 'taskDestroy', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - allowedStorage: [], - }, - taskRead: { - id: 'taskRead', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - taskAutocomplete: { - id: 'taskAutocomplete', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - taskBatch: { - id: 'taskBatch', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, noteImport: { id: 'noteImport', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], }, noteCreate: { id: 'noteCreate', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, noteEdit: { id: 'noteEdit', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, noteDestroy: { id: 'noteDestroy', - allowedRoles: [roles.admin], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], allowedStorage: [], }, noteRead: { id: 'noteRead', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, noteAutocomplete: { id: 'noteAutocomplete', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - quickstartGuideRead: { - id: 'quickstartGuideRead', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], - }, - quickstartGuideSettingsUpdate: { - id: 'quickstartGuideSettingsUpdate', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, segmentRead: { id: 'segmentRead', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, segmentCreate: { id: 'segmentCreate', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], + }, + projectGroupCreate: { + id: 'projectGroupCreate', + allowedRoles: [roles.admin], }, segmentEdit: { id: 'segmentEdit', - allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + allowedRoles: [roles.admin, roles.projectAdmin], }, customViewCreate: { id: 'customViewCreate', allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], }, customViewEdit: { id: 'customViewEdit', allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], }, customViewDestroy: { id: 'customViewDestroy', allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], }, customViewRead: { id: 'customViewRead', allowedRoles: [roles.admin, roles.readonly], - allowedPlans: [ - plans.essential, - plans.growth, - plans.eagleEye, - plans.enterprise, - plans.scale, - ], + }, + mergeActionRead: { + id: 'mergeActionRead', + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], + }, + dataIssueCreate: { + id: 'dataIssueCreate', + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], + }, + + collectionEdit: { + id: 'collectionEdit', + allowedRoles: [roles.admin], + }, + collectionRead: { + id: 'collectionRead', + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], + }, + + categoryEdit: { + id: 'categoryEdit', + allowedRoles: [roles.admin], + }, + categoryRead: { + id: 'categoryRead', + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, } } diff --git a/backend/src/security/plans.ts b/backend/src/security/plans.ts deleted file mode 100644 index 83f3bcf40b..0000000000 --- a/backend/src/security/plans.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { PLANS_CONFIG } from '../conf' - -class Plans { - static get values() { - return { - essential: 'Essential', - growth: 'Growth', - eagleEye: 'Eagle Eye', - scale: 'Scale', - enterprise: 'Enterprise', - } - } - - static selectPlanByStripePriceId(stripePriceId) { - const premiumStripePriceId = PLANS_CONFIG.stripePricePremium - - if (premiumStripePriceId === stripePriceId) { - return Plans.values.growth - } - - return Plans.values.essential - } - - static selectStripePriceIdByPlan(plan) { - if (plan === Plans.values.growth) { - return PLANS_CONFIG.stripePricePremium - } - - return null - } - - /** - * When the plan is: - * - active: The plan will be active. - * - cancel_at_period_end: The plan will remain active until the end of the period. - * - error: The plan will remain active, but a warning message will be displayed to the user. - * - canceled: The workspace plan will change to Free. - */ - static selectPlanStatus(stripePlan) { - if (!stripePlan) { - return 'canceled' - } - - const { status, cancelAtPeriodEnd } = stripePlan - - if (status === 'active') { - if (cancelAtPeriodEnd) { - return 'cancel_at_period_end' - } - - return 'active' - } - - if (status === 'canceled' || status === 'incomplete_expired') { - return 'canceled' - } - - return 'error' - } - - /** - * If the plan exists and it is not marked - * to cancel, the tenant can't be destroyed, - * because future charges might occur - */ - static allowTenantDestroy(plan, planStatus) { - if (plan === Plans.values.essential || plan === Plans.values.growth) { - return true - } - - return planStatus === 'cancel_at_period_end' - } -} - -export default Plans diff --git a/backend/src/security/roles.ts b/backend/src/security/roles.ts index 0b99628925..7718effc48 100644 --- a/backend/src/security/roles.ts +++ b/backend/src/security/roles.ts @@ -3,6 +3,7 @@ class Roles { return { admin: 'admin', readonly: 'readonly', + projectAdmin: 'projectAdmin', } } } diff --git a/backend/src/security/scopes.ts b/backend/src/security/scopes.ts new file mode 100644 index 0000000000..5f7bd77f01 --- /dev/null +++ b/backend/src/security/scopes.ts @@ -0,0 +1,19 @@ +export const SCOPES = { + READ_MEMBERS: 'read:members', + WRITE_MEMBERS: 'write:members', + READ_ORGANIZATIONS: 'read:organizations', + WRITE_ORGANIZATIONS: 'write:organizations', + READ_MEMBER_IDENTITIES: 'read:member-identities', + WRITE_MEMBER_IDENTITIES: 'write:member-identities', + READ_MAINTAINER_ROLES: 'read:maintainer-roles', + READ_WORK_EXPERIENCES: 'read:work-experiences', + WRITE_WORK_EXPERIENCES: 'write:work-experiences', + READ_PROJECT_AFFILIATIONS: 'read:project-affiliations', + WRITE_PROJECT_AFFILIATIONS: 'write:project-affiliations', + READ_AFFILIATIONS: 'read:affiliations', + READ_PACKAGES: 'read:packages', + READ_STEWARDSHIPS: 'read:stewardships', + WRITE_STEWARDSHIPS: 'write:stewardships', +} as const + +export type Scope = (typeof SCOPES)[keyof typeof SCOPES] diff --git a/backend/src/segment/addProductDataToCrowdTenant.ts b/backend/src/segment/addProductDataToCrowdTenant.ts index bea8e0e1a1..8328b26dc8 100644 --- a/backend/src/segment/addProductDataToCrowdTenant.ts +++ b/backend/src/segment/addProductDataToCrowdTenant.ts @@ -1,9 +1,11 @@ import axios from 'axios' + import { getServiceChildLogger } from '@crowd/logging' + import { CROWD_ANALYTICS_CONFIG } from '../conf' -import UserRepository from '../database/repositories/userRepository' -import TenantRepository from '../database/repositories/tenantRepository' import SequelizeRepository from '../database/repositories/sequelizeRepository' +import TenantRepository from '../database/repositories/tenantRepository' +import UserRepository from '../database/repositories/userRepository' const IS_CROWD_ANALYTICS_ENABLED = CROWD_ANALYTICS_CONFIG.isEnabled === 'true' const CROWD_ANALYTICS_TENANT_ID = CROWD_ANALYTICS_CONFIG.tenantId diff --git a/backend/src/segment/identify.ts b/backend/src/segment/identify.ts index 2f4e773c4e..f695a2448d 100644 --- a/backend/src/segment/identify.ts +++ b/backend/src/segment/identify.ts @@ -1,5 +1,6 @@ import { Edition } from '@crowd/types' -import { SEGMENT_CONFIG, API_CONFIG } from '../conf' + +import { API_CONFIG, SEGMENT_CONFIG } from '../conf' export default function identify(user) { const Analytics = require('analytics-node') diff --git a/backend/src/segment/identifyTenant.ts b/backend/src/segment/identifyTenant.ts index 5456c8a20f..a6168c772c 100644 --- a/backend/src/segment/identifyTenant.ts +++ b/backend/src/segment/identifyTenant.ts @@ -1,5 +1,6 @@ import { Edition } from '@crowd/types' -import { SEGMENT_CONFIG, API_CONFIG } from '../conf' + +import { API_CONFIG, SEGMENT_CONFIG } from '../conf' export default async function identifyTenant(req) { if (SEGMENT_CONFIG.writeKey) { diff --git a/backend/src/segment/telemetryTrack.ts b/backend/src/segment/telemetryTrack.ts index e829f1b917..4989e01691 100644 --- a/backend/src/segment/telemetryTrack.ts +++ b/backend/src/segment/telemetryTrack.ts @@ -1,9 +1,11 @@ import { getServiceChildLogger } from '@crowd/logging' import { Edition } from '@crowd/types' + import { API_CONFIG, IS_TEST_ENV, SEGMENT_CONFIG } from '../conf' import SequelizeRepository from '../database/repositories/sequelizeRepository' -import getTenatUser from './trackHelper' + import { CROWD_ANALYTICS_PLATORM_NAME } from './addProductDataToCrowdTenant' +import getTenatUser from './trackHelper' const log = getServiceChildLogger('telemetryTrack') diff --git a/backend/src/segment/track.ts b/backend/src/segment/track.ts index 9bf6708829..53f7087d56 100644 --- a/backend/src/segment/track.ts +++ b/backend/src/segment/track.ts @@ -1,10 +1,12 @@ import { getServiceChildLogger } from '@crowd/logging' import { Edition } from '@crowd/types' -import { API_CONFIG, IS_TEST_ENV, SEGMENT_CONFIG } from '../conf' -import getTenatUser from './trackHelper' -import addProductData, { CROWD_ANALYTICS_PLATORM_NAME } from './addProductDataToCrowdTenant' + +import { API_CONFIG, IS_DEV_ENV, IS_TEST_ENV, SEGMENT_CONFIG } from '../conf' import SequelizeRepository from '../database/repositories/sequelizeRepository' +import { CROWD_ANALYTICS_PLATORM_NAME } from './addProductDataToCrowdTenant' +import getTenatUser from './trackHelper' + const log = getServiceChildLogger('segment') export default async function identify( @@ -19,6 +21,7 @@ export default async function identify( }).email if ( !IS_TEST_ENV && + !IS_DEV_ENV && SEGMENT_CONFIG.writeKey && // This is only for events in the hosted version. Self-hosted has less telemetry. (API_CONFIG.edition === Edition.CROWD_HOSTED || API_CONFIG.edition === Edition.LFX) && @@ -39,6 +42,10 @@ export default async function identify( const { userIdOut, tenantIdOut } = getTenatUser(userId, options) + if (!userIdOut) { + return + } + const payload = { userId: userIdOut, event, @@ -53,13 +60,13 @@ export default async function identify( analytics.track(payload) // send product analytics data to crowd tenant workspace - await addProductData({ - userId: userIdOut, - tenantId: tenantIdOut, - event, - timestamp, - properties, - }) + // await addProductData({ + // userId: userIdOut, + // tenantId: tenantIdOut, + // event, + // timestamp, + // properties, + // }) } catch (error) { log.error(error, { payload }, 'Could not send the following payload to Segment') } diff --git a/backend/src/serverless/dbOperations/__tests__/operationsWorker.test.ts b/backend/src/serverless/dbOperations/__tests__/operationsWorker.test.ts deleted file mode 100644 index b24dbdf0b5..0000000000 --- a/backend/src/serverless/dbOperations/__tests__/operationsWorker.test.ts +++ /dev/null @@ -1,457 +0,0 @@ -import moment from 'moment' -import SequelizeTestUtils from '../../../database/utils/sequelizeTestUtils' -import ActivityService from '../../../services/activityService' -import MemberService from '../../../services/memberService' -import IntegrationService from '../../../services/integrationService' -import MicroserviceService from '../../../services/microserviceService' -import worker from '../operationsWorker' -import { PlatformType } from '@crowd/types' -import { generateUUIDv1 } from '@crowd/common' -import { populateSegments } from '../../../database/utils/segmentTestUtils' - -const db = null - -describe('Serverless database operations worker tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('Bulk upsert method for members', () => { - it('Should add a single simple member', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const member = { - username: { - [PlatformType.GITHUB]: { - username: 'member1', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.GITHUB, - } - - await worker('upsert_members', [member], mockIRepositoryOptions) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const dbMembers = (await new MemberService(mockIRepositoryOptions).findAndCountAll({})).rows - - expect(dbMembers.length).toBe(1) - expect(dbMembers[0].username[PlatformType.GITHUB]).toEqual(['member1']) - }) - - it('Should add a list of members', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const members = [ - { - username: { - [PlatformType.GITHUB]: { - username: 'member1', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.GITHUB, - }, - { - username: { - [PlatformType.SLACK]: { - username: 'member2', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.SLACK, - }, - ] - - await worker('upsert_members', members, mockIRepositoryOptions) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const dbMembers = (await new MemberService(mockIRepositoryOptions).findAndCountAll({})).rows - - expect(dbMembers.length).toBe(2) - }) - - it('Should work for an empty list', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await worker('upsert_members', [], mockIRepositoryOptions) - - const dbMembers = (await new MemberService(mockIRepositoryOptions).findAndCountAll({})).rows - - expect(dbMembers.length).toBe(0) - }) - }) - - describe('Bulk upsert method for activities with members', () => { - it('Should add a single simple activity with members', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - - const ts = moment().toDate() - const activity = { - timestamp: ts, - type: 'message', - platform: 'api', - username: 'member1', - member: { - username: { - api: { - username: 'member1', - integrationId: generateUUIDv1(), - }, - }, - }, - sourceId: '#sourceId1', - } - - await worker('upsert_activities_with_members', [activity], mockIRepositoryOptions) - - const dbActivities = (await new ActivityService(mockIRepositoryOptions).findAndCountAll({})) - .rows - - expect(dbActivities.length).toBe(1) - expect(moment(dbActivities[0].timestamp).unix()).toBe(moment(ts).unix()) - }) - - it('Should add a list of activities with members', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - - const ts = moment().toDate() - const ts2 = moment().subtract(2, 'days').toDate() - - const activities = [ - { - timestamp: ts, - type: 'message', - platform: 'api', - username: 'member1', - member: { - username: { - api: { - username: 'member1', - integrationId: generateUUIDv1(), - }, - }, - }, - sourceId: '#sourceId1', - }, - { - timestamp: ts2, - type: 'message', - platform: 'api', - username: 'member2', - member: { - username: { - api: { - username: 'member2', - integrationId: generateUUIDv1(), - }, - }, - }, - sourceId: '#sourceId2', - }, - ] - - await worker('upsert_activities_with_members', activities, mockIRepositoryOptions) - - const dbActivities = (await new ActivityService(mockIRepositoryOptions).findAndCountAll({})) - .rows - expect(dbActivities.length).toBe(2) - }) - - it('Should work for an empty list', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await worker('upsert_activities_with_members', [], mockIRepositoryOptions) - - const dbActivities = (await new ActivityService(mockIRepositoryOptions).findAndCountAll({})) - .rows - - expect(dbActivities.length).toBe(0) - }) - }) - - describe('Bulk update method for members', () => { - it('Should update a single member', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member = { - username: { - [PlatformType.GITHUB]: { - username: 'member1', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.GITHUB, - score: 1, - } - - const dbMember = await new MemberService(mockIRepositoryOptions).upsert(member) - const memberId = dbMember.id - - await worker( - 'update_members', - [{ id: memberId, update: { score: 10 } }], - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const dbMembers = (await new MemberService(mockIRepositoryOptions).findAndCountAll({})).rows - - expect(dbMembers.length).toBe(1) - expect(dbMembers[0].username[PlatformType.GITHUB]).toEqual(['member1']) - expect(dbMembers[0].score).toBe(10) - }) - - it('Should update a list of members', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const members = [ - { - username: { - [PlatformType.GITHUB]: { - username: 'member1', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.GITHUB, - score: 1, - }, - { - username: { - [PlatformType.DISCORD]: { - username: 'member2', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.DISCORD, - score: 2, - }, - ] - - const memberIds = [] - for (const member of members) { - const { id } = await new MemberService(mockIRepositoryOptions).upsert(member) - memberIds.push(id) - } - - await worker( - 'update_members', - [ - { id: memberIds[0], update: { score: 10 } }, - { id: memberIds[1], update: { score: 3 } }, - ], - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const dbMembers = (await new MemberService(mockIRepositoryOptions).findAndCountAll({})).rows - - expect(dbMembers.length).toBe(2) - expect(dbMembers[1].score).toBe(10) - expect(dbMembers[0].score).toBe(3) - }) - - it('Should work for an empty list', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await worker('update_members', [], mockIRepositoryOptions) - - const dbMembers = (await new MemberService(mockIRepositoryOptions).findAndCountAll({})).rows - - expect(dbMembers.length).toBe(0) - }) - }) - - describe('Bulk update method for integrations', () => { - it('Should update a single integration', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const integration = { - platform: PlatformType.SLACK, - integrationIdentifier: 'integration1', - status: 'todo', - } - - const dbIntegration = await new IntegrationService(mockIRepositoryOptions).create(integration) - - await worker( - 'update_integrations', - [ - { - id: dbIntegration.id, - update: { status: 'done' }, - }, - ], - mockIRepositoryOptions, - ) - - const dbIntegrations = ( - await new IntegrationService(mockIRepositoryOptions).findAndCountAll({}) - ).rows - expect(dbIntegrations.length).toBe(1) - expect(dbIntegrations[0].status).toBe('done') - }) - - it('Should update a list of integrations', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const integrations = [ - { - platform: PlatformType.SLACK, - integrationIdentifier: 'integration1', - status: 'todo', - }, - { - platform: PlatformType.SLACK, - integrationIdentifier: 'integration2', - status: 'todo', - }, - ] - - const integrationIds = [] - for (const integration of integrations) { - const { id } = await new IntegrationService(mockIRepositoryOptions).create(integration) - integrationIds.push(id) - } - - await worker( - 'update_integrations', - [ - { - id: integrationIds[0], - update: { status: 'done' }, - }, - ], - mockIRepositoryOptions, - ) - - const dbIntegrations = ( - await new IntegrationService(mockIRepositoryOptions).findAndCountAll({}) - ).rows - - expect(dbIntegrations.length).toBe(2) - expect(dbIntegrations[1].status).toBe('done') - expect(dbIntegrations[0].status).toBe('todo') - }) - - it('Should work with an empty list', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await worker('update_integrations', [], mockIRepositoryOptions) - - const dbIntegrations = ( - await new IntegrationService(mockIRepositoryOptions).findAndCountAll({}) - ).rows - - expect(dbIntegrations.length).toBe(0) - }) - }) - - describe('Bulk update method for microservice', () => { - it('Should update a single microservice', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservice = { - type: 'other', - running: false, - init: true, - variant: 'default', - } - - const dbMs = await new MicroserviceService(mockIRepositoryOptions).create(microservice) - - await worker( - 'update_microservices', - [ - { - id: dbMs.id, - update: { running: true }, - }, - ], - mockIRepositoryOptions, - ) - - const dbIntegrations = ( - await new MicroserviceService(mockIRepositoryOptions).findAndCountAll({}) - ).rows - expect(dbIntegrations.length).toBe(1) - expect(dbIntegrations[0].running).toBe(true) - }) - - it('Should update a list of microservices', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservices = [ - { - type: 'other', - running: false, - init: true, - variant: 'default', - }, - { - type: 'member_score', - running: false, - init: true, - variant: 'default', - }, - ] - - const dbMs = await new MicroserviceService(mockIRepositoryOptions).create(microservices[0]) - const dbMs2 = await new MicroserviceService(mockIRepositoryOptions).create(microservices[1]) - - await worker( - 'update_microservices', - [ - { - id: dbMs.id, - update: { running: true }, - }, - { - id: dbMs2.id, - update: { running: true }, - }, - ], - mockIRepositoryOptions, - ) - - const dbIntegrations = ( - await new MicroserviceService(mockIRepositoryOptions).findAndCountAll({}) - ).rows - expect(dbIntegrations.length).toBe(2) - expect(dbIntegrations[0].running).toBe(true) - expect(dbIntegrations[1].running).toBe(true) - }) - - it('Should work with an empty list', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await worker('update_microservices', [], mockIRepositoryOptions) - - const dbIntegrations = ( - await new MicroserviceService(mockIRepositoryOptions).findAndCountAll({}) - ).rows - - expect(dbIntegrations.length).toBe(0) - }) - }) - - describe('Unknown operation', () => { - it('Should throw an error', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await expect(worker('unknownOperation', [], mockIRepositoryOptions)).rejects.toThrow( - 'Operation unknownOperation not found', - ) - }) - }) -}) diff --git a/backend/src/serverless/dbOperations/handler.ts b/backend/src/serverless/dbOperations/handler.ts index 5cac74bc83..a0b253f577 100644 --- a/backend/src/serverless/dbOperations/handler.ts +++ b/backend/src/serverless/dbOperations/handler.ts @@ -1,8 +1,10 @@ import { getServiceChildLogger } from '@crowd/logging' + import { KUBE_MODE } from '../../conf/index' -import bulkOperations from './operationsWorker' -import getUserContext from '../../database/utils/getUserContext' import SegmentRepository from '../../database/repositories/segmentRepository' +import getUserContext from '../../database/utils/getUserContext' + +import bulkOperations from './operationsWorker' const log = getServiceChildLogger('dbOperations.handler') diff --git a/backend/src/serverless/dbOperations/operations.ts b/backend/src/serverless/dbOperations/operations.ts index 76d15128d9..6f8e1af01a 100644 --- a/backend/src/serverless/dbOperations/operations.ts +++ b/backend/src/serverless/dbOperations/operations.ts @@ -9,6 +9,4 @@ export default class Operations { static UPSERT_ACTIVITIES_WITH_MEMBERS: string = 'upsert_activities_with_members' static UPDATE_INTEGRATIONS: string = 'update_integrations' - - static UPDATE_MICROSERVICE: string = 'update_microservices' } diff --git a/backend/src/serverless/dbOperations/operationsWorker.ts b/backend/src/serverless/dbOperations/operationsWorker.ts index baa169dd17..29c0a9e4af 100644 --- a/backend/src/serverless/dbOperations/operationsWorker.ts +++ b/backend/src/serverless/dbOperations/operationsWorker.ts @@ -1,9 +1,9 @@ -import MemberService from '../../services/memberService' -import Operations from './operations' +import { IServiceOptions } from '../../services/IServiceOptions' import ActivityService from '../../services/activityService' import IntegrationService from '../../services/integrationService' -import MicroserviceService from '../../services/microserviceService' -import { IServiceOptions } from '../../services/IServiceOptions' +import MemberService from '../../services/memberService' + +import Operations from './operations' /** * Update a bulk of members @@ -41,13 +41,12 @@ async function upsertMembers(records: Array, options: IServiceOptions): Pro async function upsertActivityWithMembers( records: Array, options: IServiceOptions, - fireCrowdWebhooks: boolean = true, ): Promise { const activityService = new ActivityService(options) while (records.length > 0) { const record = records.shift() - await activityService.createWithMember(record, fireCrowdWebhooks) + await activityService.createWithMember(record) } } @@ -65,19 +64,6 @@ async function updateIntegrations(records: Array, options: IServiceOptions) } } -/** - * Update a bulk of microservices - * @param records The records to perform the operation to - */ -async function updateMicroservice(records: Array, options: IServiceOptions): Promise { - const microserviceService = new MicroserviceService(options) - - while (records.length > 0) { - const record = records.shift() - await microserviceService.update(record.id, record.update) - } -} - /** * Worker function to choose an operation to perform * @param operation Operation to perform, one in the list of Operations @@ -88,7 +74,6 @@ async function bulkOperations( operation: string, records: Array, options: IServiceOptions, - fireCrowdWebhooks: boolean = false, ): Promise { switch (operation) { case Operations.UPDATE_MEMBERS: @@ -98,14 +83,11 @@ async function bulkOperations( return upsertMembers(records, options) case Operations.UPSERT_ACTIVITIES_WITH_MEMBERS: - return upsertActivityWithMembers(records, options, fireCrowdWebhooks) + return upsertActivityWithMembers(records, options) case Operations.UPDATE_INTEGRATIONS: return updateIntegrations(records, options) - case Operations.UPDATE_MICROSERVICE: - return updateMicroservice(records, options) - default: throw new Error(`Operation ${operation} not found`) } diff --git a/backend/src/serverless/dbOperations/workDispatcher.ts b/backend/src/serverless/dbOperations/workDispatcher.ts deleted file mode 100644 index 1005831279..0000000000 --- a/backend/src/serverless/dbOperations/workDispatcher.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { consumer } from './handler' -import { NodeWorkerMessageBase } from '../../types/mq/nodeWorkerMessageBase' - -export const processDbOperationsMessage = async (msg: NodeWorkerMessageBase): Promise => { - await consumer(msg) -} diff --git a/backend/src/serverless/integrations/services/integrationProcessor.ts b/backend/src/serverless/integrations/services/integrationProcessor.ts index 885d9124e2..8682cf284f 100644 --- a/backend/src/serverless/integrations/services/integrationProcessor.ts +++ b/backend/src/serverless/integrations/services/integrationProcessor.ts @@ -1,80 +1,22 @@ import { LoggerBase } from '@crowd/logging' -import { ApiPubSubEmitter, RedisClient } from '@crowd/redis' + import IntegrationRunRepository from '../../../database/repositories/integrationRunRepository' -import IntegrationStreamRepository from '../../../database/repositories/integrationStreamRepository' import { IServiceOptions } from '../../../services/IServiceOptions' -import { NodeWorkerIntegrationProcessMessage } from '../../../types/mq/nodeWorkerIntegrationProcessMessage' -import { IntegrationRunProcessor } from './integrationRunProcessor' + import { IntegrationTickProcessor } from './integrationTickProcessor' -import { DiscourseIntegrationService } from './integrations/discourseIntegrationService' -import { TwitterIntegrationService } from './integrations/twitterIntegrationService' -import { TwitterReachIntegrationService } from './integrations/twitterReachIntegrationService' -import { WebhookProcessor } from './webhookProcessor' export class IntegrationProcessor extends LoggerBase { private readonly tickProcessor: IntegrationTickProcessor - private readonly webhookProcessor: WebhookProcessor - - private readonly runProcessor: IntegrationRunProcessor | undefined - - constructor(options: IServiceOptions, redisEmitterClient?: RedisClient) { + constructor(options: IServiceOptions) { super(options.log) - const integrationServices = [ - new TwitterIntegrationService(), - new TwitterReachIntegrationService(), - new DiscourseIntegrationService(), - ] - - this.log.debug( - { supportedIntegrations: integrationServices.map((i) => i.type) }, - 'Successfully detected supported integrations!', - ) - - let apiPubSubEmitter: ApiPubSubEmitter | undefined - - if (redisEmitterClient) { - apiPubSubEmitter = new ApiPubSubEmitter(redisEmitterClient, this.log) - } - const integrationRunRepository = new IntegrationRunRepository(options) - const integrationStreamRepository = new IntegrationStreamRepository(options) - - this.tickProcessor = new IntegrationTickProcessor( - options, - integrationServices, - integrationRunRepository, - ) - this.webhookProcessor = new WebhookProcessor(options, integrationServices) - - if (apiPubSubEmitter) { - this.runProcessor = new IntegrationRunProcessor( - options, - integrationServices, - integrationRunRepository, - integrationStreamRepository, - apiPubSubEmitter, - ) - } else { - this.log.warn('No apiPubSubEmitter provided, runProcessor will not be initialized!') - } + this.tickProcessor = new IntegrationTickProcessor(options, integrationRunRepository) } async processTick() { await this.tickProcessor.processTick() } - - async processWebhook(webhookId: string, force?: boolean, fireCrowdWebhooks?: boolean) { - await this.webhookProcessor.processWebhook(webhookId, force, fireCrowdWebhooks) - } - - async process(req: NodeWorkerIntegrationProcessMessage) { - if (this.runProcessor) { - await this.runProcessor.process(req) - } else { - throw new Error('runProcessor is not initialized!') - } - } } diff --git a/backend/src/serverless/integrations/services/integrationRunProcessor.ts b/backend/src/serverless/integrations/services/integrationRunProcessor.ts deleted file mode 100644 index afb595f151..0000000000 --- a/backend/src/serverless/integrations/services/integrationRunProcessor.ts +++ /dev/null @@ -1,574 +0,0 @@ -import moment from 'moment' -import { ApiPubSubEmitter } from '@crowd/redis' -import { Logger, getChildLogger, LoggerBase } from '@crowd/logging' -import { singleOrDefault } from '@crowd/common' -import { IntegrationRunState, PlatformType } from '@crowd/types' -import { sendSlackAlert, SlackAlertTypes } from '@crowd/alerting' -import IntegrationRepository from '../../../database/repositories/integrationRepository' -import IntegrationRunRepository from '../../../database/repositories/integrationRunRepository' -import IntegrationStreamRepository from '../../../database/repositories/integrationStreamRepository' -import MicroserviceRepository from '../../../database/repositories/microserviceRepository' -import getUserContext from '../../../database/utils/getUserContext' -import { twitterFollowers } from '../../../database/utils/keys/microserviceTypes' -import { IServiceOptions } from '../../../services/IServiceOptions' -import { - IIntegrationStream, - IProcessStreamResults, - IStepContext, -} from '../../../types/integration/stepResult' -import { IntegrationRun } from '../../../types/integrationRunTypes' -import { NodeWorkerIntegrationProcessMessage } from '../../../types/mq/nodeWorkerIntegrationProcessMessage' -import { IntegrationServiceBase } from './integrationServiceBase' -import SampleDataService from '../../../services/sampleDataService' -import { - DbIntegrationStreamCreateData, - IntegrationStream, - IntegrationStreamState, -} from '../../../types/integrationStreamTypes' -import bulkOperations from '../../dbOperations/operationsWorker' -import UserRepository from '../../../database/repositories/userRepository' -import EmailSender from '../../../services/emailSender' -import { i18n } from '../../../i18n' -import { API_CONFIG, SLACK_ALERTING_CONFIG } from '../../../conf' -import SegmentRepository from '../../../database/repositories/segmentRepository' - -export class IntegrationRunProcessor extends LoggerBase { - constructor( - options: IServiceOptions, - private readonly integrationServices: IntegrationServiceBase[], - private readonly integrationRunRepository: IntegrationRunRepository, - private readonly integrationStreamRepository: IntegrationStreamRepository, - private readonly apiPubSubEmitter?: ApiPubSubEmitter, - ) { - super(options.log) - } - - async process(req: NodeWorkerIntegrationProcessMessage) { - if (!req.runId) { - this.log.warn("No runId provided! Skipping because it's an old message.") - return - } - - this.log.info({ runId: req.runId }, 'Detected integration run!') - - const run = await this.integrationRunRepository.findById(req.runId) - - const userContext = await getUserContext(run.tenantId) - - let integration - - if (run.integrationId) { - integration = await IntegrationRepository.findById(run.integrationId, userContext) - } else if (run.microserviceId) { - const microservice = await MicroserviceRepository.findById(run.microserviceId, userContext) - - switch (microservice.type) { - case twitterFollowers: - integration = await IntegrationRepository.findByPlatform( - PlatformType.TWITTER, - userContext, - ) - break - default: - throw new Error(`Microservice type '${microservice.type}' is not supported!`) - } - } else { - this.log.error({ runId: req.runId }, 'Integration run has no integration or microservice!') - throw new Error(`Integration run '${req.runId}' has no integration or microservice!`) - } - - const segmentRepository = new SegmentRepository(userContext) - userContext.currentSegments = [await segmentRepository.findById(integration.segmentId)] - - const logger = getChildLogger('process', this.log, { - runId: req.runId, - type: integration.platform, - tenantId: integration.tenantId, - integrationId: run.integrationId, - onboarding: run.onboarding, - microserviceId: run.microserviceId, - }) - - logger.info('Processing integration!') - - userContext.log = logger - - // get the relevant integration service that is supposed to be configured already - const intService = singleOrDefault( - this.integrationServices, - (s) => s.type === integration.platform, - ) - if (intService === undefined) { - logger.error('No integration service configured!') - throw new Error(`No integration service configured for type '${integration.platform}'!`) - } - - const stepContext: IStepContext = { - startTimestamp: moment().utc().unix(), - limitCount: integration.limitCount || 0, - onboarding: run.onboarding, - pipelineData: {}, - runId: req.runId, - integration, - serviceContext: userContext, - repoContext: userContext, - logger, - } - - if (!req.streamId) { - const existingRun = await this.integrationRunRepository.findLastProcessingRun( - run.integrationId, - run.microserviceId, - req.runId, - ) - - if (existingRun) { - logger.info('Integration is already being processed!') - await this.integrationRunRepository.markError(req.runId, { - errorPoint: 'check_existing_run', - message: 'Integration is already being processed!', - existingRunId: existingRun.id, - }) - return - } - - if (run.state === IntegrationRunState.PROCESSED) { - logger.warn('Integration is already processed!') - return - } - - if (run.state === IntegrationRunState.PENDING) { - logger.info('Started processing integration!') - } else if (run.state === IntegrationRunState.DELAYED) { - logger.info('Continued processing delayed integration!') - } else if (run.state === IntegrationRunState.ERROR) { - logger.info('Restarted processing errored integration!') - } else if (run.state === IntegrationRunState.PROCESSING) { - throw new Error(`Invalid state '${run.state}' for integration run!`) - } - - await this.integrationRunRepository.markProcessing(req.runId) - run.state = IntegrationRunState.PROCESSING - - if (integration.settings.updateMemberAttributes) { - logger.trace('Updating member attributes!') - - await intService.createMemberAttributes(stepContext) - - integration.settings.updateMemberAttributes = false - await IntegrationRepository.update( - integration.id, - { settings: integration.settings }, - userContext, - ) - } - - // delete sample data on onboarding - if (run.onboarding) { - try { - await new SampleDataService(userContext).deleteSampleData() - } catch (err) { - logger.error(err, { tenantId: integration.tenantId }, 'Error deleting sample data!') - await this.integrationRunRepository.markError(req.runId, { - errorPoint: 'delete_sample_data', - message: err.message, - stack: err.stack, - errorString: JSON.stringify(err), - }) - return - } - } - } - - try { - // check global limit reset - if (intService.limitResetFrequencySeconds > 0 && integration.limitLastResetAt) { - const secondsSinceLastReset = moment() - .utc() - .diff(moment(integration.limitLastResetAt).utc(), 'seconds') - - if (secondsSinceLastReset >= intService.limitResetFrequencySeconds) { - integration.limitCount = 0 - integration.limitLastResetAt = moment().utc().toISOString() - - await IntegrationRepository.update( - integration.id, - { - limitCount: integration.limitCount, - limitLastResetAt: integration.limitLastResetAt, - }, - userContext, - ) - } - } - - // preprocess if needed - logger.trace('Preprocessing integration!') - try { - await intService.preprocess(stepContext) - } catch (err) { - if (err.rateLimitResetSeconds) { - // need to delay integration processing - logger.warn(err, 'Rate limit reached while preprocessing integration! Delaying...') - await this.handleRateLimitError(logger, run, err.rateLimitResetSeconds, stepContext) - return - } - - logger.error(err, 'Error preprocessing integration!') - await this.integrationRunRepository.markError(req.runId, { - errorPoint: 'preprocessing', - message: err.message, - stack: err.stack, - errorString: JSON.stringify(err), - }) - return - } - - // detect streams to process for this integration - - let forcedStream: IntegrationStream | undefined - if (req.streamId) { - forcedStream = await this.integrationStreamRepository.findById(req.streamId) - - if (!forcedStream) { - logger.error({ streamId: req.streamId }, 'Stream not found!') - throw new Error(`Stream '${req.streamId}' not found!`) - } - } else { - const dbStreams = await this.integrationStreamRepository.findByRunId(req.runId, 1, 1) - if (dbStreams.length > 0) { - logger.trace('Streams already detected and saved to the database!') - } else { - // need to optimize this as well since it may happen that we have a lot of streams - logger.trace('Detecting streams!') - try { - const pendingStreams = await intService.getStreams(stepContext) - const createStreams: DbIntegrationStreamCreateData[] = pendingStreams.map((s) => ({ - runId: req.runId, - tenantId: run.tenantId, - integrationId: run.integrationId, - microserviceId: run.microserviceId, - name: s.value, - metadata: s.metadata, - })) - await this.integrationStreamRepository.bulkCreate(createStreams) - await this.integrationRunRepository.touch(run.id) - } catch (err) { - if (err.rateLimitResetSeconds) { - // need to delay integration processing - logger.warn(err, 'Rate limit reached while getting integration streams! Delaying...') - await this.handleRateLimitError(logger, run, err.rateLimitResetSeconds, stepContext) - return - } - - throw err - } - } - } - - // process streams - let processedCount = 0 - let notifyCount = 0 - - let nextStream: IntegrationStream | undefined - if (forcedStream) { - nextStream = forcedStream - } else { - nextStream = await this.integrationStreamRepository.getNextStreamToProcess(req.runId) - } - - while (nextStream) { - if ((req as any).exiting) { - if (!run.onboarding) { - logger.warn('Stopped processing integration (not onboarding)!') - break - } else { - logger.warn('Stopped processing integration (onboarding)!') - const delayUntil = moment() - .add(3 * 60, 'seconds') - .toDate() - await this.integrationRunRepository.delay(req.runId, delayUntil) - break - } - } - - const stream: IIntegrationStream = { - id: nextStream.id, - value: nextStream.name, - metadata: nextStream.metadata, - } - - processedCount++ - notifyCount++ - - let processStreamResult: IProcessStreamResults - - logger.trace({ streamId: stream.id }, 'Processing stream!') - await this.integrationStreamRepository.markProcessing(stream.id) - await this.integrationRunRepository.touch(run.id) - try { - processStreamResult = await intService.processStream(stream, stepContext) - } catch (err) { - if (err.rateLimitResetSeconds) { - logger.warn( - { streamId: stream.id, message: err.message }, - 'Rate limit reached while processing stream! Delaying...', - ) - await this.handleRateLimitError( - logger, - run, - err.rateLimitResetSeconds, - stepContext, - stream, - ) - return - } - - const retries = await this.integrationStreamRepository.markError(stream.id, { - errorPoint: 'process_stream', - message: err.message, - stack: err.stack, - errorString: JSON.stringify(err), - }) - await this.integrationRunRepository.touch(run.id) - - logger.error(err, { retries, streamId: stream.id }, 'Error while processing stream!') - } - - if (processStreamResult) { - // surround with try catch so if one stream fails we try all of them as well just in case - try { - logger.trace({ stream: JSON.stringify(stream) }, `Processing stream results!`) - - if (processStreamResult.newStreams && processStreamResult.newStreams.length > 0) { - const dbCreateStreams: DbIntegrationStreamCreateData[] = - processStreamResult.newStreams.map((s) => ({ - runId: req.runId, - tenantId: run.tenantId, - integrationId: run.integrationId, - microserviceId: run.microserviceId, - name: s.value, - metadata: s.metadata, - })) - - await this.integrationStreamRepository.bulkCreate(dbCreateStreams) - await this.integrationRunRepository.touch(run.id) - - logger.info( - `Detected ${processStreamResult.newStreams.length} new streams to process!`, - ) - } - - for (const operation of processStreamResult.operations) { - if (operation.records.length > 0) { - logger.trace( - { operationType: operation.type }, - `Processing bulk operation with ${operation.records.length} records!`, - ) - stepContext.limitCount += operation.records.length - await bulkOperations( - operation.type, - operation.records, - userContext, - req.fireCrowdWebhooks ?? true, - ) - } - } - - if (processStreamResult.nextPageStream !== undefined) { - if ( - !run.onboarding && - (await intService.isProcessingFinished( - stepContext, - stream, - processStreamResult.operations, - processStreamResult.lastRecordTimestamp, - )) - ) { - logger.warn('Integration processing finished because of service implementation!') - } else { - logger.trace( - { currentStream: JSON.stringify(stream) }, - `Detected next page stream!`, - ) - await this.integrationStreamRepository.create({ - runId: req.runId, - tenantId: run.tenantId, - integrationId: run.integrationId, - microserviceId: run.microserviceId, - name: processStreamResult.nextPageStream.value, - metadata: processStreamResult.nextPageStream.metadata, - }) - await this.integrationRunRepository.touch(run.id) - } - } - - if (processStreamResult.sleep !== undefined && processStreamResult.sleep > 0) { - logger.warn( - `Stream processing resulted in a requested delay of ${processStreamResult.sleep}! Will delay remaining streams!`, - ) - - const delayUntil = moment().add(processStreamResult.sleep, 'seconds').toDate() - await this.integrationRunRepository.delay(req.runId, delayUntil) - break - } - - if (intService.globalLimit > 0 && stepContext.limitCount >= intService.globalLimit) { - // if limit reset frequency is 0 we don't need to care about limits - if (intService.limitResetFrequencySeconds > 0) { - logger.warn( - { - limitCount: stepContext.limitCount, - globalLimit: intService.globalLimit, - }, - 'We reached a global limit - stopping processing!', - ) - - integration.limitCount = stepContext.limitCount - - const secondsSinceLastReset = moment() - .utc() - .diff(moment(integration.limitLastResetAt).utc(), 'seconds') - - if (secondsSinceLastReset < intService.limitResetFrequencySeconds) { - const delayUntil = moment() - .add(intService.limitResetFrequencySeconds - secondsSinceLastReset, 'seconds') - .toDate() - await this.integrationRunRepository.delay(req.runId, delayUntil) - } - - break - } - } - - if (notifyCount === 50) { - logger.info(`Processed ${processedCount} streams!`) - notifyCount = 0 - } - - await this.integrationStreamRepository.markProcessed(stream.id) - await this.integrationRunRepository.touch(run.id) - } catch (err) { - logger.error( - err, - { stream: JSON.stringify(stream) }, - 'Error processing stream results!', - ) - await this.integrationStreamRepository.markError(stream.id, { - errorPoint: 'process_stream_results', - message: err.message, - stack: err.stack, - errorString: JSON.stringify(err), - }) - await this.integrationRunRepository.touch(run.id) - } - } - - if (forcedStream) { - break - } - - nextStream = await this.integrationStreamRepository.getNextStreamToProcess(req.runId) - } - - // postprocess integration settings - await intService.postprocess(stepContext) - - logger.info('Done processing integration!') - } catch (err) { - logger.error(err, 'Error while processing integration!') - } finally { - const newState = await this.integrationRunRepository.touchState(req.runId) - - let emailSentAt - if (newState === IntegrationRunState.PROCESSED) { - if (!integration.emailSentAt) { - const tenantUsers = await UserRepository.findAllUsersOfTenant(integration.tenantId) - emailSentAt = new Date() - for (const user of tenantUsers) { - await new EmailSender(EmailSender.TEMPLATES.INTEGRATION_DONE, { - integrationName: i18n('en', `entities.integration.name.${integration.platform}`), - link: API_CONFIG.frontendUrl, - }).sendTo(user.email) - } - } - } - - let status - switch (newState) { - case IntegrationRunState.PROCESSED: - status = 'done' - break - case IntegrationRunState.ERROR: - status = 'error' - break - default: - status = integration.status - } - - await IntegrationRepository.update( - integration.id, - { - status, - emailSentAt, - settings: stepContext.integration.settings, - refreshToken: stepContext.integration.refreshToken, - token: stepContext.integration.token, - }, - userContext, - ) - - if (newState === IntegrationRunState.PROCESSING && !req.streamId) { - const failedStreams = await this.integrationStreamRepository.findByRunId(req.runId, 1, 1, [ - IntegrationStreamState.ERROR, - ]) - if (failedStreams.length > 0) { - logger.warn('Integration ended but we are still processing - delaying for a minute!') - const delayUntil = moment().add(60, 'seconds') - await this.integrationRunRepository.delay(run.id, delayUntil.toDate()) - } else { - logger.error('Integration ended but we are still processing!') - } - } else if (newState === IntegrationRunState.ERROR) { - await sendSlackAlert({ - slackURL: SLACK_ALERTING_CONFIG.url, - alertType: SlackAlertTypes.INTEGRATION_ERROR, - integration, - userContext, - log: logger, - frameworkVersion: 'old', - }) - } - - if (run.onboarding && this.apiPubSubEmitter) { - this.apiPubSubEmitter.emitIntegrationCompleted(integration.tenantId, integration.id, status) - } - } - } - - private async handleRateLimitError( - logger: Logger, - run: IntegrationRun, - rateLimitResetSeconds: number, - context: IStepContext, - stream?: IIntegrationStream, - ): Promise { - await IntegrationRepository.update( - context.integration.id, - { - settings: context.integration.settings, - refreshToken: context.integration.refreshToken, - token: context.integration.token, - }, - context.repoContext, - ) - - logger.warn('Rate limit reached, delaying integration processing!') - const delayUntil = moment().add(rateLimitResetSeconds + 30, 'seconds') - await this.integrationRunRepository.delay(run.id, delayUntil.toDate()) - - if (stream) { - await this.integrationStreamRepository.reset(stream.id) - } - } -} diff --git a/backend/src/serverless/integrations/services/integrationServiceBase.ts b/backend/src/serverless/integrations/services/integrationServiceBase.ts deleted file mode 100644 index 6e459dbe0a..0000000000 --- a/backend/src/serverless/integrations/services/integrationServiceBase.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { SuperfaceClient } from '@superfaceai/one-sdk' -import moment from 'moment' -import crypto from 'crypto' -import { getServiceChildLogger } from '@crowd/logging' -import { IntegrationRunState, IntegrationType } from '@crowd/types' -import { IRepositoryOptions } from '../../../database/repositories/IRepositoryOptions' -import { - IIntegrationStream, - IPendingStream, - IProcessStreamResults, - IProcessWebhookResults, - IStepContext, - IStreamResultOperation, -} from '../../../types/integration/stepResult' -import { IS_TEST_ENV } from '../../../conf' -import { sendNodeWorkerMessage } from '../../utils/nodeWorkerSQS' -import { NodeWorkerIntegrationProcessMessage } from '../../../types/mq/nodeWorkerIntegrationProcessMessage' -import IntegrationRunRepository from '../../../database/repositories/integrationRunRepository' - -const logger = getServiceChildLogger('integrationService') - -/* eslint class-methods-use-this: 0 */ - -/* eslint-disable @typescript-eslint/no-unused-vars */ - -export abstract class IntegrationServiceBase { - /** - * How many records to process before we stop - */ - public globalLimit: number - - /** - * If onboarding globalLimit will be multiplied by this factor for that run - */ - public onboardingLimitModifierFactor: number - - /** - * How many seconds between global limit reset (0 for auto reset) - */ - public limitResetFrequencySeconds: number - - /** - * Every new integration should extend this class and implement its methods. - * - * @param type What integration is this? - * @param ticksBetweenChecks How many ticks to skip between each integration checks (each tick is 1 minute). If 0 it will be triggered every tick same as if it was 1. If negative it will never be triggered. - */ - protected constructor( - public readonly type: IntegrationType, - public readonly ticksBetweenChecks: number, - ) { - this.globalLimit = 0 - this.onboardingLimitModifierFactor = 1.0 - this.limitResetFrequencySeconds = 0 - } - - async triggerIntegrationCheck(integrations: any[], options: IRepositoryOptions): Promise { - const repository = new IntegrationRunRepository(options) - - for (const integration of integrations) { - const run = await repository.create({ - integrationId: integration.id, - tenantId: integration.tenantId, - onboarding: false, - state: IntegrationRunState.PENDING, - }) - - logger.info( - { integrationId: integration.id, runId: run.id }, - 'Triggering integration processing!', - ) - await sendNodeWorkerMessage( - integration.tenantId, - new NodeWorkerIntegrationProcessMessage(run.id), - ) - } - } - - async preprocess(context: IStepContext): Promise { - // do nothing - override if something is needed - } - - async createMemberAttributes(context: IStepContext): Promise { - // do nothing - override if something is needed - } - - abstract getStreams(context: IStepContext): Promise - - abstract processStream( - stream: IIntegrationStream, - context: IStepContext, - ): Promise - - async isProcessingFinished( - context: IStepContext, - currentStream: IIntegrationStream, - lastOperations: IStreamResultOperation[], - lastRecord?: any, - lastRecordTimestamp?: number, - ): Promise { - return false - } - - async postprocess(context: IStepContext): Promise { - // do nothing - override if something is needed - } - - async processWebhook(webhook: any, context: IStepContext): Promise { - throw new Error('Not implemented') - } - - static superfaceClient(): SuperfaceClient { - if (IS_TEST_ENV) { - return undefined - } - - return new SuperfaceClient() - } - - /** - * Check whether the last record is over the retrospect that we are interested in - * @param lastRecordTimestamp The last activity timestamp we got - * @param startTimestamp The timestamp when we started - * @param maxRetrospect The maximum time we want to crawl - * @returns Whether we are over the retrospect already - */ - static isRetrospectOver( - lastRecordTimestamp: number, - startTimestamp: number, - maxRetrospect: number, - ): boolean { - return startTimestamp - moment(lastRecordTimestamp).unix() > maxRetrospect - } - - /** - * Some activities will not have a remote(API) counterparts so they will miss sourceIds. - * Since we're using sourceIds to find out if an activity already exists in our DB, - * sourceIds are required when creating an activity. - * This function generates an md5 hash that can be used as a sourceId of an activity. - * Prepends string `gen-` to the beginning so generated and remote sourceIds - * can be distinguished. - * - * @param {string} uniqueRemoteId remote member id from an integration. This id needs to be unique in a platform - * @param {string} type type of the activity - * @param {string} timestamp unix timestamp of the activity - * @param {string} platform platform of the activity - * @returns 32 bit md5 hash generated from the given data, prepended with string `gen-` - */ - static generateSourceIdHash( - uniqueRemoteId: string, - type: string, - timestamp: string, - platform: string, - ) { - if (!uniqueRemoteId || !type || !timestamp || !platform) { - throw new Error('Bad hash input') - } - - const data = `${uniqueRemoteId}-${type}-${timestamp}-${platform}` - return `gen-${crypto.createHash('md5').update(data).digest('hex')}` - } - - /** - * Get the number of seconds from a date to a unix timestamp. - * Adding a 25% padding for security. - * If the unix timestamp is before the date, return 3 minutes for security - * @param date The date to get the seconds from - * @param unixTimestamp The unix timestamp to get the seconds from - * @returns The number of seconds from the date to the unix timestamp - */ - static secondsUntilTimestamp( - unixTimestamp: number, - date: Date = moment().utc().toDate(), - ): number { - const timestampedDate: number = moment.utc(date).unix() - if (timestampedDate > unixTimestamp) { - return 60 * 3 - } - return Math.floor(unixTimestamp - timestampedDate) - } -} diff --git a/backend/src/serverless/integrations/services/integrationTickProcessor.ts b/backend/src/serverless/integrations/services/integrationTickProcessor.ts index cbf427ec34..f3441571e7 100644 --- a/backend/src/serverless/integrations/services/integrationTickProcessor.ts +++ b/backend/src/serverless/integrations/services/integrationTickProcessor.ts @@ -1,21 +1,22 @@ import { processPaginated, singleOrDefault } from '@crowd/common' +import { + DataSinkWorkerEmitter, + IntegrationRunWorkerEmitter, + IntegrationStreamWorkerEmitter, +} from '@crowd/common_services' import { INTEGRATION_SERVICES } from '@crowd/integrations' import { LoggerBase, getChildLogger } from '@crowd/logging' -import { IntegrationRunWorkerEmitter, IntegrationStreamWorkerEmitter } from '@crowd/sqs' -import { IntegrationRunState, IntegrationType } from '@crowd/types' -import SequelizeRepository from '@/database/repositories/sequelizeRepository' -import MicroserviceRepository from '@/database/repositories/microserviceRepository' +import { IntegrationType } from '@crowd/types' + import IntegrationRepository from '@/database/repositories/integrationRepository' -import { IRepositoryOptions } from '@/database/repositories/IRepositoryOptions' + import IntegrationRunRepository from '../../../database/repositories/integrationRunRepository' import { IServiceOptions } from '../../../services/IServiceOptions' -import { NodeWorkerIntegrationProcessMessage } from '../../../types/mq/nodeWorkerIntegrationProcessMessage' -import { sendNodeWorkerMessage } from '../../utils/nodeWorkerSQS' import { + getDataSinkWorkerEmitter, getIntegrationRunWorkerEmitter, getIntegrationStreamWorkerEmitter, -} from '../../utils/serviceSQS' -import { IntegrationServiceBase } from './integrationServiceBase' +} from '../../utils/queueService' export class IntegrationTickProcessor extends LoggerBase { private tickTrackingMap: Map = new Map() @@ -26,17 +27,14 @@ export class IntegrationTickProcessor extends LoggerBase { private intStreamWorkerEmitter: IntegrationStreamWorkerEmitter + private dataSinkWorkerEmitter: DataSinkWorkerEmitter + constructor( options: IServiceOptions, - private readonly integrationServices: IntegrationServiceBase[], private readonly integrationRunRepository: IntegrationRunRepository, ) { super(options.log) - for (const intService of this.integrationServices) { - this.tickTrackingMap[intService.type] = 0 - } - for (const intService of INTEGRATION_SERVICES) { this.tickTrackingMap[intService.type] = 0 } @@ -46,6 +44,7 @@ export class IntegrationTickProcessor extends LoggerBase { if (!this.emittersInitialized) { this.intRunWorkerEmitter = await getIntegrationRunWorkerEmitter() this.intStreamWorkerEmitter = await getIntegrationStreamWorkerEmitter() + this.dataSinkWorkerEmitter = await getDataSinkWorkerEmitter() this.emittersInitialized = true } @@ -59,18 +58,11 @@ export class IntegrationTickProcessor extends LoggerBase { private async processCheckTick() { this.log.trace('Processing integration processor tick!') - const tickers: IIntTicker[] = this.integrationServices.map((i) => ({ + const tickers: IIntTicker[] = INTEGRATION_SERVICES.map((i) => ({ type: i.type, - ticksBetweenChecks: i.ticksBetweenChecks, + ticksBetweenChecks: i.checkEvery || -1, })) - for (const service of INTEGRATION_SERVICES) { - tickers.push({ - type: service.type, - ticksBetweenChecks: service.checkEvery || -1, - }) - } - const promises: Promise[] = [] for (const intService of tickers) { @@ -109,134 +101,82 @@ export class IntegrationTickProcessor extends LoggerBase { } } + async fixIntegrationRuns(integrationId: string, logger) { + await this.integrationRunRepository.cleanupOrphanedIntegrationRuns(integrationId) + + const stuckRuns = await this.integrationRunRepository.findStuckIntegrationRuns(integrationId) + logger.info( + `${stuckRuns.length} stuck integration runs found for integrations ${integrationId}`, + ) + await this.initEmitters() + for (const run of stuckRuns) { + logger.info(`Retrying streams for stuck run: ${run.id}`) + await this.intStreamWorkerEmitter.continueProcessingRunStreams( + run.onboarding, + undefined, + run.id, + ) + } + } + async processCheck(type: IntegrationType) { const logger = getChildLogger('processCheck', this.log, { IntegrationType: type }) logger.trace('Processing integration check!') - if (type === IntegrationType.TWITTER_REACH) { - await processPaginated( - async (page) => MicroserviceRepository.findAllByType('twitter_followers', page, 10), - async (microservices) => { - this.log.debug({ type, count: microservices.length }, 'Found microservices to check!') - for (const micro of microservices) { - const existingRun = await this.integrationRunRepository.findLastProcessingRun( - undefined, - micro.id, - ) - if (!existingRun) { - const microservice = micro as any - - const run = await this.integrationRunRepository.create({ - microserviceId: microservice.id, - tenantId: microservice.tenantId, - onboarding: false, - state: IntegrationRunState.PENDING, - }) - - this.log.debug({ type, runId: run.id }, 'Triggering microservice processing!') - - await sendNodeWorkerMessage( - microservice.tenantId, - new NodeWorkerIntegrationProcessMessage(run.id), - ) - } - } - }, - ) - } else { - const options = - (await SequelizeRepository.getDefaultIRepositoryOptions()) as IRepositoryOptions - - // get the relevant integration service that is supposed to be configured already - const intService = singleOrDefault(this.integrationServices, (s) => s.type === type) - - if (intService) { - await processPaginated( - async (page) => IntegrationRepository.findAllActive(type, page, 10), - async (integrations) => { - logger.debug( - { integrationIds: integrations.map((i) => i.id) }, - 'Found old integrations to check!', - ) - const inactiveIntegrations: any[] = [] - for (const integration of integrations as any[]) { - const existingRun = await this.integrationRunRepository.findLastProcessingRun( - integration.id, - ) - if (!existingRun) { - inactiveIntegrations.push(integration) - } - } + const newIntService = singleOrDefault(INTEGRATION_SERVICES, (i) => i.type === type) - if (inactiveIntegrations.length > 0) { - logger.info( - { integrationIds: inactiveIntegrations.map((i) => i.id) }, - 'Triggering old integration checks!', - ) - await intService.triggerIntegrationCheck(inactiveIntegrations, options) - } - }, - ) - } else { - const newIntService = singleOrDefault(INTEGRATION_SERVICES, (i) => i.type === type) + if (!newIntService) { + throw new Error(`No integration service found for type ${type}!`) + } - if (!newIntService) { - throw new Error(`No integration service found for type ${type}!`) - } + const emitter = await getIntegrationRunWorkerEmitter() - const emitter = await getIntegrationRunWorkerEmitter() - - await processPaginated( - async (page) => IntegrationRepository.findAllActive(type, page, 10), - async (integrations) => { - logger.debug( - { integrationIds: integrations.map((i) => i.id) }, - 'Found new integrations to check!', - ) - for (const integration of integrations as any[]) { - const existingRun = - await this.integrationRunRepository.findLastProcessingRunInNewFramework( - integration.id, - ) - if (!existingRun) { - logger.info({ integrationId: integration.id }, 'Triggering new integration check!') - await emitter.triggerIntegrationRun( - integration.tenantId, - integration.platform, - integration.id, - false, + await processPaginated( + async (page) => IntegrationRepository.findAllActive(type, page, 10), + async (integrations) => { + logger.debug( + { integrationIds: integrations.map((i) => i.id) }, + 'Found new integrations to check!', + ) + for (const integration of integrations as any[]) { + await this.fixIntegrationRuns(integration.id, logger) + const existingRun = + await this.integrationRunRepository.findLastProcessingRunInNewFramework(integration.id) + if (!existingRun) { + const CHUNKS = 3 // Define the number of chunks + const DELAY_BETWEEN_CHUNKS = 30 * 60 * 1000 // Define the delay between chunks in milliseconds + const rand = Math.random() * CHUNKS + const chunkIndex = Math.min(Math.floor(rand), CHUNKS - 1) + const delay = chunkIndex * DELAY_BETWEEN_CHUNKS + + // Divide integrations into chunks for Discord + if (newIntService.type === IntegrationType.DISCORD) { + setTimeout(async () => { + logger.info( + { integrationId: integration.id }, + `Triggering new delayed integration check for Discord in ${ + delay / 60 / 1000 + } minutes!`, ) - } else { - logger.info({ integrationId: integration.id }, 'Existing run found, skipping!') - } + await emitter.triggerIntegrationRun(integration.platform, integration.id, false) + }, delay) + } else { + logger.info({ integrationId: integration.id }, 'Triggering new integration check!') + await emitter.triggerIntegrationRun(integration.platform, integration.id, false) } - }, - ) - } - } + } else { + logger.info({ integrationId: integration.id }, 'Existing run found, skipping!') + } + } + }, + ) } private async processDelayedTick() { await this.initEmitters() await this.intRunWorkerEmitter.checkRuns() await this.intStreamWorkerEmitter.checkStreams() - - // TODO check streams as well - this.log.trace('Checking for delayed integration runs!') - - await processPaginated( - async (page) => this.integrationRunRepository.findDelayedRuns(page, 10), - async (delayedRuns) => { - for (const run of delayedRuns) { - this.log.info({ runId: run.id }, 'Triggering delayed integration run processing!') - - await sendNodeWorkerMessage( - new Date().toISOString(), - new NodeWorkerIntegrationProcessMessage(run.id), - ) - } - }, - ) + await this.dataSinkWorkerEmitter.checkResults() } } diff --git a/backend/src/serverless/integrations/services/integrations/discourseIntegrationService.ts b/backend/src/serverless/integrations/services/integrations/discourseIntegrationService.ts deleted file mode 100644 index f9689781bd..0000000000 --- a/backend/src/serverless/integrations/services/integrations/discourseIntegrationService.ts +++ /dev/null @@ -1,623 +0,0 @@ -import { - DISCOURSE_GRID, - DISCOURSE_MEMBER_ATTRIBUTES, - DiscourseActivityType, -} from '@crowd/integrations' -import { PlatformType, IntegrationType, MemberAttributeName } from '@crowd/types' -import he from 'he' -import moment from 'moment/moment' -import sanitizeHtml from 'sanitize-html' -import MemberAttributeSettingsService from '../../../../services/memberAttributeSettingsService' -import { - IIntegrationStream, - IPendingStream, - IProcessStreamResults, - IProcessWebhookResults, - IStepContext, -} from '../../../../types/integration/stepResult' -import Operations from '../../../dbOperations/operations' -import { - DiscourseCategoryResponse, - DiscourseConnectionParams, - DiscoursePostsByIdsInput, - DiscoursePostsByIdsResponse, - DiscoursePostsFromTopicResponse, - DiscoursePostsInput, - DiscourseTopicResponse, - DiscourseTopicsInput, - DiscourseUserResponse, - DiscourseWebhookNotification, - DiscourseWebhookPost, - DiscourseWebhookUser, -} from '../../types/discourseTypes' -import type { PlatformIdentities } from '../../types/messageTypes' -import { AddActivitiesSingle, Member } from '../../types/messageTypes' -import { getDiscourseCategories } from '../../usecases/discourse/getCategories' -import { getDiscoursePostsByIds } from '../../usecases/discourse/getPostsByIds' -import { getDiscoursePostsFromTopic } from '../../usecases/discourse/getPostsFromTopic' -import { getDiscourseTopics } from '../../usecases/discourse/getTopics' -import { getDiscourseUserByUsername } from '../../usecases/discourse/getUser' -import { IntegrationServiceBase } from '../integrationServiceBase' - -const BOT_USERNAMES = ['system', 'discobot'] - -const usernameIsBot = (username: string): boolean => BOT_USERNAMES.includes(username) - -enum DiscourseStreamType { - CATEGORIES = 'categories', - TOPICS_FROM_CATEGORY = 'topicsFromCategory', - POSTS_FROM_TOPIC = 'postsFromTopic', - POSTS_BY_IDS = 'postsByIds', -} - -enum DiscourseWebhookType { - POST_CREATED = 'post_created', - USER_CREATED = 'user_created', - LIKED_A_POST = 'notification_created', -} - -interface DiscourseProcessResult { - type: DiscourseStreamType - data: - | DiscourseCategoryResponse - | DiscourseTopicResponse - | DiscoursePostsFromTopicResponse - | DiscoursePostsByIdsResponse -} - -/* eslint class-methods-use-this: 0 */ - -/* eslint-disable @typescript-eslint/no-unused-vars */ - -/* eslint-disable no-case-declarations */ - -export class DiscourseIntegrationService extends IntegrationServiceBase { - constructor() { - // disable polling - new data will come trhough webhooks - super(IntegrationType.DISCOURSE, -1) - } - - async createMemberAttributes(context: IStepContext): Promise { - const service = new MemberAttributeSettingsService(context.repoContext) - await service.createPredefined(DISCOURSE_MEMBER_ATTRIBUTES) - } - - /** - * Set up the pipeline data that will be needed throughout the processing. - * @param context context passed along worker messages - */ - async preprocess(context: IStepContext): Promise { - const settings = context.integration.settings - context.pipelineData = { - apiKey: settings.apiKey, - apiUsername: settings.apiUsername, - forumHostname: settings.forumHostname, - } - } - - async getStreams(context: IStepContext): Promise { - return [ - { - value: DiscourseStreamType.CATEGORIES, - metadata: { - page: '', - }, - }, - ] - } - - async processStream( - stream: IIntegrationStream, - context: IStepContext, - ): Promise { - const streamType = stream.value as DiscourseStreamType - let result: DiscourseProcessResult - switch (streamType) { - case DiscourseStreamType.CATEGORIES: - result = await DiscourseIntegrationService.processCategoriesStream(stream, context) - break - case DiscourseStreamType.TOPICS_FROM_CATEGORY: - result = await DiscourseIntegrationService.processTopicsFromCategoryStream(stream, context) - break - case DiscourseStreamType.POSTS_FROM_TOPIC: - result = await DiscourseIntegrationService.processPostsFromTopicStream(stream, context) - break - case DiscourseStreamType.POSTS_BY_IDS: - result = await DiscourseIntegrationService.processPostsByIds(stream, context) - break - default: - throw new Error(`Unknown stream type: ${streamType}`) - } - - const newStreams: IPendingStream[] = [] - let nextPageStream: IPendingStream | undefined - const activities: AddActivitiesSingle[] = [] - - // another switch statement to handle the different types of results, helps with type safety - switch (result.type) { - case DiscourseStreamType.CATEGORIES: - const data = result.data as DiscourseCategoryResponse - data.category_list.categories.forEach((category) => { - newStreams.push({ - value: DiscourseStreamType.TOPICS_FROM_CATEGORY, - metadata: { - category_id: category.id, - category_slug: category.slug, - page: 0, - }, - }) - }) - break - case DiscourseStreamType.TOPICS_FROM_CATEGORY: - const data2 = result.data as DiscourseTopicResponse - if (data2?.topic_list?.topics?.length > 0) { - data2.topic_list.topics.forEach((topic) => { - newStreams.push({ - value: DiscourseStreamType.POSTS_FROM_TOPIC, - metadata: { - topicId: topic.id, - topic_slug: topic.slug, - page: 0, - }, - }) - }) - - // we aslo need to trigger nextPageStream - nextPageStream = { - value: DiscourseStreamType.TOPICS_FROM_CATEGORY, - metadata: { - category_id: stream.metadata.category_id, - category_slug: stream.metadata.category_slug, - page: stream.metadata.page + 1, - }, - } - } - break - case DiscourseStreamType.POSTS_FROM_TOPIC: - const data3 = result.data as DiscoursePostsFromTopicResponse - const batchSize = 30 - const postBatches: number[][] = [] - - data3?.post_stream?.stream?.forEach((postId, index) => { - if (index % batchSize === 0) { - postBatches.push([]) - } - postBatches[postBatches.length - 1].push(postId) - }) - - postBatches.forEach((postBatch, index) => { - newStreams.push({ - value: DiscourseStreamType.POSTS_BY_IDS, - metadata: { - topicId: data3.id, - topicSlug: data3.slug, - topicTitle: data3.title, - postIds: postBatch, - lastIdInPreviousBatch: - index === 0 ? undefined : postBatches[index - 1][postBatches[index - 1].length - 1], - }, - }) - }) - - break - case DiscourseStreamType.POSTS_BY_IDS: - // no new streams to launch - // just add the activities - const data4 = result.data as DiscoursePostsByIdsResponse - const { topicId, lastIdInPreviousBatch } = stream.metadata - const posts = data4?.post_stream?.posts - for (const post of posts) { - if (usernameIsBot(post.username)) { - /* eslint-disable no-continue */ - continue - } - const user = await getDiscourseUserByUsername( - { - forumHostname: context.pipelineData.forumHostname, - apiKey: context.pipelineData.apiKey, - apiUsername: context.pipelineData.apiUsername, - }, - { username: post.username }, - context.logger, - ) - - if (!user) { - return { - operations: [], - } - } - - const member = DiscourseIntegrationService.parseUserIntoMember( - user, - context.pipelineData.forumHostname, - context, - ) - - const activity: AddActivitiesSingle = { - member, - username: member.username[PlatformType.DISCOURSE].username, - platform: PlatformType.DISCOURSE, - tenant: context.integration.tenantId, - sourceId: `${topicId}-${post.post_number}`, - sourceParentId: post.post_number === 1 ? null : `${topicId}-${post.post_number - 1}`, - type: - post.post_number === 1 - ? DiscourseActivityType.CREATE_TOPIC - : DiscourseActivityType.MESSAGE_IN_TOPIC, - timestamp: moment(post.created_at).utc().toDate(), - body: sanitizeHtml(he.decode(post.cooked)), - title: post.post_number === 1 ? stream.metadata.topicTitle : null, - url: `${context.pipelineData.forumHostname}/t/${stream.metadata.topicSlug}/${topicId}/${post.post_number}`, - channel: stream.metadata.topicTitle, - score: - post.post_number === 1 - ? DISCOURSE_GRID[DiscourseActivityType.CREATE_TOPIC].score - : DISCOURSE_GRID[DiscourseActivityType.MESSAGE_IN_TOPIC].score, - isContribution: - post.post_number === 1 - ? DISCOURSE_GRID[DiscourseActivityType.CREATE_TOPIC].isContribution - : DISCOURSE_GRID[DiscourseActivityType.MESSAGE_IN_TOPIC].isContribution, - } - activities.push(activity) - } - break - default: - throw new Error(`Unknown stream type: ${streamType}`) - } - - return { - operations: [ - { - type: Operations.UPSERT_ACTIVITIES_WITH_MEMBERS, - records: activities, - }, - ], - newStreams, - nextPageStream, - } - } - - static async processCategoriesStream( - stream: IIntegrationStream, - context: IStepContext, - ): Promise { - const { forumHostname, apiKey, apiUsername } = context.pipelineData - const params: DiscourseConnectionParams = { - forumHostname, - apiKey, - apiUsername, - } - const discourseCategories = await getDiscourseCategories(params, context.logger) - return { - type: DiscourseStreamType.CATEGORIES, - data: discourseCategories, - } - } - - static async processTopicsFromCategoryStream( - stream: IIntegrationStream, - context: IStepContext, - ): Promise { - const { forumHostname, apiKey, apiUsername } = context.pipelineData - const params: DiscourseConnectionParams = { - forumHostname, - apiKey, - apiUsername, - } - - const input: DiscourseTopicsInput = { - category_id: stream.metadata.category_id, - category_slug: stream.metadata.category_slug, - page: stream.metadata.page, - } - - const discourseTopics = await getDiscourseTopics(params, input, context.logger) - return { - type: DiscourseStreamType.TOPICS_FROM_CATEGORY, - data: discourseTopics, - } - } - - static async processPostsFromTopicStream( - stream: IIntegrationStream, - context: IStepContext, - ): Promise { - const { forumHostname, apiKey, apiUsername } = context.pipelineData - const params: DiscourseConnectionParams = { - forumHostname, - apiKey, - apiUsername, - } - - const input: DiscoursePostsInput = { - topic_slug: stream.metadata.topic_slug, - topic_id: stream.metadata.topicId, - page: stream.metadata.page, - } - - const discourseTopics = await getDiscoursePostsFromTopic(params, input, context.logger) - return { - type: DiscourseStreamType.POSTS_FROM_TOPIC, - data: discourseTopics, - } - } - - static async processPostsByIds( - stream: IIntegrationStream, - context: IStepContext, - ): Promise { - const { forumHostname, apiKey, apiUsername } = context.pipelineData - const params: DiscourseConnectionParams = { - forumHostname, - apiKey, - apiUsername, - } - - const input: DiscoursePostsByIdsInput = { - topic_id: stream.metadata.topicId, - post_ids: stream.metadata.postIds, - } - - const discoursePosts = await getDiscoursePostsByIds(params, input, context.logger) - return { - type: DiscourseStreamType.POSTS_BY_IDS, - data: discoursePosts, - } - } - - async processWebhook(webhook: any, context: IStepContext): Promise { - const { event, data } = webhook.payload - - switch (event) { - case DiscourseWebhookType.POST_CREATED: - return this.processPostCreatedWebhook(data as DiscourseWebhookPost, context) - case DiscourseWebhookType.USER_CREATED: - return this.processUserCreatedWebhook(data as DiscourseWebhookUser, context) - case DiscourseWebhookType.LIKED_A_POST: - const localData = data as DiscourseWebhookNotification - if (localData.notification.notification_type === 5) { - return this.processLikedAPostWebhook(localData, context) - } - break - default: - context.logger.warn( - { - event, - data, - }, - 'No record created for event!', - ) - - return { - operations: [], - } - } - - return { - operations: [], - } - } - - async processPostCreatedWebhook( - data: DiscourseWebhookPost, - context: IStepContext, - ): Promise { - const post = data.post - if (usernameIsBot(post.username)) { - return { - operations: [], - } - } - const user = await getDiscourseUserByUsername( - { - forumHostname: context.integration.settings.forumHostname, - apiKey: context.integration.settings.apiKey, - apiUsername: context.integration.settings.apiUsername, - }, - { username: post.username }, - context.logger, - ) - - if (!user) { - return { - operations: [], - } - } - - const member = DiscourseIntegrationService.parseUserIntoMember( - user, - context.integration.settings.forumHostname, - context, - ) - - const activity: AddActivitiesSingle = { - member, - username: member.username[PlatformType.DISCOURSE][0].username, - platform: PlatformType.DISCOURSE, - tenant: context.integration.tenantId, - sourceId: `${post.topic_id}-${post.post_number}`, - sourceParentId: post.post_number === 1 ? null : `${post.topic_id}-${post.post_number - 1}`, - type: - post.post_number === 1 - ? DiscourseActivityType.CREATE_TOPIC - : DiscourseActivityType.MESSAGE_IN_TOPIC, - timestamp: moment(post.created_at).utc().toDate(), - body: sanitizeHtml(he.decode(post.cooked)), - title: post.post_number === 1 ? post.topic_title : null, - url: `${context.integration.settings.forumHostname}/t/${post.topic_slug}/${post.topic_id}/${post.post_number}`, - channel: post.topic_title, - score: - post.post_number === 1 - ? DISCOURSE_GRID[DiscourseActivityType.CREATE_TOPIC].score - : DISCOURSE_GRID[DiscourseActivityType.MESSAGE_IN_TOPIC].score, - isContribution: - post.post_number === 1 - ? DISCOURSE_GRID[DiscourseActivityType.CREATE_TOPIC].isContribution - : DISCOURSE_GRID[DiscourseActivityType.MESSAGE_IN_TOPIC].isContribution, - } - - return { - operations: [ - { - type: Operations.UPSERT_ACTIVITIES_WITH_MEMBERS, - records: [activity], - }, - ], - } - } - - async processUserCreatedWebhook( - data: DiscourseWebhookUser, - context: IStepContext, - ): Promise { - const user = data.user - if (usernameIsBot(user.username)) { - return { - operations: [], - } - } - const member = DiscourseIntegrationService.parseUserIntoMember( - { - user: user as any, - user_badges: [], - badges: [], - badge_types: [], - users: [], - }, - context.integration.settings.forumHostname, - context, - ) - - const activity: AddActivitiesSingle = { - member, - username: member.username[PlatformType.DISCOURSE][0].username, - platform: PlatformType.DISCOURSE, - tenant: context.integration.tenantId, - sourceId: `${user.id}`, - type: DiscourseActivityType.JOIN, - timestamp: moment(user.created_at).utc().toDate(), - body: null, - title: null, - url: `${context.integration.settings.forumHostname}/u/${user.username}`, - channel: null, - score: DISCOURSE_GRID[DiscourseActivityType.JOIN].score, - isContribution: DISCOURSE_GRID[DiscourseActivityType.JOIN].isContribution, - } - - return { - operations: [ - { - type: Operations.UPSERT_ACTIVITIES_WITH_MEMBERS, - records: [activity], - }, - ], - } - } - - async processLikedAPostWebhook( - data: DiscourseWebhookNotification, - context: IStepContext, - ): Promise { - const notification = data.notification - const username = notification.data.username - ? notification.data.username - : notification.data.original_username - const channel = notification.fancy_title - ? notification.fancy_title - : notification.data.topic_title - - if (usernameIsBot(username)) { - return { - operations: [], - } - } - - const user = await getDiscourseUserByUsername( - { - forumHostname: context.integration.settings.forumHostname, - apiKey: context.integration.settings.apiKey, - apiUsername: context.integration.settings.apiUsername, - }, - { username }, - context.logger, - ) - - if (!user) { - return { - operations: [], - } - } - - const member = DiscourseIntegrationService.parseUserIntoMember( - user, - context.integration.settings.forumHostname, - context, - ) - - const activity: AddActivitiesSingle = { - member, - username: member.username[PlatformType.DISCOURSE][0].username, - platform: PlatformType.DISCOURSE, - tenant: context.integration.tenantId, - sourceId: `${notification.id}`, - type: DiscourseActivityType.LIKE, - timestamp: moment(notification.created_at).utc().toDate(), - body: null, - title: null, - channel, - score: DISCOURSE_GRID[DiscourseActivityType.LIKE].score, - isContribution: DISCOURSE_GRID[DiscourseActivityType.LIKE].isContribution, - attributes: { - topicURL: `${context.integration.settings.forumHostname}/t/${notification.slug}/${notification.topic_id}`, - }, - } - - return { - operations: [ - { - type: Operations.UPSERT_ACTIVITIES_WITH_MEMBERS, - records: [activity], - }, - ], - } - } - - static parseUserIntoMember( - user: DiscourseUserResponse, - forumHostname: string, - context: IStepContext, - ): Member { - return { - username: { - [PlatformType.DISCOURSE]: [ - { - username: user.user.username, - integrationId: context.integration.id, - }, - ], - } as PlatformIdentities, - displayName: user.user.name, - attributes: { - [MemberAttributeName.URL]: { - [PlatformType.DISCOURSE]: `${forumHostname}/u/${user.user.username}`, - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.DISCOURSE]: user.user.website || '', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.DISCOURSE]: user.user.location || '', - }, - [MemberAttributeName.BIO]: { - [PlatformType.DISCOURSE]: user.user.bio_cooked - ? sanitizeHtml(he.decode(user.user.bio_cooked)) - : '', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.DISCOURSE]: - `${forumHostname}${user.user.avatar_template.replace('{size}', '200')}` || '', - }, - }, - emails: user.user.email ? [user.user.email] : [], - } - } -} diff --git a/backend/src/serverless/integrations/services/integrations/githubIntegrationService.ts b/backend/src/serverless/integrations/services/integrations/githubIntegrationService.ts deleted file mode 100644 index df1e1a6fb8..0000000000 --- a/backend/src/serverless/integrations/services/integrations/githubIntegrationService.ts +++ /dev/null @@ -1,2114 +0,0 @@ -import moment from 'moment/moment' -import { createAppAuth } from '@octokit/auth-app' -import verifyGithubWebhook from 'verify-github-webhook' -import { - GITHUB_GRID, - GITHUB_MEMBER_ATTRIBUTES, - GithubActivityType, - GithubPullRequestEvents, - TWITTER_MEMBER_ATTRIBUTES, -} from '@crowd/integrations' -import { - IActivityScoringGrid, - IntegrationRunState, - IntegrationType, - MemberAttributeName, - PlatformType, -} from '@crowd/types' -import { RedisCache, getRedisClient } from '@crowd/redis' -import { timeout, singleOrDefault } from '@crowd/common' -import { Repo, Repos } from '../../types/regularTypes' -import { - IIntegrationStream, - IPendingStream, - IProcessStreamResults, - IProcessWebhookResults, - IStepContext, -} from '../../../../types/integration/stepResult' -import MemberAttributeSettingsService from '../../../../services/memberAttributeSettingsService' -import { GITHUB_CONFIG, IS_TEST_ENV, REDIS_CONFIG } from '../../../../conf' -import StargazersQuery from '../../usecases/github/graphql/stargazers' -import { IntegrationServiceBase } from '../integrationServiceBase' -import PullRequestsQuery from '../../usecases/github/graphql/pullRequests' -import PullRequestCommentsQuery from '../../usecases/github/graphql/pullRequestComments' -import IssuesQuery from '../../usecases/github/graphql/issues' -import IssueCommentsQuery from '../../usecases/github/graphql/issueComments' -import ForksQuery from '../../usecases/github/graphql/forks' -import DiscussionsQuery from '../../usecases/github/graphql/discussions' -import DiscussionCommentsQuery from '../../usecases/github/graphql/discussionComments' -import { AddActivitiesSingle, Member, PlatformIdentities } from '../../types/messageTypes' -import Operations from '../../../dbOperations/operations' -import getOrganization from '../../usecases/github/graphql/organizations' -import { AppTokenResponse, getAppToken } from '../../usecases/github/rest/getAppToken' -import getMember from '../../usecases/github/graphql/members' -import PullRequestReviewThreadsQuery from '../../usecases/github/graphql/pullRequestReviewThreads' -import PullRequestReviewThreadCommentsQuery from '../../usecases/github/graphql/pullRequestReviewThreadComments' -import PullRequestCommitsQuery, { - PullRequestCommit, -} from '../../usecases/github/graphql/pullRequestCommits' -import PullRequestCommitsQueryNoAdditions, { - PullRequestCommitNoAdditions, -} from '../../usecases/github/graphql/pullRequestCommitsNoAdditions' -import IntegrationRunRepository from '../../../../database/repositories/integrationRunRepository' -import IntegrationStreamRepository from '../../../../database/repositories/integrationStreamRepository' -import { DbIntegrationStreamCreateData } from '../../../../types/integrationStreamTypes' -import { sendNodeWorkerMessage } from '../../../utils/nodeWorkerSQS' -import { NodeWorkerIntegrationProcessMessage } from '../../../../types/mq/nodeWorkerIntegrationProcessMessage' -import TeamsQuery from '../../usecases/github/graphql/teams' -import { GithubWebhookTeam } from '../../usecases/github/graphql/types' - -/* eslint class-methods-use-this: 0 */ - -/* eslint-disable @typescript-eslint/no-unused-vars */ - -/* eslint-disable no-case-declarations */ - -enum GithubStreamType { - STARGAZERS = 'stargazers', - FORKS = 'forks', - PULLS = 'pulls', - PULL_COMMENTS = 'pull-comments', - PULL_REVIEW_THREADS = 'pull-review-threads', - PULL_REVIEW_THREAD_COMMENTS = 'pull-review-thread-comments', - PULL_COMMITS = 'pull-commits', - ISSUES = 'issues', - ISSUE_COMMENTS = 'issue-comments', - DISCUSSIONS = 'discussions', - DISCUSSION_COMMENTS = 'discussion-comments', -} - -const IS_GITHUB_COMMIT_DATA_ENABLED = GITHUB_CONFIG.isCommitDataEnabled === 'true' - -const privateKey = GITHUB_CONFIG.privateKey - ? Buffer.from(GITHUB_CONFIG.privateKey, 'base64').toString('ascii') - : undefined - -export class GithubIntegrationService extends IntegrationServiceBase { - private static githubAuthenticator = privateKey - ? createAppAuth({ - appId: GITHUB_CONFIG.appId, - clientId: GITHUB_CONFIG.clientId, - clientSecret: GITHUB_CONFIG.clientSecret, - privateKey, - }) - : undefined - - constructor() { - super(IntegrationType.GITHUB, -1) - - this.globalLimit = GITHUB_CONFIG.globalLimit || 0 - } - - async createMemberAttributes(context: IStepContext): Promise { - const service = new MemberAttributeSettingsService(context.repoContext) - await service.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await service.createPredefined( - MemberAttributeSettingsService.pickAttributes( - [MemberAttributeName.URL], - TWITTER_MEMBER_ATTRIBUTES, - ), - ) - } - - async preprocess(context: IStepContext): Promise { - const redis = await getRedisClient(REDIS_CONFIG, true) - const emailCache = new RedisCache('github-emails', redis, context.logger) - - const repos: Repos = [] - const unavailableRepos: Repos = [] - - const reposToCheck = [ - ...context.integration.settings.repos, - ...(context.integration.settings.unavailableRepos || []), - ] - - for (const repo of reposToCheck) { - try { - // we don't need to get default 100 item per page, just 1 is enough to check if repo is available - const stargazersQuery = new StargazersQuery(repo, context.integration.token, 1) - await stargazersQuery.getSinglePage('') - repos.push(repo) - } catch (e) { - if (e.rateLimitResetSeconds) { - throw e - } else { - context.logger.warn( - `Repo ${repo.name} will not be parsed. It is not available with the github token`, - ) - unavailableRepos.push(repo) - } - } - } - - context.integration.settings.repos = repos - context.integration.settings.unavailableRepos = unavailableRepos - - context.pipelineData = { - repos, - unavailableRepos, - emailCache, - } - } - - async getStreams(context: IStepContext): Promise { - return context.pipelineData.repos.reduce((acc, repo) => { - for (const endpoint of [ - GithubStreamType.STARGAZERS, - GithubStreamType.FORKS, - GithubStreamType.PULLS, - GithubStreamType.ISSUES, - GithubStreamType.DISCUSSIONS, - ]) { - acc.push({ - value: endpoint, - metadata: { repo, page: '' }, - }) - } - return acc - }, []) - } - - async processStream( - stream: IIntegrationStream, - context: IStepContext, - ): Promise { - await timeout(1000) - - const repoName = stream.metadata.repo.name - const event = stream.value as GithubStreamType - - const repo = GithubIntegrationService.getRepoByName(repoName, context) - - if (repo === null) { - throw new Error(`Repo ${repoName} not found!`) - } - - if (!repo.available) { - throw new Error( - `Stream ${stream.value} can't be processed since repo ${repoName} is not available!`, - ) - } - - let result - let newStreams: IPendingStream[] - - switch (event) { - case GithubStreamType.STARGAZERS: - const stargazersQuery = new StargazersQuery(repo, context.integration.token) - result = await stargazersQuery.getSinglePage(stream.metadata.page) - result.data = result.data.filter((i) => (i as any).node?.login) - break - case GithubStreamType.FORKS: - const forksQuery = new ForksQuery(repo, context.integration.token) - result = await forksQuery.getSinglePage(stream.metadata.page) - - // filter out activities without authors (such as bots) -- may not the case for forks, but filter out anyway - result.data = result.data.filter((i) => (i as any).owner?.login) - break - case GithubStreamType.PULLS: - const pullRequestsQuery = new PullRequestsQuery(repo, context.integration.token) - result = await pullRequestsQuery.getSinglePage(stream.metadata.page) - - // filter out activities without authors (such as bots) - result.data = result.data.filter((i) => (i as any).author?.login) - - // add new stream for each PR comments - const prCommentStreams = result.data.map((pr) => ({ - value: GithubStreamType.PULL_COMMENTS, - metadata: { - page: '', - repo: stream.metadata.repo, - prNumber: pr.number, - }, - })) - - // add new stream for each PR review thread comments - const prReviewThreads = result.data.map((pr) => ({ - value: GithubStreamType.PULL_REVIEW_THREADS, - metadata: { - page: '', - repo: stream.metadata.repo, - prNumber: pr.number, - }, - })) - - let prCommitsStreams: IPendingStream[] = [] - if (IS_GITHUB_COMMIT_DATA_ENABLED) { - prCommitsStreams = result.data.map((pr) => ({ - value: GithubStreamType.PULL_COMMITS, - metadata: { - page: '', - repo: stream.metadata.repo, - prNumber: pr.number, - }, - })) - } - - // It is very important to keep commits first. Otherwise, we have problems - // creating conversations if the Git integration has already ran for those data points. - newStreams = [...prCommitsStreams, ...prCommentStreams, ...prReviewThreads] - break - case GithubStreamType.PULL_COMMENTS: { - const pullRequestNumber = stream.metadata.prNumber - const pullRequestCommentsQuery = new PullRequestCommentsQuery( - repo, - pullRequestNumber, - context.integration.token, - ) - - result = await pullRequestCommentsQuery.getSinglePage(stream.metadata.page) - result.data = result.data.filter((i) => (i as any).author?.login) - break - } - case GithubStreamType.PULL_REVIEW_THREADS: { - const pullRequestNumber = stream.metadata.prNumber - const pullRequestReviewThreadsQuery = new PullRequestReviewThreadsQuery( - repo, - pullRequestNumber, - context.integration.token, - ) - - result = await pullRequestReviewThreadsQuery.getSinglePage(stream.metadata.page) - - // add each review thread as separate stream for comments inside - newStreams = result.data.map((reviewThread) => ({ - value: GithubStreamType.PULL_REVIEW_THREAD_COMMENTS, - metadata: { - page: '', - repo: stream.metadata.repo, - reviewThreadId: reviewThread.id, - }, - })) - - break - } - case GithubStreamType.PULL_REVIEW_THREAD_COMMENTS: { - const reviewThreadId = stream.metadata.reviewThreadId - const pullRequestReviewThreadCommentsQuery = new PullRequestReviewThreadCommentsQuery( - repo, - reviewThreadId, - context.integration.token, - ) - - result = await pullRequestReviewThreadCommentsQuery.getSinglePage(stream.metadata.page) - - // filter out activities without authors (such as bots) - result.data = result.data.filter((i) => (i as any).author?.login) - - break - } - case GithubStreamType.PULL_COMMITS: - const pullRequestNumber = stream.metadata.prNumber - const pullRequestCommitsQuery = new PullRequestCommitsQuery( - repo, - pullRequestNumber, - context.integration.token, - ) - - try { - result = await pullRequestCommitsQuery.getSinglePage(stream.metadata.page) - } catch (err) { - context.logger.warn( - { - err, - repo, - pullRequestNumber, - }, - 'Error while fetching pull request commits. Trying again without additions.', - ) - const pullRequestCommitsQueryNoAdditions = new PullRequestCommitsQueryNoAdditions( - repo, - pullRequestNumber, - context.integration.token, - ) - result = await pullRequestCommitsQueryNoAdditions.getSinglePage(stream.metadata.page) - } - break - case GithubStreamType.ISSUES: - const issuesQuery = new IssuesQuery(repo, context.integration.token) - result = await issuesQuery.getSinglePage(stream.metadata.page) - - // filter out activities without authors (such as bots) - result.data = result.data.filter((i) => (i as any).author?.login) - - // add each issue as separate stream - newStreams = result.data.map((issue) => ({ - value: GithubStreamType.ISSUE_COMMENTS, - metadata: { - page: '', - repo: stream.metadata.repo, - issueNumber: issue.number, - }, - })) - break - case GithubStreamType.ISSUE_COMMENTS: - const issueNumber = stream.metadata.issueNumber - const issueCommentsQuery = new IssueCommentsQuery( - repo, - issueNumber, - context.integration.token, - ) - result = await issueCommentsQuery.getSinglePage(stream.metadata.page) - result.data = result.data.filter((i) => (i as any).author?.login) - break - case GithubStreamType.DISCUSSIONS: - const discussionsQuery = new DiscussionsQuery(repo, context.integration.token) - result = await discussionsQuery.getSinglePage(stream.metadata.page) - - result.data = result.data.filter((i) => (i as any).author?.login) - newStreams = result.data - .filter((d) => d.comments.totalCount > 0) - .map((d) => ({ - value: GithubStreamType.DISCUSSION_COMMENTS, - metadata: { - page: '', - repo: stream.metadata.repo, - discussionNumber: d.number, - }, - })) - break - case GithubStreamType.DISCUSSION_COMMENTS: - const discussionNumber = stream.metadata.discussionNumber - const discussionCommentsQuery = new DiscussionCommentsQuery( - repo, - discussionNumber, - context.integration.token, - ) - result = await discussionCommentsQuery.getSinglePage(stream.metadata.page) - result.data = result.data.filter((i) => (i as any).author?.login) - break - default: - throw new Error(`Unknown event '${event}'!`) - } - - const nextPageStream = result.hasPreviousPage - ? { value: stream.value, metadata: { repo: stream.metadata.repo, page: result.startCursor } } - : undefined - - const activities = await GithubIntegrationService.parseActivities( - result.data, - stream.value as GithubStreamType, - repo, - context, - ) - - return { - operations: [ - { - type: Operations.UPSERT_ACTIVITIES_WITH_MEMBERS, - records: activities, - }, - ], - newStreams, - nextPageStream, - } - } - - async processWebhook(webhook: any, context: IStepContext): Promise { - const records: AddActivitiesSingle[] | undefined = [] - - await GithubIntegrationService.verifyWebhookSignature( - webhook.payload.signature, - webhook.payload.data, - ) - - const event = webhook.payload.event - const payload = webhook.payload.data - - const redis = await getRedisClient(REDIS_CONFIG, true) - const emailCache = new RedisCache('github-emails', redis, context.logger) - - context.pipelineData = { - emailCache, - } - - switch (event) { - case 'issues': { - const record = await GithubIntegrationService.parseWebhookIssue(payload, context) - if (record) { - records.push(record) - } - break - } - - case 'discussion': { - const record = await GithubIntegrationService.parseWebhookDiscussion(payload, context) - if (record) { - records.push(record) - } - break - } - - case 'pull_request': { - // handle case of multiple reviewers (by assigning a team as a reviewer) - if (payload.action === 'review_requested' && payload.requested_team) { - // a team sent as reviewer, first we need to find members in this team - const team: GithubWebhookTeam = payload.requested_team - const teamMembers = await new TeamsQuery( - team.node_id, - context.integration.token, - ).getSinglePage('') - - for (const teamMember of teamMembers.data) { - const reviewRequestActivity = await GithubIntegrationService.parseWebhookPullRequest( - { ...payload, requested_reviewer: teamMember }, - context, - ) - - if (reviewRequestActivity) { - records.push(reviewRequestActivity) - } - } - - break - } - - if (payload.action === 'closed' && payload.pull_request.merged) { - const revisedPayload = { ...payload, action: 'merged' } - revisedPayload.pull_request.state = 'merged' - - const prMergedRecord = await GithubIntegrationService.parseWebhookPullRequest( - revisedPayload, - context, - ) - if (prMergedRecord) { - records.push(prMergedRecord) - } - } - - const prRecord = await GithubIntegrationService.parseWebhookPullRequest(payload, context) - if (prRecord) { - records.push(prRecord) - } - - break - } - - case 'pull_request_review': { - const record = await GithubIntegrationService.parseWebhookPullRequestReview( - payload, - context, - ) - if (record) { - records.push(record) - } - break - } - - case 'star': { - const record = await GithubIntegrationService.parseWebhookStar(payload, context) - if (record) { - records.push(record) - } - break - } - - case 'fork': { - const record = await GithubIntegrationService.parseWebhookFork(payload, context) - if (record) { - records.push(record) - } - break - } - - case 'discussion_comment': - case 'issue_comment': { - const record = await GithubIntegrationService.parseWebhookComment(event, payload, context) - if (record) { - records.push(record) - } - break - } - - case 'pull_request_review_comment': { - const record = await GithubIntegrationService.parseWebhookPullRequestReviewThreadComment( - payload, - context, - ) - if (record) { - records.push(record) - } - break - } - - default: - } - - if (records.length === 0) { - context.logger.warn( - { - event, - action: payload.action, - }, - 'No record created for event!', - ) - - return { - operations: [], - } - } - - return { - operations: [ - { - type: Operations.UPSERT_ACTIVITIES_WITH_MEMBERS, - records, - }, - ], - } - } - - private static verifyWebhookSignature(signature: string, data: any): void { - if (IS_TEST_ENV) { - return - } - - const secret = GITHUB_CONFIG.webhookSecret - - let isVerified: boolean - try { - isVerified = verifyGithubWebhook(signature, JSON.stringify(data), secret) // Returns true if verification succeeds; otherwise, false. - } catch (err) { - throw new Error(`Webhook not verified\n${err}`) - } - - if (!isVerified) { - throw new Error('Webhook not verified') - } - } - - /** - * Parses various activity types into crowd activities. - * @param records List of activities to be parsed - * @param event - * @param repo - * @param context - * @returns parsed activities that can be saved to the database. - */ - private static async parseActivities( - records: any[], - event: GithubStreamType, - repo: Repo, - context: IStepContext, - ): Promise { - let activities: AddActivitiesSingle[] = [] - - switch (event) { - case GithubStreamType.STARGAZERS: - activities = await GithubIntegrationService.parseStars(records, repo, context) - break - case GithubStreamType.FORKS: - activities = await GithubIntegrationService.parseForks(records, repo, context) - break - case GithubStreamType.PULLS: - activities = await GithubIntegrationService.parsePullRequests(records, repo, context) - break - case GithubStreamType.PULL_COMMENTS: - activities = await GithubIntegrationService.parsePullRequestComments(records, repo, context) - break - case GithubStreamType.ISSUES: - activities = await GithubIntegrationService.parseIssues(records, repo, context) - break - case GithubStreamType.ISSUE_COMMENTS: - activities = await GithubIntegrationService.parseIssueComments(records, repo, context) - break - case GithubStreamType.DISCUSSIONS: - activities = await GithubIntegrationService.parseDiscussions(records, repo, context) - break - case GithubStreamType.DISCUSSION_COMMENTS: - activities = await GithubIntegrationService.parseDiscussionComments(records, repo, context) - break - case GithubStreamType.PULL_REVIEW_THREADS: - // empty result data, we only care about comments inside review threads, this stream won't generate any activities - activities = [] - break - case GithubStreamType.PULL_REVIEW_THREAD_COMMENTS: - activities = await GithubIntegrationService.parsePullRequestReviewThreadComments( - records, - repo, - context, - ) - break - case GithubStreamType.PULL_COMMITS: - activities = await GithubIntegrationService.parsePullRequestCommits(records, repo, context) - break - default: - throw new Error(`Event not supported '${event}'!`) - } - - return activities - } - - public static async parseWebhookStar( - payload: any, - context: IStepContext, - ): Promise { - let type: GithubActivityType - switch (payload.action) { - case 'created': { - type = GithubActivityType.STAR - break - } - - case 'deleted': { - type = GithubActivityType.UNSTAR - break - } - - default: { - return undefined - } - } - const member = await GithubIntegrationService.parseWebhookMember(payload.sender.login, context) - - if ( - member && - (type === GithubActivityType.UNSTAR || - (type === GithubActivityType.STAR && payload.starred_at !== null)) - ) { - const starredAt = - type === GithubActivityType.STAR ? moment(payload.starred_at).utc() : moment().utc() - - return { - member, - username: member.username[PlatformType.GITHUB].username, - type, - timestamp: starredAt.toDate(), - platform: PlatformType.GITHUB, - tenant: context.integration.tenantId, - sourceId: IntegrationServiceBase.generateSourceIdHash( - payload.sender.login, - type, - starredAt.unix().toString(), - PlatformType.GITHUB, - ), - sourceParentId: null, - channel: payload.repository.html_url, - score: - type === 'star' - ? GITHUB_GRID[GithubActivityType.STAR].score - : GITHUB_GRID[GithubActivityType.UNSTAR].score, - isContribution: - type === 'star' - ? GITHUB_GRID[GithubActivityType.STAR].isContribution - : GITHUB_GRID[GithubActivityType.UNSTAR].isContribution, - } - } - - return undefined - } - - private static async parseStars( - records: any[], - repo: Repo, - context: IStepContext, - ): Promise { - const out: AddActivitiesSingle[] = [] - for (const record of records) { - const member = await GithubIntegrationService.parseMember(record.node, context) - out.push({ - tenant: context.integration.tenantId, - username: member.username[PlatformType.GITHUB].username, - platform: PlatformType.GITHUB, - type: GithubActivityType.STAR, - sourceId: IntegrationServiceBase.generateSourceIdHash( - record.node.login, - GithubActivityType.STAR, - moment(record.starredAt).utc().unix().toString(), - PlatformType.GITHUB, - ), - sourceParentId: '', - timestamp: moment(record.starredAt).utc().toDate(), - channel: repo.url, - member, - score: GITHUB_GRID.star.score, - isContribution: GITHUB_GRID.star.isContribution, - }) - } - return out - } - - public static async parseWebhookFork( - payload: any, - context: IStepContext, - ): Promise { - const member: Member = await GithubIntegrationService.parseWebhookMember( - payload.sender.login, - context, - ) - - if (member) { - return { - member, - username: member.username[PlatformType.GITHUB].username, - type: GithubActivityType.FORK, - timestamp: moment(payload.forkee.created_at).utc().toDate(), - platform: PlatformType.GITHUB, - tenant: context.integration.tenantId, - sourceId: payload.forkee.node_id.toString(), - sourceParentId: null, - channel: payload.repository.html_url, - score: GITHUB_GRID.fork.score, - isContribution: GITHUB_GRID.fork.isContribution, - } - } - return undefined - } - - private static async parseForks( - records: any[], - repo: Repo, - context: IStepContext, - ): Promise { - const out: AddActivitiesSingle[] = [] - - for (const record of records) { - const member = await GithubIntegrationService.parseMember(record.owner, context) - out.push({ - username: member.username[PlatformType.GITHUB].username, - tenant: context.integration.tenantId, - platform: PlatformType.GITHUB, - type: GithubActivityType.FORK, - sourceId: record.id, - sourceParentId: '', - timestamp: moment(record.createdAt).utc().toDate(), - channel: repo.url, - member, - score: GITHUB_GRID.fork.score, - isContribution: GITHUB_GRID.fork.isContribution, - }) - } - - return out - } - - public static async parseWebhookPullRequestReviewThreadComment( - payload: any, - context: IStepContext, - ): Promise { - let type: GithubActivityType - let scoreGrid: IActivityScoringGrid - let timestamp: string - let sourceParentId: string - let sourceId: string - let body: string = '' - - switch (payload.action) { - case 'created': { - type = GithubActivityType.PULL_REQUEST_REVIEW_THREAD_COMMENT - scoreGrid = GITHUB_GRID[GithubActivityType.PULL_REQUEST_REVIEW_THREAD_COMMENT] - timestamp = payload.comment.created_at - sourceParentId = payload.pull_request.node_id - sourceId = payload.comment.node_id - body = payload.comment.body - break - } - default: { - return undefined - } - } - - const member = await GithubIntegrationService.parseWebhookMember( - payload.comment.user.login, - context, - ) - - if (member) { - return { - member, - username: member.username[PlatformType.GITHUB].username, - type, - timestamp: moment(timestamp).utc().toDate(), - platform: PlatformType.GITHUB, - tenant: context.integration.tenantId, - sourceId, - sourceParentId, - url: payload.comment.html_url, - title: '', - channel: payload.repository.html_url, - body, - score: scoreGrid.score, - isContribution: scoreGrid.isContribution, - attributes: { - state: payload.pull_request.state, - authorAssociation: payload.pull_request.author_association, - labels: payload.pull_request.labels.map((l) => l.name), - }, - } - } - - return undefined - } - - public static async parseWebhookPullRequestReview( - payload: any, - context: IStepContext, - ): Promise { - let type: GithubActivityType - let scoreGrid: IActivityScoringGrid - let timestamp: string - let sourceParentId: string - let sourceId: string - let body: string = '' - - switch (payload.action) { - case 'submitted': { - // additional comments to existing review threads also result in submitted events - // since these will be handled in pull_request_review_comment.created events - // we're ignoring when state is commented and it has no body. - if (payload.review.state === 'commented' && payload.review.body === null) { - return undefined - } - - type = GithubActivityType.PULL_REQUEST_REVIEWED - scoreGrid = GITHUB_GRID[GithubActivityType.PULL_REQUEST_REVIEWED] - timestamp = payload.review.submitted_at - sourceParentId = payload.pull_request.node_id.toString() - sourceId = `gen-PRR_${payload.pull_request.node_id.toString()}_${ - payload.sender.login - }_${moment(payload.review.submitted_at).utc().toISOString()}` - body = payload.review.body - break - } - default: { - return undefined - } - } - - const review = payload.review - const pull = payload.pull_request - const member = await GithubIntegrationService.parseWebhookMember(payload.sender.login, context) - - if (member) { - return { - member, - username: member.username[PlatformType.GITHUB].username, - type, - timestamp: moment(timestamp).utc().toDate(), - platform: PlatformType.GITHUB, - tenant: context.integration.tenantId, - sourceId, - sourceParentId, - url: pull.html_url, - title: '', - channel: payload.repository.html_url, - body, - score: scoreGrid.score, - isContribution: scoreGrid.isContribution, - attributes: { - reviewState: (payload.review?.state as string).toUpperCase(), - state: pull.state, - authorAssociation: pull.author_association, - labels: pull.labels.map((l) => l.name), - }, - } - } - - return undefined - } - - public static async parseWebhookPullRequest( - payload: any, - context: IStepContext, - ): Promise { - let type: GithubActivityType - let scoreGrid: IActivityScoringGrid - let timestamp: string - let sourceParentId: string - let sourceId: string - let objectMember: Member = null - let objectMemberUsername: string = null - let body: string = '' - let title: string = '' - - switch (payload.action) { - case 'edited': - case 'opened': - case 'reopened': { - type = GithubActivityType.PULL_REQUEST_OPENED - scoreGrid = GITHUB_GRID[GithubActivityType.PULL_REQUEST_OPENED] - timestamp = payload.pull_request.created_at - sourceId = payload.pull_request.node_id.toString() - sourceParentId = null - body = payload.pull_request.body - title = payload.pull_request.title - break - } - - case 'closed': { - type = GithubActivityType.PULL_REQUEST_CLOSED - scoreGrid = GITHUB_GRID[GithubActivityType.PULL_REQUEST_CLOSED] - timestamp = payload.pull_request.closed_at - sourceParentId = payload.pull_request.node_id.toString() - sourceId = `gen-CE_${payload.pull_request.node_id.toString()}_${ - payload.sender.login - }_${moment(payload.pull_request.closed_at).utc().toISOString()}` - break - } - - case 'assigned': { - type = GithubActivityType.PULL_REQUEST_ASSIGNED - scoreGrid = GITHUB_GRID[GithubActivityType.PULL_REQUEST_ASSIGNED] - timestamp = payload.pull_request.updated_at - sourceParentId = payload.pull_request.node_id.toString() - sourceId = `gen-AE_${payload.pull_request.node_id.toString()}_${payload.sender.login}_${ - payload.assignee.login - }_${moment(payload.pull_request.updated_at).utc().toISOString()}` - objectMember = await GithubIntegrationService.parseWebhookMember( - payload.assignee.login, - context, - ) - objectMemberUsername = objectMember.username[PlatformType.GITHUB].username - break - } - - case 'review_requested': { - type = GithubActivityType.PULL_REQUEST_REVIEW_REQUESTED - scoreGrid = GITHUB_GRID[GithubActivityType.PULL_REQUEST_REVIEW_REQUESTED] - timestamp = payload.pull_request.updated_at - sourceParentId = payload.pull_request.node_id.toString() - sourceId = `gen-RRE_${payload.pull_request.node_id.toString()}_${payload.sender.login}_${ - payload.requested_reviewer.login - }_${moment(payload.pull_request.updated_at).utc().toISOString()}` - objectMember = await GithubIntegrationService.parseWebhookMember( - payload.requested_reviewer.login, - context, - ) - objectMemberUsername = objectMember.username[PlatformType.GITHUB].username - break - } - - case 'merged': { - type = GithubActivityType.PULL_REQUEST_MERGED - scoreGrid = GITHUB_GRID[GithubActivityType.PULL_REQUEST_MERGED] - timestamp = payload.pull_request.merged_at - sourceParentId = payload.pull_request.node_id.toString() - sourceId = `gen-ME_${payload.pull_request.node_id.toString()}_${ - payload.pull_request.merged_by.login - }_${moment(payload.pull_request.merged_at).utc().toISOString()}` - break - } - - // this event is triggered whdn a head branch of PR receives a new commit - case 'synchronize': { - if (!IS_GITHUB_COMMIT_DATA_ENABLED) { - return undefined - } - const prNumber = payload.number - const integrationId = context.integration.id - const tenantId = context.integration.tenantId - const repoContext = context.repoContext - const runRepo = new IntegrationRunRepository(repoContext) - - let run - let isExistingRun = false - - const existingRun = await runRepo.findLastProcessingRun(integrationId) - - // if there is existing delayed, pending or processing run, use it - if (existingRun) { - run = existingRun - isExistingRun = true - } else { - // otherwise create a new run - run = await runRepo.create({ - integrationId, - tenantId, - onboarding: false, - state: IntegrationRunState.PENDING, - }) - } - - const githubRepo: Repo = { - name: payload.repository.name, - owner: payload.repository.owner.login, - url: payload.repository.html_url, - createdAt: payload.repository.created_at, - } - - const streamRepo = new IntegrationStreamRepository(repoContext) - const stream: DbIntegrationStreamCreateData = { - runId: run.id, // we tie up a stream to an existing run or to a new one - tenantId, - integrationId, - name: GithubStreamType.PULL_COMMITS, - metadata: { - page: '', - repo: githubRepo, - prNumber, - }, - } - // create a new stream - await streamRepo.create(stream) - - if (!isExistingRun) { - // if we created a new run, we need to notify the node worker - // test again - await sendNodeWorkerMessage(tenantId, new NodeWorkerIntegrationProcessMessage(run.id)) - } - return undefined - } - - default: { - return undefined - } - } - - const member = await GithubIntegrationService.parseWebhookMember(payload.sender.login, context) - - const pull = payload.pull_request - - if (member) { - return { - member, - username: member.username[PlatformType.GITHUB].username, - objectMemberUsername, - objectMember, - type, - timestamp: moment(timestamp).utc().toDate(), - platform: PlatformType.GITHUB, - tenant: context.integration.tenantId, - sourceId, - sourceParentId, - url: pull.html_url, - title, - channel: payload.repository.html_url, - body, - score: scoreGrid.score, - isContribution: scoreGrid.isContribution, - attributes: { - state: pull.state, - additions: pull.additions, - deletions: pull.deletions, - changedFiles: pull.changed_files, - authorAssociation: pull.author_association, - labels: pull.labels.map((l) => l.name), - }, - } - } - - return undefined - } - - private static async parsePullRequestEvents( - records: any[], - pullRequest: AddActivitiesSingle, - context: IStepContext, - ): Promise { - const out: AddActivitiesSingle[] = [] - - for (const record of records) { - switch (record.__typename) { - case GithubPullRequestEvents.ASSIGN: - if (record.actor?.login && record.assignee?.login) { - const member = await GithubIntegrationService.parseMember(record.actor, context) - const objectMember = await GithubIntegrationService.parseMember( - record.assignee, - context, - ) - out.push({ - username: member.username[PlatformType.GITHUB].username, - objectMemberUsername: objectMember.username[PlatformType.GITHUB].username, - tenant: context.integration.tenantId, - platform: PlatformType.GITHUB, - type: GithubActivityType.PULL_REQUEST_ASSIGNED, - sourceId: `gen-AE_${pullRequest.sourceId}_${record.actor.login}_${ - record.assignee.login - }_${moment(record.createdAt).utc().toISOString()}`, - sourceParentId: pullRequest.sourceId, - timestamp: moment(record.createdAt).utc().toDate(), - body: '', - url: pullRequest.url, - channel: pullRequest.channel, - title: '', - attributes: { - state: (pullRequest.attributes as any).state, - additions: (pullRequest.attributes as any).additions, - deletions: (pullRequest.attributes as any).deletions, - changedFiles: (pullRequest.attributes as any).changedFiles, - authorAssociation: (pullRequest.attributes as any).authorAssociation, - labels: (pullRequest.attributes as any).labels, - }, - member, - objectMember, - score: GITHUB_GRID[GithubActivityType.PULL_REQUEST_ASSIGNED].score, - isContribution: GITHUB_GRID[GithubActivityType.PULL_REQUEST_ASSIGNED].isContribution, - }) - } - - break - case GithubPullRequestEvents.REQUEST_REVIEW: - if ( - record?.actor?.login && - (record?.requestedReviewer?.login || record?.requestedReviewer?.members) - ) { - // Requested review from single member - if (record?.requestedReviewer?.login) { - const member = await GithubIntegrationService.parseMember(record.actor, context) - const objectMember = await GithubIntegrationService.parseMember( - record.requestedReviewer, - context, - ) - out.push({ - username: member.username[PlatformType.GITHUB].username, - objectMemberUsername: objectMember.username[PlatformType.GITHUB].username, - tenant: context.integration.tenantId, - platform: PlatformType.GITHUB, - type: GithubActivityType.PULL_REQUEST_REVIEW_REQUESTED, - sourceId: `gen-RRE_${pullRequest.sourceId}_${record.actor.login}_${ - record.requestedReviewer.login - }_${moment(record.createdAt).utc().toISOString()}`, - sourceParentId: pullRequest.sourceId, - timestamp: moment(record.createdAt).utc().toDate(), - body: '', - url: pullRequest.url, - channel: pullRequest.channel, - title: '', - attributes: { - state: (pullRequest.attributes as any).state, - additions: (pullRequest.attributes as any).additions, - deletions: (pullRequest.attributes as any).deletions, - changedFiles: (pullRequest.attributes as any).changedFiles, - authorAssociation: (pullRequest.attributes as any).authorAssociation, - labels: (pullRequest.attributes as any).labels, - }, - member, - objectMember, - score: GITHUB_GRID[GithubActivityType.PULL_REQUEST_REVIEW_REQUESTED].score, - isContribution: - GITHUB_GRID[GithubActivityType.PULL_REQUEST_REVIEW_REQUESTED].isContribution, - }) - } else if (record?.requestedReviewer?.members) { - // review is requested from a team - const member = await GithubIntegrationService.parseMember(record.actor, context) - - for (const teamMember of record.requestedReviewer.members.nodes) { - const objectMember = await GithubIntegrationService.parseMember(teamMember, context) - - out.push({ - username: member.username[PlatformType.GITHUB].username, - objectMemberUsername: objectMember.username[PlatformType.GITHUB].username, - tenant: context.integration.tenantId, - platform: PlatformType.GITHUB, - type: GithubActivityType.PULL_REQUEST_REVIEW_REQUESTED, - sourceId: `gen-RRE_${pullRequest.sourceId}_${record.actor.login}_${ - objectMember.username[PlatformType.GITHUB].username - }_${moment(record.createdAt).utc().toISOString()}`, - sourceParentId: pullRequest.sourceId, - timestamp: moment(record.createdAt).utc().toDate(), - body: '', - url: pullRequest.url, - channel: pullRequest.channel, - title: '', - attributes: { - state: (pullRequest.attributes as any).state, - additions: (pullRequest.attributes as any).additions, - deletions: (pullRequest.attributes as any).deletions, - changedFiles: (pullRequest.attributes as any).changedFiles, - authorAssociation: (pullRequest.attributes as any).authorAssociation, - labels: (pullRequest.attributes as any).labels, - }, - member, - objectMember, - score: GITHUB_GRID[GithubActivityType.PULL_REQUEST_REVIEW_REQUESTED].score, - isContribution: - GITHUB_GRID[GithubActivityType.PULL_REQUEST_REVIEW_REQUESTED].isContribution, - }) - } - } - } - - break - case GithubPullRequestEvents.REVIEW: - if (record?.author?.login && record?.submittedAt) { - const member = await GithubIntegrationService.parseMember(record.author, context) - out.push({ - username: member.username[PlatformType.GITHUB].username, - tenant: context.integration.tenantId, - platform: PlatformType.GITHUB, - type: GithubActivityType.PULL_REQUEST_REVIEWED, - sourceId: `gen-PRR_${pullRequest.sourceId}_${record.author.login}_${moment( - record.submittedAt, - ) - .utc() - .toISOString()}`, - sourceParentId: pullRequest.sourceId, - timestamp: moment(record.submittedAt).utc().toDate(), - body: record.body, - url: pullRequest.url, - channel: pullRequest.channel, - title: '', - attributes: { - reviewState: record.state, - state: (pullRequest.attributes as any).state, - additions: (pullRequest.attributes as any).additions, - deletions: (pullRequest.attributes as any).deletions, - changedFiles: (pullRequest.attributes as any).changedFiles, - authorAssociation: (pullRequest.attributes as any).authorAssociation, - labels: (pullRequest.attributes as any).labels, - }, - member, - score: GITHUB_GRID[GithubActivityType.PULL_REQUEST_REVIEWED].score, - isContribution: GITHUB_GRID[GithubActivityType.PULL_REQUEST_REVIEWED].isContribution, - }) - } - - break - case GithubPullRequestEvents.MERGE: - if (record?.actor?.login) { - const member = await GithubIntegrationService.parseMember(record.actor, context) - out.push({ - username: member.username[PlatformType.GITHUB].username, - tenant: context.integration.tenantId, - platform: PlatformType.GITHUB, - type: GithubActivityType.PULL_REQUEST_MERGED, - sourceId: `gen-ME_${pullRequest.sourceId}_${record.actor.login}_${moment( - record.createdAt, - ) - .utc() - .toISOString()}`, - sourceParentId: pullRequest.sourceId, - timestamp: moment(record.createdAt).utc().toDate(), - body: '', - url: pullRequest.url, - channel: pullRequest.channel, - title: '', - attributes: { - state: (pullRequest.attributes as any).state, - additions: (pullRequest.attributes as any).additions, - deletions: (pullRequest.attributes as any).deletions, - changedFiles: (pullRequest.attributes as any).changedFiles, - authorAssociation: (pullRequest.attributes as any).authorAssociation, - labels: (pullRequest.attributes as any).labels, - }, - member, - score: GITHUB_GRID[GithubActivityType.PULL_REQUEST_MERGED].score, - isContribution: GITHUB_GRID[GithubActivityType.PULL_REQUEST_MERGED].isContribution, - }) - } - - break - case GithubPullRequestEvents.CLOSE: - if (record?.actor?.login) { - const member = await GithubIntegrationService.parseMember(record.actor, context) - out.push({ - username: member.username[PlatformType.GITHUB].username, - tenant: context.integration.tenantId, - platform: PlatformType.GITHUB, - type: GithubActivityType.PULL_REQUEST_CLOSED, - sourceId: `gen-CE_${pullRequest.sourceId}_${record.actor.login}_${moment( - record.createdAt, - ) - .utc() - .toISOString()}`, - sourceParentId: pullRequest.sourceId, - timestamp: moment(record.createdAt).utc().toDate(), - body: '', - url: pullRequest.url, - channel: pullRequest.channel, - title: '', - attributes: { - state: (pullRequest.attributes as any).state, - additions: (pullRequest.attributes as any).additions, - deletions: (pullRequest.attributes as any).deletions, - changedFiles: (pullRequest.attributes as any).changedFiles, - authorAssociation: (pullRequest.attributes as any).authorAssociation, - labels: (pullRequest.attributes as any).labels, - }, - member, - score: GITHUB_GRID[GithubActivityType.PULL_REQUEST_CLOSED].score, - isContribution: GITHUB_GRID[GithubActivityType.PULL_REQUEST_CLOSED].isContribution, - }) - } - - break - default: - context.logger.warn( - `Unsupported pull request event: ${record.__typename}. This event will not be parsed.`, - ) - } - } - return out - } - - private static async parsePullRequestCommits( - records: any[], - repo: Repo, - context: IStepContext, - ): Promise { - const out: AddActivitiesSingle[] = [] - const data = records[0] as PullRequestCommit | PullRequestCommitNoAdditions - const commits = data.repository.pullRequest.commits.nodes - - for (const record of commits) { - for (const author of record.commit.authors.nodes) { - if (!author || !author.user || !author.user.login) { - // eslint-disable-next-line no-continue - continue - } - const member = await GithubIntegrationService.parseMember(author.user, context) - out.push({ - tenant: context.integration.tenantId, - username: member.username[PlatformType.GITHUB].username, - platform: PlatformType.GITHUB, - channel: repo.url, - url: `${repo.url}/commit/${record.commit.oid}`, - body: record.commit.message, - type: 'authored-commit', - sourceId: record.commit.oid, - sourceParentId: `${data.repository.pullRequest.id}`, - timestamp: moment(record.commit.authoredDate).utc().toDate(), - attributes: { - insertions: 'additions' in record.commit ? record.commit.additions : 0, - deletions: 'deletions' in record.commit ? record.commit.deletions : 0, - lines: - 'additions' in record.commit && 'deletions' in record.commit - ? record.commit.additions - record.commit.deletions - : 0, - isMerge: record.commit.parents.totalCount > 1, - }, - member, - }) - } - } - - return out - } - - private static async parsePullRequestReviewThreadComments( - records: any[], - repo: Repo, - context: IStepContext, - ): Promise { - const out: AddActivitiesSingle[] = [] - - for (const record of records) { - const member = await GithubIntegrationService.parseMember(record.author, context) - out.push({ - tenant: context.integration.tenantId, - username: member.username[PlatformType.GITHUB].username, - platform: PlatformType.GITHUB, - type: GithubActivityType.PULL_REQUEST_REVIEW_THREAD_COMMENT, - sourceId: record.id, - sourceParentId: record.pullRequest.id, - timestamp: moment(record.createdAt).utc().toDate(), - body: record.bodyText, - url: record.url, - channel: repo.url, - title: '', - attributes: { - state: record.pullRequest.state.toLowerCase(), - additions: record.pullRequest.additions, - deletions: record.pullRequest.deletions, - changedFiles: record.pullRequest.changedFiles, - authorAssociation: record.pullRequest.authorAssociation, - labels: record.pullRequest.labels?.nodes.map((l) => l.name), - }, - member, - score: GITHUB_GRID[GithubActivityType.PULL_REQUEST_REVIEW_THREAD_COMMENT].score, - isContribution: - GITHUB_GRID[GithubActivityType.PULL_REQUEST_REVIEW_THREAD_COMMENT].isContribution, - }) - } - - return out - } - - private static async parsePullRequests( - records: any[], - repo: Repo, - context: IStepContext, - ): Promise { - const out: AddActivitiesSingle[] = [] - - for (const record of records) { - const member = await GithubIntegrationService.parseMember(record.author, context) - out.push({ - tenant: context.integration.tenantId, - username: member.username[PlatformType.GITHUB].username, - platform: PlatformType.GITHUB, - type: GithubActivityType.PULL_REQUEST_OPENED, - sourceId: record.id, - sourceParentId: '', - timestamp: moment(record.createdAt).utc().toDate(), - body: record.bodyText, - url: record.url ? record.url : '', - channel: repo.url, - title: record.title, - attributes: { - state: record.state.toLowerCase(), - additions: record.additions, - deletions: record.deletions, - changedFiles: record.changedFiles, - authorAssociation: record.authorAssociation, - labels: record.labels?.nodes.map((l) => l.name), - }, - member, - score: GITHUB_GRID[GithubActivityType.PULL_REQUEST_OPENED].score, - isContribution: GITHUB_GRID[GithubActivityType.PULL_REQUEST_OPENED].isContribution, - }) - - // parse pr events - out.push( - ...(await GithubIntegrationService.parsePullRequestEvents( - record.timelineItems.nodes, - out[out.length - 1], - context, - )), - ) - } - - return out - } - - public static async parseWebhookComment( - event: string, - payload: any, - context: IStepContext, - ): Promise { - let type: GithubActivityType - let sourceParentId: string | undefined - - switch (event) { - case 'discussion_comment': { - switch (payload.action) { - case 'created': - case 'edited': - type = GithubActivityType.DISCUSSION_COMMENT - sourceParentId = payload.discussion.node_id.toString() - break - default: - return undefined - } - break - } - - case 'issue_comment': { - switch (payload.action) { - case 'created': - case 'edited': { - if ('pull_request' in payload.issue) { - type = GithubActivityType.PULL_REQUEST_COMMENT - } else { - type = GithubActivityType.ISSUE_COMMENT - } - sourceParentId = payload.issue.node_id.toString() - break - } - - default: - return undefined - } - break - } - - default: { - return undefined - } - } - - const member = await GithubIntegrationService.parseWebhookMember(payload.sender.login, context) - if (member) { - const comment = payload.comment - return { - member, - username: member.username[PlatformType.GITHUB].username, - type, - timestamp: moment(comment.created_at).utc().toDate(), - platform: PlatformType.GITHUB, - tenant: context.integration.tenantId, - sourceId: comment.node_id.toString(), - sourceParentId, - url: comment.html_url, - body: comment.body, - channel: payload.repository.html_url, - score: GITHUB_GRID[type].score, - isContribution: GITHUB_GRID[type].isContribution, - } - } - - return undefined - } - - private static async parsePullRequestComments( - records: any[], - repo: Repo, - context: IStepContext, - ): Promise { - const out: AddActivitiesSingle[] = [] - for (const record of records) { - const member = await GithubIntegrationService.parseMember(record.author, context) - out.push({ - username: member.username[PlatformType.GITHUB].username, - tenant: context.integration.tenantId, - platform: PlatformType.GITHUB, - type: GithubActivityType.PULL_REQUEST_COMMENT, - sourceId: record.id, - sourceParentId: record.pullRequest.id, - timestamp: moment(record.createdAt).utc().toDate(), - url: record.url, - body: record.bodyText, - channel: repo.url, - member, - score: GITHUB_GRID[GithubActivityType.PULL_REQUEST_COMMENT].score, - isContribution: GITHUB_GRID[GithubActivityType.PULL_REQUEST_COMMENT].isContribution, - }) - } - return out - } - - public static async parseWebhookIssue( - payload: any, - context: IStepContext, - ): Promise { - let type: GithubActivityType - let scoreGrid: IActivityScoringGrid - let timestamp: string - let sourceId: string - let sourceParentId: string - let body: string = '' - let title: string = '' - - switch (payload.action) { - case 'edited': - case 'opened': - case 'reopened': - type = GithubActivityType.ISSUE_OPENED - scoreGrid = GITHUB_GRID[GithubActivityType.ISSUE_OPENED] - timestamp = payload.issue.created_at - sourceParentId = null - sourceId = payload.issue.node_id.toString() - body = payload.issue.body - title = payload.issue.title - break - - case 'closed': - type = GithubActivityType.ISSUE_CLOSED - scoreGrid = GITHUB_GRID[GithubActivityType.ISSUE_CLOSED] - timestamp = payload.issue.closed_at - sourceParentId = payload.issue.node_id.toString() - sourceId = `gen-CE_${payload.issue.node_id.toString()}_${payload.sender.login}_${moment( - payload.issue.closed_at, - ) - .utc() - .toISOString()}` - break - - default: - return undefined - } - - const issue = payload.issue - const member = await GithubIntegrationService.parseWebhookMember(payload.sender.login, context) - - if (member) { - return { - member, - username: member.username[PlatformType.GITHUB].username, - type, - timestamp: moment(timestamp).utc().toDate(), - platform: PlatformType.GITHUB, - tenant: context.integration.tenantId, - sourceId, - sourceParentId, - url: issue.html_url, - title, - channel: payload.repository.html_url, - body, - attributes: { - state: issue.state, - }, - score: scoreGrid.score, - isContribution: scoreGrid.isContribution, - } - } - - return undefined - } - - private static async parseIssues( - records: any[], - repo: Repo, - context: IStepContext, - ): Promise { - const out: AddActivitiesSingle[] = [] - - for (const record of records) { - const member = await GithubIntegrationService.parseMember(record.author, context) - out.push({ - tenant: context.integration.tenantId, - username: member.username[PlatformType.GITHUB].username, - platform: PlatformType.GITHUB, - type: GithubActivityType.ISSUE_OPENED, - sourceId: record.id, - sourceParentId: '', - timestamp: moment(record.createdAt).utc().toDate(), - body: record.bodyText, - url: record.url ? record.url : '', - channel: repo.url, - title: record.title.replace(/\0/g, ''), - attributes: { - state: record.state.toLowerCase(), - }, - member, - score: GITHUB_GRID[GithubActivityType.ISSUE_OPENED].score, - isContribution: GITHUB_GRID[GithubActivityType.ISSUE_OPENED].isContribution, - }) - - // parse issue events - out.push( - ...(await GithubIntegrationService.parseIssueEvents( - record.timelineItems.nodes, - out[out.length - 1], - context, - )), - ) - } - - return out - } - - private static async parseIssueEvents( - records: any[], - issue: AddActivitiesSingle, - context: IStepContext, - ): Promise { - const out: AddActivitiesSingle[] = [] - - for (const record of records) { - switch (record.__typename) { - case GithubPullRequestEvents.CLOSE: - if (record.actor?.login) { - const member = await GithubIntegrationService.parseMember(record.actor, context) - out.push({ - username: member.username[PlatformType.GITHUB].username, - tenant: context.integration.tenantId, - platform: PlatformType.GITHUB, - type: GithubActivityType.ISSUE_CLOSED, - sourceId: `gen-CE_${issue.sourceId}_${record.actor.login}_${moment(record.createdAt) - .utc() - .toISOString()}`, - sourceParentId: issue.sourceId, - timestamp: moment(record.createdAt).utc().toDate(), - body: '', - url: issue.url, - channel: issue.channel, - title: '', - attributes: { - state: (issue.attributes as any).state, - }, - member, - score: GITHUB_GRID[GithubActivityType.ISSUE_CLOSED].score, - isContribution: GITHUB_GRID[GithubActivityType.ISSUE_CLOSED].isContribution, - }) - } - - break - default: - context.logger.warn( - `Unsupported issue event: ${record.__typename}. This event will not be parsed.`, - ) - } - } - return out - } - - private static async parseIssueComments( - records: any[], - repo: Repo, - context: IStepContext, - ): Promise { - const out: AddActivitiesSingle[] = [] - for (const record of records) { - const member = await GithubIntegrationService.parseMember(record.author, context) - out.push({ - tenant: context.integration.tenantId, - username: member.username[PlatformType.GITHUB].username, - platform: PlatformType.GITHUB, - type: GithubActivityType.ISSUE_COMMENT, - sourceId: record.id, - sourceParentId: record.issue.id, - timestamp: moment(record.createdAt).utc().toDate(), - url: record.url, - body: record.bodyText, - channel: repo.url, - member, - score: GITHUB_GRID[GithubActivityType.ISSUE_COMMENT].score, - isContribution: GITHUB_GRID[GithubActivityType.ISSUE_COMMENT].isContribution, - }) - } - return out - } - - public static async parseWebhookDiscussion( - payload: any, - context: IStepContext, - ): Promise { - if (payload.action === 'answered') { - return this.parseWebhookDiscussionComments(payload, context) - } - - if (!['edited', 'created'].includes(payload.action)) { - return undefined - } - - const discussion = payload.discussion - const member = await GithubIntegrationService.parseWebhookMember(discussion.user.login, context) - - if (member) { - return { - member, - username: member.username[PlatformType.GITHUB].username, - type: GithubActivityType.DISCUSSION_STARTED, - timestamp: moment(discussion.created_at).utc().toDate(), - platform: PlatformType.GITHUB, - tenant: context.integration.tenantId, - sourceId: discussion.node_id.toString(), - sourceParentId: null, - url: discussion.html_url, - title: discussion.title, - channel: payload.repository.html_url, - body: discussion.body, - attributes: { - category: { - id: discussion.category.node_id, - isAnswerable: discussion.category.is_answerable, - name: discussion.category.name, - slug: discussion.category.slug, - emoji: discussion.category.emoji, - description: discussion.category.description, - }, - }, - score: GITHUB_GRID[GithubActivityType.DISCUSSION_STARTED].score, - isContribution: GITHUB_GRID[GithubActivityType.DISCUSSION_STARTED].isContribution, - } - } - - return undefined - } - - private static async parseDiscussions( - records: any[], - repo: Repo, - context: IStepContext, - ): Promise { - const out: AddActivitiesSingle[] = [] - - for (const record of records) { - const member = await GithubIntegrationService.parseMember(record.author, context) - out.push({ - username: member.username[PlatformType.GITHUB].username, - tenant: context.integration.tenantId, - platform: PlatformType.GITHUB, - type: GithubActivityType.DISCUSSION_STARTED, - sourceId: record.id, - sourceParentId: '', - timestamp: moment(record.createdAt).utc().toDate(), - body: record.bodyText, - url: record.url ? record.url : '', - channel: repo.url, - title: record.title, - attributes: { - category: { - id: record.category.id, - isAnswerable: record.category.isAnswerable, - name: record.category.name, - slug: record.category.slug, - emoji: record.category.emoji, - description: record.category.description, - }, - }, - member, - score: GITHUB_GRID[GithubActivityType.DISCUSSION_STARTED].score, - isContribution: GITHUB_GRID[GithubActivityType.DISCUSSION_STARTED].isContribution, - }) - } - return out - } - - private static async parseWebhookDiscussionComments( - payload: any, - context: IStepContext, - ): Promise { - const member: Member = await this.parseWebhookMember(payload.sender.login, context) - - if (member) { - const answer = payload.answer - return { - member, - username: member.username[PlatformType.GITHUB].username, - type: GithubActivityType.DISCUSSION_COMMENT, - timestamp: moment(answer.created_at).utc().toDate(), - platform: PlatformType.GITHUB, - tenant: context.integration.tenantId, - sourceId: answer.node_id.toString(), - sourceParentId: payload.discussion.node_id.toString(), - attributes: { - isSelectedAnswer: true, - }, - channel: payload.repository.html_url, - body: answer.body, - url: answer.html_url, - score: GITHUB_GRID[GithubActivityType.DISCUSSION_COMMENT].score + 2, - isContribution: GITHUB_GRID[GithubActivityType.DISCUSSION_COMMENT].isContribution, - } - } - - return undefined - } - - private static async parseDiscussionComments( - records: any[], - repo: Repo, - context: IStepContext, - ): Promise { - const out: AddActivitiesSingle[] = [] - - for (const record of records) { - if (!('author' in record)) { - // eslint-disable-next-line no-continue - continue - } - const commentId = record.id - const member = await GithubIntegrationService.parseMember(record.author, context) - - out.push({ - username: member.username[PlatformType.GITHUB].username, - tenant: context.integration.tenantId, - platform: PlatformType.GITHUB, - type: GithubActivityType.DISCUSSION_COMMENT, - sourceId: commentId, - sourceParentId: record.discussion.id, - timestamp: moment(record.createdAt).utc().toDate(), - url: record.url, - body: record.bodyText, - channel: repo.url, - attributes: { - isAnswer: record.isAnswer ?? undefined, - }, - member, - score: record.isAnswer - ? GITHUB_GRID[GithubActivityType.DISCUSSION_COMMENT].score + 2 - : GITHUB_GRID[GithubActivityType.DISCUSSION_COMMENT].score, - isContribution: GITHUB_GRID[GithubActivityType.DISCUSSION_COMMENT].isContribution, - }) - - for (const reply of record.replies.nodes) { - if (!('author' in reply) || !reply?.author || !reply?.author?.login) { - // eslint-disable-next-line no-continue - continue - } - const member = await GithubIntegrationService.parseMember(reply.author, context) - out.push({ - username: member.username[PlatformType.GITHUB].username, - tenant: context.integration.tenantId, - platform: PlatformType.GITHUB, - type: GithubActivityType.DISCUSSION_COMMENT, - sourceId: reply.id, - sourceParentId: commentId, - timestamp: moment(reply.createdAt).utc().toDate(), - url: reply.url, - body: reply.bodyText, - channel: repo.url, - member, - score: GITHUB_GRID[GithubActivityType.DISCUSSION_COMMENT].score, - isContribution: GITHUB_GRID[GithubActivityType.DISCUSSION_COMMENT].isContribution, - }) - } - } - - return out - } - - private static async getAppToken(context: IStepContext): Promise { - if (this.githubAuthenticator) { - let appToken: AppTokenResponse - if (context.pipelineData.appToken) { - // check expiration - const expiration = moment(context.pipelineData.appToken.expiration).add(5, 'minutes') - if (expiration.isAfter(moment())) { - // need to refresh - const authResponse = await this.githubAuthenticator({ type: 'app' }) - const jwtToken = authResponse.token - appToken = await getAppToken(jwtToken, context.integration.integrationIdentifier) - } else { - appToken = context.pipelineData.appToken - } - } else { - const authResponse = await this.githubAuthenticator({ type: 'app' }) - const jwtToken = authResponse.token - appToken = await getAppToken(jwtToken, context.integration.integrationIdentifier) - } - - context.pipelineData.appToken = appToken - - return appToken.token - } - - throw new Error('GitHub integration is not configured!') - } - - private static async getMemberData(context: IStepContext, login: string): Promise { - const appToken = await this.getAppToken(context) - return getMember(login, appToken) - } - - private static async getMemberEmail(context: IStepContext, login: string): Promise { - if (IS_TEST_ENV) { - return '' - } - - const cache: RedisCache = context.pipelineData.emailCache - - const existing = await cache.get(login) - if (existing) { - if (existing === 'null') { - return '' - } - - return existing - } - - const member = await this.getMemberData(context, login) - const email = (member && member.email ? member.email : '').trim() - if (email && email.length > 0) { - await cache.set(login, email, 60 * 60) - return email - } - - await cache.set(login, 'null', 60 * 60) - return '' - } - - private static async parseWebhookMember( - login: string, - context: IStepContext, - ): Promise { - if (IS_TEST_ENV) { - return { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: context.integration.id, - }, - } as PlatformIdentities, - } - } - - const member = await getMember(login, context.integration.token) - if (member) { - return GithubIntegrationService.parseMember(member, context) - } - - return undefined - } - - public static async parseMember(memberFromApi: any, context: IStepContext): Promise { - const email = await this.getMemberEmail(context, memberFromApi.login) - - const member: Member = { - username: { - [PlatformType.GITHUB]: { - username: memberFromApi.login, - integrationId: context.integration.id, - }, - } as PlatformIdentities, - displayName: memberFromApi?.name?.trim() || memberFromApi.login, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: memberFromApi.isHireable || false, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: memberFromApi.url, - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: memberFromApi.bio || '', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: memberFromApi.location || '', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.GITHUB]: memberFromApi.avatarUrl || '', - }, - }, - emails: email ? [email] : [], - } - - if (memberFromApi.websiteUrl) { - member.attributes[MemberAttributeName.WEBSITE_URL] = { - [PlatformType.GITHUB]: memberFromApi.websiteUrl, - } - } - - if (memberFromApi.company) { - if (IS_TEST_ENV) { - member.organizations = [{ name: 'crowd.dev' }] - } else { - const company = memberFromApi.company.replace('@', '').trim() - const fromAPI = await getOrganization(company, context.integration.token) - - if (fromAPI) { - member.organizations = [ - { - name: fromAPI.name, - description: fromAPI.description ?? null, - location: fromAPI.location ?? null, - logo: fromAPI.avatarUrl ?? null, - url: fromAPI.url ?? null, - github: fromAPI.url - ? { handle: fromAPI.url.replace('https://github.com/', '') } - : null, - twitter: fromAPI.twitterUsername ? { handle: fromAPI.twitterUsername } : null, - website: fromAPI.websiteUrl ?? null, - }, - ] - } else { - member.organizations = [{ name: company }] - } - } - } - // TODO Fix this (multiple member identities with secondary identities) - // if (memberFromApi.twitterUsername) { - // member.attributes[MemberAttributeName.URL][ - // PlatformType.TWITTER - // ] = `https://twitter.com/${memberFromApi.twitterUsername}` - // member.username[PlatformType.TWITTER] = { - // username: memberFromApi.twitterUsername, - // integrationId: context.integration.id, - // } - // } - - if (memberFromApi.followers && memberFromApi.followers.totalCount > 0) { - member.reach = { [PlatformType.GITHUB]: memberFromApi.followers.totalCount } - } - - return member - } - - /** - * Searches given repository name among installed repositories - * Returns null if given repo is not found. - * @param name The tenant we are working on - * @param context - * @returns Found repo object - */ - private static getRepoByName(name: string, context: IStepContext): Repo | null { - const availableRepo: Repo | undefined = singleOrDefault( - context.pipelineData.repos, - (r) => r.name === name, - ) - if (availableRepo) { - return { ...availableRepo, available: true } - } - - const unavailableRepo: Repo | undefined = singleOrDefault( - context.pipelineData.unavailableRepos, - (r) => r.name === name, - ) - if (unavailableRepo) { - return { ...unavailableRepo, available: false } - } - - return null - } -} diff --git a/backend/src/serverless/integrations/services/integrations/twitterIntegrationService.ts b/backend/src/serverless/integrations/services/integrations/twitterIntegrationService.ts deleted file mode 100644 index 002694f0c3..0000000000 --- a/backend/src/serverless/integrations/services/integrations/twitterIntegrationService.ts +++ /dev/null @@ -1,433 +0,0 @@ -import moment from 'moment' -import lodash from 'lodash' -import { TWITTER_GRID, TWITTER_MEMBER_ATTRIBUTES, TwitterActivityType } from '@crowd/integrations' -import { IntegrationType, MemberAttributeName, PlatformType } from '@crowd/types' -import { IntegrationServiceBase } from '../integrationServiceBase' -import { TWITTER_CONFIG } from '../../../../conf' -import { - IIntegrationStream, - IPendingStream, - IProcessStreamResults, - IStepContext, - IStreamResultOperation, -} from '../../../../types/integration/stepResult' -import MemberAttributeSettingsService from '../../../../services/memberAttributeSettingsService' -import { Endpoint } from '../../types/regularTypes' -import { TwitterMembers, TwitterParsedPosts } from '../../types/twitterTypes' -import getFollowers from '../../usecases/twitter/getFollowers' -import findPostsByMention from '../../usecases/twitter/getPostsByMention' -import findPostsByHashtag from '../../usecases/twitter/getPostsByHashtag' -import { AddActivitiesSingle, PlatformIdentities } from '../../types/messageTypes' -import Operations from '../../../dbOperations/operations' -import IntegrationRepository from '../../../../database/repositories/integrationRepository' - -/* eslint class-methods-use-this: 0 */ - -/* eslint-disable @typescript-eslint/no-unused-vars */ - -export class TwitterIntegrationService extends IntegrationServiceBase { - static maxRetrospect: number = TWITTER_CONFIG.maxRetrospectInSeconds || 7380 - - constructor() { - super(IntegrationType.TWITTER, 30) - - this.globalLimit = TWITTER_CONFIG.globalLimit || 0 - this.onboardingLimitModifierFactor = 0.7 - this.limitResetFrequencySeconds = (TWITTER_CONFIG.limitResetFrequencyDays || 0) * 24 * 60 * 60 - } - - async preprocess(context: IStepContext): Promise { - await TwitterIntegrationService.refreshToken(context) - context.pipelineData.followers = new Set(context.integration.followers) - } - - async createMemberAttributes(context: IStepContext): Promise { - const service = new MemberAttributeSettingsService(context.serviceContext) - await service.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - } - - async getStreams(context: IStepContext): Promise { - const hashtags = context.integration.settings.hashtags - - // twitter deprecated followers endpoint, so we are left only with mentions - return ['mentions'].concat((hashtags || []).map((h) => `hashtag/${h}`)).map((s) => ({ - value: s, - metadata: { page: '' }, - })) - } - - async processStream( - stream: IIntegrationStream, - context: IStepContext, - ): Promise { - const { fn, arg } = TwitterIntegrationService.getUsecase( - stream.value, - context.pipelineData.profileId, - ) - - const afterDate = context.onboarding - ? undefined - : moment().utc().subtract(TwitterIntegrationService.maxRetrospect, 'seconds').toISOString() - - const { records, nextPage, limit, timeUntilReset } = await fn( - { - token: context.integration.token, - page: stream.metadata.page, - perPage: 100, - ...arg, - }, - context.logger, - ) - - const nextPageStream = nextPage - ? { value: stream.value, metadata: { page: nextPage } } - : undefined - const sleep = limit <= 1 ? timeUntilReset : undefined - - if (records === undefined) { - context.logger.error( - { - stream: stream.value, - page: stream.metadata.page, - profileId: context.pipelineData.profileId, - }, - 'No records returned!', - ) - - throw new Error(`No records returned for stream ${stream.value}!`) - } - - if (records.length === 0) { - return { - operations: [], - nextPageStream, - sleep, - } - } - - const activities = this.parseActivities(context, records, stream) - - const lastRecord = activities.length > 0 ? activities[activities.length - 1] : undefined - - return { - operations: [ - { - type: Operations.UPSERT_ACTIVITIES_WITH_MEMBERS, - records: activities, - }, - ], - lastRecord, - lastRecordTimestamp: lastRecord ? lastRecord.timestamp.getTime() : undefined, - nextPageStream, - sleep, - } - } - - async isProcessingFinished( - context: IStepContext, - currentStream: IIntegrationStream, - lastOperations: IStreamResultOperation[], - lastRecord?: any, - lastRecordTimestamp?: number, - ): Promise { - switch (currentStream.value) { - case 'followers': - return TwitterIntegrationService.isJoin( - context.pipelineData.followers, - TwitterIntegrationService.mapToPath( - lastOperations.flatMap((o) => o.records), - 'member.attributes.twitter.sourceId', - ), - ) - - default: - if (lastRecordTimestamp === undefined) return true - - return IntegrationServiceBase.isRetrospectOver( - lastRecordTimestamp, - context.startTimestamp, - TwitterIntegrationService.maxRetrospect, - ) - } - } - - async postprocess(context: IStepContext): Promise { - if (context.onboarding) { - // When we are onboarding we reset the frequency to RESET_FREQUENCY_DAYS.in_hours - 6 hours. - // This is because the tweets allowed during onboarding are free. Like this, the limit will reset 6h after the onboarding. - context.integration.limitLastResetAt = moment() - .utc() - .subtract(this.limitResetFrequencySeconds * 2, 'seconds') - .format('YYYY-MM-DD HH:mm:ss') - } - - context.integration.settings.followers = Array.from(context.pipelineData.followers.values()) - } - - /** - * Get the activities and members formatted as SQS message bodies from - * the set of records obtained in the API. - * @param context process step context data - * @param records List of records coming from the API - * @param stream integration stream that we are currently processing - * @returns The set of messages and the date of the last activity - */ - parseActivities( - context: IStepContext, - records: Array, - stream: IIntegrationStream, - ): AddActivitiesSingle[] { - switch (stream.value) { - case 'followers': { - const followers = this.parseFollowers(context, records) - - // Update the follower set - context.pipelineData.followers = new Set([ - ...context.pipelineData.followers, - ...records.map((record) => record.id), - ]) - - if (followers.length > 0) { - return followers - } - return [] - } - default: { - return this.parsePosts(context, records, stream) - } - } - } - - /** - * Map the follower records to the format of the message to add activities and members - * @param context process step context data - * @param records List of records coming from the API - * @returns List of activities and members - */ - parseFollowers(context: IStepContext, records: TwitterMembers): Array { - const timestampObj = context.onboarding - ? moment('1970-01-01T00:00:00+00:00').utc() - : moment().utc() - let out = records.map((record) => ({ - username: record.username, - tenant: context.integration.tenantId, - platform: PlatformType.TWITTER, - type: 'follow', - sourceId: IntegrationServiceBase.generateSourceIdHash( - record.username, - 'follow', - moment('1970-01-01T00:00:00+00:00').utc().unix().toString(), - PlatformType.TWITTER, - ), - // When onboarding we need an old date. Otherwise, we can place it in a 2h window - timestamp: timestampObj.toDate(), - url: `https://twitter.com/${record.username}`, - member: { - username: { - [PlatformType.TWITTER]: { - username: record.username, - integrationId: context.integration.id, - sourceId: record.id, - }, - } as PlatformIdentities, - reach: { [PlatformType.TWITTER]: record.public_metrics.followers_count }, - attributes: { - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.TWITTER]: record.id, - }, - ...(record.profile_image_url && { - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: record.profile_image_url, - }, - }), - ...(record.location && { - [MemberAttributeName.LOCATION]: { - [PlatformType.TWITTER]: record.location, - }, - }), - ...(record.description && { - [MemberAttributeName.BIO]: { - [PlatformType.TWITTER]: record.description, - }, - }), - [MemberAttributeName.URL]: { - [PlatformType.TWITTER]: `https://twitter.com/${record.username}`, - }, - }, - }, - score: TWITTER_GRID[TwitterActivityType.FOLLOW].score, - isContribution: TWITTER_GRID[TwitterActivityType.FOLLOW].isContribution, - })) - - // It is imperative that we remove the followers we have already seen. - // Since they come without timestamps, and we have set the followers timestamp to now(), - // this would cause repeated activities otherwise - out = out.filter( - (activity) => - !context.pipelineData.followers.has( - activity.member.attributes[MemberAttributeName.SOURCE_ID][PlatformType.TWITTER], - ), - ) - - return out - } - - /** - * Map the posts (mentions or hashtags) records to the format of the message to add activities and members - * @param context process step context data - * @param records List of records coming from the API - * @param stream integration stream that we are currently processing - * @returns List of activities and members - */ - parsePosts( - context: IStepContext, - records: TwitterParsedPosts, - stream: IIntegrationStream, - ): Array { - return records.map((record) => { - const out: any = { - username: record.member.username, - tenant: context.integration.tenantId, - platform: PlatformType.TWITTER, - type: stream.value === 'mentions' ? 'mention' : 'hashtag', - sourceId: record.id, - timestamp: moment(Date.parse(record.created_at)).utc().toDate(), - body: record.text ? record.text : '', - url: `https://twitter.com/i/status/${record.id}`, - attributes: { - attachments: record.attachments ? record.attachments : [], - entities: record.entities ? record.entities : [], - }, - member: { - username: record.member.username, - attributes: { - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.TWITTER]: record.member.id, - }, - [MemberAttributeName.URL]: { - [PlatformType.TWITTER]: `https://twitter.com/${record.member.username}`, - }, - ...(record.member.profile_image_url && { - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: record.member.profile_image_url, - }, - }), - ...(record.member.location && { - [MemberAttributeName.LOCATION]: { - [PlatformType.TWITTER]: record.member.location, - }, - }), - ...(record.member.description && { - [MemberAttributeName.BIO]: { - [PlatformType.TWITTER]: record.member.description, - }, - }), - }, - reach: { [PlatformType.TWITTER]: record.member.public_metrics.followers_count }, - }, - score: - stream.value === 'mentions' - ? TWITTER_GRID[TwitterActivityType.MENTION].score - : TWITTER_GRID[TwitterActivityType.HASHTAG].score, - isContribution: - stream.value === 'mentions' - ? TWITTER_GRID[TwitterActivityType.MENTION].isContribution - : TWITTER_GRID[TwitterActivityType.HASHTAG].isContribution, - } - - if (stream.value.includes('hashtag')) { - out.attributes.hashtag = TwitterIntegrationService.getHashtag(stream.value) - } - return out - }) - } - - /** - * Get a hashtag for `attributes.hashtag` - * @param endpoint The current endpoint - * @returns The name of the hashtag - */ - private static getHashtag(endpoint: Endpoint): string { - return endpoint.includes('#') - ? endpoint.slice(endpoint.indexOf('#') + 1) - : endpoint.slice(endpoint.indexOf('/') + 1) - } - - /** - * Map a field of activities given a path - * - ([{attributes: 1}, {attributes: 2}], attributes) => [1, 2] - * @param activities Array of activities to be mapped - * @param path Path to the field of the activity we want - * @returns A list of the values of the field of the activities - */ - private static mapToPath(activities: Array, path: string) { - return activities.map((activity) => lodash.get(activity, path)) - } - - /** - * Checks whether any element of the array is the same of any element in the set - * @param set Set of elements - * @param array Array of elements - * @returns Boolean - */ - private static isJoin(set: Set, array: Array): boolean { - const arrayToSet = new Set(array) - return new Set([...set, ...arrayToSet]).size !== set.size + arrayToSet.size - } - - /** - * Get the usecase for the given endpoint with its main argument - * @param stream The stream we are currently targeting - * @param profileId The ID of the profile we are getting data for - * @returns The function to call, as well as its main argument - */ - private static getUsecase( - stream: string, - profileId: string, - ): { - fn - arg: any - } { - switch (stream) { - case 'followers': - return { fn: getFollowers, arg: { profileId } } - case 'mentions': - return { fn: findPostsByMention, arg: { profileId } } - default: { - const hashtag = stream.includes('#') - ? stream.slice(stream.indexOf('#') + 1) - : stream.slice(stream.indexOf('/') + 1) - return { fn: findPostsByHashtag, arg: { hashtag } } - } - } - } - - public static async refreshToken(context: IStepContext): Promise { - const superface = IntegrationServiceBase.superfaceClient() - const profile = await superface.getProfile('oauth2/refresh-token') - const profileWithNewTokens: any = ( - await profile.getUseCase('GetAccessTokenFromRefreshToken').perform({ - refreshToken: context.integration.refreshToken, - clientId: TWITTER_CONFIG.clientId, - clientSecret: TWITTER_CONFIG.clientSecret, - }) - ).unwrap() - - context.integration.refreshToken = profileWithNewTokens.refreshToken - context.integration.token = profileWithNewTokens.accessToken - - context.pipelineData = { - ...context.pipelineData, - profileId: context.integration.integrationIdentifier, - } - - await IntegrationRepository.update( - context.integration.id, - { - token: context.integration.token, - refreshToken: context.integration.refreshToken, - }, - context.repoContext, - ) - } -} diff --git a/backend/src/serverless/integrations/services/integrations/twitterReachIntegrationService.ts b/backend/src/serverless/integrations/services/integrations/twitterReachIntegrationService.ts deleted file mode 100644 index 6949592b3e..0000000000 --- a/backend/src/serverless/integrations/services/integrations/twitterReachIntegrationService.ts +++ /dev/null @@ -1,141 +0,0 @@ -import lodash from 'lodash' -import { IntegrationType, PlatformType } from '@crowd/types' -import { IntegrationServiceBase } from '../integrationServiceBase' -import { - IIntegrationStream, - IProcessStreamResults, - IStepContext, - IStreamResultOperation, -} from '../../../../types/integration/stepResult' -import { TwitterIntegrationService } from './twitterIntegrationService' -import MemberRepository from '../../../../database/repositories/memberRepository' -import getProfiles from '../../usecases/twitter/getProfiles' -import { Updates } from '../../types/messageTypes' -import MemberService from '../../../../services/memberService' -import Operations from '../../../dbOperations/operations' - -/* eslint class-methods-use-this: 0 */ - -/* eslint-disable @typescript-eslint/no-unused-vars */ - -export class TwitterReachIntegrationService extends IntegrationServiceBase { - static readonly TWITTER_API_MAX_USERNAME_LENGTH = 15 - - constructor() { - super(IntegrationType.TWITTER_REACH, 24 * 60) - } - - async preprocess(context: IStepContext): Promise { - await TwitterIntegrationService.refreshToken(context) - - context.pipelineData.members = await MemberRepository.findAndCountAll( - { filter: { platform: PlatformType.TWITTER } }, - context.repoContext, - ) - } - - async getStreams(context: IStepContext, metadata?: any): Promise { - // Map to object filtering out undefined and long usernames - const results = context.pipelineData.members.rows.reduce((acc, m) => { - const username = m.username.twitter - if ( - username !== undefined && - username.length < TwitterReachIntegrationService.TWITTER_API_MAX_USERNAME_LENGTH - ) { - acc.push({ - id: m.id, - username: username.toLowerCase(), - reach: m.reach, - }) - } - return acc - }, []) - - const chunks = lodash.chunk(results, 99) - - let chunkIndex = 1 - return chunks.map((c) => ({ - value: `chunk-${chunkIndex++}`, - metadata: { - members: c, - }, - })) - } - - async processStream( - stream: IIntegrationStream, - context: IStepContext, - ): Promise { - const members = stream.metadata.members.map((m) => m.username) - const { records, nextPage, limit, timeUntilReset } = await getProfiles( - { - usernames: members, - token: context.integration.token, - }, - context.logger, - ) - - const nextPageStream = nextPage - ? { value: stream.value, metadata: { page: nextPage } } - : undefined - const sleep = limit <= 1 ? timeUntilReset : undefined - - if (records.length === 0) { - return { - operations: [], - nextPageStream, - sleep, - } - } - - const results = this.parseReach(records, stream.metadata.members) - - return { - operations: [ - { - type: Operations.UPDATE_MEMBERS, - records: results, - }, - ], - nextPageStream, - } - } - - async isProcessingFinished( - context: IStepContext, - currentStream: IIntegrationStream, - lastOperations: IStreamResultOperation[], - lastRecord?: any, - lastRecordTimestamp?: number, - ): Promise { - return true - } - - /** - * Get the followers number of followers - * @param records List of records coming from the API - * @param members Usernames we are working on - * @returns The number of followers - */ - parseReach(records: Array, members: any[]): Updates { - const out = [] - - const hashedMembers = lodash.keyBy(members, 'username') - records.forEach((record) => { - record.username = record.username.toLowerCase() - const member = hashedMembers[record.username] - if (record.followersCount !== member.reach.twitter) { - out.push({ - id: member.id, - update: { - reach: MemberService.calculateReach(member.reach || {}, { - [PlatformType.TWITTER]: record.followersCount, - }), - }, - }) - } - }) - - return out - } -} diff --git a/backend/src/serverless/integrations/services/webhookProcessor.ts b/backend/src/serverless/integrations/services/webhookProcessor.ts deleted file mode 100644 index af4ad4d1d1..0000000000 --- a/backend/src/serverless/integrations/services/webhookProcessor.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { LoggerBase, getChildLogger } from '@crowd/logging' -import moment from 'moment' -import { singleOrDefault } from '@crowd/common' -import { IRepositoryOptions } from '../../../database/repositories/IRepositoryOptions' -import IncomingWebhookRepository from '../../../database/repositories/incomingWebhookRepository' -import IntegrationRepository from '../../../database/repositories/integrationRepository' -import SequelizeRepository from '../../../database/repositories/sequelizeRepository' -import getUserContext from '../../../database/utils/getUserContext' -import { IServiceOptions } from '../../../services/IServiceOptions' -import { IStepContext } from '../../../types/integration/stepResult' -import { NodeWorkerProcessWebhookMessage } from '../../../types/mq/nodeWorkerProcessWebhookMessage' -import { WebhookState } from '../../../types/webhooks' -import bulkOperations from '../../dbOperations/operationsWorker' -import { sendNodeWorkerMessage } from '../../utils/nodeWorkerSQS' -import { IntegrationServiceBase } from './integrationServiceBase' -import SegmentRepository from '../../../database/repositories/segmentRepository' - -export class WebhookProcessor extends LoggerBase { - constructor( - options: IServiceOptions, - private readonly integrationServices: IntegrationServiceBase[], - ) { - super(options.log) - } - - static readonly MAX_RETRY_LIMIT = 5 - - async processWebhook(webhookId: string, force?: boolean, fireCrowdWebhooks?: boolean) { - const options = (await SequelizeRepository.getDefaultIRepositoryOptions()) as IRepositoryOptions - const repo = new IncomingWebhookRepository(options) - const webhook = await repo.findById(webhookId) - let logger = getChildLogger('processWebhook', this.log, { webhookId }) - - if (webhook === null || webhook === undefined) { - logger.error('Webhook not found!') - return - } - - logger.debug('Processing webhook!') - - logger = getChildLogger('processWebhook', this.log, { - type: webhook.type, - tenantId: webhook.tenantId, - integrationId: webhook.integrationId, - }) - - logger.debug('Webhook found!') - - if (!(force === true) && webhook.state !== WebhookState.PENDING) { - logger.error({ state: webhook.state }, 'Webhook is not in pending state!') - return - } - - const userContext = await getUserContext(webhook.tenantId) - userContext.log = logger - - const integration = await IntegrationRepository.findById(webhook.integrationId, userContext) - if (integration.platform === 'github' || integration.platform === 'discord') { - return - } - const segment = await new SegmentRepository(userContext).findById(integration.segmentId) - userContext.currentSegments = [segment] - - const intService = singleOrDefault( - this.integrationServices, - (s) => s.type === integration.platform, - ) - if (intService === undefined) { - logger.error('No integration service configured!') - throw new Error(`No integration service configured for type '${integration.platform}'!`) - } - - const stepContext: IStepContext = { - startTimestamp: moment().utc().unix(), - limitCount: integration.limitCount || 0, - onboarding: false, - pipelineData: {}, - webhook, - integration, - serviceContext: userContext, - repoContext: userContext, - logger, - } - - if (integration.settings.updateMemberAttributes) { - logger.trace('Updating member attributes!') - - await intService.createMemberAttributes(stepContext) - - integration.settings.updateMemberAttributes = false - await IntegrationRepository.update( - integration.id, - { settings: integration.settings }, - userContext, - ) - } - - const whContext = { ...userContext } - whContext.transaction = await SequelizeRepository.createTransaction(whContext) - - try { - const result = await intService.processWebhook(webhook, stepContext) - for (const operation of result.operations) { - if (operation.records.length > 0) { - logger.trace( - { operationType: operation.type }, - `Processing bulk operation with ${operation.records.length} records!`, - ) - await bulkOperations(operation.type, operation.records, userContext, fireCrowdWebhooks) - } - } - await repo.markCompleted(webhook.id) - logger.debug('Webhook processed!') - } catch (err) { - if (err.rateLimitResetSeconds) { - logger.warn(err, 'Rate limit reached while processing webhook! Delaying...') - await sendNodeWorkerMessage( - integration.tenantId, - new NodeWorkerProcessWebhookMessage(integration.tenantId, webhookId), - err.rateLimitResetSeconds + 5, - ) - } else { - logger.error(err, 'Error processing webhook!') - await repo.markError(webhook.id, err) - } - } finally { - await SequelizeRepository.commitTransaction(whContext.transaction) - } - } -} diff --git a/backend/src/serverless/integrations/types/iteratorTypes.ts b/backend/src/serverless/integrations/types/iteratorTypes.ts index 2d95340029..a7768322c3 100644 --- a/backend/src/serverless/integrations/types/iteratorTypes.ts +++ b/backend/src/serverless/integrations/types/iteratorTypes.ts @@ -22,10 +22,6 @@ export interface TwitterOutput extends BaseOutput { export interface TwitterReachOutput extends BaseOutput {} -export interface DiscordOutput extends BaseOutput { - channels: any[] -} - export interface SlackOutput extends BaseOutput { channels: any[] users: Object diff --git a/backend/src/serverless/integrations/types/messageTypes.ts b/backend/src/serverless/integrations/types/messageTypes.ts index fb4915a419..ae2836f87a 100644 --- a/backend/src/serverless/integrations/types/messageTypes.ts +++ b/backend/src/serverless/integrations/types/messageTypes.ts @@ -1,4 +1,5 @@ -import { PlatformType } from '@crowd/types' +import { MemberIdentityType, PlatformType } from '@crowd/types' + import { State } from './regularTypes' export type IntegrationsMessage = { @@ -10,11 +11,6 @@ export type IntegrationsMessage = { args: object } -export type MicroserviceMessage = { - service: string - tenant: string -} - export interface DevtoIntegrationMessage extends IntegrationsMessage { integrationId: string } @@ -32,13 +28,6 @@ export interface TwitterReachMessage extends IntegrationsMessage { } } -export interface DiscordIntegrationMessage extends IntegrationsMessage { - args: { - guildId: string - channels?: any - } -} - export interface SlackIntegrationMessage extends IntegrationsMessage { args: { channels?: any @@ -48,7 +37,8 @@ export interface SlackIntegrationMessage extends IntegrationsMessage { export interface GithubIntegrationMessage extends IntegrationsMessage {} export interface MemberIdentity { - username: string + value: string + type: MemberIdentityType integrationId: string sourceId?: string } @@ -66,7 +56,6 @@ export type Member = { bio?: string reach?: number | any location?: string - lastEnriched?: Date | null enrichedBy?: string[] | null contributions?: any } @@ -88,7 +77,6 @@ export type AddActivitiesSingle = { url?: string channel?: string score?: number - isContribution?: boolean } export type AddActivities = Array diff --git a/backend/src/serverless/integrations/types/regularTypes.ts b/backend/src/serverless/integrations/types/regularTypes.ts index 0516cd52d6..57c5a79d06 100644 --- a/backend/src/serverless/integrations/types/regularTypes.ts +++ b/backend/src/serverless/integrations/types/regularTypes.ts @@ -1,12 +1,16 @@ +import { PlatformType } from '@crowd/types' + export type Repo = { url: string name: string - createdAt: string - owner: string + updatedAt?: string + createdAt?: string + owner?: string available?: boolean fork?: boolean private?: boolean cloneUrl?: string + forkedFrom?: string | null } export type Repos = Array @@ -20,3 +24,38 @@ export type State = { endpoint: string page: string } + +export interface IntegrationProgressDataGithubItem { + db: number + remote: number + status: 'ok' | 'in-progress' + percentage: number + message: string +} + +export interface IntegrationProgressDataGithub { + forks: IntegrationProgressDataGithubItem + stars: IntegrationProgressDataGithubItem + issues: IntegrationProgressDataGithubItem + pullRequests: IntegrationProgressDataGithubItem + other: IntegrationProgressDataOtherItem +} + +export interface IntegrationProgressDataOtherItem { + db: number + message: string + status: 'ok' | 'in-progress' +} + +export interface IntegrationProgressDataOther { + other: IntegrationProgressDataOtherItem +} + +export interface IntegrationProgress { + type: 'github' | 'other' + platform: PlatformType + segmentId: string + segmentName: string + reportStatus: 'calculating' | 'ok' | 'integration-is-not-in-progress' + data?: IntegrationProgressDataGithub | IntegrationProgressDataOther +} diff --git a/backend/src/serverless/integrations/types/superfaceTypes.ts b/backend/src/serverless/integrations/types/superfaceTypes.ts deleted file mode 100644 index 699702358a..0000000000 --- a/backend/src/serverless/integrations/types/superfaceTypes.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type SocialResponse = { - records: Array - nextPage: string - limit: number - timeUntilReset: number -} diff --git a/backend/src/serverless/integrations/usecases/__tests__/devto.api.test.ts b/backend/src/serverless/integrations/usecases/__tests__/devto.api.test.ts deleted file mode 100644 index c5b2d3c1aa..0000000000 --- a/backend/src/serverless/integrations/usecases/__tests__/devto.api.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { getOrganizationArticles } from '../devto/getOrganizationArticles' -import { getArticleComments } from '../devto/getArticleComments' -import { getUserArticles } from '../devto/getUserArticles' -import { getUserById } from '../devto/getUser' - -function expectDefinedNumber(val: any) { - expect(val).toBeDefined() - expect(typeof val).toBe('number') -} - -function expectDefinedString(val: any) { - expect(val).toBeDefined() - expect(typeof val).toBe('string') -} - -function expectDefinedStringOrNull(val: any) { - expect(val).toBeDefined() - expect(typeof val === 'string' || val === null).toBeTruthy() -} - -function expectDefinedArray(val: any) { - expect(val).toBeDefined() - expect(Array.isArray(val)).toBeTruthy() -} - -describe('Devto API tests', () => { - const organization = 'digitalocean' - const organizationArticleId = 524804 - - const username = 'kukicado' - const userId = 139953 - - it('Should return correct required properties when fetching organization articles', async () => { - const articles = await getOrganizationArticles(organization, 1, 1) - - expect(articles.length).toEqual(1) - - const article = articles[0] - expectDefinedNumber(article.id) - expectDefinedString(article.title) - expectDefinedString(article.description) - expectDefinedString(article.readable_publish_date) - expectDefinedArray(article.tag_list) - expectDefinedString(article.slug) - expectDefinedString(article.url) - expectDefinedNumber(article.comments_count) - expectDefinedString(article.published_at) - expectDefinedString(article.last_comment_at) - }) - - it('Should return the correct required properties when fetching article comments', async () => { - const comments = await getArticleComments(organizationArticleId) - expect(comments.length > 0).toBeTruthy() - - const comment = comments[0] - expectDefinedString(comment.id_code) - expectDefinedString(comment.created_at) - expectDefinedString(comment.body_html) - expectDefinedString(comment.body_html) - expectDefinedArray(comment.children) - expect(comment.user).toBeDefined() - - // check comment user properties - expectDefinedNumber(comment.user.user_id) - expectDefinedString(comment.user.name) - expectDefinedString(comment.user.username) - expectDefinedStringOrNull(comment.user.twitter_username) - expectDefinedStringOrNull(comment.user.github_username) - expectDefinedStringOrNull(comment.user.website_url) - expectDefinedString(comment.user.profile_image) - expectDefinedString(comment.user.profile_image_90) - }) - - it('Should return the correct required properties when fetching user articles', async () => { - const articles = await getUserArticles(username, 1, 1) - - expect(articles.length).toEqual(1) - - const article = articles[0] - expectDefinedNumber(article.id) - expectDefinedString(article.title) - expectDefinedString(article.description) - expectDefinedString(article.readable_publish_date) - expectDefinedArray(article.tag_list) - expectDefinedString(article.slug) - expectDefinedString(article.url) - expectDefinedNumber(article.comments_count) - expectDefinedString(article.published_at) - expectDefinedString(article.last_comment_at) - }) - - it('Should return the correct required properties when fetching a user', async () => { - const user = await getUserById(userId) - - expectDefinedNumber(user.id) - expectDefinedString(user.name) - expectDefinedString(user.username) - expectDefinedStringOrNull(user.twitter_username) - expectDefinedStringOrNull(user.github_username) - expectDefinedStringOrNull(user.website_url) - expectDefinedStringOrNull(user.location) - expectDefinedStringOrNull(user.summary) - expectDefinedString(user.profile_image) - }) -}) diff --git a/backend/src/serverless/integrations/usecases/__tests__/isInvalid.test.ts b/backend/src/serverless/integrations/usecases/__tests__/isInvalid.test.ts deleted file mode 100644 index 698b3c5008..0000000000 --- a/backend/src/serverless/integrations/usecases/__tests__/isInvalid.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import isInvalid from '../isInvalid' - -describe('Is invalid tests', () => { - it('It should return valid when the result is correct', async () => { - const result = { - value: { - followers: [1, 2, 3], - nextPage: '', - }, - } - expect(isInvalid(result, 'followers')).toBe(false) - }) - - it('It should also work for other keys', async () => { - const result = { - value: { - mentions: [1, 2, 3], - nextPage: '', - }, - } - expect(isInvalid(result, 'mentions')).toBe(false) - }) - - it('It return invalid when no value also work for other keys', async () => { - const result = { - broken: true, - } - expect(isInvalid(result, 'mentions')).toBe(true) - }) - - it('It return invalid when no key', async () => { - const result = { - value: { - broken: true, - }, - } - expect(isInvalid(result, 'mentions')).toBe(true) - }) - - it('It return invalid when wrong key', async () => { - const result = { - value: { - mentions: [1, 2, 3], - nextPage: '', - }, - } - expect(isInvalid(result, 'followers')).toBe(true) - }) - - it('It return valid when empty list', async () => { - const result = { - value: { - mentions: [], - nextPage: '', - }, - } - expect(isInvalid(result, 'mentions')).toBe(false) - }) -}) diff --git a/backend/src/serverless/integrations/usecases/chat/getChannels.ts b/backend/src/serverless/integrations/usecases/chat/getChannels.ts deleted file mode 100644 index 0a28bee682..0000000000 --- a/backend/src/serverless/integrations/usecases/chat/getChannels.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { getServiceChildLogger } from '@crowd/logging' -import { SuperfaceClient } from '@superfaceai/one-sdk' -import { timeout } from '@crowd/common' -import { cleanSuperfaceError } from '../cleanError' -import isInvalid from '../isInvalid' - -const log = getServiceChildLogger('getChannels') - -/** - * Try if a channel is readable - * @param accessToken Discord bot token - * @param channel Channel ID - * @returns Limit if the channel is readable, false otherwise - */ -async function tryChannel( - client: SuperfaceClient, - source: string, - accessToken: string, - channel: any, -): Promise { - try { - const input = { - destination: channel.id, - limit: 1, - } - const profile = await client.getProfile('chat/messages') - const provider = await client.getProvider(source) - const result: any = await profile.getUseCase('GetMessages').perform(input, { - provider, - parameters: { accessToken }, - }) - - if (result.value) { - if ('rateLimit' in result.value) { - return result.value.rateLimit.remainingRequests - } - return 10 - } - return false - } catch (err) { - return false - } -} - -async function getChannels( - client: SuperfaceClient, - source: string, - input: any, - accessToken: string, - tryChannels = true, -) { - try { - const profile = await client.getProfile('chat/channels') - const provider = await client.getProvider(source) - const parameters = { accessToken } - const result: any = await profile - .getUseCase('GetChannels') - .perform(input, { provider, parameters }) - if (isInvalid(result, 'channels')) { - log.warn({ input, result }, 'Invalid request in getChannels') - } - if (tryChannels) { - const out: any[] = [] - for (const channel of result.value.channels) { - const limit = await tryChannel(client, source, accessToken, channel) - if (limit) { - const toOut: any = { - name: channel.name, - id: channel.id, - } - out.push(toOut) - if (limit <= 1 && limit !== false) { - await timeout(5 * 1000) - } - } - } - return out - } - - return result.value.channels.map((c) => ({ - name: c.name, - id: c.id, - })) - } catch (err) { - throw cleanSuperfaceError(err) - } -} - -export default getChannels - -// getDestinations('877903817948147752', 'ODc3OTEwNjM0MzA4NzA2MzI0.YR5f_g.TrYuoK2yWA5-LpPlDQ0Nlzc8dOE') diff --git a/backend/src/serverless/integrations/usecases/chat/getMembers.ts b/backend/src/serverless/integrations/usecases/chat/getMembers.ts deleted file mode 100644 index 61085cf77b..0000000000 --- a/backend/src/serverless/integrations/usecases/chat/getMembers.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { getServiceChildLogger } from '@crowd/logging' -import { SuperfaceClient } from '@superfaceai/one-sdk' -import { SocialResponse } from '../../types/superfaceTypes' -import { cleanSuperfaceError } from '../cleanError' -import isInvalid from '../isInvalid' - -const log = getServiceChildLogger('getMembers') - -async function getMembers( - client: SuperfaceClient, - source: string, - accessToken: string, - server: string, - page: string, - perPage: number = 100, -): Promise { - try { - const input: any = { - limit: perPage, - page: page || undefined, - } - if (server) { - input.server = server - } - - const profile = await client.getProfile('chat/members') - const provider = await client.getProvider(source) - const result: any = await profile.getUseCase('GetMembers').perform(input, { - provider, - parameters: { accessToken }, - }) - - if (isInvalid(result, 'members')) { - log.warn({ input, result }, 'Invalid request in hashtag') - } - let limit - let timeUntilReset - if (result.value.rateLimit) { - limit = result.value.rateLimit.remainingRequests - timeUntilReset = result.value.rateLimit.resetAfter - } else { - limit = 100 - timeUntilReset = 1 - } - - return { - records: result.value.members, - nextPage: result.value.members.length < input.limit ? undefined : result.value.nextPage, - limit, - timeUntilReset, - } - } catch (err) { - throw cleanSuperfaceError(err) - } -} - -export default getMembers diff --git a/backend/src/serverless/integrations/usecases/chat/getMessages.ts b/backend/src/serverless/integrations/usecases/chat/getMessages.ts deleted file mode 100644 index 284a35edee..0000000000 --- a/backend/src/serverless/integrations/usecases/chat/getMessages.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { SuperfaceClient } from '@superfaceai/one-sdk' -import { SocialResponse } from '../../types/superfaceTypes' -import { cleanSuperfaceError } from '../cleanError' - -async function getMessages( - client: SuperfaceClient, - source: string, - accessToken: string, - channelId: string, - page: string, - perPage: number = 100, -): Promise { - try { - const input = { - destination: channelId, - limit: perPage, - page: page || undefined, - } - const profile = await client.getProfile('chat/messages') - const provider = await client.getProvider(source) - const result: any = await profile.getUseCase('GetMessages').perform(input, { - provider, - parameters: { accessToken }, - }) - - // TODO No-SF, do we need this? - // if ('error' in result) { - // if (result.error.statusCode === 500) { - // log.error(result.error, `Error in messages: ${result.error.properties.detail}`) - // return { - // records: [], - // nextPage: page, - // limit: 0, - // timeUntilReset: 180, - // } - // } - // } - - let limit - let timeUntilReset - if (result.value.rateLimit) { - limit = result.value.rateLimit.remainingRequests - timeUntilReset = result.value.rateLimit.resetAfter - } else { - limit = 100 - timeUntilReset = 1 - } - - return { - records: result.value.messages, - nextPage: result.value.messages.length < input.limit ? '' : result.value.nextPage, - limit, - timeUntilReset, - } - } catch (err) { - throw cleanSuperfaceError(err) - } -} - -export default getMessages diff --git a/backend/src/serverless/integrations/usecases/chat/getMessagesThreads.ts b/backend/src/serverless/integrations/usecases/chat/getMessagesThreads.ts deleted file mode 100644 index 1edc086a40..0000000000 --- a/backend/src/serverless/integrations/usecases/chat/getMessagesThreads.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { getServiceChildLogger } from '@crowd/logging' -import { SuperfaceClient } from '@superfaceai/one-sdk' -import { IIntegrationStream } from '../../../../types/integration/stepResult' -import { SocialResponse } from '../../types/superfaceTypes' -import { cleanSuperfaceError } from '../cleanError' -import isInvalid from '../isInvalid' - -const log = getServiceChildLogger('getMessagesThreads') - -async function getMessagesThreads( - client: SuperfaceClient, - source: string, - accessToken: string, - stream: IIntegrationStream, - page: string, - perPage: number = 100, -): Promise { - try { - const threadInfo = stream.metadata - const input = { - destination: threadInfo.channelId, - threadId: threadInfo.threadId, - limit: perPage, - page: page || undefined, - } - const profile = await client.getProfile('chat/messages-threads') - const provider = await client.getProvider(source) - const result: any = await profile.getUseCase('GetMessagesThreads').perform(input, { - provider, - parameters: { accessToken }, - }) - - if (isInvalid(result, 'messages')) { - log.warn({ input, result }, 'Invalid request in usecase') - } - - let limit - let timeUntilReset - if (result.value.rateLimit) { - limit = result.value.rateLimit.limit - timeUntilReset = result.value.rateLimit.resetAfter - } else { - limit = 100 - timeUntilReset = 1 - } - - return { - records: result.value.messages, - nextPage: result.value.messages.length < input.limit ? '' : result.value.nextPage, - limit, - timeUntilReset, - } - } catch (err) { - throw cleanSuperfaceError(err) - } -} - -export default getMessagesThreads - -// getMessagesThreads('909473151757455420', 'ODc3OTEwNjM0MzA4NzA2MzI0.YR5f_g.TrYuoK2yWA5-LpPlDQ0Nlzc8dOE') diff --git a/backend/src/serverless/integrations/usecases/chat/getThreads.ts b/backend/src/serverless/integrations/usecases/chat/getThreads.ts deleted file mode 100644 index 554787fea4..0000000000 --- a/backend/src/serverless/integrations/usecases/chat/getThreads.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { getServiceChildLogger } from '@crowd/logging' -import { SuperfaceClient } from '@superfaceai/one-sdk' -import { PlatformType } from '@crowd/types' -import { cleanSuperfaceError } from '../cleanError' -import isInvalid from '../isInvalid' - -const log = getServiceChildLogger('getThreads') - -async function getChannels( - client: SuperfaceClient, - serverId: string, - accessToken: string, -): Promise { - try { - const input = { server: serverId.toString() } - const profile = await client.getProfile('chat/threads') - const provider = await client.getProvider(PlatformType.DISCORD) - const result: any = await profile.getUseCase('GetThreads').perform(input, { - provider, - parameters: { accessToken }, - }) - if (isInvalid(result, 'threads')) { - log.warn({ input, result }, 'Invalid request in getChannels') - } - return result.value.threads.map((thread) => ({ - name: thread.name, - id: thread.id, - thread: true, - })) - } catch (err) { - throw cleanSuperfaceError(err) - } -} - -export default getChannels diff --git a/backend/src/serverless/integrations/usecases/cleanError.ts b/backend/src/serverless/integrations/usecases/cleanError.ts deleted file mode 100644 index 4e528f3d56..0000000000 --- a/backend/src/serverless/integrations/usecases/cleanError.ts +++ /dev/null @@ -1,36 +0,0 @@ -function safeDelete(value: any, key: string) { - if (value[key] !== undefined) { - delete value[key] - } -} - -export const cleanSuperfaceError = (err: any): any => { - if (err.metadata === undefined) return err - const keys = Object.keys(err.metadata) - - for (const key of keys) { - const value = err.metadata[key] - switch (key) { - case 'node': - safeDelete(value, 'source') - safeDelete(value, 'sourceMap') - safeDelete(value, 'location') - break - case 'ast': - safeDelete(value, 'location') - safeDelete(value, 'astMetadata') - if (value.header) { - safeDelete(value.header, 'location') - if (value.header.profile) { - safeDelete(value.header.profile, 'version') - } - } - safeDelete(value, 'definitions') - break - default: - break - } - } - - return err -} diff --git a/backend/src/serverless/integrations/usecases/devto/getArticleComments.ts b/backend/src/serverless/integrations/usecases/devto/getArticleComments.ts index 37b16bcc1b..0c55aa1603 100644 --- a/backend/src/serverless/integrations/usecases/devto/getArticleComments.ts +++ b/backend/src/serverless/integrations/usecases/devto/getArticleComments.ts @@ -1,5 +1,7 @@ import axios from 'axios' + import { timeout } from '@crowd/common' + import { DevtoComment } from './types' /** diff --git a/backend/src/serverless/integrations/usecases/devto/getOrganization.ts b/backend/src/serverless/integrations/usecases/devto/getOrganization.ts index a7bd320942..038cb03cf0 100644 --- a/backend/src/serverless/integrations/usecases/devto/getOrganization.ts +++ b/backend/src/serverless/integrations/usecases/devto/getOrganization.ts @@ -1,5 +1,7 @@ import axios from 'axios' + import { timeout } from '@crowd/common' + import { DevtoOrganization } from './types' /** diff --git a/backend/src/serverless/integrations/usecases/devto/getOrganizationArticles.ts b/backend/src/serverless/integrations/usecases/devto/getOrganizationArticles.ts index 68c2ec8ffd..35a81c1adc 100644 --- a/backend/src/serverless/integrations/usecases/devto/getOrganizationArticles.ts +++ b/backend/src/serverless/integrations/usecases/devto/getOrganizationArticles.ts @@ -1,5 +1,7 @@ import axios from 'axios' + import { timeout } from '@crowd/common' + import { DevtoArticle } from './types' /** diff --git a/backend/src/serverless/integrations/usecases/devto/getUser.ts b/backend/src/serverless/integrations/usecases/devto/getUser.ts index 0aad28e7e3..249b3e35f3 100644 --- a/backend/src/serverless/integrations/usecases/devto/getUser.ts +++ b/backend/src/serverless/integrations/usecases/devto/getUser.ts @@ -1,5 +1,7 @@ import axios from 'axios' + import { timeout } from '@crowd/common' + import { DevtoUser } from './types' /** diff --git a/backend/src/serverless/integrations/usecases/devto/getUserArticles.ts b/backend/src/serverless/integrations/usecases/devto/getUserArticles.ts index ad9da973ca..ddda9830fa 100644 --- a/backend/src/serverless/integrations/usecases/devto/getUserArticles.ts +++ b/backend/src/serverless/integrations/usecases/devto/getUserArticles.ts @@ -1,5 +1,7 @@ import axios from 'axios' + import { timeout } from '@crowd/common' + import { DevtoArticle } from './types' /** diff --git a/backend/src/serverless/integrations/usecases/discourse/getCategories.ts b/backend/src/serverless/integrations/usecases/discourse/getCategories.ts index 6299005ae6..42a9536f25 100644 --- a/backend/src/serverless/integrations/usecases/discourse/getCategories.ts +++ b/backend/src/serverless/integrations/usecases/discourse/getCategories.ts @@ -1,6 +1,8 @@ import axios, { AxiosRequestConfig } from 'axios' + import { Logger } from '@crowd/logging' import { RateLimitError } from '@crowd/types' + import type { DiscourseConnectionParams } from '../../types/discourseTypes' import { DiscourseCategoryResponse } from '../../types/discourseTypes' diff --git a/backend/src/serverless/integrations/usecases/discourse/getPostsByIds.ts b/backend/src/serverless/integrations/usecases/discourse/getPostsByIds.ts index e473fd1adc..b37a213d3b 100644 --- a/backend/src/serverless/integrations/usecases/discourse/getPostsByIds.ts +++ b/backend/src/serverless/integrations/usecases/discourse/getPostsByIds.ts @@ -1,10 +1,12 @@ import axios, { AxiosRequestConfig } from 'axios' + import { Logger } from '@crowd/logging' import { RateLimitError } from '@crowd/types' + import type { DiscourseConnectionParams } from '../../types/discourseTypes' -import { DiscoursePostsByIdsResponse, DiscoursePostsByIdsInput } from '../../types/discourseTypes' +import { DiscoursePostsByIdsInput, DiscoursePostsByIdsResponse } from '../../types/discourseTypes' -const serializeArrayToQueryString = (params: Object) => +const serializeObjectToQueryString = (params: Object) => Object.entries(params) .map(([key, value]) => { if (Array.isArray(value)) { @@ -33,7 +35,7 @@ export const getDiscoursePostsByIds = async ( post_ids: input.post_ids, } - const queryString = serializeArrayToQueryString(queryParameters) + const queryString = serializeObjectToQueryString(queryParameters) const config: AxiosRequestConfig = { method: 'get', diff --git a/backend/src/serverless/integrations/usecases/discourse/getPostsFromTopic.ts b/backend/src/serverless/integrations/usecases/discourse/getPostsFromTopic.ts index c5b36a1df3..a6522f48c1 100644 --- a/backend/src/serverless/integrations/usecases/discourse/getPostsFromTopic.ts +++ b/backend/src/serverless/integrations/usecases/discourse/getPostsFromTopic.ts @@ -1,6 +1,8 @@ import axios, { AxiosRequestConfig } from 'axios' + import { Logger } from '@crowd/logging' import { RateLimitError } from '@crowd/types' + import type { DiscourseConnectionParams } from '../../types/discourseTypes' import { DiscoursePostsFromTopicResponse, DiscoursePostsInput } from '../../types/discourseTypes' diff --git a/backend/src/serverless/integrations/usecases/discourse/getTopics.ts b/backend/src/serverless/integrations/usecases/discourse/getTopics.ts index 0caa12c1b1..ddda729955 100644 --- a/backend/src/serverless/integrations/usecases/discourse/getTopics.ts +++ b/backend/src/serverless/integrations/usecases/discourse/getTopics.ts @@ -1,6 +1,8 @@ import axios, { AxiosRequestConfig } from 'axios' + import { Logger } from '@crowd/logging' import { RateLimitError } from '@crowd/types' + import type { DiscourseConnectionParams } from '../../types/discourseTypes' import { DiscourseCategoryResponse, DiscourseTopicsInput } from '../../types/discourseTypes' @@ -10,7 +12,7 @@ export const getDiscourseTopics = async ( logger: Logger, ): Promise => { logger.info({ - message: 'Fetching categories from Discourse', + message: 'Fetching topics from Discourse', forumHostName: params.forumHostname, }) const config: AxiosRequestConfig = { @@ -33,7 +35,7 @@ export const getDiscourseTopics = async ( // wait 5 mins throw new RateLimitError(5 * 60, 'discourse/gettopics') } - logger.error({ err, params }, 'Error while getting Discourse categories') + logger.error({ err, params }, 'Error while getting Discourse topics') throw err } } diff --git a/backend/src/serverless/integrations/usecases/discourse/getUser.ts b/backend/src/serverless/integrations/usecases/discourse/getUser.ts index d330148e8f..85a29ffae1 100644 --- a/backend/src/serverless/integrations/usecases/discourse/getUser.ts +++ b/backend/src/serverless/integrations/usecases/discourse/getUser.ts @@ -1,11 +1,11 @@ import axios, { AxiosRequestConfig } from 'axios' + import { Logger } from '@crowd/logging' import { RateLimitError } from '@crowd/types' + import type { DiscourseConnectionParams } from '../../types/discourseTypes' import { DiscourseUserResponse, DisourseUserByUsernameInput } from '../../types/discourseTypes' -// this methods returns ids of posts in a topic -// then we need to parse each topic individually (can be batched) export const getDiscourseUserByUsername = async ( params: DiscourseConnectionParams, input: DisourseUserByUsernameInput, diff --git a/backend/src/serverless/integrations/usecases/github/graphql/baseQuery.ts b/backend/src/serverless/integrations/usecases/github/graphql/baseQuery.ts deleted file mode 100644 index fa46892084..0000000000 --- a/backend/src/serverless/integrations/usecases/github/graphql/baseQuery.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { graphql } from '@octokit/graphql' -import { GraphQlQueryResponseData } from '@octokit/graphql/dist-types/types' -import moment from 'moment' -import { GraphQlQueryResponse } from '../../../types/messageTypes' -import { RateLimitError } from '../../../../../types/integration/rateLimitError' - -class BaseQuery { - static BASE_URL = 'https://api.github.com/graphql' - - static USER_SELECT = `{ - login - name - avatarUrl - id - isHireable - twitterUsername - url - websiteUrl - email - bio - company - location - followers { - totalCount - } - }` - - static ORGANIZATION_SELECT = `{ - email - url - location - name - twitterUsername - websiteUrl - description - avatarUrl - }` - - static PAGE_SELECT = `{ - hasPreviousPage - startCursor - }` - - graphQL - - query - - githubToken - - additionalHeaders - - perPage - - eventType - - constructor(githubToken: string, query: string, eventType: string, perPage: number) { - this.githubToken = githubToken - this.query = query - this.perPage = perPage - this.eventType = eventType - this.graphQL = graphql.defaults({ - headers: { - authorization: `token ${this.githubToken}`, - }, - }) - } - - /** - * Substitutes a variable like string $var with given variable - * in a string. Useful when reusing the same string template - * for multiple graphql paging requests. - * $var in the string is substituted with obj[var] - * @param str string to make the substitution - * @param obj object containing variable to interpolate - * @returns interpolated string - */ - static interpolate(str: string, obj: any): string { - return str.replace(/\${([^}]+)}/g, (_, prop) => obj[prop]) - } - - /** - * Gets a single page result given a cursor. - * Single page before the given cursor will be fetched. - * @param beforeCursor Cursor to paginate records before it - * @returns parsed graphQl result - */ - async getSinglePage(beforeCursor: string): Promise { - const paginatedQuery = BaseQuery.interpolate(this.query, { - beforeCursor: BaseQuery.getPagination(beforeCursor), - }) - - try { - const result = await this.graphQL(paginatedQuery) - return this.getEventData(result) - } catch (err) { - throw BaseQuery.processGraphQLError(err) - } - } - - /** - * Parses graphql result into an object. - * Object contains information about paging, and fetched data. - * @param result from graphql query - * @returns parsed result into paging and data values. - */ - getEventData(result: GraphQlQueryResponseData): GraphQlQueryResponse { - return { - hasPreviousPage: result.repository[this.eventType].pageInfo?.hasPreviousPage, - startCursor: result.repository[this.eventType].pageInfo?.startCursor, - data: [{}], - } - } - - /** - * Returns pagination string given cursor. - * @param beforeCursor cursor to use for the pagination - * @returns pagination string that can be injected into a graphql query. - */ - static getPagination(beforeCursor: string): string { - if (beforeCursor) { - return `before: "${beforeCursor}"` - } - return '' - } - - static processGraphQLError(err: any): any { - if (err.errors && err.errors[0].type === 'RATE_LIMITED') { - if (err.headers && err.headers['x-ratelimit-reset']) { - const query = - err.request && err.request.query ? err.request.query : 'Unknown GraphQL query!' - - const epochReset = parseInt(err.headers['x-ratelimit-reset'], 10) - const resetDate = moment.unix(epochReset) - const diffInSeconds = resetDate.diff(moment(), 'seconds') - - return new RateLimitError(diffInSeconds + 5, query, err) - } - } - - return err - } -} - -export default BaseQuery diff --git a/backend/src/serverless/integrations/usecases/github/graphql/discussionComments.ts b/backend/src/serverless/integrations/usecases/github/graphql/discussionComments.ts deleted file mode 100644 index cbc41cd9d5..0000000000 --- a/backend/src/serverless/integrations/usecases/github/graphql/discussionComments.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Repo } from '../../../types/regularTypes' -import BaseQuery from './baseQuery' - -/* eslint class-methods-use-this: 0 */ -class DiscussionCommentsQuery extends BaseQuery { - repo: Repo - - constructor( - repo: Repo, - discussionNumber: string, - githubToken: string, - perPage: number = 100, - maxRepliesPerComment: number = 100, - ) { - const discussionCommentsQuery = `{ - repository(name: "${repo.name}", owner: "${repo.owner}") { - discussion(number: ${discussionNumber}) { - comments(first: ${perPage}, \${beforeCursor}) { - pageInfo ${BaseQuery.PAGE_SELECT} - nodes { - author { - ... on User ${BaseQuery.USER_SELECT} - } - bodyText - url - id - createdAt - isAnswer - replies(first: ${maxRepliesPerComment}) { - nodes { - author { - ... on User ${BaseQuery.USER_SELECT} - } - bodyText - url - id - createdAt - } - } - discussion { - url - id - title - } - } - - } - } - } - }` - - super(githubToken, discussionCommentsQuery, 'discussionComments', perPage) - - this.repo = repo - } - - getEventData(result) { - return { - hasPreviousPage: result.repository?.discussion?.comments?.pageInfo?.hasPreviousPage, - startCursor: result.repository?.discussion?.comments?.pageInfo?.startCursor, - data: result.repository?.discussion?.comments?.nodes, - } - } -} - -export default DiscussionCommentsQuery diff --git a/backend/src/serverless/integrations/usecases/github/graphql/discussions.ts b/backend/src/serverless/integrations/usecases/github/graphql/discussions.ts deleted file mode 100644 index e4864f4603..0000000000 --- a/backend/src/serverless/integrations/usecases/github/graphql/discussions.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Repo } from '../../../types/regularTypes' -import BaseQuery from './baseQuery' - -/* eslint class-methods-use-this: 0 */ -class DiscussionsQuery extends BaseQuery { - repo: Repo - - constructor(repo: Repo, githubToken: string, perPage: number = 100) { - const discussionsQuery = `{ - repository(owner: "${repo.owner}", name: "${repo.name}") { - discussions(last: ${perPage}, \${beforeCursor}) { - pageInfo ${BaseQuery.PAGE_SELECT} - nodes { - author { - ... on User ${BaseQuery.USER_SELECT} - } - number - bodyText - title - id - url - createdAt - comments { - totalCount - } - category { - id - isAnswerable - name - slug - emoji - description - } - } - } - } - }` - - super(githubToken, discussionsQuery, 'discussions', perPage) - - this.repo = repo - } - - getEventData(result) { - return { ...super.getEventData(result), data: result.repository?.discussions?.nodes } - } -} - -export default DiscussionsQuery diff --git a/backend/src/serverless/integrations/usecases/github/graphql/forks.ts b/backend/src/serverless/integrations/usecases/github/graphql/forks.ts deleted file mode 100644 index 562141725d..0000000000 --- a/backend/src/serverless/integrations/usecases/github/graphql/forks.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Repo } from '../../../types/regularTypes' -import BaseQuery from './baseQuery' - -/* eslint class-methods-use-this: 0 */ -class ForksQuery extends BaseQuery { - repo: Repo - - constructor(repo: Repo, githubToken: string, perPage: number = 100) { - const forksQuery = `{ - repository(owner: "${repo.owner}", name: "${repo.name}") { - forks(last: ${perPage}, \${beforeCursor}) { - pageInfo ${BaseQuery.PAGE_SELECT} - nodes { - owner { - ... on User ${BaseQuery.USER_SELECT} - } - name - url - id - createdAt - } - } - } - }` - - super(githubToken, forksQuery, 'forks', perPage) - - this.repo = repo - } - - getEventData(result) { - return { ...super.getEventData(result), data: result.repository?.forks?.nodes } - } -} - -export default ForksQuery diff --git a/backend/src/serverless/integrations/usecases/github/graphql/issueComments.ts b/backend/src/serverless/integrations/usecases/github/graphql/issueComments.ts deleted file mode 100644 index 1533c65bca..0000000000 --- a/backend/src/serverless/integrations/usecases/github/graphql/issueComments.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Repo } from '../../../types/regularTypes' -import BaseQuery from './baseQuery' - -/* eslint class-methods-use-this: 0 */ -class IssueCommentsQuery extends BaseQuery { - repo: Repo - - constructor(repo: Repo, issueNumber: string, githubToken: string, perPage: number = 100) { - const issueCommentsQuery = `{ - repository(name: "${repo.name}", owner: "${repo.owner}") { - issue(number: ${issueNumber}) { - comments(first: ${perPage}, \${beforeCursor}) { - pageInfo ${BaseQuery.PAGE_SELECT} - nodes { - author { - ... on User ${BaseQuery.USER_SELECT} - } - bodyText - url - id - createdAt - issue { - url - id - title - } - repository { - url - } - } - } - } - } - }` - - super(githubToken, issueCommentsQuery, 'issueComments', perPage) - - this.repo = repo - } - - getEventData(result) { - return { - hasPreviousPage: result.repository?.issue?.comments?.pageInfo?.hasPreviousPage, - startCursor: result.repository?.issue?.comments?.pageInfo?.startCursor, - data: result.repository?.issue?.comments?.nodes, - } - } -} - -export default IssueCommentsQuery diff --git a/backend/src/serverless/integrations/usecases/github/graphql/issues.ts b/backend/src/serverless/integrations/usecases/github/graphql/issues.ts deleted file mode 100644 index 0219425848..0000000000 --- a/backend/src/serverless/integrations/usecases/github/graphql/issues.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Repo } from '../../../types/regularTypes' -import BaseQuery from './baseQuery' - -/* eslint class-methods-use-this: 0 */ -class IssuesQuery extends BaseQuery { - repo: Repo - - constructor(repo: Repo, githubToken: string, perPage: number = 100) { - const issuesQuery = `{ - repository(owner: "${repo.owner}", name: "${repo.name}") { - issues(last: ${perPage}, \${beforeCursor}) { - pageInfo ${BaseQuery.PAGE_SELECT} - nodes { - author { - ... on User ${BaseQuery.USER_SELECT} - } - bodyText - state - id - title - url - createdAt - number - timelineItems(first: 100, itemTypes: [CLOSED_EVENT]) { - nodes { - ... on ClosedEvent { - __typename - id - actor { - ... on User ${BaseQuery.USER_SELECT} - } - createdAt - } - } - } - } - } - } - }` - - super(githubToken, issuesQuery, 'issues', perPage) - - this.repo = repo - } - - getEventData(result) { - return { ...super.getEventData(result), data: result.repository?.issues?.nodes } - } -} - -export default IssuesQuery diff --git a/backend/src/serverless/integrations/usecases/github/graphql/members.ts b/backend/src/serverless/integrations/usecases/github/graphql/members.ts deleted file mode 100644 index fdb2310121..0000000000 --- a/backend/src/serverless/integrations/usecases/github/graphql/members.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { graphql } from '@octokit/graphql' -import BaseQuery from './baseQuery' - -/** - * Get information from a member using the GitHub GraphQL API. - * @param username GitHub username - * @param token GitHub personal access token - * @returns Information from member - */ -const getMember = async (username: string, token: string): Promise => { - let user: string | null - try { - const graphqlWithAuth = graphql.defaults({ - headers: { - authorization: `token ${token}`, - }, - }) - - user = ( - (await graphqlWithAuth(`{ - user(login: "${username}") ${BaseQuery.USER_SELECT} - } - `)) as any - ).user - } catch (err) { - // It may be that the user was not found, if for example it is a bot - // In that case we want to return null instead of throwing an error - if (err.errors && err.errors[0].type === 'NOT_FOUND') { - user = null - } else { - throw BaseQuery.processGraphQLError(err) - } - } - return user -} - -export default getMember diff --git a/backend/src/serverless/integrations/usecases/github/graphql/organizations.ts b/backend/src/serverless/integrations/usecases/github/graphql/organizations.ts deleted file mode 100644 index fb923c955c..0000000000 --- a/backend/src/serverless/integrations/usecases/github/graphql/organizations.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { getServiceChildLogger } from '@crowd/logging' -import { graphql } from '@octokit/graphql' -import BaseQuery from './baseQuery' - -const logger = getServiceChildLogger('github.getOrganization') - -/** - * Get information from a organization using the GitHub GraphQL API. - * @param name Name of the organization in GitHub - * @param token GitHub personal access token - * @returns Information from organization - */ -const getOrganization = async (name: string, token: string): Promise => { - let organization: string | null - try { - const graphqlWithAuth = graphql.defaults({ - headers: { - authorization: `token ${token}`, - }, - }) - - const sanitizedName = name.replaceAll('\\', '').replaceAll('"', '') - - const organizationsQuery = `{ - search(query: "type:org ${sanitizedName}", type: USER, first: 10) { - nodes { - ... on Organization ${BaseQuery.ORGANIZATION_SELECT} - } - } - rateLimit { - limit - cost - remaining - resetAt - } - }` - - organization = (await graphqlWithAuth(organizationsQuery)) as any - - organization = - (organization as any).search.nodes.length > 0 ? (organization as any).search.nodes[0] : null - } catch (err) { - logger.error(err, { name }, 'Error getting organization!') - // It may be that the organization was not found, if for example it is a bot - // In that case we want to return null instead of throwing an error - if (err.errors && err.errors[0].type === 'NOT_FOUND') { - organization = null - } else { - throw BaseQuery.processGraphQLError(err) - } - } - return organization -} - -export default getOrganization diff --git a/backend/src/serverless/integrations/usecases/github/graphql/pullRequestComments.ts b/backend/src/serverless/integrations/usecases/github/graphql/pullRequestComments.ts deleted file mode 100644 index 42fc68bfb7..0000000000 --- a/backend/src/serverless/integrations/usecases/github/graphql/pullRequestComments.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Repo } from '../../../types/regularTypes' -import BaseQuery from './baseQuery' - -/* eslint class-methods-use-this: 0 */ -class PullRequestCommentsQuery extends BaseQuery { - repo: Repo - - constructor(repo: Repo, pullRequestNumber: string, githubToken: string, perPage: number = 100) { - const pullRequestCommentsQuery = `{ - repository(name: "${repo.name}", owner: "${repo.owner}") { - pullRequest(number: ${pullRequestNumber}) { - comments(first: ${perPage}, \${beforeCursor}) { - pageInfo ${BaseQuery.PAGE_SELECT} - nodes { - author { - ... on User ${BaseQuery.USER_SELECT} - } - bodyText - url - id - createdAt - pullRequest { - url - id - title - } - repository { - url - } - } - } - } - } - }` - - super(githubToken, pullRequestCommentsQuery, 'pullRequestComments', perPage) - - this.repo = repo - } - - getEventData(result) { - return { - hasPreviousPage: result.repository?.pullRequest?.comments?.pageInfo?.hasPreviousPage, - startCursor: result.repository?.pullRequest?.comments?.pageInfo?.startCursor, - data: result.repository?.pullRequest?.comments?.nodes, - } - } -} - -export default PullRequestCommentsQuery diff --git a/backend/src/serverless/integrations/usecases/github/graphql/pullRequestCommits.ts b/backend/src/serverless/integrations/usecases/github/graphql/pullRequestCommits.ts deleted file mode 100644 index cdbe6902cf..0000000000 --- a/backend/src/serverless/integrations/usecases/github/graphql/pullRequestCommits.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Repo } from '../../../types/regularTypes' -import BaseQuery from './baseQuery' - -export interface PullRequestCommit { - repository: { - pullRequest: { - id: string - number: number - baseRefName: string - headRefName: string - commits: { - pageInfo: { - hasPreviousPage: boolean - startCursor: string - } - nodes: { - commit: { - authoredDate: string - committedDate: string - additions: number - changedFilesIfAvailable: number - deletions: number - oid: string - message: string - url: string - parents: { - totalCount: number - } - authors: { - nodes: { - user: { - login: string - name: string - avatarUrl: string - id: string - isHireable: boolean - twitterUsername: string | null - url: string - websiteUrl: string | null - email: string - bio: string - company: string - location: string | null - followers: { - totalCount: number - } - } - }[] - } - } - }[] - } - } - } -} - -/* eslint class-methods-use-this: 0 */ -class PullRequestCommitsQuery extends BaseQuery { - repo: Repo - - constructor( - repo: Repo, - pullRequestNumber: string, - githubToken: string, - perPage: number = 100, - maxAuthors: number = 1, - ) { - const pullRequestCommitsQuery = `{ - repository(name: "${repo.name}", owner: "${repo.owner}") { - pullRequest(number: ${pullRequestNumber}) { - id - number - baseRefName - headRefName - commits(first: ${perPage}, \${beforeCursor}) { - pageInfo ${BaseQuery.PAGE_SELECT} - nodes { - commit { - authoredDate - committedDate - additions - changedFilesIfAvailable - deletions - oid - message - url - parents(first: 2) { - totalCount - } - authors(first: ${maxAuthors}) { - nodes { - user ${BaseQuery.USER_SELECT} - } - } - } - } - } - } - } - }` - - super(githubToken, pullRequestCommitsQuery, 'pullRequestCommits', perPage) - - this.repo = repo - } - - // Override the getEventData method to process commit details - getEventData(result) { - const commitData = result as PullRequestCommit - - return { - hasPreviousPage: result.repository?.pullRequest?.commits?.pageInfo?.hasPreviousPage, - startCursor: result.repository?.pullRequest?.commits?.pageInfo?.startCursor, - data: [commitData], // returning an array to match the parseActivities function - } - } -} - -export default PullRequestCommitsQuery diff --git a/backend/src/serverless/integrations/usecases/github/graphql/pullRequestCommitsNoAdditions.ts b/backend/src/serverless/integrations/usecases/github/graphql/pullRequestCommitsNoAdditions.ts deleted file mode 100644 index 77c1e0b253..0000000000 --- a/backend/src/serverless/integrations/usecases/github/graphql/pullRequestCommitsNoAdditions.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Repo } from '../../../types/regularTypes' -import BaseQuery from './baseQuery' - -export interface PullRequestCommitNoAdditions { - repository: { - pullRequest: { - id: string - number: number - baseRefName: string - headRefName: string - commits: { - pageInfo: { - hasPreviousPage: boolean - startCursor: string - } - nodes: { - commit: { - authoredDate: string - committedDate: string - changedFilesIfAvailable: number - oid: string - message: string - url: string - parents: { - totalCount: number - } - authors: { - nodes: { - user: { - login: string - name: string - avatarUrl: string - id: string - isHireable: boolean - twitterUsername: string | null - url: string - websiteUrl: string | null - email: string - bio: string - company: string - location: string | null - followers: { - totalCount: number - } - } - }[] - } - } - }[] - } - } - } -} - -/* eslint class-methods-use-this: 0 */ -class PullRequestCommitsQueryNoAdditions extends BaseQuery { - repo: Repo - - constructor( - repo: Repo, - pullRequestNumber: string, - githubToken: string, - perPage: number = 100, - maxAuthors: number = 1, - ) { - const pullRequestCommitsQuery = `{ - repository(name: "${repo.name}", owner: "${repo.owner}") { - pullRequest(number: ${pullRequestNumber}) { - id - number - baseRefName - headRefName - commits(first: ${perPage}, \${beforeCursor}) { - pageInfo ${BaseQuery.PAGE_SELECT} - nodes { - commit { - authoredDate - committedDate - changedFilesIfAvailable - oid - message - url - parents(first: 2) { - totalCount - } - authors(first: ${maxAuthors}) { - nodes { - user ${BaseQuery.USER_SELECT} - } - } - } - } - } - } - } - }` - - super(githubToken, pullRequestCommitsQuery, 'pullRequestCommits', perPage) - - this.repo = repo - } - - // Override the getEventData method to process commit details - getEventData(result) { - const commitData = result as PullRequestCommitNoAdditions - - return { - hasPreviousPage: result.repository?.pullRequest?.commits?.pageInfo?.hasPreviousPage, - startCursor: result.repository?.pullRequest?.commits?.pageInfo?.startCursor, - data: [commitData], // returning an array to match the parseActivities function - } - } -} - -export default PullRequestCommitsQueryNoAdditions diff --git a/backend/src/serverless/integrations/usecases/github/graphql/pullRequestReviewThreadComments.ts b/backend/src/serverless/integrations/usecases/github/graphql/pullRequestReviewThreadComments.ts deleted file mode 100644 index e2d5b36780..0000000000 --- a/backend/src/serverless/integrations/usecases/github/graphql/pullRequestReviewThreadComments.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Repo } from '../../../types/regularTypes' -import BaseQuery from './baseQuery' - -/* eslint class-methods-use-this: 0 */ -class PullRequestReviewThreadCommentsQuery extends BaseQuery { - repo: Repo - - constructor(repo: Repo, reviewThreadId: string, githubToken: string, perPage: number = 50) { - const pullRequestReviewThreadCommentsQuery = `{ - node(id: "${reviewThreadId}") { - ... on PullRequestReviewThread { - comments(first: ${perPage}, \${beforeCursor}) { - pageInfo ${BaseQuery.PAGE_SELECT} - nodes { - author { - ... on User ${BaseQuery.USER_SELECT} - } - pullRequestReview { - submittedAt - author { - ... on User ${BaseQuery.USER_SELECT} - } - } - bodyText - url - id - createdAt - pullRequest { - url - id - title - additions - deletions - changedFiles - authorAssociation - state - repository{ - url - } - } - } - } - } - } - }` - - super( - githubToken, - pullRequestReviewThreadCommentsQuery, - 'pullRequestReviewThreadComments', - perPage, - ) - - this.repo = repo - } - - getEventData(result) { - return { - hasPreviousPage: result.node?.comments?.pageInfo?.hasPreviousPage, - startCursor: result.node?.comments?.pageInfo?.startCursor, - data: result.node?.comments?.nodes, - } - } -} - -export default PullRequestReviewThreadCommentsQuery diff --git a/backend/src/serverless/integrations/usecases/github/graphql/pullRequestReviewThreads.ts b/backend/src/serverless/integrations/usecases/github/graphql/pullRequestReviewThreads.ts deleted file mode 100644 index ddd99b40ae..0000000000 --- a/backend/src/serverless/integrations/usecases/github/graphql/pullRequestReviewThreads.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Repo } from '../../../types/regularTypes' -import BaseQuery from './baseQuery' - -/* eslint class-methods-use-this: 0 */ -class PullRequestReviewThreadsQuery extends BaseQuery { - repo: Repo - - constructor(repo: Repo, pullRequestNumber: string, githubToken: string, perPage: number = 100) { - const pullRequestReviewThreadsQuery = `{ - repository(name: "${repo.name}", owner: "${repo.owner}") { - pullRequest(number: ${pullRequestNumber}) { - id - reviewDecision - reviewThreads(first: ${perPage}, \${beforeCursor}) { - pageInfo ${BaseQuery.PAGE_SELECT} - nodes { - id - } - } - } - } - }` - - super(githubToken, pullRequestReviewThreadsQuery, 'pullRequestReviewThreads', perPage) - - this.repo = repo - } - - getEventData(result) { - return { - hasPreviousPage: result.repository?.pullRequest?.reviewThreads?.pageInfo?.hasPreviousPage, - startCursor: result.repository?.pullRequest?.reviewThreads?.pageInfo?.startCursor, - data: result.repository?.pullRequest?.reviewThreads?.nodes, - } - } -} - -export default PullRequestReviewThreadsQuery diff --git a/backend/src/serverless/integrations/usecases/github/graphql/pullRequests.ts b/backend/src/serverless/integrations/usecases/github/graphql/pullRequests.ts deleted file mode 100644 index 4d91b65ec9..0000000000 --- a/backend/src/serverless/integrations/usecases/github/graphql/pullRequests.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { Repo } from '../../../types/regularTypes' -import BaseQuery from './baseQuery' - -/* eslint class-methods-use-this: 0 */ -class PullRequestsQuery extends BaseQuery { - repo: Repo - - constructor(repo: Repo, githubToken: string, perPage: number = 20) { - const pullRequestsQuery = `{ - repository(owner: "${repo.owner}", name: "${repo.name}") { - pullRequests(last: ${perPage}, \${beforeCursor}) { - pageInfo ${BaseQuery.PAGE_SELECT} - nodes { - author { - ... on User ${BaseQuery.USER_SELECT} - } - bodyText - state - title - id - url - createdAt - number - additions - deletions - changedFiles - authorAssociation - labels(first: 10) { - nodes { - name - } - } - timelineItems( - first: 100 - itemTypes: [PULL_REQUEST_REVIEW, MERGED_EVENT, ASSIGNED_EVENT, REVIEW_REQUESTED_EVENT, CLOSED_EVENT] - ) { - nodes { - ... on ReviewRequestedEvent { - __typename - id - createdAt - actor { - ... on User ${BaseQuery.USER_SELECT} - } - requestedReviewer { - ... on User ${BaseQuery.USER_SELECT} - } - } - ... on PullRequestReview { - __typename - id - state - submittedAt - body - author { - ... on User ${BaseQuery.USER_SELECT} - } - } - ... on AssignedEvent { - __typename - id - assignee { - ... on User ${BaseQuery.USER_SELECT} - } - actor { - ... on User ${BaseQuery.USER_SELECT} - } - createdAt - } - ... on MergedEvent { - __typename - id - createdAt - actor { - ... on User ${BaseQuery.USER_SELECT} - } - createdAt - } - ... on ClosedEvent{ - __typename - id - actor { - ... on User ${BaseQuery.USER_SELECT} - } - createdAt - } - } - } - } - } - } - }` - - super(githubToken, pullRequestsQuery, 'pullRequests', perPage) - - this.repo = repo - } - - getEventData(result) { - return { ...super.getEventData(result), data: result.repository?.pullRequests?.nodes } - } -} - -export default PullRequestsQuery diff --git a/backend/src/serverless/integrations/usecases/github/graphql/stargazers.ts b/backend/src/serverless/integrations/usecases/github/graphql/stargazers.ts deleted file mode 100644 index fc30ba8496..0000000000 --- a/backend/src/serverless/integrations/usecases/github/graphql/stargazers.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Repo } from '../../../types/regularTypes' -import BaseQuery from './baseQuery' - -/* eslint class-methods-use-this: 0 */ -class StargazersQuery extends BaseQuery { - repo: Repo - - constructor(repo: Repo, githubToken: string, perPage: number = 100) { - const stargazersQuery = `{ - repository(owner: "${repo.owner}", name: "${repo.name}") { - stargazers(last: ${perPage}, \${beforeCursor}) { - pageInfo ${BaseQuery.PAGE_SELECT} - totalCount - edges { - starredAt - node ${BaseQuery.USER_SELECT} - } - } - } - }` - - super(githubToken, stargazersQuery, 'stargazers', perPage) - - this.repo = repo - } - - getEventData(result) { - return { ...super.getEventData(result), data: result.repository?.stargazers?.edges } - } -} - -export default StargazersQuery diff --git a/backend/src/serverless/integrations/usecases/github/graphql/teams.ts b/backend/src/serverless/integrations/usecases/github/graphql/teams.ts deleted file mode 100644 index 5878f4c5ea..0000000000 --- a/backend/src/serverless/integrations/usecases/github/graphql/teams.ts +++ /dev/null @@ -1,24 +0,0 @@ -import BaseQuery from './baseQuery' - -/* eslint class-methods-use-this: 0 */ -class TeamsQuery extends BaseQuery { - constructor(teamNodeId: string, githubToken: string, perPage: number = 50) { - const teamsQuery = `{ - node(id: "${teamNodeId}") { - ... on Team { - members { - nodes ${BaseQuery.USER_SELECT} - } - } - } - }` - - super(githubToken, teamsQuery, 'teams', perPage) - } - - getEventData(result) { - return { hasPreviousPage: false, startCursor: '', data: result.node?.members?.nodes } - } -} - -export default TeamsQuery diff --git a/backend/src/serverless/integrations/usecases/github/graphql/types.ts b/backend/src/serverless/integrations/usecases/github/graphql/types.ts deleted file mode 100644 index 3ab5863d06..0000000000 --- a/backend/src/serverless/integrations/usecases/github/graphql/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface GithubWebhookTeam { - name: string - id: number - node_id: string - slug: string - description: string - privacy: string - notification_setting: string - url: string - html_url: string - members_url: string - repositories_url: string - permissions: string - parent?: string -} diff --git a/backend/src/serverless/integrations/usecases/github/rest/getAppToken.ts b/backend/src/serverless/integrations/usecases/github/rest/getAppToken.ts deleted file mode 100644 index 606d82ade5..0000000000 --- a/backend/src/serverless/integrations/usecases/github/rest/getAppToken.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { getServiceChildLogger } from '@crowd/logging' -import axios, { AxiosRequestConfig } from 'axios' - -const log = getServiceChildLogger('getAppToken') - -export interface AppTokenResponse { - token: string - expiresAt: string -} - -export const getAppToken = async ( - jwt: string, - installationId: number, -): Promise => { - try { - const config = { - method: 'post', - url: `https://api.github.com/app/installations/${installationId}/access_tokens`, - headers: { - Authorization: `Bearer ${jwt}`, - Accept: 'application/vnd.github+json', - }, - } as AxiosRequestConfig - - const response = await axios(config) - - const data = response.data as any - - return { - token: data.token, - expiresAt: data.expires_at, - } - } catch (err: any) { - log.error(err, { installationId }, 'Error fetching app token!') - throw err - } -} diff --git a/backend/src/serverless/integrations/usecases/github/rest/getInstalledRepositories.ts b/backend/src/serverless/integrations/usecases/github/rest/getInstalledRepositories.ts index 4a23b1901f..949d96e406 100644 --- a/backend/src/serverless/integrations/usecases/github/rest/getInstalledRepositories.ts +++ b/backend/src/serverless/integrations/usecases/github/rest/getInstalledRepositories.ts @@ -1,12 +1,28 @@ -import { getServiceChildLogger } from '@crowd/logging' import axios, { AxiosRequestConfig } from 'axios' -import { Repos } from '../../../types/regularTypes' -import { GITHUB_CONFIG } from '../../../../../conf' -const IS_GITHUB_COMMIT_DATA_ENABLED = GITHUB_CONFIG.isCommitDataEnabled === 'true' +import { getServiceChildLogger } from '@crowd/logging' + +import { Repos } from '../../../types/regularTypes' const log = getServiceChildLogger('getInstalledRepositories') +/** + * Normalizes forkedFrom URL for special cases. + */ +const normalizeForkedFrom = (forkedFrom: string | null): string | null => { + if (!forkedFrom) { + return null + } + + // Special case: Linux kernel on GitHub should map to the official kernel.org git repository + // because that's the one onboarded in our system, not the GitHub mirror. + if (forkedFrom.endsWith('github.com/torvalds/linux')) { + return 'https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux' + } + + return forkedFrom +} + const getRepositoriesFromGH = async (page: number, installToken: string): Promise => { const REPOS_PER_PAGE = 100 @@ -34,6 +50,7 @@ const parseRepos = (repositories: any): Repos => { fork: repo.fork, private: repo.private, cloneUrl: repo.clone_url, + forkedFrom: normalizeForkedFrom(repo.parent?.html_url || null), }) } @@ -57,7 +74,7 @@ export const getInstalledRepositories = async (installToken: string): Promise 0 && data.total_count > repos.length page += 1 } - return repos.filter((repo) => !IS_GITHUB_COMMIT_DATA_ENABLED || !(repo.fork || repo.private)) + return repos.filter((repo) => !repo.private && !repo.fork) } catch (err: any) { log.error(err, 'Error fetching installed repositories!') throw err diff --git a/backend/src/serverless/integrations/usecases/github/rest/getRemoteStats.ts b/backend/src/serverless/integrations/usecases/github/rest/getRemoteStats.ts new file mode 100644 index 0000000000..491ada86dc --- /dev/null +++ b/backend/src/serverless/integrations/usecases/github/rest/getRemoteStats.ts @@ -0,0 +1,124 @@ +import axios, { AxiosResponse } from 'axios' + +import { getServiceChildLogger } from '@crowd/logging' + +import { Repos } from '../../../types/regularTypes' + +const commitsRegExp = /&page=(\d+)>; rel="last"/ + +const log = getServiceChildLogger('getRemoteStats') + +export interface GitHubStats { + stars: number + forks: number + totalIssues: number + totalPRs: number +} + +const checkHeaders = (response: AxiosResponse, defaultValue = 0): number => { + const link = response.headers.link + if (link) { + const matches = link.match(commitsRegExp) + if (matches) { + return parseInt(matches?.[1] as string, 10) + } + return defaultValue + } + return defaultValue +} + +const getStatsForRepo = async (repoUrl: string, token: string): Promise => { + try { + const [owner, repo] = repoUrl.split('/').slice(-2) + + const query = ` + query { + repository(owner: "${owner}", name: "${repo}") { + starCount: stargazers { + totalCount + } + forkCountDirect: forks { + totalCount + } + forkCount + issuesOpened: issues(states: OPEN) { + totalCount + } + issuesClosed: issues(states: CLOSED) { + totalCount + } + } + }` + + const result = await axios.post( + 'https://api.github.com/graphql', + { + query, + }, + { + headers: { + Authorization: `bearer ${token}`, + }, + }, + ) + + const prsAll = await axios.get( + `https://api.github.com/repos/${owner}/${repo}/pulls?state=all&per_page=1`, + { + headers: { + Authorization: `bearer ${token}`, + }, + }, + ) + + let out + + try { + out = { + stars: result.data.data.repository.starCount.totalCount, + forks: result.data.data.repository.forkCountDirect.totalCount, + totalIssues: + result.data.data.repository.issuesOpened.totalCount + + result.data.data.repository.issuesClosed.totalCount, + totalPRs: checkHeaders(prsAll), + } + } catch (e) { + log.error('Error getting stats for repo', e) + throw e + } + + return out + } catch (error) { + log.error(`Error fetching GitHub stats for repo ${repoUrl}:`, error) + return { + stars: 0, + forks: 0, + totalIssues: 0, + totalPRs: 0, + } + } +} + +export const getGitHubRemoteStats = async ( + installToken: string, + repos: Repos, +): Promise => { + const stats = { + stars: 0, + forks: 0, + totalIssues: 0, + totalPRs: 0, + } + + const statsPromises = repos.map((repo) => getStatsForRepo(repo.url, installToken)) + const allRepoStats = await Promise.all(statsPromises) + + allRepoStats.forEach((repoStats) => { + stats.stars += repoStats.stars + stats.forks += repoStats.forks + stats.totalIssues += repoStats.totalIssues + stats.totalPRs += repoStats.totalPRs + }) + + return stats +} diff --git a/backend/src/serverless/integrations/usecases/gitlab/getProjects.ts b/backend/src/serverless/integrations/usecases/gitlab/getProjects.ts new file mode 100644 index 0000000000..41f05205e5 --- /dev/null +++ b/backend/src/serverless/integrations/usecases/gitlab/getProjects.ts @@ -0,0 +1,81 @@ +import axios from 'axios' + +export async function fetchAllGitlabGroups(accessToken: string) { + const groups = [] + let page = 1 + let hasMorePages = true + + while (hasMorePages) { + const response = await axios.get('https://gitlab.com/api/v4/groups', { + headers: { Authorization: `Bearer ${accessToken}` }, + params: { page, per_page: 100 }, + }) + groups.push(...response.data) + hasMorePages = response.headers['x-next-page'] !== '' + page++ + } + + return groups.map((group) => ({ + id: group.id, + name: group.name as string, + path: group.path as string, + avatarUrl: group.avatar_url as string, + })) +} + +export async function fetchGitlabGroupProjects(accessToken: string, groups: any[]) { + const groupProjects = {} + + for (const group of groups) { + const projects = [] + let page = 1 + let hasMorePages = true + + while (hasMorePages) { + const response = await axios.get(`https://gitlab.com/api/v4/groups/${group.id}/projects`, { + headers: { Authorization: `Bearer ${accessToken}` }, + params: { page, per_page: 100, archived: false }, + }) + projects.push(...response.data) + hasMorePages = response.headers['x-next-page'] !== '' + page++ + } + + groupProjects[group.id] = projects.map((project) => ({ + groupId: group.id, + groupName: group.name, + groupPath: group.path, + id: project.id, + name: project.name, + path_with_namespace: project.path_with_namespace, + enabled: false, + forkedFrom: project?.forked_from_project?.web_url || null, + })) + } + + return groupProjects as Record +} + +export async function fetchGitlabUserProjects(accessToken: string, userId: number) { + const projects = [] + let page = 1 + let hasMorePages = true + + while (hasMorePages) { + const response = await axios.get(`https://gitlab.com/api/v4/users/${userId}/projects`, { + headers: { Authorization: `Bearer ${accessToken}` }, + params: { page, per_page: 100, archived: false }, + }) + projects.push(...response.data) + hasMorePages = response.headers['x-next-page'] !== '' + page++ + } + + return projects.map((project) => ({ + id: project.id, + name: project.name, + path_with_namespace: project.path_with_namespace, + enabled: false, + forkedFrom: project?.forked_from_project?.web_url || null, + })) +} diff --git a/backend/src/serverless/integrations/usecases/gitlab/removeWebhooks.ts b/backend/src/serverless/integrations/usecases/gitlab/removeWebhooks.ts new file mode 100644 index 0000000000..b1e018264c --- /dev/null +++ b/backend/src/serverless/integrations/usecases/gitlab/removeWebhooks.ts @@ -0,0 +1,43 @@ +import axios from 'axios' + +interface WebhookRemovalResult { + projectId: number + success: boolean + error?: string +} + +export async function removeGitlabWebhooks( + accessToken: string, + projectIds: number[], + hookIds: number[], +): Promise { + const results: WebhookRemovalResult[] = [] + + for (const projectId of projectIds) { + for (const hookId of hookIds) { + try { + // Delete the webhook + const deleteResponse = await axios.delete( + `https://gitlab.com/api/v4/projects/${projectId}/hooks/${hookId}`, + { + headers: { Authorization: `Bearer ${accessToken}` }, + }, + ) + + if (deleteResponse.status === 204) { + results.push({ projectId, success: true }) + } else { + results.push({ + projectId, + success: false, + error: `Unexpected response status: ${deleteResponse.status}`, + }) + } + } catch (error) { + results.push({ projectId, success: false, error: error.message }) + } + } + } + + return results +} diff --git a/backend/src/serverless/integrations/usecases/gitlab/setupWebhooks.ts b/backend/src/serverless/integrations/usecases/gitlab/setupWebhooks.ts new file mode 100644 index 0000000000..856556133a --- /dev/null +++ b/backend/src/serverless/integrations/usecases/gitlab/setupWebhooks.ts @@ -0,0 +1,64 @@ +import axios from 'axios' + +import { API_CONFIG, GITLAB_CONFIG } from '@/conf' + +interface WebhookSetupResult { + projectId: number + hookId?: number + success: boolean + error?: string +} + +const webhookBase = `${API_CONFIG.url}/webhooks` + +const createWebhookUrl = (integrationId: string) => `${webhookBase}/gitlab/${integrationId}` + +export async function setupGitlabWebhooks( + accessToken: string, + projectIds: number[], + integrationId: string, +): Promise { + const results: WebhookSetupResult[] = [] + + if (!GITLAB_CONFIG.webhookToken) { + throw new Error('Gitlab webhook token is not set') + } + + for (const projectId of projectIds) { + try { + const response = await axios.post( + `https://gitlab.com/api/v4/projects/${projectId}/hooks`, + { + token: GITLAB_CONFIG.webhookToken, + url: createWebhookUrl(integrationId), + push_events: false, + issues_events: true, + confidential_issues_events: true, + merge_requests_events: true, + note_events: true, // This covers discussions + job_events: false, + pipeline_events: false, + wiki_page_events: false, + enable_ssl_verification: true, + }, + { + headers: { Authorization: `Bearer ${accessToken}` }, + }, + ) + + if (response.status === 201) { + results.push({ projectId, success: true, hookId: response.data.id }) + } else { + results.push({ + projectId, + success: false, + error: `Unexpected response status: ${response.status}`, + }) + } + } catch (error) { + results.push({ projectId, success: false, error: error.message }) + } + } + + return results +} diff --git a/backend/src/serverless/integrations/usecases/groupsio/getGroupsHierarchy.ts b/backend/src/serverless/integrations/usecases/groupsio/getGroupsHierarchy.ts new file mode 100644 index 0000000000..21d15cbe1c --- /dev/null +++ b/backend/src/serverless/integrations/usecases/groupsio/getGroupsHierarchy.ts @@ -0,0 +1,37 @@ +interface GroupsInput { + id: number + name: string + slug: string +} + +type GroupsArray = GroupsInput[] + +interface GroupHierarchy { + [mainGroup: string]: { + mainGroup: GroupsInput | null + subGroups: GroupsInput[] + } +} + +export const getGroupsHierarchy = (groups: GroupsArray): GroupHierarchy => { + const hierarchy: GroupHierarchy = {} + + groups.forEach((group) => { + const [mainGroupSlug, subGroupSlug] = group.slug.split('+') + + if (!hierarchy[mainGroupSlug]) { + hierarchy[mainGroupSlug] = { + mainGroup: null, + subGroups: [], + } + } + + if (subGroupSlug) { + hierarchy[mainGroupSlug].subGroups.push(group) + } else { + hierarchy[mainGroupSlug].mainGroup = group + } + }) + + return hierarchy +} diff --git a/backend/src/serverless/integrations/usecases/groupsio/getUserSubscriptions.ts b/backend/src/serverless/integrations/usecases/groupsio/getUserSubscriptions.ts new file mode 100644 index 0000000000..7cfccdf5d5 --- /dev/null +++ b/backend/src/serverless/integrations/usecases/groupsio/getUserSubscriptions.ts @@ -0,0 +1,73 @@ +import axios from 'axios' + +import { getServiceChildLogger } from '@crowd/logging' + +const log = getServiceChildLogger('getGroupsIoUserSubscriptions') + +interface Subscription { + id: number + object: string + created: string + updated: string + user_id: number + group_id: number + group_name: string + nice_group_name: string + status: string + post_status: string + email_delivery: string + message_selection: string + auto_follow_replies: boolean + max_attachment_size: string + approved_posts: number + mod_status: string + email: string + user_status: string + user_name: string + timezone: string + full_name: string +} + +interface GetSubscriptionsResponse { + object: string + total_count: number + start_item: number + end_item: number + has_more: boolean + next_page_token?: number + sort_field: string + second_order: string + query: string + sort_dir: string + data: Subscription[] +} + +export const getUserSubscriptions = async (cookie: string): Promise => { + let allSubscriptions: Subscription[] = [] + let nextPageToken: number | undefined + + do { + const url = 'https://groups.io/api/v1/getsubs' + const params = { + limit: 100, + ...(nextPageToken ? { page_token: nextPageToken } : {}), + } + + try { + const response = await axios.get(url, { + params, + headers: { + Cookie: cookie, + }, + }) + + allSubscriptions = [...allSubscriptions, ...response.data.data] + nextPageToken = response.data.next_page_token + } catch (error) { + log.error('Error fetching groups.io subscriptions:', error) + throw error + } + } while (nextPageToken) + + return allSubscriptions +} diff --git a/backend/src/serverless/integrations/usecases/groupsio/types.ts b/backend/src/serverless/integrations/usecases/groupsio/types.ts index 60449a3029..cbac3a43b0 100644 --- a/backend/src/serverless/integrations/usecases/groupsio/types.ts +++ b/backend/src/serverless/integrations/usecases/groupsio/types.ts @@ -1,7 +1,13 @@ export interface GroupsioIntegrationData { email: string token: string - groupNames: GroupName[] + tokenExpiry: string + password: string + groups: GroupDetails[] + autoImports?: { + mainGroup: string + isAllowed: boolean + }[] } export interface GroupsioGetToken { @@ -15,4 +21,11 @@ export interface GroupsioVerifyGroup { cookie: string } +export interface GroupDetails { + id: number + slug: string + name: string + groupAddedOn?: Date +} + export type GroupName = string diff --git a/backend/src/serverless/integrations/usecases/linkedin/errorHandler.ts b/backend/src/serverless/integrations/usecases/linkedin/errorHandler.ts index 3d3ee3ba77..d853713cab 100644 --- a/backend/src/serverless/integrations/usecases/linkedin/errorHandler.ts +++ b/backend/src/serverless/integrations/usecases/linkedin/errorHandler.ts @@ -1,6 +1,8 @@ import { AxiosError, AxiosRequestConfig } from 'axios' import moment from 'moment' + import { Logger } from '@crowd/logging' + import { RateLimitError } from '../../../../types/integration/rateLimitError' export const handleLinkedinError = ( diff --git a/backend/src/serverless/integrations/usecases/linkedin/getOrganizations.ts b/backend/src/serverless/integrations/usecases/linkedin/getOrganizations.ts index 70ce3140ee..e83759ab55 100644 --- a/backend/src/serverless/integrations/usecases/linkedin/getOrganizations.ts +++ b/backend/src/serverless/integrations/usecases/linkedin/getOrganizations.ts @@ -1,10 +1,13 @@ import axios, { AxiosRequestConfig } from 'axios' + import { Logger } from '@crowd/logging' import { PlatformType } from '@crowd/types' -import { handleLinkedinError } from './errorHandler' + import { ILinkedInOrganization } from '../../types/linkedinTypes' import getToken from '../nango/getToken' +import { handleLinkedinError } from './errorHandler' + export const getOrganizations = async ( nangoId: string, logger: Logger, diff --git a/backend/src/serverless/integrations/usecases/nango/getToken.ts b/backend/src/serverless/integrations/usecases/nango/getToken.ts index 4ea4bc54db..c54c442c01 100644 --- a/backend/src/serverless/integrations/usecases/nango/getToken.ts +++ b/backend/src/serverless/integrations/usecases/nango/getToken.ts @@ -1,5 +1,7 @@ import axios from 'axios' + import { Logger } from '@crowd/logging' + import { NANGO_CONFIG } from '../../../../conf' async function getToken(connectionId: string, providerConfigKey: string, logger: Logger) { diff --git a/backend/src/serverless/integrations/usecases/twitter/errorHandler.ts b/backend/src/serverless/integrations/usecases/twitter/errorHandler.ts deleted file mode 100644 index 32278359bf..0000000000 --- a/backend/src/serverless/integrations/usecases/twitter/errorHandler.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { AxiosError, AxiosRequestConfig } from 'axios' -import { Logger } from '@crowd/logging' -import { RateLimitError } from '../../../../types/integration/rateLimitError' - -export const handleTwitterError = ( - err: AxiosError, - config: AxiosRequestConfig, - input: any, - logger: Logger, -): any => { - const queryParams: string[] = [] - if (config.params) { - for (const [key, value] of Object.entries(config.params)) { - queryParams.push(`${key}=${encodeURIComponent(value as any)}`) - } - } - - const url = `${config.url}?${queryParams.join('&')}` - - // https://developer.twitter.com/en/docs/twitter-api/rate-limits - if (err && err.response && err.response.status === 429) { - logger.warn('Twitter API rate limit exceeded') - let rateLimitResetSeconds = 60 - - if (err.response.headers['x-rate-limit-reset']) { - rateLimitResetSeconds = parseInt(err.response.headers['x-rate-limit-reset'], 10) - } - - throw new RateLimitError(rateLimitResetSeconds, url, err) - } else { - logger.error(err, { input }, `Error while calling Twitter API URL: ${url}`) - throw err - } -} diff --git a/backend/src/serverless/integrations/usecases/twitter/getFollowers.ts b/backend/src/serverless/integrations/usecases/twitter/getFollowers.ts deleted file mode 100644 index ec7e1431db..0000000000 --- a/backend/src/serverless/integrations/usecases/twitter/getFollowers.ts +++ /dev/null @@ -1,73 +0,0 @@ -import axios, { AxiosRequestConfig } from 'axios' -import moment from 'moment' -import { Logger } from '@crowd/logging' -import { TwitterGetFollowersInput, TwitterGetFollowersOutput } from '../../types/twitterTypes' -import { handleTwitterError } from './errorHandler' - -/** - * Get all followers of an account - * @param input Input parameters - * @returns Followers - */ -const getFollowers = async ( - input: TwitterGetFollowersInput, - logger: Logger, -): Promise => { - const config: AxiosRequestConfig = { - method: 'get', - url: `https://api.twitter.com/2/users/${input.profileId}/followers`, - params: { - 'user.fields': 'name,description,location,public_metrics,url,verified,profile_image_url', - }, - headers: { - Authorization: `Bearer ${input.token}`, - }, - } - - if (input.perPage) { - config.params.max_results = input.perPage - } - - if (input.page) { - config.params.pagination_token = input.page - } - - try { - const response = await axios(config) - - let limit: number - let timeUntilReset: number - if (response.headers['x-rate-limit-remaining'] && response.headers['x-rate-limit-reset']) { - limit = parseInt(response.headers['x-rate-limit-remaining'], 10) - const resetTs = parseInt(response.headers['x-rate-limit-reset'], 10) * 1000 - timeUntilReset = moment(resetTs).diff(moment(), 'seconds') - } else { - limit = 0 - timeUntilReset = 0 - } - - if ( - response.data.meta && - response.data.meta.result_count && - response.data.meta.result_count > 0 - ) { - return { - records: response.data.data, - nextPage: response.data?.meta?.next_token || '', - limit, - timeUntilReset, - } - } - return { - records: [], - nextPage: '', - limit, - timeUntilReset, - } - } catch (err) { - const newErr = handleTwitterError(err, config, input, logger) - throw newErr - } -} - -export default getFollowers diff --git a/backend/src/serverless/integrations/usecases/twitter/getProfiles.ts b/backend/src/serverless/integrations/usecases/twitter/getProfiles.ts deleted file mode 100644 index 6a2768472c..0000000000 --- a/backend/src/serverless/integrations/usecases/twitter/getProfiles.ts +++ /dev/null @@ -1,48 +0,0 @@ -import axios, { AxiosRequestConfig } from 'axios' -import moment from 'moment' -import { Logger } from '@crowd/logging' -import { - TwitterGetFollowersOutput, - TwitterGetProfilesByUsernameInput, -} from '../../types/twitterTypes' -import { handleTwitterError } from './errorHandler' - -/** - * Get profiles by username - * @param input Input parameters - * @returns Profiles - */ -const getProfiles = async ( - input: TwitterGetProfilesByUsernameInput, - logger: Logger, -): Promise => { - const config: AxiosRequestConfig = { - method: 'get', - url: 'https://api.twitter.com/2/users/by', - params: { - usernames: input.usernames.join(','), - 'user.fields': 'name,description,location,public_metrics,url,verified,profile_image_url', - }, - headers: { - Authorization: `Bearer ${input.token}`, - }, - } - - try { - const response = await axios(config) - const limit = parseInt(response.headers['x-rate-limit-remaining'], 10) - const resetTs = parseInt(response.headers['x-rate-limit-reset'], 10) * 1000 - const timeUntilReset = moment(resetTs).diff(moment(), 'seconds') - return { - records: response.data.data, - nextPage: response.data?.meta?.next_token || '', - limit, - timeUntilReset, - } - } catch (err) { - const newErr = handleTwitterError(err, config, input, logger) - throw newErr - } -} - -export default getProfiles diff --git a/backend/src/serverless/integrations/webhooks/__tests__/events.ts b/backend/src/serverless/integrations/webhooks/__tests__/events.ts deleted file mode 100644 index d736d70c8a..0000000000 --- a/backend/src/serverless/integrations/webhooks/__tests__/events.ts +++ /dev/null @@ -1,10585 +0,0 @@ -export default class TestEvents { - public static issues = { - event: 'issues', - opened: { - action: 'opened', - issue: { - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29', - repository_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/labels{/name}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/comments', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/events', - html_url: 'https://github.com/CrowdHQ/crowd-postgres/issues/29', - id: 1173761480, - node_id: 'I_kwDOGsy6M85F9i3I', - number: 29, - title: 'New title', - user: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - labels: [], - state: 'open', - locked: false, - assignee: null, - assignees: [], - milestone: null, - comments: 0, - created_at: '2022-03-18T16:07:31Z', - updated_at: '2022-03-18T16:07:31Z', - closed_at: null, - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: 'Body here', - reactions: { - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/timeline', - performed_via_github_app: null, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T15:36:54Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7101, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 2, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 2, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdHQ', - repos_url: 'https://api.github.com/orgs/CrowdHQ/repos', - events_url: 'https://api.github.com/orgs/CrowdHQ/events', - hooks_url: 'https://api.github.com/orgs/CrowdHQ/hooks', - issues_url: 'https://api.github.com/orgs/CrowdHQ/issues', - members_url: 'https://api.github.com/orgs/CrowdHQ/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdHQ/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjM1ODU4MTY=', - }, - }, - edited: { - action: 'edited', - issue: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267', - repository_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - labels_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/labels{/name}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/comments', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/events', - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/issues/267', - id: 1341717370, - node_id: 'I_kwDOGsy6M85F9i3I', - number: 267, - title: 'test issue (EDITED)', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - labels: [], - state: 'open', - locked: false, - assignee: null, - assignees: [], - milestone: null, - comments: 2, - created_at: '2022-08-17T12:50:27Z', - updated_at: '2022-08-21T14:54:22Z', - closed_at: null, - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: null, - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/timeline', - performed_via_github_app: null, - state_reason: 'reopened', - }, - changes: { - title: { - from: 'test issue no3', - }, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-21T13:49:08Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 25880, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 6, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 6, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - reopened: { - action: 'reopened', - issue: { - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29', - repository_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/labels{/name}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/comments', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/events', - html_url: 'https://github.com/CrowdHQ/crowd-postgres/issues/29', - id: 1173761480, - node_id: 'I_kwDOGsy6M85F9i3I', - number: 29, - title: 'New title', - user: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - labels: [], - state: 'open', - locked: false, - assignee: null, - assignees: [], - milestone: null, - comments: 0, - created_at: '2022-03-18T16:07:31Z', - updated_at: '2022-03-18T16:07:31Z', - closed_at: null, - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: 'Body here', - reactions: { - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/timeline', - performed_via_github_app: null, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T15:36:54Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7101, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 2, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 2, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdHQ', - repos_url: 'https://api.github.com/orgs/CrowdHQ/repos', - events_url: 'https://api.github.com/orgs/CrowdHQ/events', - hooks_url: 'https://api.github.com/orgs/CrowdHQ/hooks', - issues_url: 'https://api.github.com/orgs/CrowdHQ/issues', - members_url: 'https://api.github.com/orgs/CrowdHQ/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdHQ/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjM1ODU4MTY=', - }, - }, - closed: { - action: 'closed', - issue: { - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29', - repository_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/labels{/name}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/comments', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/events', - html_url: 'https://github.com/CrowdHQ/crowd-postgres/issues/29', - id: 1173761480, - node_id: 'I_kwDOGsy6M85F9i3I', - number: 29, - title: 'New title', - user: { - login: 'mariobalca', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - labels: [], - state: 'closed', - locked: false, - assignee: null, - assignees: [], - milestone: null, - comments: 0, - created_at: '2022-03-18T16:07:31Z', - updated_at: '2022-03-18T19:01:48Z', - closed_at: '2022-03-18T19:01:48Z', - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: 'Body here', - reactions: { - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/timeline', - performed_via_github_app: null, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T18:07:14Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7121, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 1, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 1, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdHQ', - repos_url: 'https://api.github.com/orgs/CrowdHQ/repos', - events_url: 'https://api.github.com/orgs/CrowdHQ/events', - hooks_url: 'https://api.github.com/orgs/CrowdHQ/hooks', - issues_url: 'https://api.github.com/orgs/CrowdHQ/issues', - members_url: 'https://api.github.com/orgs/CrowdHQ/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdHQ/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjM1ODU4MTY=', - }, - }, - } - - static pullRequestReviews = { - event: 'pull_request_review', - submitted: { - action: 'submitted', - review: { - id: 1420752578, - node_id: 'PRR_kwDOHksjGM5UrvbC', - user: { - login: 'epipav', - id: 12017738, - node_id: 'MDQ6VXNlcjEyMDE3NzM4', - avatar_url: 'https://avatars.githubusercontent.com/u/12017738?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/epipav', - html_url: 'https://github.com/epipav', - followers_url: 'https://api.github.com/users/epipav/followers', - following_url: 'https://api.github.com/users/epipav/following{/other_user}', - gists_url: 'https://api.github.com/users/epipav/gists{/gist_id}', - starred_url: 'https://api.github.com/users/epipav/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/epipav/subscriptions', - organizations_url: 'https://api.github.com/users/epipav/orgs', - repos_url: 'https://api.github.com/users/epipav/repos', - events_url: 'https://api.github.com/users/epipav/events{/privacy}', - received_events_url: 'https://api.github.com/users/epipav/received_events', - type: 'User', - site_admin: false, - }, - body: '', - commit_id: '22db2e60206ee7445e457685367407f540553e50', - submitted_at: '2023-05-10T14:15:24Z', - state: 'approved', - html_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/843#pullrequestreview-1420752578', - pull_request_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/843', - author_association: 'CONTRIBUTOR', - _links: { - html: { - href: 'https://github.com/CrowdDotDev/crowd.dev/pull/843#pullrequestreview-1420752578', - }, - pull_request: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/843', - }, - }, - }, - pull_request: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/843', - id: 1344074031, - node_id: 'PR_kwDOHksjGM5QHPEv', - html_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/843', - diff_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/843.diff', - patch_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/843.patch', - issue_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/843', - number: 843, - state: 'open', - locked: false, - title: 'Fix custom activity type filter', - user: { - login: 'joanagmaia', - id: 20134207, - node_id: 'MDQ6VXNlcjIwMTM0MjA3', - avatar_url: 'https://avatars.githubusercontent.com/u/20134207?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanagmaia', - html_url: 'https://github.com/joanagmaia', - followers_url: 'https://api.github.com/users/joanagmaia/followers', - following_url: 'https://api.github.com/users/joanagmaia/following{/other_user}', - gists_url: 'https://api.github.com/users/joanagmaia/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanagmaia/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanagmaia/subscriptions', - organizations_url: 'https://api.github.com/users/joanagmaia/orgs', - repos_url: 'https://api.github.com/users/joanagmaia/repos', - events_url: 'https://api.github.com/users/joanagmaia/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanagmaia/received_events', - type: 'User', - site_admin: false, - }, - body: '# Changes proposed ✍️\r\nBug: Due to this previous change ([PR](https://github.com/CrowdDotDev/crowd.dev/pull/823/files#diff-987f06572bad79c47ef62d518a7a4e419dda775d942d365da55dc5509acf5291R44)), the `activityType` and `platform` were being stored in lowerCase in all activities. However, in the settings object it remained with the original value. So currently there is an inconsistency between custom activity type keys in activities and settings.\r\nTo fix this I\'m lowerCasing custom activity type and platform keys on their creation and migrating the existing ones to lowerCase as well.\r\n\r\nExample of problem:\r\nTenant settings:\r\n```\r\n{\r\n "Conference": {\r\n "Registered to a conference": {\r\n "display": {\r\n "short": "Registered to a conference",\r\n "channel": "",\r\n "default": "Registered to a conference"\r\n },\r\n "isContribution": false\r\n },\r\n },\r\n "other": {\r\n "This is a test": {\r\n "display": {\r\n "short": "This is a test",\r\n "channel": "",\r\n "default": "This is a test"\r\n },\r\n "isContribution": false\r\n }\r\n }\r\n}\r\n```\r\nActivity payload:\r\n`platform` and `activityType` fields:\r\n```\r\nplatform: \'luma\',\r\nactivityType: \'registered to a conference\'\r\n\r\nor\r\n\r\nplatform: \'other\',\r\nactivityType: \'this is a test\'\r\n```\r\n\r\nWith these changes the activity payload should remain the same but the tenant settings should be fixed to:\r\n```\r\n{\r\n "conference": {\r\n "registered to a conference": {\r\n "display": {\r\n "short": "Registered to a conference",\r\n "channel": "",\r\n "default": "Registered to a conference"\r\n },\r\n "isContribution": false\r\n },\r\n },\r\n "other": {\r\n "this is a test": {\r\n "display": {\r\n "short": "This is a test",\r\n "channel": "",\r\n "default": "This is a test"\r\n },\r\n "isContribution": false\r\n }\r\n }\r\n}\r\n```\r\n\r\n### What\r\n\r\n### 🤖 Generated by Copilot at b11dfa1\r\n\r\nThe pull request standardizes the keys for custom activity types to use lowercase in the `settings` table and the `SettingsService` class. This avoids case sensitivity issues and improves data consistency across the application.\r\n​\r\n\r\n### 🤖 Generated by Copilot at b11dfa1\r\n\r\n> _`customActivityTypes`_\r\n> _Lowercase keys for all seasons_\r\n> _`typeKey` follows_\r\n\r\n### Why\r\n\r\n\r\n### How\r\n\r\n### 🤖 Generated by Copilot at b11dfa1\r\n\r\n* Standardize the keys for custom activity types to use lowercase in the database and the service layer ([link](https://github.com/CrowdDotDev/crowd.dev/pull/843/files?diff=unified&w=0#diff-a9626422cfa5c6888ed594d5114bffc0c4113699b7f39d1c4c456da8bd72c812L1-R13), [link](https://github.com/CrowdDotDev/crowd.dev/pull/843/files?diff=unified&w=0#diff-2908c7bb18ca4494942ee153161abc5555bdd9516fc2d225a406d785b5787711L24-R24))\r\n* Update the `customActivityTypes` column in the `settings` table using the SQL script `V1683627959__customActivityTypesKeys.sql` ([link](https://github.com/CrowdDotDev/crowd.dev/pull/843/files?diff=unified&w=0#diff-a9626422cfa5c6888ed594d5114bffc0c4113699b7f39d1c4c456da8bd72c812L1-R13))\r\n* Modify the `typeKey` variable in the `SettingsService` class to match the database format ([link](https://github.com/CrowdDotDev/crowd.dev/pull/843/files?diff=unified&w=0#diff-2908c7bb18ca4494942ee153161abc5555bdd9516fc2d225a406d785b5787711L24-R24))\r\n\r\n## Checklist ✅\r\n- [x] Label appropriately with `Feature`, `Improvement`, or `Bug`.\r\n- [ ] Add screehshots to the PR description for relevant FE changes\r\n- [ ] New backend functionality has been unit-tested.\r\n- [ ] API documentation has been updated (if necessary) (see [docs on API documentation](https://docs.crowd.dev/docs/updating-api-documentation)).\r\n- [ ] [Quality standards](https://github.com/CrowdDotDev/crowd-github-test-public/blob/main/CONTRIBUTING.md#quality-standards) are met.\r\n', - created_at: '2023-05-09T16:52:03Z', - updated_at: '2023-05-10T14:15:25Z', - closed_at: null, - merged_at: null, - merge_commit_sha: '358d960c38d27bb9377ff2ed71f2a416433656c8', - assignee: { - login: 'joanagmaia', - id: 20134207, - node_id: 'MDQ6VXNlcjIwMTM0MjA3', - avatar_url: 'https://avatars.githubusercontent.com/u/20134207?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanagmaia', - html_url: 'https://github.com/joanagmaia', - followers_url: 'https://api.github.com/users/joanagmaia/followers', - following_url: 'https://api.github.com/users/joanagmaia/following{/other_user}', - gists_url: 'https://api.github.com/users/joanagmaia/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanagmaia/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanagmaia/subscriptions', - organizations_url: 'https://api.github.com/users/joanagmaia/orgs', - repos_url: 'https://api.github.com/users/joanagmaia/repos', - events_url: 'https://api.github.com/users/joanagmaia/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanagmaia/received_events', - type: 'User', - site_admin: false, - }, - assignees: [ - { - login: 'joanagmaia', - id: 20134207, - node_id: 'MDQ6VXNlcjIwMTM0MjA3', - avatar_url: 'https://avatars.githubusercontent.com/u/20134207?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanagmaia', - html_url: 'https://github.com/joanagmaia', - followers_url: 'https://api.github.com/users/joanagmaia/followers', - following_url: 'https://api.github.com/users/joanagmaia/following{/other_user}', - gists_url: 'https://api.github.com/users/joanagmaia/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanagmaia/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanagmaia/subscriptions', - organizations_url: 'https://api.github.com/users/joanagmaia/orgs', - repos_url: 'https://api.github.com/users/joanagmaia/repos', - events_url: 'https://api.github.com/users/joanagmaia/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanagmaia/received_events', - type: 'User', - site_admin: false, - }, - ], - requested_reviewers: [ - { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - ], - requested_teams: [], - labels: [], - milestone: null, - draft: false, - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/843/commits', - review_comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/843/comments', - review_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments{/number}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/843/comments', - statuses_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/22db2e60206ee7445e457685367407f540553e50', - head: { - label: 'CrowdDotDev:bug/activity-type-filter', - ref: 'bug/activity-type-filter', - sha: '22db2e60206ee7445e457685367407f540553e50', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data for DevTool companies.', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-10T07:55:10Z', - pushed_at: '2023-05-10T13:25:16Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 24469, - stargazers_count: 535, - watchers_count: 535, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 48, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 81, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'community', - 'community-led-growth', - 'community-management', - 'developer-advocacy', - 'devrel', - 'javascript', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 48, - open_issues: 81, - watchers: 535, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: false, - allow_rebase_merge: false, - allow_auto_merge: false, - delete_branch_on_merge: true, - allow_update_branch: true, - use_squash_pr_title_as_default: true, - squash_merge_commit_message: 'BLANK', - squash_merge_commit_title: 'PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - base: { - label: 'CrowdDotDev:main', - ref: 'main', - sha: 'ceb9b6ed619cf09ddd4c5f6c41913d1e1a4607e7', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data for DevTool companies.', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-10T07:55:10Z', - pushed_at: '2023-05-10T13:25:16Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 24469, - stargazers_count: 535, - watchers_count: 535, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 48, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 81, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'community', - 'community-led-growth', - 'community-management', - 'developer-advocacy', - 'devrel', - 'javascript', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 48, - open_issues: 81, - watchers: 535, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: false, - allow_rebase_merge: false, - allow_auto_merge: false, - delete_branch_on_merge: true, - allow_update_branch: true, - use_squash_pr_title_as_default: true, - squash_merge_commit_message: 'BLANK', - squash_merge_commit_title: 'PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - _links: { - self: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/843', - }, - html: { - href: 'https://github.com/CrowdDotDev/crowd.dev/pull/843', - }, - issue: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/843', - }, - comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/843/comments', - }, - review_comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/843/comments', - }, - review_comment: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments{/number}', - }, - commits: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/843/commits', - }, - statuses: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/22db2e60206ee7445e457685367407f540553e50', - }, - }, - author_association: 'CONTRIBUTOR', - auto_merge: null, - active_lock_reason: null, - }, - repository: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data for DevTool companies.', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-10T07:55:10Z', - pushed_at: '2023-05-10T13:25:16Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 24469, - stargazers_count: 535, - watchers_count: 535, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 48, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 81, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'community', - 'community-led-growth', - 'community-management', - 'developer-advocacy', - 'devrel', - 'javascript', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 48, - open_issues: 81, - watchers: 535, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: - 'Open-source community and data tools built to unlock community-led growth for developer tools.', - }, - sender: { - login: 'epipav', - id: 12017738, - node_id: 'MDQ6VXNlcjEyMDE3NzM4', - avatar_url: 'https://avatars.githubusercontent.com/u/12017738?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/epipav', - html_url: 'https://github.com/epipav', - followers_url: 'https://api.github.com/users/epipav/followers', - following_url: 'https://api.github.com/users/epipav/following{/other_user}', - gists_url: 'https://api.github.com/users/epipav/gists{/gist_id}', - starred_url: 'https://api.github.com/users/epipav/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/epipav/subscriptions', - organizations_url: 'https://api.github.com/users/epipav/orgs', - repos_url: 'https://api.github.com/users/epipav/repos', - events_url: 'https://api.github.com/users/epipav/events{/privacy}', - received_events_url: 'https://api.github.com/users/epipav/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 29211772, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjkyMTE3NzI=', - }, - }, - } - - static pullRequestReviewThreadComment = { - event: '', - created: { - action: 'created', - comment: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments/1192271420', - pull_request_review_id: 1424328394, - id: 1192271420, - node_id: 'PRRC_kwDOHksjGM5HEJ48', - diff_hunk: - '@@ -29,9 +29,7 @@\n \n \n \n- \n+ ', - path: 'frontend/src/modules/activity/pages/activity-list-page.vue', - commit_id: '97bd5109ff1eed738baa728cc03c91687be393bc', - original_commit_id: '97bd5109ff1eed738baa728cc03c91687be393bc', - user: { - login: 'joanagmaia', - id: 20134207, - node_id: 'MDQ6VXNlcjIwMTM0MjA3', - avatar_url: 'https://avatars.githubusercontent.com/u/20134207?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanagmaia', - html_url: 'https://github.com/joanagmaia', - followers_url: 'https://api.github.com/users/joanagmaia/followers', - following_url: 'https://api.github.com/users/joanagmaia/following{/other_user}', - gists_url: 'https://api.github.com/users/joanagmaia/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanagmaia/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanagmaia/subscriptions', - organizations_url: 'https://api.github.com/users/joanagmaia/orgs', - repos_url: 'https://api.github.com/users/joanagmaia/repos', - events_url: 'https://api.github.com/users/joanagmaia/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanagmaia/received_events', - type: 'User', - site_admin: false, - }, - body: "Shouldn't we only add this once we have everything ready? So that you can merge this PR without any blocker because it doesn't update anything in the UI?", - created_at: '2023-05-12T11:57:03Z', - updated_at: '2023-05-12T12:01:35Z', - html_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/853#discussion_r1192271420', - pull_request_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/853', - author_association: 'CONTRIBUTOR', - _links: { - self: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments/1192271420', - }, - html: { - href: 'https://github.com/CrowdDotDev/crowd.dev/pull/853#discussion_r1192271420', - }, - pull_request: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/853', - }, - }, - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments/1192271420/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - start_line: null, - original_start_line: null, - start_side: null, - line: 32, - original_line: 32, - side: 'RIGHT', - original_position: 7, - position: 7, - subject_type: 'line', - }, - pull_request: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/853', - id: 1347649482, - node_id: 'PR_kwDOHksjGM5QU3_K', - html_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/853', - diff_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/853.diff', - patch_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/853.patch', - issue_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/853', - number: 853, - state: 'open', - locked: false, - title: 'Filters configs & basic logic & typescript', - user: { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - body: "# Changes proposed ✍️\r\n\r\n### What\r\n\n### 🤖 Generated by Copilot at 950f2ce\n\nThis pull request adds TypeScript support to the frontend and introduces a new `cr-filter` component to enable more flexible filtering options for the activity and member modules. It also updates some dependencies and adjusts the code accordingly. The pull request affects the following files: `frontend/.eslintrc.js`, `frontend/package.json`, `frontend/src/main.ts`, `frontend/src/modules/activity/pages/activity-list-page.vue`, and several files under `frontend/src/modules/activity/config/filters` and `frontend/src/modules/member/config/filters`.\r\n​\r\n\n### 🤖 Generated by Copilot at 950f2ce\n\n> _We're breaking the chains of the old code base_\n> _We're rising from the ashes with TypeScript and Vue_\n> _We're filtering the data with custom components_\n> _We're the masters of the frontend, we're the chosen few_\r\n\r\n### Why\r\n\r\n\r\n### How\r\n\n### 🤖 Generated by Copilot at 950f2ce\n\n* Added TypeScript support and linting rules to the project ([link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-093b2d5625f20b6d845fe73f1af12762fc64ba9d0b9d94b14e8aa287e2d2ed1dL11-R16), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-093b2d5625f20b6d845fe73f1af12762fc64ba9d0b9d94b14e8aa287e2d2ed1dL39-R47), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-093b2d5625f20b6d845fe73f1af12762fc64ba9d0b9d94b14e8aa287e2d2ed1dR71), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-093b2d5625f20b6d845fe73f1af12762fc64ba9d0b9d94b14e8aa287e2d2ed1dR80), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-da6498268e99511d9ba0df3c13e439d10556a812881c9d03955b2ef7c6c1c655R100), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-a235fca1f464b6af0fb049401b64264244eda9d624234c1fee162f6c663cee7bL3-R5), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-880807f0963877836924cebde24ebb161aefc0458ab92f7dadf867e12b7075b4L22-R22))\n* Updated `vue` and downgraded `webpack` dependencies to avoid compatibility issues ([link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-da6498268e99511d9ba0df3c13e439d10556a812881c9d03955b2ef7c6c1c655L62-R62), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-da6498268e99511d9ba0df3c13e439d10556a812881c9d03955b2ef7c6c1c655L71-R83))\n* Added custom filter components and configurations for the activity and member modules using the `cr-filter` component ([link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-f7e958bff5ca82a24a2bcc809b0d5868c76f6c7378c61dcb0b8bd287a646edd2R1-R23), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-cd8e780c8488cffad4194da57b9385504fa0d90b0b7084b6523dc9a05795a651R1-R22), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-69b2549353f117ee8aa475b258a2b7e5c4d1dabf070389e6fb0cc4515961bf2bR1-R23), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-a9caf8fef766c417b77c51900f046e8bbab2f34427bd53ac5a3ee24fa50febe8R1-R22), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-000975af376ec51052285dec3052d0d2b7b3b5bb5a32c937973df3719fab0af7R1-R21), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-eef270ab4e86feefead9e965f6749b81fabc04515ae0d016c9b617b49c67bfc4R1-R32), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-23ca53439be6cf4f27ec40bc2c06f04ea84246c16d275d2f87590ec836448089R1-R16), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-e2ef4c4cfd836c42d5d9305bfb8920729b1ac2076a16d0d9a1d431429732b038R1-R21), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-acaec04bcea58bbc89fb09cdcf9c13486baeb8031fbf36443bdf9a63194cee40R1-R35), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-3a3c6c5d10f2d009d72f8eba26e51c75b589b33911727ad0e566568b574e201aR1-R21), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-7bb1b7dc4c35d5e952e9185fe927581811c55dc23a9dc510d496abff0b41aeb1R1-R23), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-0ce6547bee0155186a048d673be3dafaab869e51b92902f7c48d524d07467c58L32-R32), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-0ce6547bee0155186a048d673be3dafaab869e51b92902f7c48d524d07467c58L67-R65), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-0ce6547bee0155186a048d673be3dafaab869e51b92902f7c48d524d07467c58R72-R73), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-0ce6547bee0155186a048d673be3dafaab869e51b92902f7c48d524d07467c58R79), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-0ce6547bee0155186a048d673be3dafaab869e51b92902f7c48d524d07467c58L85-R86), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-0ce6547bee0155186a048d673be3dafaab869e51b92902f7c48d524d07467c58R96), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-0b2256f166e26e7a5fe115419cf20ec23d7cea5aed42e3b7fc9cc00292678fedR1-R31), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-59768c401b689d0104e67273c59a560ab299fbcb88d3c52dc2b4cd3e42ed02ccR1-R23), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-f95205f0059ef9af70f69e3d1e744b7255e6892e74e4938b214149a612524223R1-R22))\n* Added type annotations and casts to avoid TypeScript errors in the `main.ts` file and the Hotjar script ([link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-a235fca1f464b6af0fb049401b64264244eda9d624234c1fee162f6c663cee7bL50-R56), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-a235fca1f464b6af0fb049401b64264244eda9d624234c1fee162f6c663cee7bL62-R64), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-a235fca1f464b6af0fb049401b64264244eda9d624234c1fee162f6c663cee7bL84-R87), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-a235fca1f464b6af0fb049401b64264244eda9d624234c1fee162f6c663cee7bL94-R113))\r\n\r\n## Checklist ✅\r\n- [x] Label appropriately with `Feature`, `Improvement`, or `Bug`.\r\n- [ ] Add screehshots to the PR description for relevant FE changes\r\n- [ ] New backend functionality has been unit-tested.\r\n- [ ] API documentation has been updated (if necessary) (see [docs on API documentation](https://docs.crowd.dev/docs/updating-api-documentation)).\r\n- [ ] [Quality standards](https://github.com/CrowdDotDev/crowd-github-test-public/blob/main/CONTRIBUTING.md#quality-standards) are met.\r\n", - created_at: '2023-05-11T19:54:58Z', - updated_at: '2023-05-12T12:01:36Z', - closed_at: null, - merged_at: null, - merge_commit_sha: '3e84dcd09fc73ba6abffbed93ce874f833eddc9b', - assignee: { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - assignees: [ - { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - ], - requested_reviewers: [], - requested_teams: [], - labels: [ - { - id: 4771856507, - node_id: 'LA_kwDOHksjGM8AAAABHGzAew', - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels/Feature', - name: 'Feature', - color: 'BB87FC', - default: false, - description: 'Created by Linear-GitHub Sync', - }, - ], - milestone: null, - draft: false, - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/853/commits', - review_comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/853/comments', - review_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments{/number}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/853/comments', - statuses_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/97bd5109ff1eed738baa728cc03c91687be393bc', - head: { - label: 'CrowdDotDev:feature/filters-config', - ref: 'feature/filters-config', - sha: '97bd5109ff1eed738baa728cc03c91687be393bc', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data for DevTool companies.', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-11T09:41:38Z', - pushed_at: '2023-05-12T11:26:53Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 24949, - stargazers_count: 536, - watchers_count: 536, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 49, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 82, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'community', - 'community-led-growth', - 'community-management', - 'developer-advocacy', - 'devrel', - 'javascript', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 49, - open_issues: 82, - watchers: 536, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: false, - allow_rebase_merge: false, - allow_auto_merge: false, - delete_branch_on_merge: true, - allow_update_branch: true, - use_squash_pr_title_as_default: true, - squash_merge_commit_message: 'BLANK', - squash_merge_commit_title: 'PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - base: { - label: 'CrowdDotDev:feature/filters', - ref: 'feature/filters', - sha: '3d953d3fa7f92c9ccdaf2247e5af0ea4b877651b', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data for DevTool companies.', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-11T09:41:38Z', - pushed_at: '2023-05-12T11:26:53Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 24949, - stargazers_count: 536, - watchers_count: 536, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 49, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 82, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'community', - 'community-led-growth', - 'community-management', - 'developer-advocacy', - 'devrel', - 'javascript', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 49, - open_issues: 82, - watchers: 536, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: false, - allow_rebase_merge: false, - allow_auto_merge: false, - delete_branch_on_merge: true, - allow_update_branch: true, - use_squash_pr_title_as_default: true, - squash_merge_commit_message: 'BLANK', - squash_merge_commit_title: 'PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - _links: { - self: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/853', - }, - html: { - href: 'https://github.com/CrowdDotDev/crowd.dev/pull/853', - }, - issue: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/853', - }, - comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/853/comments', - }, - review_comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/853/comments', - }, - review_comment: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments{/number}', - }, - commits: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/853/commits', - }, - statuses: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/97bd5109ff1eed738baa728cc03c91687be393bc', - }, - }, - author_association: 'CONTRIBUTOR', - auto_merge: null, - active_lock_reason: null, - }, - repository: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data for DevTool companies.', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-11T09:41:38Z', - pushed_at: '2023-05-12T11:26:53Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 24949, - stargazers_count: 536, - watchers_count: 536, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 49, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 82, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'community', - 'community-led-growth', - 'community-management', - 'developer-advocacy', - 'devrel', - 'javascript', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 49, - open_issues: 82, - watchers: 536, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: - 'Open-source community and data tools built to unlock community-led growth for developer tools.', - }, - sender: { - login: 'joanagmaia', - id: 20134207, - node_id: 'MDQ6VXNlcjIwMTM0MjA3', - avatar_url: 'https://avatars.githubusercontent.com/u/20134207?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanagmaia', - html_url: 'https://github.com/joanagmaia', - followers_url: 'https://api.github.com/users/joanagmaia/followers', - following_url: 'https://api.github.com/users/joanagmaia/following{/other_user}', - gists_url: 'https://api.github.com/users/joanagmaia/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanagmaia/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanagmaia/subscriptions', - organizations_url: 'https://api.github.com/users/joanagmaia/orgs', - repos_url: 'https://api.github.com/users/joanagmaia/repos', - events_url: 'https://api.github.com/users/joanagmaia/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanagmaia/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 29211772, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjkyMTE3NzI=', - }, - }, - } - - static pullRequestReviewComments = { - event: 'pull_request_review_comment', - created: {}, - } - - static pullRequests = { - event: 'pull_request', - opened: { - action: 'opened', - number: 30, - pull_request: { - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30', - id: 883733019, - node_id: 'PR_kwDOGsy6M840rLIb', - html_url: 'https://github.com/CrowdHQ/crowd-postgres/pull/30', - diff_url: 'https://github.com/CrowdHQ/crowd-postgres/pull/30.diff', - patch_url: 'https://github.com/CrowdHQ/crowd-postgres/pull/30.patch', - issue_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30', - number: 30, - state: 'open', - locked: false, - title: 'Feature/webhooks', - user: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - body: '# Description\r\n\r\n\r\n## Checklist 🔏\r\n\r\nSome important checks that must be ensured before merging:\r\n\r\n### Deploy to staging\r\n\r\n#### Deployment checklist\r\n\r\n- [ ] Make sure all AWS λ functions are up to date with your version (if applicable)\r\n- [ ] Make sure the anton-environment and soa-environment are updated and pushed\r\n\r\n\r\n### Functionality\r\n\r\n- [ ] Has the functionality been checked in a normal staging tenant? (not local)\r\n- [ ] Has the functionality been checked in the large tenant? (team+large@crowd.dev)\r\n- [ ] Has the functionality been checked in an empty tenant? (team+empty@crowd.dev)\r\n- [ ] Is there any more edge cases that should be taken into account?\r\n\r\n### Code quality\r\n\r\n- [ ] Are there comments in the main functionality of the code?\r\n- [ ] Are all tests passing?\r\n- [ ] Are all URLs to external services in `.env` files? Never hard-coded\r\n- [ ] Are all secrets in `anton-environment`? Never, ever hard-coded\r\n\r\n🔥🚀💪🏼\r\n', - created_at: '2022-03-18T19:15:59Z', - updated_at: '2022-03-18T19:15:59Z', - closed_at: null, - merged_at: null, - merge_commit_sha: null, - assignee: null, - assignees: [], - requested_reviewers: [], - requested_teams: [], - labels: [], - milestone: null, - draft: false, - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/commits', - review_comments_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/comments', - review_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/comments{/number}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30/comments', - statuses_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/a36398d1b6aa55a6614db6dec1690c1f21baaef1', - head: { - label: 'CrowdHQ:feature/webhooks', - ref: 'feature/webhooks', - sha: 'a36398d1b6aa55a6614db6dec1690c1f21baaef1', - user: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T19:16:00Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7125, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 2, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 2, - watchers: 1, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: true, - allow_rebase_merge: true, - allow_auto_merge: false, - delete_branch_on_merge: false, - allow_update_branch: false, - }, - }, - base: { - label: 'CrowdHQ:main', - ref: 'main', - sha: 'cc785a28b2dfc28f58bf457669ce5859f8c30593', - user: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T19:16:00Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7125, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 2, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 2, - watchers: 1, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: true, - allow_rebase_merge: true, - allow_auto_merge: false, - delete_branch_on_merge: false, - allow_update_branch: false, - }, - }, - _links: { - self: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30', - }, - html: { - href: 'https://github.com/CrowdHQ/crowd-postgres/pull/30', - }, - issue: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30', - }, - comments: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30/comments', - }, - review_comments: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/comments', - }, - review_comment: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/comments{/number}', - }, - commits: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/commits', - }, - statuses: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/a36398d1b6aa55a6614db6dec1690c1f21baaef1', - }, - }, - author_association: 'CONTRIBUTOR', - auto_merge: null, - active_lock_reason: null, - merged: false, - mergeable: null, - rebaseable: null, - mergeable_state: 'unknown', - merged_by: null, - comments: 0, - review_comments: 0, - maintainer_can_modify: false, - commits: 5, - additions: 1053, - deletions: 22, - changed_files: 11, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T19:16:00Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7125, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 2, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 2, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdHQ', - repos_url: 'https://api.github.com/orgs/CrowdHQ/repos', - events_url: 'https://api.github.com/orgs/CrowdHQ/events', - hooks_url: 'https://api.github.com/orgs/CrowdHQ/hooks', - issues_url: 'https://api.github.com/orgs/CrowdHQ/issues', - members_url: 'https://api.github.com/orgs/CrowdHQ/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdHQ/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjM1ODU4MTY=', - }, - }, - edited: { - action: 'edited', - number: 266, - pull_request: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls/266', - id: 1028781659, - node_id: 'PR_kwDOGsy6M840rLIb', - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266', - diff_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266.diff', - patch_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266.patch', - issue_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266', - number: 266, - state: 'open', - locked: false, - title: 'Feature/nodejs GitHub integration', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - body: "# Changes proposed ✍️\r\n- github integration moved from python to nodejs\r\n- new github endpoint: discussions for webhooks and iterator\r\n- issue comments, pr comments and discussion comments now processed as separate endpoints (edited)\r\n\r\n## Checklist ✅\r\n- [x] Label appropriately with `type:feature 🚀`, `type:enhancement ✨`, `type:bug 🐞`, or `type:documentation 📜`.\r\n- [ ] Tests are passing. \r\n- [ ] New backend functionality has been unit-tested.\r\n- [ ] Environment variables have been updated\r\n - [ ] Front-end: `frontend/.env.dist`\r\n - [ ] Backend: `backend/.env.dist`, `backend/.env.dist.staging`, `backend/.env.dist.staging`.\r\n - [ ] [Configuration docs](https://docs.crowd.dev/docs/configuration) have been updated.\r\n - [ ] Team members only: update environment variables in Password manager and update the team\r\n- [ ] API documentation has been updated (if necessary) (see [docs on API documentation](https://docs.crowd.dev/docs/updating-api-documentation)). \r\n- [ ] [Quality standards](https://github.com/CrowdDotDev/crowd-github-test-public/blob/main/CONTRIBUTING.md#quality-standards) are met. \r\n- [ ] All changes have been tested in a staging site. \r\n- [ ] All changes are working locally running crowd.dev's Docker local environment.", - created_at: '2022-08-17T12:35:00Z', - updated_at: '2022-08-21T15:06:38Z', - closed_at: null, - merged_at: null, - merge_commit_sha: 'b5e53951e4340c9c4ab03799d8a52450843d7c1b', - assignee: null, - assignees: [], - requested_reviewers: [], - requested_teams: [], - labels: [ - { - id: 4374821209, - node_id: 'LA_kwDOGsy6M88AAAABBMJ5WQ', - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels/type:enhancement%20%E2%9C%A8', - name: 'type:enhancement ✨', - color: 'B57798', - default: false, - description: '', - }, - { - id: 4374821465, - node_id: 'LA_kwDOGsy6M88AAAABBMJ6WQ', - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels/type:feature%20%F0%9F%9A%80', - name: 'type:feature 🚀', - color: 'A3DF2C', - default: false, - description: '', - }, - ], - milestone: null, - draft: false, - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls/266/commits', - review_comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls/266/comments', - review_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls/comments{/number}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/comments', - statuses_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/e7879626e65089f4ebbc0a1f36cdab54f54cef31', - head: { - label: 'CrowdDotDev:feature/nodejs-github-integration', - ref: 'feature/nodejs-github-integration', - sha: 'e7879626e65089f4ebbc0a1f36cdab54f54cef31', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-21T13:49:08Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 25880, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 6, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 6, - watchers: 1, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: true, - allow_rebase_merge: true, - allow_auto_merge: false, - delete_branch_on_merge: false, - allow_update_branch: false, - use_squash_pr_title_as_default: false, - squash_merge_commit_message: 'COMMIT_MESSAGES', - squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - base: { - label: 'CrowdDotDev:main', - ref: 'main', - sha: '974da23a0edcb771de968cce75d2e8770a99e751', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-21T13:49:08Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 25880, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 6, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 6, - watchers: 1, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: true, - allow_rebase_merge: true, - allow_auto_merge: false, - delete_branch_on_merge: false, - allow_update_branch: false, - use_squash_pr_title_as_default: false, - squash_merge_commit_message: 'COMMIT_MESSAGES', - squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - _links: { - self: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls/266', - }, - html: { - href: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266', - }, - issue: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266', - }, - comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/comments', - }, - review_comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls/266/comments', - }, - review_comment: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls/comments{/number}', - }, - commits: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls/266/commits', - }, - statuses: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/e7879626e65089f4ebbc0a1f36cdab54f54cef31', - }, - }, - author_association: 'CONTRIBUTOR', - auto_merge: null, - active_lock_reason: null, - merged: false, - mergeable: true, - rebaseable: true, - mergeable_state: 'unstable', - merged_by: null, - comments: 3, - review_comments: 0, - maintainer_can_modify: false, - commits: 13, - additions: 3005, - deletions: 126, - changed_files: 25, - }, - changes: { - body: { - from: "# Changes proposed ✍️\r\n- github integration moved from python to nodejs\r\n- new github endpoint: discussions for webhooks and iterator\r\n- issue comments, pr comments and discussion comments now processed as separate endpoints\r\n \r\n## Checklist ✅\r\n- [x] Label appropriately with `type:feature 🚀`, `type:enhancement ✨`, `type:bug 🐞`, or `type:documentation 📜`.\r\n- [ ] Tests are passing. \r\n- [ ] New backend functionality has been unit-tested.\r\n- [ ] Environment variables have been updated\r\n - [ ] Front-end: `frontend/.env.dist`\r\n - [ ] Backend: `backend/.env.dist`, `backend/.env.dist.staging`, `backend/.env.dist.staging`.\r\n - [ ] [Configuration docs](https://docs.crowd.dev/docs/configuration) have been updated.\r\n - [ ] Team members only: update environment variables in Password manager and update the team\r\n- [ ] API documentation has been updated (if necessary) (see [docs on API documentation](https://docs.crowd.dev/docs/updating-api-documentation)). \r\n- [ ] [Quality standards](https://github.com/CrowdDotDev/crowd-github-test-public/blob/main/CONTRIBUTING.md#quality-standards) are met. \r\n- [ ] All changes have been tested in a staging site. \r\n- [ ] All changes are working locally running crowd.dev's Docker local environment.", - }, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-21T13:49:08Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 25880, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 6, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 6, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - reopened: { - action: 'reopened', - number: 30, - pull_request: { - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30', - id: 883733019, - node_id: 'PR_kwDOGsy6M840rLIb', - html_url: 'https://github.com/CrowdHQ/crowd-postgres/pull/30', - diff_url: 'https://github.com/CrowdHQ/crowd-postgres/pull/30.diff', - patch_url: 'https://github.com/CrowdHQ/crowd-postgres/pull/30.patch', - issue_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30', - number: 30, - state: 'open', - locked: false, - title: 'Feature/webhooks', - user: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - body: '# Description\r\n\r\n\r\n## Checklist 🔏\r\n\r\nSome important checks that must be ensured before merging:\r\n\r\n### Deploy to staging\r\n\r\n#### Deployment checklist\r\n\r\n- [ ] Make sure all AWS λ functions are up to date with your version (if applicable)\r\n- [ ] Make sure the anton-environment and soa-environment are updated and pushed\r\n\r\n\r\n### Functionality\r\n\r\n- [ ] Has the functionality been checked in a normal staging tenant? (not local)\r\n- [ ] Has the functionality been checked in the large tenant? (team+large@crowd.dev)\r\n- [ ] Has the functionality been checked in an empty tenant? (team+empty@crowd.dev)\r\n- [ ] Is there any more edge cases that should be taken into account?\r\n\r\n### Code quality\r\n\r\n- [ ] Are there comments in the main functionality of the code?\r\n- [ ] Are all tests passing?\r\n- [ ] Are all URLs to external services in `.env` files? Never hard-coded\r\n- [ ] Are all secrets in `anton-environment`? Never, ever hard-coded\r\n\r\n🔥🚀💪🏼\r\n', - created_at: '2022-03-18T19:15:59Z', - updated_at: '2022-03-18T19:15:59Z', - closed_at: null, - merged_at: null, - merge_commit_sha: null, - assignee: null, - assignees: [], - requested_reviewers: [], - requested_teams: [], - labels: [], - milestone: null, - draft: false, - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/commits', - review_comments_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/comments', - review_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/comments{/number}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30/comments', - statuses_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/a36398d1b6aa55a6614db6dec1690c1f21baaef1', - head: { - label: 'CrowdHQ:feature/webhooks', - ref: 'feature/webhooks', - sha: 'a36398d1b6aa55a6614db6dec1690c1f21baaef1', - user: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T19:16:00Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7125, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 2, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 2, - watchers: 1, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: true, - allow_rebase_merge: true, - allow_auto_merge: false, - delete_branch_on_merge: false, - allow_update_branch: false, - }, - }, - base: { - label: 'CrowdHQ:main', - ref: 'main', - sha: 'cc785a28b2dfc28f58bf457669ce5859f8c30593', - user: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T19:16:00Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7125, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 2, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 2, - watchers: 1, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: true, - allow_rebase_merge: true, - allow_auto_merge: false, - delete_branch_on_merge: false, - allow_update_branch: false, - }, - }, - _links: { - self: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30', - }, - html: { - href: 'https://github.com/CrowdHQ/crowd-postgres/pull/30', - }, - issue: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30', - }, - comments: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30/comments', - }, - review_comments: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/comments', - }, - review_comment: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/comments{/number}', - }, - commits: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/commits', - }, - statuses: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/a36398d1b6aa55a6614db6dec1690c1f21baaef1', - }, - }, - author_association: 'CONTRIBUTOR', - auto_merge: null, - active_lock_reason: null, - merged: false, - mergeable: null, - rebaseable: null, - mergeable_state: 'unknown', - merged_by: null, - comments: 0, - review_comments: 0, - maintainer_can_modify: false, - commits: 5, - additions: 1053, - deletions: 22, - changed_files: 11, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T19:16:00Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7125, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 2, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 2, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdHQ', - repos_url: 'https://api.github.com/orgs/CrowdHQ/repos', - events_url: 'https://api.github.com/orgs/CrowdHQ/events', - hooks_url: 'https://api.github.com/orgs/CrowdHQ/hooks', - issues_url: 'https://api.github.com/orgs/CrowdHQ/issues', - members_url: 'https://api.github.com/orgs/CrowdHQ/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdHQ/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjM1ODU4MTY=', - }, - }, - closed: { - action: 'closed', - number: 30, - pull_request: { - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30', - id: 883733019, - node_id: 'PR_kwDOGsy6M840rLIb', - html_url: 'https://github.com/CrowdHQ/crowd-postgres/pull/30', - diff_url: 'https://github.com/CrowdHQ/crowd-postgres/pull/30.diff', - patch_url: 'https://github.com/CrowdHQ/crowd-postgres/pull/30.patch', - issue_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30', - number: 30, - state: 'closed', - locked: false, - title: 'Feature/webhooks', - user: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - body: '# Description\r\n\r\n\r\n## Checklist 🔏\r\n\r\nSome important checks that must be ensured before merging:\r\n\r\n### Deploy to staging\r\n\r\n#### Deployment checklist\r\n\r\n- [ ] Make sure all AWS λ functions are up to date with your version (if applicable)\r\n- [ ] Make sure the anton-environment and soa-environment are updated and pushed\r\n\r\n\r\n### Functionality\r\n\r\n- [ ] Has the functionality been checked in a normal staging tenant? (not local)\r\n- [ ] Has the functionality been checked in the large tenant? (team+large@crowd.dev)\r\n- [ ] Has the functionality been checked in an empty tenant? (team+empty@crowd.dev)\r\n- [ ] Is there any more edge cases that should be taken into account?\r\n\r\n### Code quality\r\n\r\n- [ ] Are there comments in the main functionality of the code?\r\n- [ ] Are all tests passing?\r\n- [ ] Are all URLs to external services in `.env` files? Never hard-coded\r\n- [ ] Are all secrets in `anton-environment`? Never, ever hard-coded\r\n\r\n🔥🚀💪🏼\r\n', - created_at: '2022-03-18T19:15:59Z', - updated_at: '2022-03-18T19:18:54Z', - closed_at: '2022-03-18T19:18:54Z', - merged_at: null, - merge_commit_sha: '8b114a07a097b4903bde26918780edc0fc8ff707', - assignee: null, - assignees: [], - requested_reviewers: [], - requested_teams: [], - labels: [], - milestone: null, - draft: false, - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/commits', - review_comments_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/comments', - review_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/comments{/number}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30/comments', - statuses_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/a36398d1b6aa55a6614db6dec1690c1f21baaef1', - head: { - label: 'CrowdHQ:feature/webhooks', - ref: 'feature/webhooks', - sha: 'a36398d1b6aa55a6614db6dec1690c1f21baaef1', - user: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T19:16:00Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7125, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 1, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 1, - watchers: 1, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: true, - allow_rebase_merge: true, - allow_auto_merge: false, - delete_branch_on_merge: false, - allow_update_branch: false, - }, - }, - base: { - label: 'CrowdHQ:main', - ref: 'main', - sha: 'cc785a28b2dfc28f58bf457669ce5859f8c30593', - user: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T19:16:00Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7125, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 1, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 1, - watchers: 1, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: true, - allow_rebase_merge: true, - allow_auto_merge: false, - delete_branch_on_merge: false, - allow_update_branch: false, - }, - }, - _links: { - self: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30', - }, - html: { - href: 'https://github.com/CrowdHQ/crowd-postgres/pull/30', - }, - issue: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30', - }, - comments: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30/comments', - }, - review_comments: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/comments', - }, - review_comment: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/comments{/number}', - }, - commits: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/commits', - }, - statuses: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/a36398d1b6aa55a6614db6dec1690c1f21baaef1', - }, - }, - author_association: 'CONTRIBUTOR', - auto_merge: null, - active_lock_reason: null, - merged: false, - mergeable: true, - rebaseable: false, - mergeable_state: 'clean', - merged_by: null, - comments: 0, - review_comments: 0, - maintainer_can_modify: false, - commits: 5, - additions: 1053, - deletions: 22, - changed_files: 11, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T19:16:00Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7125, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 1, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 1, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdHQ', - repos_url: 'https://api.github.com/orgs/CrowdHQ/repos', - events_url: 'https://api.github.com/orgs/CrowdHQ/events', - hooks_url: 'https://api.github.com/orgs/CrowdHQ/hooks', - issues_url: 'https://api.github.com/orgs/CrowdHQ/issues', - members_url: 'https://api.github.com/orgs/CrowdHQ/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdHQ/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjM1ODU4MTY=', - }, - }, - assigned: { - action: 'assigned', - number: 898, - pull_request: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/898', - id: 1362932082, - node_id: 'PR_kwDOHksjGM5RPLFy', - html_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/898', - diff_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/898.diff', - patch_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/898.patch', - issue_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/898', - number: 898, - state: 'open', - locked: false, - title: 'Github issue closed support and improvements', - user: { - login: 'epipav', - id: 12017738, - node_id: 'MDQ6VXNlcjEyMDE3NzM4', - avatar_url: 'https://avatars.githubusercontent.com/u/12017738?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/epipav', - html_url: 'https://github.com/epipav', - followers_url: 'https://api.github.com/users/epipav/followers', - following_url: 'https://api.github.com/users/epipav/following{/other_user}', - gists_url: 'https://api.github.com/users/epipav/gists{/gist_id}', - starred_url: 'https://api.github.com/users/epipav/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/epipav/subscriptions', - organizations_url: 'https://api.github.com/users/epipav/orgs', - repos_url: 'https://api.github.com/users/epipav/repos', - events_url: 'https://api.github.com/users/epipav/events{/privacy}', - received_events_url: 'https://api.github.com/users/epipav/received_events', - type: 'User', - site_admin: false, - }, - body: '# Changes proposed ✍️\r\n\r\n### What\r\n\n### 🤖 Generated by Copilot at ff1b5de\n\nThis pull request improves the GitHub integration service by using the correct source identifier for pull request and issue activities, and by fetching and parsing the closed events of the issues. It modifies the `githubIntegrationService.ts` and the `issues.ts` files to implement these changes.\r\n​\r\n\n### 🤖 Generated by Copilot at ff1b5de\n\n> _`IssuesQuery` grows_\n> _Fetching timeline items now_\n> _Winter of closed bugs_\r\n\r\n### Why\r\n\r\n\r\n### How\r\n\n### 🤖 Generated by Copilot at ff1b5de\n\n* Modify the `sourceId` of the activities generated from pull request events to use the `sender.login` instead of the `user.login` of the pull request author, to reflect the user who performed the action ([link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-93f2daaa968f5bec5c40cd31fc27ac6422ea3dcbdabecf3ef80f3c16802d1c30L909-R909), [link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-93f2daaa968f5bec5c40cd31fc27ac6422ea3dcbdabecf3ef80f3c16802d1c30L919-R921), [link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-93f2daaa968f5bec5c40cd31fc27ac6422ea3dcbdabecf3ef80f3c16802d1c30L937-R937))\n* Declare and assign four variables (`sourceId`, `sourceParentId`, `body`, and `title`) at the beginning of the `parseWebhook` method in `githubIntegrationService.ts`, to be used later for generating activities based on the issue events, to avoid repeating the same logic and to make the code more readable and consistent ([link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-93f2daaa968f5bec5c40cd31fc27ac6422ea3dcbdabecf3ef80f3c16802d1c30R1424-R1427), [link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-93f2daaa968f5bec5c40cd31fc27ac6422ea3dcbdabecf3ef80f3c16802d1c30R1436-R1439), [link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-93f2daaa968f5bec5c40cd31fc27ac6422ea3dcbdabecf3ef80f3c16802d1c30R1446-R1451), [link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-93f2daaa968f5bec5c40cd31fc27ac6422ea3dcbdabecf3ef80f3c16802d1c30L1459-R1474))\n* Modify the `parseWebhookMember` method call in `githubIntegrationService.ts`, to use the `sender.login` instead of the `issue.user.login`, to get the member information of the user who triggered the issue event, to match the logic of using the `sender.login` for the `sourceId` of the activity ([link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-93f2daaa968f5bec5c40cd31fc27ac6422ea3dcbdabecf3ef80f3c16802d1c30L1449-R1459))\n* Add a call to the `parseIssueEvents` method in `githubIntegrationService.ts`, which is a new method that parses the timeline items of the issue and generates activities for each supported event type, such as `CLOSED_EVENT`, and concatenates the output array with the existing output array ([link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-93f2daaa968f5bec5c40cd31fc27ac6422ea3dcbdabecf3ef80f3c16802d1c30R1514-R1522))\n* Add the implementation of the `parseIssueEvents` method in `githubIntegrationService.ts`, which iterates over the records of the timeline items, and uses a switch statement to handle different event types, and pushes a new activity object to the output array, with the appropriate properties and attributes based on the event type, and logs a warning message for any unsupported event type ([link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-93f2daaa968f5bec5c40cd31fc27ac6422ea3dcbdabecf3ef80f3c16802d1c30R1528-R1572))\n* Add a new field to the `IssuesQuery` class in `issues.ts`, which is a GraphQL query that fetches the issues from the GitHub API, to get the information of the closed events of the issues, such as the actor and the created date, which are needed for generating the activities in the `parseIssueEvents` method, and uses a fragment to select the relevant fields of the actor ([link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-75aae4f028e206f107d0ea6dd3e99f71271f27f46f4cb260b199ba9dfdc9340bR24-R35))\r\n\r\n## Checklist ✅\r\n- [ ] Label appropriately with `Feature`, `Improvement`, or `Bug`.\r\n- [ ] Add screehshots to the PR description for relevant FE changes\r\n- [ ] New backend functionality has been unit-tested.\r\n- [ ] API documentation has been updated (if necessary) (see [docs on API documentation](https://docs.crowd.dev/docs/updating-api-documentation)).\r\n- [ ] [Quality standards](https://github.com/CrowdDotDev/crowd-github-test-public/blob/main/CONTRIBUTING.md#quality-standards) are met.\r\n', - created_at: '2023-05-24T11:40:14Z', - updated_at: '2023-05-24T13:11:07Z', - closed_at: null, - merged_at: null, - merge_commit_sha: 'c51ad2b5557f314f5567cc2c0be85bfc6a36c95e', - assignee: { - login: 'epipav', - id: 12017738, - node_id: 'MDQ6VXNlcjEyMDE3NzM4', - avatar_url: 'https://avatars.githubusercontent.com/u/12017738?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/epipav', - html_url: 'https://github.com/epipav', - followers_url: 'https://api.github.com/users/epipav/followers', - following_url: 'https://api.github.com/users/epipav/following{/other_user}', - gists_url: 'https://api.github.com/users/epipav/gists{/gist_id}', - starred_url: 'https://api.github.com/users/epipav/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/epipav/subscriptions', - organizations_url: 'https://api.github.com/users/epipav/orgs', - repos_url: 'https://api.github.com/users/epipav/repos', - events_url: 'https://api.github.com/users/epipav/events{/privacy}', - received_events_url: 'https://api.github.com/users/epipav/received_events', - type: 'User', - site_admin: false, - }, - assignees: [ - { - login: 'epipav', - id: 12017738, - node_id: 'MDQ6VXNlcjEyMDE3NzM4', - avatar_url: 'https://avatars.githubusercontent.com/u/12017738?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/epipav', - html_url: 'https://github.com/epipav', - followers_url: 'https://api.github.com/users/epipav/followers', - following_url: 'https://api.github.com/users/epipav/following{/other_user}', - gists_url: 'https://api.github.com/users/epipav/gists{/gist_id}', - starred_url: 'https://api.github.com/users/epipav/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/epipav/subscriptions', - organizations_url: 'https://api.github.com/users/epipav/orgs', - repos_url: 'https://api.github.com/users/epipav/repos', - events_url: 'https://api.github.com/users/epipav/events{/privacy}', - received_events_url: 'https://api.github.com/users/epipav/received_events', - type: 'User', - site_admin: false, - }, - { - login: 'joanagmaia', - id: 20134207, - node_id: 'MDQ6VXNlcjIwMTM0MjA3', - avatar_url: 'https://avatars.githubusercontent.com/u/20134207?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanagmaia', - html_url: 'https://github.com/joanagmaia', - followers_url: 'https://api.github.com/users/joanagmaia/followers', - following_url: 'https://api.github.com/users/joanagmaia/following{/other_user}', - gists_url: 'https://api.github.com/users/joanagmaia/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanagmaia/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanagmaia/subscriptions', - organizations_url: 'https://api.github.com/users/joanagmaia/orgs', - repos_url: 'https://api.github.com/users/joanagmaia/repos', - events_url: 'https://api.github.com/users/joanagmaia/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanagmaia/received_events', - type: 'User', - site_admin: false, - }, - ], - requested_reviewers: [], - requested_teams: [], - labels: [], - milestone: null, - draft: true, - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/898/commits', - review_comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/898/comments', - review_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments{/number}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/898/comments', - statuses_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/ff1b5de3e7ad22bba4941484bd5723b561374f98', - head: { - label: 'CrowdDotDev:improvement/gh-integration-issues-closed', - ref: 'improvement/gh-integration-issues-closed', - sha: 'ff1b5de3e7ad22bba4941484bd5723b561374f98', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data in one place', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-24T08:35:21Z', - pushed_at: '2023-05-24T11:40:15Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 25942, - stargazers_count: 554, - watchers_count: 554, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 51, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 86, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'analytics', - 'cdp', - 'community', - 'community-driven', - 'community-led-growth', - 'community-management', - 'customer-data-platform', - 'developer-advocacy', - 'developer-led-growth', - 'developer-marketing', - 'developer-relations', - 'devrel', - 'javascript', - 'postgres', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 51, - open_issues: 86, - watchers: 554, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: false, - allow_rebase_merge: false, - allow_auto_merge: false, - delete_branch_on_merge: true, - allow_update_branch: true, - use_squash_pr_title_as_default: true, - squash_merge_commit_message: 'BLANK', - squash_merge_commit_title: 'PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - base: { - label: 'CrowdDotDev:main', - ref: 'main', - sha: '73d23c92e24621a5153d3c378deca3c8e6f050b7', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data in one place', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-24T08:35:21Z', - pushed_at: '2023-05-24T11:40:15Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 25942, - stargazers_count: 554, - watchers_count: 554, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 51, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 86, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'analytics', - 'cdp', - 'community', - 'community-driven', - 'community-led-growth', - 'community-management', - 'customer-data-platform', - 'developer-advocacy', - 'developer-led-growth', - 'developer-marketing', - 'developer-relations', - 'devrel', - 'javascript', - 'postgres', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 51, - open_issues: 86, - watchers: 554, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: false, - allow_rebase_merge: false, - allow_auto_merge: false, - delete_branch_on_merge: true, - allow_update_branch: true, - use_squash_pr_title_as_default: true, - squash_merge_commit_message: 'BLANK', - squash_merge_commit_title: 'PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - _links: { - self: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/898', - }, - html: { - href: 'https://github.com/CrowdDotDev/crowd.dev/pull/898', - }, - issue: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/898', - }, - comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/898/comments', - }, - review_comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/898/comments', - }, - review_comment: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments{/number}', - }, - commits: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/898/commits', - }, - statuses: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/ff1b5de3e7ad22bba4941484bd5723b561374f98', - }, - }, - author_association: 'CONTRIBUTOR', - auto_merge: null, - active_lock_reason: null, - merged: false, - mergeable: true, - rebaseable: true, - mergeable_state: 'unstable', - merged_by: null, - comments: 0, - review_comments: 0, - maintainer_can_modify: false, - commits: 1, - additions: 92, - deletions: 16, - changed_files: 2, - }, - assignee: { - login: 'joanagmaia', - id: 20134207, - node_id: 'MDQ6VXNlcjIwMTM0MjA3', - avatar_url: 'https://avatars.githubusercontent.com/u/20134207?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanagmaia', - html_url: 'https://github.com/joanagmaia', - followers_url: 'https://api.github.com/users/joanagmaia/followers', - following_url: 'https://api.github.com/users/joanagmaia/following{/other_user}', - gists_url: 'https://api.github.com/users/joanagmaia/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanagmaia/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanagmaia/subscriptions', - organizations_url: 'https://api.github.com/users/joanagmaia/orgs', - repos_url: 'https://api.github.com/users/joanagmaia/repos', - events_url: 'https://api.github.com/users/joanagmaia/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanagmaia/received_events', - type: 'User', - site_admin: false, - }, - repository: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data in one place', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-24T08:35:21Z', - pushed_at: '2023-05-24T11:40:15Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 25942, - stargazers_count: 554, - watchers_count: 554, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 51, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 86, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'analytics', - 'cdp', - 'community', - 'community-driven', - 'community-led-growth', - 'community-management', - 'customer-data-platform', - 'developer-advocacy', - 'developer-led-growth', - 'developer-marketing', - 'developer-relations', - 'devrel', - 'javascript', - 'postgres', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 51, - open_issues: 86, - watchers: 554, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: - 'Open-source community and data tools built to unlock community-led growth for developer tools.', - }, - sender: { - login: 'epipav', - id: 12017738, - node_id: 'MDQ6VXNlcjEyMDE3NzM4', - avatar_url: 'https://avatars.githubusercontent.com/u/12017738?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/epipav', - html_url: 'https://github.com/epipav', - followers_url: 'https://api.github.com/users/epipav/followers', - following_url: 'https://api.github.com/users/epipav/following{/other_user}', - gists_url: 'https://api.github.com/users/epipav/gists{/gist_id}', - starred_url: 'https://api.github.com/users/epipav/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/epipav/subscriptions', - organizations_url: 'https://api.github.com/users/epipav/orgs', - repos_url: 'https://api.github.com/users/epipav/repos', - events_url: 'https://api.github.com/users/epipav/events{/privacy}', - received_events_url: 'https://api.github.com/users/epipav/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 29211772, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjkyMTE3NzI=', - }, - }, - review_requested: { - action: 'review_requested', - number: 897, - pull_request: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/897', - id: 1362645865, - node_id: 'PR_kwDOHksjGM5ROFNp', - html_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/897', - diff_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/897.diff', - patch_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/897.patch', - issue_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/897', - number: 897, - state: 'open', - locked: false, - title: 'Boolean base filter', - user: { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - body: '# Changes proposed ✍️\r\nimage\r\n\r\n\r\n### What\r\ncopilot:summary\r\n​\r\ncopilot:poem\r\n\r\n### Why\r\n\r\n\r\n### How\r\ncopilot:walkthrough\r\n\r\n## Checklist ✅\r\n- [x] Label appropriately with `Feature`, `Improvement`, or `Bug`.\r\n- [ ] Add screehshots to the PR description for relevant FE changes\r\n- [ ] New backend functionality has been unit-tested.\r\n- [ ] API documentation has been updated (if necessary) (see [docs on API documentation](https://docs.crowd.dev/docs/updating-api-documentation)).\r\n- [ ] [Quality standards](https://github.com/CrowdDotDev/crowd-github-test-public/blob/main/CONTRIBUTING.md#quality-standards) are met.\r\n', - created_at: '2023-05-24T08:36:33Z', - updated_at: '2023-05-24T08:36:33Z', - closed_at: null, - merged_at: null, - merge_commit_sha: null, - assignee: { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - assignees: [ - { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - ], - requested_reviewers: [ - { - login: 'joanagmaia', - id: 20134207, - node_id: 'MDQ6VXNlcjIwMTM0MjA3', - avatar_url: 'https://avatars.githubusercontent.com/u/20134207?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanagmaia', - html_url: 'https://github.com/joanagmaia', - followers_url: 'https://api.github.com/users/joanagmaia/followers', - following_url: 'https://api.github.com/users/joanagmaia/following{/other_user}', - gists_url: 'https://api.github.com/users/joanagmaia/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanagmaia/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanagmaia/subscriptions', - organizations_url: 'https://api.github.com/users/joanagmaia/orgs', - repos_url: 'https://api.github.com/users/joanagmaia/repos', - events_url: 'https://api.github.com/users/joanagmaia/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanagmaia/received_events', - type: 'User', - site_admin: false, - }, - ], - requested_teams: [], - labels: [ - { - id: 4771856507, - node_id: 'LA_kwDOHksjGM8AAAABHGzAew', - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels/Feature', - name: 'Feature', - color: 'BB87FC', - default: false, - description: 'Created by Linear-GitHub Sync', - }, - ], - milestone: null, - draft: false, - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/897/commits', - review_comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/897/comments', - review_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments{/number}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/897/comments', - statuses_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/bf1e4d306886ce988cbe085f7496127c360362a0', - head: { - label: 'CrowdDotDev:feature/boolean-base', - ref: 'feature/boolean-base', - sha: 'bf1e4d306886ce988cbe085f7496127c360362a0', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data in one place', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-24T08:35:21Z', - pushed_at: '2023-05-24T08:36:33Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 25886, - stargazers_count: 554, - watchers_count: 554, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 51, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 86, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'analytics', - 'cdp', - 'community', - 'community-driven', - 'community-led-growth', - 'community-management', - 'customer-data-platform', - 'developer-advocacy', - 'developer-led-growth', - 'developer-marketing', - 'developer-relations', - 'devrel', - 'javascript', - 'postgres', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 51, - open_issues: 86, - watchers: 554, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: false, - allow_rebase_merge: false, - allow_auto_merge: false, - delete_branch_on_merge: true, - allow_update_branch: true, - use_squash_pr_title_as_default: true, - squash_merge_commit_message: 'BLANK', - squash_merge_commit_title: 'PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - base: { - label: 'CrowdDotDev:feature/filters', - ref: 'feature/filters', - sha: 'cdf5317cc76c0943a23827e4b06499c84d46fcbf', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data in one place', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-24T08:35:21Z', - pushed_at: '2023-05-24T08:36:33Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 25886, - stargazers_count: 554, - watchers_count: 554, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 51, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 86, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'analytics', - 'cdp', - 'community', - 'community-driven', - 'community-led-growth', - 'community-management', - 'customer-data-platform', - 'developer-advocacy', - 'developer-led-growth', - 'developer-marketing', - 'developer-relations', - 'devrel', - 'javascript', - 'postgres', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 51, - open_issues: 86, - watchers: 554, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: false, - allow_rebase_merge: false, - allow_auto_merge: false, - delete_branch_on_merge: true, - allow_update_branch: true, - use_squash_pr_title_as_default: true, - squash_merge_commit_message: 'BLANK', - squash_merge_commit_title: 'PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - _links: { - self: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/897', - }, - html: { - href: 'https://github.com/CrowdDotDev/crowd.dev/pull/897', - }, - issue: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/897', - }, - comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/897/comments', - }, - review_comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/897/comments', - }, - review_comment: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments{/number}', - }, - commits: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/897/commits', - }, - statuses: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/bf1e4d306886ce988cbe085f7496127c360362a0', - }, - }, - author_association: 'CONTRIBUTOR', - auto_merge: null, - active_lock_reason: null, - merged: false, - mergeable: null, - rebaseable: null, - mergeable_state: 'unknown', - merged_by: null, - comments: 0, - review_comments: 0, - maintainer_can_modify: false, - commits: 1, - additions: 179, - deletions: 32, - changed_files: 14, - }, - requested_reviewer: { - login: 'joanagmaia', - id: 20134207, - node_id: 'MDQ6VXNlcjIwMTM0MjA3', - avatar_url: 'https://avatars.githubusercontent.com/u/20134207?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanagmaia', - html_url: 'https://github.com/joanagmaia', - followers_url: 'https://api.github.com/users/joanagmaia/followers', - following_url: 'https://api.github.com/users/joanagmaia/following{/other_user}', - gists_url: 'https://api.github.com/users/joanagmaia/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanagmaia/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanagmaia/subscriptions', - organizations_url: 'https://api.github.com/users/joanagmaia/orgs', - repos_url: 'https://api.github.com/users/joanagmaia/repos', - events_url: 'https://api.github.com/users/joanagmaia/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanagmaia/received_events', - type: 'User', - site_admin: false, - }, - repository: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data in one place', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-24T08:35:21Z', - pushed_at: '2023-05-24T08:36:33Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 25886, - stargazers_count: 554, - watchers_count: 554, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 51, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 86, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'analytics', - 'cdp', - 'community', - 'community-driven', - 'community-led-growth', - 'community-management', - 'customer-data-platform', - 'developer-advocacy', - 'developer-led-growth', - 'developer-marketing', - 'developer-relations', - 'devrel', - 'javascript', - 'postgres', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 51, - open_issues: 86, - watchers: 554, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: - 'Open-source community and data tools built to unlock community-led growth for developer tools.', - }, - sender: { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 29211772, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjkyMTE3NzI=', - }, - }, - merged: { - action: 'merged', - number: 896, - pull_request: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/896', - id: 1362439701, - node_id: 'PR_kwDOHksjGM5RNS4V', - html_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/896', - diff_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/896.diff', - patch_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/896.patch', - issue_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/896', - number: 896, - state: 'merged', - locked: false, - title: 'Hubspot book a call', - user: { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - body: '# Changes proposed ✍️\r\nimage\r\n\r\n\r\n### What\r\n\n### 🤖 Generated by Copilot at a54ff06\n\nThis pull request adds the initial implementation of the HubSpot integration for the app, which allows users to book a custom plan consultation with CrowdDotDev. It adds a new `hubspot` module with its configuration and a custom connect component, and modifies the `integrations-config.js` and `integration-connect.vue` files to enable and render the integration.\r\n​\r\n\n### 🤖 Generated by Copilot at a54ff06\n\n> _We\'re building a bridge to the HubSpot realm_\n> _With a button of doom and a custom component_\n> _We\'re loading it dynamically with a conditional spell_\n> _We\'re adding it to the config, it\'s our final ascent_\r\n\r\n### Why\r\n\r\n\r\n### How\r\n\n### 🤖 Generated by Copilot at a54ff06\n\n* Add a new HubSpot integration that allows users to book a custom plan consultation with CrowdDotDev ([link](https://github.com/CrowdDotDev/crowd.dev/pull/896/files?diff=unified&w=0#diff-7078523d220bab5012a66ec2faabe0cef0ed4d714385e62a16ff802afd2cce41R1-R13), [link](https://github.com/CrowdDotDev/crowd.dev/pull/896/files?diff=unified&w=0#diff-de6ef036cf00503f519f1f15f641613fe22da0177f1e0b345e8aea6ac288a45eR1-R12), [link](https://github.com/CrowdDotDev/crowd.dev/pull/896/files?diff=unified&w=0#diff-16679253103a6ebf5da26212a4a7bd72a61d6ee8c5e53776f43ba5b8756cc10eR1-R3), [link](https://github.com/CrowdDotDev/crowd.dev/pull/896/files?diff=unified&w=0#diff-f07d5ed683ecfc23d0c65f6e2f467b3a7e5cf2aa76be9cb71d83f0994940b569R7), [link](https://github.com/CrowdDotDev/crowd.dev/pull/896/files?diff=unified&w=0#diff-f07d5ed683ecfc23d0c65f6e2f467b3a7e5cf2aa76be9cb71d83f0994940b569R33), [link](https://github.com/CrowdDotDev/crowd.dev/pull/896/files?diff=unified&w=0#diff-24fb48f9bdeccd0436bd8106f405e6a176e9ae94a8a44295c48ad4c6c6b6974cR35-R39))\r\n\r\n## Checklist ✅\r\n- [x] Label appropriately with `Feature`, `Improvement`, or `Bug`.\r\n- [ ] Add screehshots to the PR description for relevant FE changes\r\n- [ ] New backend functionality has been unit-tested.\r\n- [ ] API documentation has been updated (if necessary) (see [docs on API documentation](https://docs.crowd.dev/docs/updating-api-documentation)).\r\n- [ ] [Quality standards](https://github.com/CrowdDotDev/crowd-github-test-public/blob/main/CONTRIBUTING.md#quality-standards) are met.\r\n', - created_at: '2023-05-24T06:03:07Z', - updated_at: '2023-05-24T08:37:07Z', - closed_at: '2023-05-24T08:37:06Z', - merged_at: '2023-05-24T08:37:06Z', - merge_commit_sha: '73d23c92e24621a5153d3c378deca3c8e6f050b7', - assignee: { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - assignees: [ - { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - ], - requested_reviewers: [], - requested_teams: [], - labels: [ - { - id: 4771856507, - node_id: 'LA_kwDOHksjGM8AAAABHGzAew', - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels/Feature', - name: 'Feature', - color: 'BB87FC', - default: false, - description: 'Created by Linear-GitHub Sync', - }, - ], - milestone: null, - draft: false, - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/896/commits', - review_comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/896/comments', - review_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments{/number}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/896/comments', - statuses_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/a54ff069ff46231c30406d2d88d5af16b57894ef', - head: { - label: 'CrowdDotDev:feature/hubspot-book-call', - ref: 'feature/hubspot-book-call', - sha: 'a54ff069ff46231c30406d2d88d5af16b57894ef', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data in one place', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-24T08:35:21Z', - pushed_at: '2023-05-24T08:37:06Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 25886, - stargazers_count: 554, - watchers_count: 554, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 51, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 85, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'analytics', - 'cdp', - 'community', - 'community-driven', - 'community-led-growth', - 'community-management', - 'customer-data-platform', - 'developer-advocacy', - 'developer-led-growth', - 'developer-marketing', - 'developer-relations', - 'devrel', - 'javascript', - 'postgres', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 51, - open_issues: 85, - watchers: 554, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: false, - allow_rebase_merge: false, - allow_auto_merge: false, - delete_branch_on_merge: true, - allow_update_branch: true, - use_squash_pr_title_as_default: true, - squash_merge_commit_message: 'BLANK', - squash_merge_commit_title: 'PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - base: { - label: 'CrowdDotDev:main', - ref: 'main', - sha: 'a9493e2d08ae7b53625a32db59fc0cce0a9f2d01', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data in one place', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-24T08:35:21Z', - pushed_at: '2023-05-24T08:37:06Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 25886, - stargazers_count: 554, - watchers_count: 554, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 51, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 85, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'analytics', - 'cdp', - 'community', - 'community-driven', - 'community-led-growth', - 'community-management', - 'customer-data-platform', - 'developer-advocacy', - 'developer-led-growth', - 'developer-marketing', - 'developer-relations', - 'devrel', - 'javascript', - 'postgres', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 51, - open_issues: 85, - watchers: 554, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: false, - allow_rebase_merge: false, - allow_auto_merge: false, - delete_branch_on_merge: true, - allow_update_branch: true, - use_squash_pr_title_as_default: true, - squash_merge_commit_message: 'BLANK', - squash_merge_commit_title: 'PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - _links: { - self: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/896', - }, - html: { - href: 'https://github.com/CrowdDotDev/crowd.dev/pull/896', - }, - issue: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/896', - }, - comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/896/comments', - }, - review_comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/896/comments', - }, - review_comment: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments{/number}', - }, - commits: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/896/commits', - }, - statuses: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/a54ff069ff46231c30406d2d88d5af16b57894ef', - }, - }, - author_association: 'CONTRIBUTOR', - auto_merge: null, - active_lock_reason: null, - merged: true, - mergeable: null, - rebaseable: null, - mergeable_state: 'unknown', - merged_by: { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - comments: 0, - review_comments: 0, - maintainer_can_modify: false, - commits: 1, - additions: 35, - deletions: 0, - changed_files: 6, - }, - repository: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data in one place', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-24T08:35:21Z', - pushed_at: '2023-05-24T08:37:06Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 25886, - stargazers_count: 554, - watchers_count: 554, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 51, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 85, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'analytics', - 'cdp', - 'community', - 'community-driven', - 'community-led-growth', - 'community-management', - 'customer-data-platform', - 'developer-advocacy', - 'developer-led-growth', - 'developer-marketing', - 'developer-relations', - 'devrel', - 'javascript', - 'postgres', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 51, - open_issues: 85, - watchers: 554, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: - 'Open-source community and data tools built to unlock community-led growth for developer tools.', - }, - sender: { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 29211772, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjkyMTE3NzI=', - }, - }, - } - - static star = { - event: 'star', - created: { - action: 'created', - starred_at: '2022-03-20T16:13:07Z', - repository: { - id: 462431237, - node_id: 'R_kgDOG5AkBQ', - name: 'core', - full_name: 'CrowdHQ/core', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/core', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/core', - forks_url: 'https://api.github.com/repos/CrowdHQ/core/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/core/keys{/key_id}', - collaborators_url: 'https://api.github.com/repos/CrowdHQ/core/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/core/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/core/hooks', - issue_events_url: 'https://api.github.com/repos/CrowdHQ/core/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/core/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/core/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/core/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/core/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/core/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/core/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/core/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/core/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/core/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/core/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/core/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/core/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/core/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/core/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/core/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdHQ/core/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/core/comments{/number}', - issue_comment_url: 'https://api.github.com/repos/CrowdHQ/core/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/core/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdHQ/core/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/core/merges', - archive_url: 'https://api.github.com/repos/CrowdHQ/core/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/core/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/core/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/core/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdHQ/core/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/core/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/core/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/core/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/core/deployments', - created_at: '2022-02-22T18:47:48Z', - updated_at: '2022-03-20T16:13:07Z', - pushed_at: '2022-03-20T16:12:45Z', - git_url: 'git://github.com/CrowdHQ/core.git', - ssh_url: 'git@github.com:CrowdHQ/core.git', - clone_url: 'https://github.com/CrowdHQ/core.git', - svn_url: 'https://github.com/CrowdHQ/core', - homepage: null, - size: 37251, - stargazers_count: 1, - watchers_count: 1, - language: 'Python', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 2, - license: null, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 2, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdHQ', - repos_url: 'https://api.github.com/orgs/CrowdHQ/repos', - events_url: 'https://api.github.com/orgs/CrowdHQ/events', - hooks_url: 'https://api.github.com/orgs/CrowdHQ/hooks', - issues_url: 'https://api.github.com/orgs/CrowdHQ/issues', - members_url: 'https://api.github.com/orgs/CrowdHQ/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdHQ/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjM1ODU4MTY=', - }, - }, - deleted: { - action: 'deleted', - starred_at: null, - repository: { - id: 462431237, - node_id: 'R_kgDOG5AkBQ', - name: 'core', - full_name: 'CrowdHQ/core', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/core', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/core', - forks_url: 'https://api.github.com/repos/CrowdHQ/core/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/core/keys{/key_id}', - collaborators_url: 'https://api.github.com/repos/CrowdHQ/core/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/core/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/core/hooks', - issue_events_url: 'https://api.github.com/repos/CrowdHQ/core/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/core/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/core/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/core/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/core/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/core/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/core/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/core/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/core/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/core/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/core/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/core/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/core/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/core/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/core/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/core/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdHQ/core/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/core/comments{/number}', - issue_comment_url: 'https://api.github.com/repos/CrowdHQ/core/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/core/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdHQ/core/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/core/merges', - archive_url: 'https://api.github.com/repos/CrowdHQ/core/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/core/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/core/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/core/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdHQ/core/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/core/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/core/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/core/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/core/deployments', - created_at: '2022-02-22T18:47:48Z', - updated_at: '2022-03-20T16:13:05Z', - pushed_at: '2022-03-20T16:12:45Z', - git_url: 'git://github.com/CrowdHQ/core.git', - ssh_url: 'git@github.com:CrowdHQ/core.git', - clone_url: 'https://github.com/CrowdHQ/core.git', - svn_url: 'https://github.com/CrowdHQ/core', - homepage: null, - size: 37251, - stargazers_count: 0, - watchers_count: 0, - language: 'Python', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 2, - license: null, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 2, - watchers: 0, - default_branch: 'main', - }, - organization: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdHQ', - repos_url: 'https://api.github.com/orgs/CrowdHQ/repos', - events_url: 'https://api.github.com/orgs/CrowdHQ/events', - hooks_url: 'https://api.github.com/orgs/CrowdHQ/hooks', - issues_url: 'https://api.github.com/orgs/CrowdHQ/issues', - members_url: 'https://api.github.com/orgs/CrowdHQ/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdHQ/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjM1ODU4MTY=', - }, - }, - } - - static fork = { - event: 'fork', - created: { - forkee: { - id: 472040578, - node_id: 'R_kgDOHCLEgg', - name: 'awesome-community-building', - full_name: 'PipeTestOrg/awesome-community-building', - private: false, - owner: { - login: 'PipeTestOrg', - id: 99810325, - node_id: 'O_kgDOBfL8FQ', - avatar_url: 'https://avatars.githubusercontent.com/u/99810325?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/PipeTestOrg', - html_url: 'https://github.com/PipeTestOrg', - followers_url: 'https://api.github.com/users/PipeTestOrg/followers', - following_url: 'https://api.github.com/users/PipeTestOrg/following{/other_user}', - gists_url: 'https://api.github.com/users/PipeTestOrg/gists{/gist_id}', - starred_url: 'https://api.github.com/users/PipeTestOrg/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/PipeTestOrg/subscriptions', - organizations_url: 'https://api.github.com/users/PipeTestOrg/orgs', - repos_url: 'https://api.github.com/users/PipeTestOrg/repos', - events_url: 'https://api.github.com/users/PipeTestOrg/events{/privacy}', - received_events_url: 'https://api.github.com/users/PipeTestOrg/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/PipeTestOrg/awesome-community-building', - description: 'A curated list of awesome resources on building developer communities. 🥑', - fork: true, - url: 'https://api.github.com/repos/PipeTestOrg/awesome-community-building', - forks_url: 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/forks', - keys_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/teams', - hooks_url: 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/hooks', - issue_events_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/issues/events{/number}', - events_url: 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/events', - assignees_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/assignees{/user}', - branches_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/branches{/branch}', - tags_url: 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/tags', - blobs_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/git/blobs{/sha}', - git_tags_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/git/tags{/sha}', - git_refs_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/git/refs{/sha}', - trees_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/git/trees{/sha}', - statuses_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/statuses/{sha}', - languages_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/languages', - stargazers_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/stargazers', - contributors_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/contributors', - subscribers_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/subscribers', - subscription_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/subscription', - commits_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/git/commits{/sha}', - comments_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/issues/comments{/number}', - contents_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/contents/{+path}', - compare_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/merges', - archive_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/{archive_format}{/ref}', - downloads_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/downloads', - issues_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/issues{/number}', - pulls_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/notifications{?since,all,participating}', - labels_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/labels{/name}', - releases_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/releases{/id}', - deployments_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/deployments', - created_at: '2022-03-20T16:42:47Z', - updated_at: '2022-02-14T11:14:39Z', - pushed_at: '2021-11-22T10:48:14Z', - git_url: 'git://github.com/PipeTestOrg/awesome-community-building.git', - ssh_url: 'git@github.com:PipeTestOrg/awesome-community-building.git', - clone_url: 'https://github.com/PipeTestOrg/awesome-community-building.git', - svn_url: 'https://github.com/PipeTestOrg/awesome-community-building', - homepage: null, - size: 4, - stargazers_count: 0, - watchers_count: 0, - language: null, - has_issues: false, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 0, - license: null, - allow_forking: true, - is_template: false, - topics: [], - visibility: 'public', - forks: 0, - open_issues: 0, - watchers: 0, - default_branch: 'main', - public: true, - }, - repository: { - id: 389910135, - node_id: 'MDEwOlJlcG9zaXRvcnkzODk5MTAxMzU=', - name: 'awesome-community-building', - full_name: 'CrowdHQ/awesome-community-building', - private: false, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/awesome-community-building', - description: 'A curated list of awesome resources on building developer communities. 🥑', - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building', - forks_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/events', - assignees_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/assignees{/user}', - branches_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/tags', - blobs_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/git/blobs{/sha}', - git_tags_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/git/tags{/sha}', - git_refs_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/git/refs{/sha}', - trees_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/git/trees{/sha}', - statuses_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/languages', - stargazers_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/stargazers', - contributors_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/contributors', - subscribers_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/subscribers', - subscription_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/subscription', - commits_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/git/commits{/sha}', - comments_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/issues/comments{/number}', - contents_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/merges', - archive_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/downloads', - issues_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/labels{/name}', - releases_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/releases{/id}', - deployments_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/deployments', - created_at: '2021-07-27T08:43:58Z', - updated_at: '2022-02-14T11:14:39Z', - pushed_at: '2021-11-22T10:48:14Z', - git_url: 'git://github.com/CrowdHQ/awesome-community-building.git', - ssh_url: 'git@github.com:CrowdHQ/awesome-community-building.git', - clone_url: 'https://github.com/CrowdHQ/awesome-community-building.git', - svn_url: 'https://github.com/CrowdHQ/awesome-community-building', - homepage: null, - size: 4, - stargazers_count: 6, - watchers_count: 6, - language: null, - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 3, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 0, - license: null, - allow_forking: true, - is_template: false, - topics: [], - visibility: 'public', - forks: 3, - open_issues: 0, - watchers: 6, - default_branch: 'main', - }, - organization: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdHQ', - repos_url: 'https://api.github.com/orgs/CrowdHQ/repos', - events_url: 'https://api.github.com/orgs/CrowdHQ/events', - hooks_url: 'https://api.github.com/orgs/CrowdHQ/hooks', - issues_url: 'https://api.github.com/orgs/CrowdHQ/issues', - members_url: 'https://api.github.com/orgs/CrowdHQ/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdHQ/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'joanreyero', - id: 99810325, - node_id: 'O_kgDOBfL8FQ', - avatar_url: 'https://avatars.githubusercontent.com/u/99810325?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/PipeTestOrg', - html_url: 'https://github.com/PipeTestOrg', - followers_url: 'https://api.github.com/users/PipeTestOrg/followers', - following_url: 'https://api.github.com/users/PipeTestOrg/following{/other_user}', - gists_url: 'https://api.github.com/users/PipeTestOrg/gists{/gist_id}', - starred_url: 'https://api.github.com/users/PipeTestOrg/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/PipeTestOrg/subscriptions', - organizations_url: 'https://api.github.com/users/PipeTestOrg/orgs', - repos_url: 'https://api.github.com/users/PipeTestOrg/repos', - events_url: 'https://api.github.com/users/PipeTestOrg/events{/privacy}', - received_events_url: 'https://api.github.com/users/PipeTestOrg/received_events', - type: 'Organization', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjMyODkwMjI=', - }, - }, - } - - static comment = { - event: 'issue_comment', - issue: { - created: { - action: 'created', - issue: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267', - repository_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - labels_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/labels{/name}', - comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/comments', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/events', - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/issues/267', - id: 1341717370, - node_id: 'I_kwDOGsy6M85P-Pt6', - number: 267, - title: 'test issue no3', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - labels: [], - state: 'open', - locked: false, - assignee: null, - assignees: [], - milestone: null, - comments: 2, - created_at: '2022-08-17T12:50:27Z', - updated_at: '2022-08-21T14:22:44Z', - closed_at: null, - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: null, - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/timeline', - performed_via_github_app: null, - state_reason: 'reopened', - }, - comment: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/1221555775', - html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/issues/267#issuecomment-1221555775', - issue_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267', - id: 1221555775, - node_id: 'IC_kwDOGsy6M85Iz3Y_', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - created_at: '2022-08-21T14:22:44Z', - updated_at: '2022-08-21T14:22:44Z', - author_association: 'CONTRIBUTOR', - body: 'A test comment', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/1221555775/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - performed_via_github_app: null, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-21T13:49:08Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 25872, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 6, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 6, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - edited: { - action: 'edited', - changes: { - body: { - from: 'A test comment', - }, - }, - issue: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267', - repository_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - labels_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/labels{/name}', - comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/comments', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/events', - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/issues/267', - id: 1341717370, - node_id: 'I_kwDOGsy6M85P-Pt6', - number: 267, - title: 'test issue no3', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - labels: [], - state: 'open', - locked: false, - assignee: null, - assignees: [], - milestone: null, - comments: 2, - created_at: '2022-08-17T12:50:27Z', - updated_at: '2022-08-21T14:24:48Z', - closed_at: null, - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: null, - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/timeline', - performed_via_github_app: null, - state_reason: 'reopened', - }, - comment: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/1221555775', - html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/issues/267#issuecomment-1221555775', - issue_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267', - id: 1221555775, - node_id: 'IC_kwDOGsy6M85Iz3Y_', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - created_at: '2022-08-21T14:22:44Z', - updated_at: '2022-08-21T14:24:48Z', - author_association: 'CONTRIBUTOR', - body: 'A test comment (EDITED)', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/1221555775/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - performed_via_github_app: null, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-21T13:49:08Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 25872, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 6, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 6, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - }, - pullRequest: { - created: { - action: 'created', - issue: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266', - repository_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - labels_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/labels{/name}', - comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/comments', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/events', - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266', - id: 1341697727, - node_id: 'PR_kwDOGsy6M849UfZb', - number: 266, - title: 'Feature/nodejs GitHub integration', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - labels: [ - { - id: 4374821209, - node_id: 'LA_kwDOGsy6M88AAAABBMJ5WQ', - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels/type:enhancement%20%E2%9C%A8', - name: 'type:enhancement ✨', - color: 'B57798', - default: false, - description: '', - }, - { - id: 4374821465, - node_id: 'LA_kwDOGsy6M88AAAABBMJ6WQ', - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels/type:feature%20%F0%9F%9A%80', - name: 'type:feature 🚀', - color: 'A3DF2C', - default: false, - description: '', - }, - ], - state: 'open', - locked: false, - assignee: null, - assignees: [], - milestone: null, - comments: 3, - created_at: '2022-08-17T12:35:00Z', - updated_at: '2022-08-21T14:34:09Z', - closed_at: null, - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - draft: false, - pull_request: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls/266', - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266', - diff_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266.diff', - patch_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266.patch', - merged_at: null, - }, - body: "# Changes proposed ✍️\r\n- github integration moved from python to nodejs\r\n- new github endpoint: discussions for webhooks and iterator\r\n- issue comments, pr comments and discussion comments now processed as separate endpoints\r\n \r\n## Checklist ✅\r\n- [x] Label appropriately with `type:feature 🚀`, `type:enhancement ✨`, `type:bug 🐞`, or `type:documentation 📜`.\r\n- [ ] Tests are passing. \r\n- [ ] New backend functionality has been unit-tested.\r\n- [ ] Environment variables have been updated\r\n - [ ] Front-end: `frontend/.env.dist`\r\n - [ ] Backend: `backend/.env.dist`, `backend/.env.dist.staging`, `backend/.env.dist.staging`.\r\n - [ ] [Configuration docs](https://docs.crowd.dev/docs/configuration) have been updated.\r\n - [ ] Team members only: update environment variables in Password manager and update the team\r\n- [ ] API documentation has been updated (if necessary) (see [docs on API documentation](https://docs.crowd.dev/docs/updating-api-documentation)). \r\n- [ ] [Quality standards](https://github.com/CrowdDotDev/crowd-github-test-public/blob/main/CONTRIBUTING.md#quality-standards) are met. \r\n- [ ] All changes have been tested in a staging site. \r\n- [ ] All changes are working locally running crowd.dev's Docker local environment.", - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/timeline', - performed_via_github_app: null, - state_reason: null, - }, - comment: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/1221557738', - html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/pull/266#issuecomment-1221557738', - issue_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266', - id: 1221557738, - node_id: 'IC_kwDOGsy6M85Iz33q', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - created_at: '2022-08-21T14:34:09Z', - updated_at: '2022-08-21T14:34:09Z', - author_association: 'CONTRIBUTOR', - body: 'a test pr comment', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/1221557738/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - performed_via_github_app: null, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-21T13:49:08Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 25872, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 6, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 6, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - edited: { - action: 'edited', - changes: { - body: { - from: 'a test pr comment', - }, - }, - issue: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266', - repository_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - labels_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/labels{/name}', - comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/comments', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/events', - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266', - id: 1341697727, - node_id: 'PR_kwDOGsy6M849UfZb', - number: 266, - title: 'Feature/nodejs GitHub integration', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - labels: [ - { - id: 4374821209, - node_id: 'LA_kwDOGsy6M88AAAABBMJ5WQ', - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels/type:enhancement%20%E2%9C%A8', - name: 'type:enhancement ✨', - color: 'B57798', - default: false, - description: '', - }, - { - id: 4374821465, - node_id: 'LA_kwDOGsy6M88AAAABBMJ6WQ', - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels/type:feature%20%F0%9F%9A%80', - name: 'type:feature 🚀', - color: 'A3DF2C', - default: false, - description: '', - }, - ], - state: 'open', - locked: false, - assignee: null, - assignees: [], - milestone: null, - comments: 3, - created_at: '2022-08-17T12:35:00Z', - updated_at: '2022-08-21T14:35:08Z', - closed_at: null, - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - draft: false, - pull_request: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls/266', - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266', - diff_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266.diff', - patch_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266.patch', - merged_at: null, - }, - body: "# Changes proposed ✍️\r\n- github integration moved from python to nodejs\r\n- new github endpoint: discussions for webhooks and iterator\r\n- issue comments, pr comments and discussion comments now processed as separate endpoints\r\n \r\n## Checklist ✅\r\n- [x] Label appropriately with `type:feature 🚀`, `type:enhancement ✨`, `type:bug 🐞`, or `type:documentation 📜`.\r\n- [ ] Tests are passing. \r\n- [ ] New backend functionality has been unit-tested.\r\n- [ ] Environment variables have been updated\r\n - [ ] Front-end: `frontend/.env.dist`\r\n - [ ] Backend: `backend/.env.dist`, `backend/.env.dist.staging`, `backend/.env.dist.staging`.\r\n - [ ] [Configuration docs](https://docs.crowd.dev/docs/configuration) have been updated.\r\n - [ ] Team members only: update environment variables in Password manager and update the team\r\n- [ ] API documentation has been updated (if necessary) (see [docs on API documentation](https://docs.crowd.dev/docs/updating-api-documentation)). \r\n- [ ] [Quality standards](https://github.com/CrowdDotDev/crowd-github-test-public/blob/main/CONTRIBUTING.md#quality-standards) are met. \r\n- [ ] All changes have been tested in a staging site. \r\n- [ ] All changes are working locally running crowd.dev's Docker local environment.", - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/timeline', - performed_via_github_app: null, - state_reason: null, - }, - comment: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/1221557738', - html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/pull/266#issuecomment-1221557738', - issue_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266', - id: 1221557738, - node_id: 'IC_kwDOGsy6M85Iz33q', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - created_at: '2022-08-21T14:34:09Z', - updated_at: '2022-08-21T14:35:08Z', - author_association: 'CONTRIBUTOR', - body: 'a test pr comment (EDITED)', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/1221557738/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - performed_via_github_app: null, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-21T13:49:08Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 25872, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 6, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 6, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - }, - } - - static failed = [ - { - event: 'pull_request', - payload: { - action: 'closed', - number: 18, - pull_request: { - url: 'https://api.github.com/repos/verida/vault-common/pulls/18', - id: 879216015, - node_id: 'PR_kwDOEPAOdM40Z8WP', - html_url: 'https://github.com/verida/vault-common/pull/18', - diff_url: 'https://github.com/verida/vault-common/pull/18.diff', - patch_url: 'https://github.com/verida/vault-common/pull/18.patch', - issue_url: 'https://api.github.com/repos/verida/vault-common/issues/18', - number: 18, - state: 'closed', - locked: false, - title: '#315: Better error handling', - user: { - login: 'pkhien95', - id: 12524553, - node_id: 'MDQ6VXNlcjEyNTI0NTUz', - avatar_url: 'https://avatars.githubusercontent.com/u/12524553?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/pkhien95', - html_url: 'https://github.com/pkhien95', - followers_url: 'https://api.github.com/users/pkhien95/followers', - following_url: 'https://api.github.com/users/pkhien95/following{/other_user}', - gists_url: 'https://api.github.com/users/pkhien95/gists{/gist_id}', - starred_url: 'https://api.github.com/users/pkhien95/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/pkhien95/subscriptions', - organizations_url: 'https://api.github.com/users/pkhien95/orgs', - repos_url: 'https://api.github.com/users/pkhien95/repos', - events_url: 'https://api.github.com/users/pkhien95/events{/privacy}', - received_events_url: 'https://api.github.com/users/pkhien95/received_events', - type: 'User', - site_admin: false, - }, - body: null, - created_at: '2022-03-14T15:39:38Z', - updated_at: '2022-03-20T15:16:47Z', - closed_at: '2022-03-20T15:16:47Z', - merged_at: '2022-03-20T15:16:47Z', - merge_commit_sha: 'b92db9efdf320ef414cc7437ab9bda2436093b2d', - assignee: { - login: 'pkhien95', - id: 12524553, - node_id: 'MDQ6VXNlcjEyNTI0NTUz', - avatar_url: 'https://avatars.githubusercontent.com/u/12524553?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/pkhien95', - html_url: 'https://github.com/pkhien95', - followers_url: 'https://api.github.com/users/pkhien95/followers', - following_url: 'https://api.github.com/users/pkhien95/following{/other_user}', - gists_url: 'https://api.github.com/users/pkhien95/gists{/gist_id}', - starred_url: 'https://api.github.com/users/pkhien95/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/pkhien95/subscriptions', - organizations_url: 'https://api.github.com/users/pkhien95/orgs', - repos_url: 'https://api.github.com/users/pkhien95/repos', - events_url: 'https://api.github.com/users/pkhien95/events{/privacy}', - received_events_url: 'https://api.github.com/users/pkhien95/received_events', - type: 'User', - site_admin: false, - }, - assignees: [ - { - login: 'pkhien95', - id: 12524553, - node_id: 'MDQ6VXNlcjEyNTI0NTUz', - avatar_url: 'https://avatars.githubusercontent.com/u/12524553?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/pkhien95', - html_url: 'https://github.com/pkhien95', - followers_url: 'https://api.github.com/users/pkhien95/followers', - following_url: 'https://api.github.com/users/pkhien95/following{/other_user}', - gists_url: 'https://api.github.com/users/pkhien95/gists{/gist_id}', - starred_url: 'https://api.github.com/users/pkhien95/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/pkhien95/subscriptions', - organizations_url: 'https://api.github.com/users/pkhien95/orgs', - repos_url: 'https://api.github.com/users/pkhien95/repos', - events_url: 'https://api.github.com/users/pkhien95/events{/privacy}', - received_events_url: 'https://api.github.com/users/pkhien95/received_events', - type: 'User', - site_admin: false, - }, - ], - requested_reviewers: [ - { - login: 'saadibrahim', - id: 4365774, - node_id: 'MDQ6VXNlcjQzNjU3NzQ=', - avatar_url: 'https://avatars.githubusercontent.com/u/4365774?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/saadibrahim', - html_url: 'https://github.com/saadibrahim', - followers_url: 'https://api.github.com/users/saadibrahim/followers', - following_url: 'https://api.github.com/users/saadibrahim/following{/other_user}', - gists_url: 'https://api.github.com/users/saadibrahim/gists{/gist_id}', - starred_url: 'https://api.github.com/users/saadibrahim/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/saadibrahim/subscriptions', - organizations_url: 'https://api.github.com/users/saadibrahim/orgs', - repos_url: 'https://api.github.com/users/saadibrahim/repos', - events_url: 'https://api.github.com/users/saadibrahim/events{/privacy}', - received_events_url: 'https://api.github.com/users/saadibrahim/received_events', - type: 'User', - site_admin: false, - }, - { - login: 'ram-verida', - id: 97266181, - node_id: 'U_kgDOBcwqBQ', - avatar_url: 'https://avatars.githubusercontent.com/u/97266181?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/ram-verida', - html_url: 'https://github.com/ram-verida', - followers_url: 'https://api.github.com/users/ram-verida/followers', - following_url: 'https://api.github.com/users/ram-verida/following{/other_user}', - gists_url: 'https://api.github.com/users/ram-verida/gists{/gist_id}', - starred_url: 'https://api.github.com/users/ram-verida/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/ram-verida/subscriptions', - organizations_url: 'https://api.github.com/users/ram-verida/orgs', - repos_url: 'https://api.github.com/users/ram-verida/repos', - events_url: 'https://api.github.com/users/ram-verida/events{/privacy}', - received_events_url: 'https://api.github.com/users/ram-verida/received_events', - type: 'User', - site_admin: false, - }, - ], - requested_teams: [], - labels: [ - { - id: 2245374491, - node_id: 'MDU6TGFiZWwyMjQ1Mzc0NDkx', - url: 'https://api.github.com/repos/verida/vault-common/labels/enhancement', - name: 'enhancement', - color: 'a2eeef', - default: true, - description: 'New feature or request', - }, - ], - milestone: null, - draft: false, - commits_url: 'https://api.github.com/repos/verida/vault-common/pulls/18/commits', - review_comments_url: 'https://api.github.com/repos/verida/vault-common/pulls/18/comments', - review_comment_url: - 'https://api.github.com/repos/verida/vault-common/pulls/comments{/number}', - comments_url: 'https://api.github.com/repos/verida/vault-common/issues/18/comments', - statuses_url: - 'https://api.github.com/repos/verida/vault-common/statuses/e2605b438d4e3910613e914b07937e304cdce3ce', - head: { - label: 'verida:feature/315_better_error_handling', - ref: 'feature/315_better_error_handling', - sha: 'e2605b438d4e3910613e914b07937e304cdce3ce', - user: { - login: 'verida', - id: 59584064, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjU5NTg0MDY0', - avatar_url: 'https://avatars.githubusercontent.com/u/59584064?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/verida', - html_url: 'https://github.com/verida', - followers_url: 'https://api.github.com/users/verida/followers', - following_url: 'https://api.github.com/users/verida/following{/other_user}', - gists_url: 'https://api.github.com/users/verida/gists{/gist_id}', - starred_url: 'https://api.github.com/users/verida/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/verida/subscriptions', - organizations_url: 'https://api.github.com/users/verida/orgs', - repos_url: 'https://api.github.com/users/verida/repos', - events_url: 'https://api.github.com/users/verida/events{/privacy}', - received_events_url: 'https://api.github.com/users/verida/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 284167796, - node_id: 'MDEwOlJlcG9zaXRvcnkyODQxNjc3OTY=', - name: 'vault-common', - full_name: 'verida/vault-common', - private: true, - owner: { - login: 'verida', - id: 59584064, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjU5NTg0MDY0', - avatar_url: 'https://avatars.githubusercontent.com/u/59584064?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/verida', - html_url: 'https://github.com/verida', - followers_url: 'https://api.github.com/users/verida/followers', - following_url: 'https://api.github.com/users/verida/following{/other_user}', - gists_url: 'https://api.github.com/users/verida/gists{/gist_id}', - starred_url: 'https://api.github.com/users/verida/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/verida/subscriptions', - organizations_url: 'https://api.github.com/users/verida/orgs', - repos_url: 'https://api.github.com/users/verida/repos', - events_url: 'https://api.github.com/users/verida/events{/privacy}', - received_events_url: 'https://api.github.com/users/verida/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/verida/vault-common', - description: null, - fork: false, - url: 'https://api.github.com/repos/verida/vault-common', - forks_url: 'https://api.github.com/repos/verida/vault-common/forks', - keys_url: 'https://api.github.com/repos/verida/vault-common/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/verida/vault-common/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/verida/vault-common/teams', - hooks_url: 'https://api.github.com/repos/verida/vault-common/hooks', - issue_events_url: - 'https://api.github.com/repos/verida/vault-common/issues/events{/number}', - events_url: 'https://api.github.com/repos/verida/vault-common/events', - assignees_url: 'https://api.github.com/repos/verida/vault-common/assignees{/user}', - branches_url: 'https://api.github.com/repos/verida/vault-common/branches{/branch}', - tags_url: 'https://api.github.com/repos/verida/vault-common/tags', - blobs_url: 'https://api.github.com/repos/verida/vault-common/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/verida/vault-common/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/verida/vault-common/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/verida/vault-common/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/verida/vault-common/statuses/{sha}', - languages_url: 'https://api.github.com/repos/verida/vault-common/languages', - stargazers_url: 'https://api.github.com/repos/verida/vault-common/stargazers', - contributors_url: 'https://api.github.com/repos/verida/vault-common/contributors', - subscribers_url: 'https://api.github.com/repos/verida/vault-common/subscribers', - subscription_url: 'https://api.github.com/repos/verida/vault-common/subscription', - commits_url: 'https://api.github.com/repos/verida/vault-common/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/verida/vault-common/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/verida/vault-common/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/verida/vault-common/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/verida/vault-common/contents/{+path}', - compare_url: - 'https://api.github.com/repos/verida/vault-common/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/verida/vault-common/merges', - archive_url: - 'https://api.github.com/repos/verida/vault-common/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/verida/vault-common/downloads', - issues_url: 'https://api.github.com/repos/verida/vault-common/issues{/number}', - pulls_url: 'https://api.github.com/repos/verida/vault-common/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/verida/vault-common/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/verida/vault-common/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/verida/vault-common/labels{/name}', - releases_url: 'https://api.github.com/repos/verida/vault-common/releases{/id}', - deployments_url: 'https://api.github.com/repos/verida/vault-common/deployments', - created_at: '2020-08-01T02:07:23Z', - updated_at: '2022-02-20T22:38:56Z', - pushed_at: '2022-03-20T15:16:47Z', - git_url: 'git://github.com/verida/vault-common.git', - ssh_url: 'git@github.com:verida/vault-common.git', - clone_url: 'https://github.com/verida/vault-common.git', - svn_url: 'https://github.com/verida/vault-common', - homepage: null, - size: 994, - stargazers_count: 0, - watchers_count: 0, - language: 'TypeScript', - has_issues: true, - has_projects: false, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 1, - license: null, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 1, - watchers: 0, - default_branch: 'develop', - allow_squash_merge: true, - allow_merge_commit: true, - allow_rebase_merge: true, - allow_auto_merge: false, - delete_branch_on_merge: false, - allow_update_branch: false, - }, - }, - base: { - label: 'verida:develop', - ref: 'develop', - sha: '25fc567eb360c30e0496aa62abaa305721a70776', - user: { - login: 'verida', - id: 59584064, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjU5NTg0MDY0', - avatar_url: 'https://avatars.githubusercontent.com/u/59584064?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/verida', - html_url: 'https://github.com/verida', - followers_url: 'https://api.github.com/users/verida/followers', - following_url: 'https://api.github.com/users/verida/following{/other_user}', - gists_url: 'https://api.github.com/users/verida/gists{/gist_id}', - starred_url: 'https://api.github.com/users/verida/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/verida/subscriptions', - organizations_url: 'https://api.github.com/users/verida/orgs', - repos_url: 'https://api.github.com/users/verida/repos', - events_url: 'https://api.github.com/users/verida/events{/privacy}', - received_events_url: 'https://api.github.com/users/verida/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 284167796, - node_id: 'MDEwOlJlcG9zaXRvcnkyODQxNjc3OTY=', - name: 'vault-common', - full_name: 'verida/vault-common', - private: true, - owner: { - login: 'verida', - id: 59584064, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjU5NTg0MDY0', - avatar_url: 'https://avatars.githubusercontent.com/u/59584064?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/verida', - html_url: 'https://github.com/verida', - followers_url: 'https://api.github.com/users/verida/followers', - following_url: 'https://api.github.com/users/verida/following{/other_user}', - gists_url: 'https://api.github.com/users/verida/gists{/gist_id}', - starred_url: 'https://api.github.com/users/verida/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/verida/subscriptions', - organizations_url: 'https://api.github.com/users/verida/orgs', - repos_url: 'https://api.github.com/users/verida/repos', - events_url: 'https://api.github.com/users/verida/events{/privacy}', - received_events_url: 'https://api.github.com/users/verida/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/verida/vault-common', - description: null, - fork: false, - url: 'https://api.github.com/repos/verida/vault-common', - forks_url: 'https://api.github.com/repos/verida/vault-common/forks', - keys_url: 'https://api.github.com/repos/verida/vault-common/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/verida/vault-common/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/verida/vault-common/teams', - hooks_url: 'https://api.github.com/repos/verida/vault-common/hooks', - issue_events_url: - 'https://api.github.com/repos/verida/vault-common/issues/events{/number}', - events_url: 'https://api.github.com/repos/verida/vault-common/events', - assignees_url: 'https://api.github.com/repos/verida/vault-common/assignees{/user}', - branches_url: 'https://api.github.com/repos/verida/vault-common/branches{/branch}', - tags_url: 'https://api.github.com/repos/verida/vault-common/tags', - blobs_url: 'https://api.github.com/repos/verida/vault-common/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/verida/vault-common/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/verida/vault-common/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/verida/vault-common/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/verida/vault-common/statuses/{sha}', - languages_url: 'https://api.github.com/repos/verida/vault-common/languages', - stargazers_url: 'https://api.github.com/repos/verida/vault-common/stargazers', - contributors_url: 'https://api.github.com/repos/verida/vault-common/contributors', - subscribers_url: 'https://api.github.com/repos/verida/vault-common/subscribers', - subscription_url: 'https://api.github.com/repos/verida/vault-common/subscription', - commits_url: 'https://api.github.com/repos/verida/vault-common/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/verida/vault-common/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/verida/vault-common/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/verida/vault-common/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/verida/vault-common/contents/{+path}', - compare_url: - 'https://api.github.com/repos/verida/vault-common/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/verida/vault-common/merges', - archive_url: - 'https://api.github.com/repos/verida/vault-common/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/verida/vault-common/downloads', - issues_url: 'https://api.github.com/repos/verida/vault-common/issues{/number}', - pulls_url: 'https://api.github.com/repos/verida/vault-common/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/verida/vault-common/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/verida/vault-common/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/verida/vault-common/labels{/name}', - releases_url: 'https://api.github.com/repos/verida/vault-common/releases{/id}', - deployments_url: 'https://api.github.com/repos/verida/vault-common/deployments', - created_at: '2020-08-01T02:07:23Z', - updated_at: '2022-02-20T22:38:56Z', - pushed_at: '2022-03-20T15:16:47Z', - git_url: 'git://github.com/verida/vault-common.git', - ssh_url: 'git@github.com:verida/vault-common.git', - clone_url: 'https://github.com/verida/vault-common.git', - svn_url: 'https://github.com/verida/vault-common', - homepage: null, - size: 994, - stargazers_count: 0, - watchers_count: 0, - language: 'TypeScript', - has_issues: true, - has_projects: false, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 1, - license: null, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 1, - watchers: 0, - default_branch: 'develop', - allow_squash_merge: true, - allow_merge_commit: true, - allow_rebase_merge: true, - allow_auto_merge: false, - delete_branch_on_merge: false, - allow_update_branch: false, - }, - }, - _links: { - self: { - href: 'https://api.github.com/repos/verida/vault-common/pulls/18', - }, - html: { - href: 'https://github.com/verida/vault-common/pull/18', - }, - issue: { - href: 'https://api.github.com/repos/verida/vault-common/issues/18', - }, - comments: { - href: 'https://api.github.com/repos/verida/vault-common/issues/18/comments', - }, - review_comments: { - href: 'https://api.github.com/repos/verida/vault-common/pulls/18/comments', - }, - review_comment: { - href: 'https://api.github.com/repos/verida/vault-common/pulls/comments{/number}', - }, - commits: { - href: 'https://api.github.com/repos/verida/vault-common/pulls/18/commits', - }, - statuses: { - href: 'https://api.github.com/repos/verida/vault-common/statuses/e2605b438d4e3910613e914b07937e304cdce3ce', - }, - }, - author_association: 'COLLABORATOR', - auto_merge: null, - active_lock_reason: null, - merged: true, - mergeable: null, - rebaseable: null, - mergeable_state: 'unknown', - merged_by: { - login: 'pkhien95', - id: 12524553, - node_id: 'MDQ6VXNlcjEyNTI0NTUz', - avatar_url: 'https://avatars.githubusercontent.com/u/12524553?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/pkhien95', - html_url: 'https://github.com/pkhien95', - followers_url: 'https://api.github.com/users/pkhien95/followers', - following_url: 'https://api.github.com/users/pkhien95/following{/other_user}', - gists_url: 'https://api.github.com/users/pkhien95/gists{/gist_id}', - starred_url: 'https://api.github.com/users/pkhien95/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/pkhien95/subscriptions', - organizations_url: 'https://api.github.com/users/pkhien95/orgs', - repos_url: 'https://api.github.com/users/pkhien95/repos', - events_url: 'https://api.github.com/users/pkhien95/events{/privacy}', - received_events_url: 'https://api.github.com/users/pkhien95/received_events', - type: 'User', - site_admin: false, - }, - comments: 0, - review_comments: 2, - maintainer_can_modify: false, - commits: 3, - additions: 512, - deletions: 1141, - changed_files: 4, - }, - repository: { - id: 284167796, - node_id: 'MDEwOlJlcG9zaXRvcnkyODQxNjc3OTY=', - name: 'vault-common', - full_name: 'verida/vault-common', - private: true, - owner: { - login: 'verida', - id: 59584064, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjU5NTg0MDY0', - avatar_url: 'https://avatars.githubusercontent.com/u/59584064?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/verida', - html_url: 'https://github.com/verida', - followers_url: 'https://api.github.com/users/verida/followers', - following_url: 'https://api.github.com/users/verida/following{/other_user}', - gists_url: 'https://api.github.com/users/verida/gists{/gist_id}', - starred_url: 'https://api.github.com/users/verida/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/verida/subscriptions', - organizations_url: 'https://api.github.com/users/verida/orgs', - repos_url: 'https://api.github.com/users/verida/repos', - events_url: 'https://api.github.com/users/verida/events{/privacy}', - received_events_url: 'https://api.github.com/users/verida/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/verida/vault-common', - description: null, - fork: false, - url: 'https://api.github.com/repos/verida/vault-common', - forks_url: 'https://api.github.com/repos/verida/vault-common/forks', - keys_url: 'https://api.github.com/repos/verida/vault-common/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/verida/vault-common/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/verida/vault-common/teams', - hooks_url: 'https://api.github.com/repos/verida/vault-common/hooks', - issue_events_url: - 'https://api.github.com/repos/verida/vault-common/issues/events{/number}', - events_url: 'https://api.github.com/repos/verida/vault-common/events', - assignees_url: 'https://api.github.com/repos/verida/vault-common/assignees{/user}', - branches_url: 'https://api.github.com/repos/verida/vault-common/branches{/branch}', - tags_url: 'https://api.github.com/repos/verida/vault-common/tags', - blobs_url: 'https://api.github.com/repos/verida/vault-common/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/verida/vault-common/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/verida/vault-common/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/verida/vault-common/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/verida/vault-common/statuses/{sha}', - languages_url: 'https://api.github.com/repos/verida/vault-common/languages', - stargazers_url: 'https://api.github.com/repos/verida/vault-common/stargazers', - contributors_url: 'https://api.github.com/repos/verida/vault-common/contributors', - subscribers_url: 'https://api.github.com/repos/verida/vault-common/subscribers', - subscription_url: 'https://api.github.com/repos/verida/vault-common/subscription', - commits_url: 'https://api.github.com/repos/verida/vault-common/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/verida/vault-common/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/verida/vault-common/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/verida/vault-common/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/verida/vault-common/contents/{+path}', - compare_url: 'https://api.github.com/repos/verida/vault-common/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/verida/vault-common/merges', - archive_url: 'https://api.github.com/repos/verida/vault-common/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/verida/vault-common/downloads', - issues_url: 'https://api.github.com/repos/verida/vault-common/issues{/number}', - pulls_url: 'https://api.github.com/repos/verida/vault-common/pulls{/number}', - milestones_url: 'https://api.github.com/repos/verida/vault-common/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/verida/vault-common/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/verida/vault-common/labels{/name}', - releases_url: 'https://api.github.com/repos/verida/vault-common/releases{/id}', - deployments_url: 'https://api.github.com/repos/verida/vault-common/deployments', - created_at: '2020-08-01T02:07:23Z', - updated_at: '2022-02-20T22:38:56Z', - pushed_at: '2022-03-20T15:16:47Z', - git_url: 'git://github.com/verida/vault-common.git', - ssh_url: 'git@github.com:verida/vault-common.git', - clone_url: 'https://github.com/verida/vault-common.git', - svn_url: 'https://github.com/verida/vault-common', - homepage: null, - size: 994, - stargazers_count: 0, - watchers_count: 0, - language: 'TypeScript', - has_issues: true, - has_projects: false, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 1, - license: null, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 1, - watchers: 0, - default_branch: 'develop', - }, - organization: { - login: 'verida', - id: 59584064, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjU5NTg0MDY0', - url: 'https://api.github.com/orgs/verida', - repos_url: 'https://api.github.com/orgs/verida/repos', - events_url: 'https://api.github.com/orgs/verida/events', - hooks_url: 'https://api.github.com/orgs/verida/hooks', - issues_url: 'https://api.github.com/orgs/verida/issues', - members_url: 'https://api.github.com/orgs/verida/members{/member}', - public_members_url: 'https://api.github.com/orgs/verida/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/59584064?v=4', - description: '', - }, - sender: { - login: 'pkhien95', - id: 12524553, - node_id: 'MDQ6VXNlcjEyNTI0NTUz', - avatar_url: 'https://avatars.githubusercontent.com/u/12524553?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/pkhien95', - html_url: 'https://github.com/pkhien95', - followers_url: 'https://api.github.com/users/pkhien95/followers', - following_url: 'https://api.github.com/users/pkhien95/following{/other_user}', - gists_url: 'https://api.github.com/users/pkhien95/gists{/gist_id}', - starred_url: 'https://api.github.com/users/pkhien95/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/pkhien95/subscriptions', - organizations_url: 'https://api.github.com/users/pkhien95/orgs', - repos_url: 'https://api.github.com/users/pkhien95/repos', - events_url: 'https://api.github.com/users/pkhien95/events{/privacy}', - received_events_url: 'https://api.github.com/users/pkhien95/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjMzMzkwNjQ=', - }, - }, - }, - { - event: 'issue_comment', - payload: { - action: 'created', - issue: { - url: 'https://api.github.com/repos/verida/blockchain-research/issues/16', - repository_url: 'https://api.github.com/repos/verida/blockchain-research', - labels_url: - 'https://api.github.com/repos/verida/blockchain-research/issues/16/labels{/name}', - comments_url: - 'https://api.github.com/repos/verida/blockchain-research/issues/16/comments', - events_url: 'https://api.github.com/repos/verida/blockchain-research/issues/16/events', - html_url: 'https://github.com/verida/blockchain-research/issues/16', - id: 1147572838, - node_id: 'I_kwDOGO8S3s5EZpJm', - number: 16, - title: '[research] Biconomy gasless transactions', - user: { - login: 'tahpot', - id: 164973, - node_id: 'MDQ6VXNlcjE2NDk3Mw==', - avatar_url: 'https://avatars.githubusercontent.com/u/164973?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/tahpot', - html_url: 'https://github.com/tahpot', - followers_url: 'https://api.github.com/users/tahpot/followers', - following_url: 'https://api.github.com/users/tahpot/following{/other_user}', - gists_url: 'https://api.github.com/users/tahpot/gists{/gist_id}', - starred_url: 'https://api.github.com/users/tahpot/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/tahpot/subscriptions', - organizations_url: 'https://api.github.com/users/tahpot/orgs', - repos_url: 'https://api.github.com/users/tahpot/repos', - events_url: 'https://api.github.com/users/tahpot/events{/privacy}', - received_events_url: 'https://api.github.com/users/tahpot/received_events', - type: 'User', - site_admin: false, - }, - labels: [ - { - id: 3461134068, - node_id: 'LA_kwDOGO8S3s7OTLb0', - url: 'https://api.github.com/repos/verida/blockchain-research/labels/research', - name: 'research', - color: '97EB12', - default: false, - description: '', - }, - ], - state: 'open', - locked: false, - assignee: { - login: 'ITStar10', - id: 15656252, - node_id: 'MDQ6VXNlcjE1NjU2MjUy', - avatar_url: 'https://avatars.githubusercontent.com/u/15656252?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/ITStar10', - html_url: 'https://github.com/ITStar10', - followers_url: 'https://api.github.com/users/ITStar10/followers', - following_url: 'https://api.github.com/users/ITStar10/following{/other_user}', - gists_url: 'https://api.github.com/users/ITStar10/gists{/gist_id}', - starred_url: 'https://api.github.com/users/ITStar10/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/ITStar10/subscriptions', - organizations_url: 'https://api.github.com/users/ITStar10/orgs', - repos_url: 'https://api.github.com/users/ITStar10/repos', - events_url: 'https://api.github.com/users/ITStar10/events{/privacy}', - received_events_url: 'https://api.github.com/users/ITStar10/received_events', - type: 'User', - site_admin: false, - }, - assignees: [ - { - login: 'ITStar10', - id: 15656252, - node_id: 'MDQ6VXNlcjE1NjU2MjUy', - avatar_url: 'https://avatars.githubusercontent.com/u/15656252?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/ITStar10', - html_url: 'https://github.com/ITStar10', - followers_url: 'https://api.github.com/users/ITStar10/followers', - following_url: 'https://api.github.com/users/ITStar10/following{/other_user}', - gists_url: 'https://api.github.com/users/ITStar10/gists{/gist_id}', - starred_url: 'https://api.github.com/users/ITStar10/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/ITStar10/subscriptions', - organizations_url: 'https://api.github.com/users/ITStar10/orgs', - repos_url: 'https://api.github.com/users/ITStar10/repos', - events_url: 'https://api.github.com/users/ITStar10/events{/privacy}', - received_events_url: 'https://api.github.com/users/ITStar10/received_events', - type: 'User', - site_admin: false, - }, - ], - milestone: null, - comments: 4, - created_at: '2022-02-23T02:50:27Z', - updated_at: '2022-03-20T06:45:13Z', - closed_at: null, - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: "The Verida network will enable end users to access Verida smart contracts by either:\r\n\r\n- Paying no gas (Sponsored gas fees)\r\n- Paying gas with Verida tokens (VDA token gas fees)\r\n\r\nBiconomy supports both of these strategies. However, Verida does not have a tradeable token so it's not feasible to pay gas fees using the VDA token when we launch.\r\n\r\nAs such, we need to implement sponsored gas fees for phase 1 and in the future look at paying gas with Verida tokens.\r\n\r\n## Research required\r\n\r\n1. Biconomy supports [multiple implementation methods](https://docs.biconomy.io/products/enable-gasless-transactions) for gasless transactions where Verida pays for transactions. We need to investigate and list the pros and cons of each.\r\n2. Create a proof of concept that implements gasless transactions on Polygon to pay for the creation of a DID document using the [vda-did-registry](https://github.com/verida/blockchain-vda-did-registry) smart contract.\r\n3. Give some thought on how we can protect ourselves from spammers. ie: Someone creating 1 million DID documents to exhaust our token pool that has been deposited to fund these transactions.\r\n\r\n## Background information\r\n\r\n- [Biconomy Getting Started Documentation](https://docs.biconomy.io/)\r\n- @tahpot has spoken with Biconomy and we have access to their engineering team if we have questions or require support", - reactions: { - url: 'https://api.github.com/repos/verida/blockchain-research/issues/16/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: - 'https://api.github.com/repos/verida/blockchain-research/issues/16/timeline', - performed_via_github_app: null, - }, - comment: { - url: 'https://api.github.com/repos/verida/blockchain-research/issues/comments/1073180195', - html_url: - 'https://github.com/verida/blockchain-research/issues/16#issuecomment-1073180195', - issue_url: 'https://api.github.com/repos/verida/blockchain-research/issues/16', - id: 1073180195, - node_id: 'IC_kwDOGO8S3s4_924j', - user: { - login: 'tahpot', - id: 164973, - node_id: 'MDQ6VXNlcjE2NDk3Mw==', - avatar_url: 'https://avatars.githubusercontent.com/u/164973?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/tahpot', - html_url: 'https://github.com/tahpot', - followers_url: 'https://api.github.com/users/tahpot/followers', - following_url: 'https://api.github.com/users/tahpot/following{/other_user}', - gists_url: 'https://api.github.com/users/tahpot/gists{/gist_id}', - starred_url: 'https://api.github.com/users/tahpot/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/tahpot/subscriptions', - organizations_url: 'https://api.github.com/users/tahpot/orgs', - repos_url: 'https://api.github.com/users/tahpot/repos', - events_url: 'https://api.github.com/users/tahpot/events{/privacy}', - received_events_url: 'https://api.github.com/users/tahpot/received_events', - type: 'User', - site_admin: false, - }, - created_at: '2022-03-20T06:45:13Z', - updated_at: '2022-03-20T06:45:13Z', - author_association: 'CONTRIBUTOR', - body: '@ITStar10 How much work will it be to make a generic server that will pay for any transaction on a whitelist of smart contracts?', - reactions: { - url: 'https://api.github.com/repos/verida/blockchain-research/issues/comments/1073180195/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - performed_via_github_app: null, - }, - repository: { - id: 418321118, - node_id: 'R_kgDOGO8S3g', - name: 'blockchain-research', - full_name: 'verida/blockchain-research', - private: true, - owner: { - login: 'verida', - id: 59584064, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjU5NTg0MDY0', - avatar_url: 'https://avatars.githubusercontent.com/u/59584064?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/verida', - html_url: 'https://github.com/verida', - followers_url: 'https://api.github.com/users/verida/followers', - following_url: 'https://api.github.com/users/verida/following{/other_user}', - gists_url: 'https://api.github.com/users/verida/gists{/gist_id}', - starred_url: 'https://api.github.com/users/verida/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/verida/subscriptions', - organizations_url: 'https://api.github.com/users/verida/orgs', - repos_url: 'https://api.github.com/users/verida/repos', - events_url: 'https://api.github.com/users/verida/events{/privacy}', - received_events_url: 'https://api.github.com/users/verida/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/verida/blockchain-research', - description: null, - fork: false, - url: 'https://api.github.com/repos/verida/blockchain-research', - forks_url: 'https://api.github.com/repos/verida/blockchain-research/forks', - keys_url: 'https://api.github.com/repos/verida/blockchain-research/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/verida/blockchain-research/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/verida/blockchain-research/teams', - hooks_url: 'https://api.github.com/repos/verida/blockchain-research/hooks', - issue_events_url: - 'https://api.github.com/repos/verida/blockchain-research/issues/events{/number}', - events_url: 'https://api.github.com/repos/verida/blockchain-research/events', - assignees_url: 'https://api.github.com/repos/verida/blockchain-research/assignees{/user}', - branches_url: 'https://api.github.com/repos/verida/blockchain-research/branches{/branch}', - tags_url: 'https://api.github.com/repos/verida/blockchain-research/tags', - blobs_url: 'https://api.github.com/repos/verida/blockchain-research/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/verida/blockchain-research/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/verida/blockchain-research/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/verida/blockchain-research/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/verida/blockchain-research/statuses/{sha}', - languages_url: 'https://api.github.com/repos/verida/blockchain-research/languages', - stargazers_url: 'https://api.github.com/repos/verida/blockchain-research/stargazers', - contributors_url: 'https://api.github.com/repos/verida/blockchain-research/contributors', - subscribers_url: 'https://api.github.com/repos/verida/blockchain-research/subscribers', - subscription_url: 'https://api.github.com/repos/verida/blockchain-research/subscription', - commits_url: 'https://api.github.com/repos/verida/blockchain-research/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/verida/blockchain-research/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/verida/blockchain-research/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/verida/blockchain-research/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/verida/blockchain-research/contents/{+path}', - compare_url: - 'https://api.github.com/repos/verida/blockchain-research/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/verida/blockchain-research/merges', - archive_url: - 'https://api.github.com/repos/verida/blockchain-research/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/verida/blockchain-research/downloads', - issues_url: 'https://api.github.com/repos/verida/blockchain-research/issues{/number}', - pulls_url: 'https://api.github.com/repos/verida/blockchain-research/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/verida/blockchain-research/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/verida/blockchain-research/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/verida/blockchain-research/labels{/name}', - releases_url: 'https://api.github.com/repos/verida/blockchain-research/releases{/id}', - deployments_url: 'https://api.github.com/repos/verida/blockchain-research/deployments', - created_at: '2021-10-18T02:48:02Z', - updated_at: '2022-02-18T00:33:59Z', - pushed_at: '2022-03-20T06:32:29Z', - git_url: 'git://github.com/verida/blockchain-research.git', - ssh_url: 'git@github.com:verida/blockchain-research.git', - clone_url: 'https://github.com/verida/blockchain-research.git', - svn_url: 'https://github.com/verida/blockchain-research', - homepage: null, - size: 753, - stargazers_count: 0, - watchers_count: 0, - language: 'TypeScript', - has_issues: true, - has_projects: false, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 13, - license: null, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 13, - watchers: 0, - default_branch: 'main', - }, - organization: { - login: 'verida', - id: 59584064, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjU5NTg0MDY0', - url: 'https://api.github.com/orgs/verida', - repos_url: 'https://api.github.com/orgs/verida/repos', - events_url: 'https://api.github.com/orgs/verida/events', - hooks_url: 'https://api.github.com/orgs/verida/hooks', - issues_url: 'https://api.github.com/orgs/verida/issues', - members_url: 'https://api.github.com/orgs/verida/members{/member}', - public_members_url: 'https://api.github.com/orgs/verida/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/59584064?v=4', - description: '', - }, - sender: { - login: 'tahpot', - id: 164973, - node_id: 'MDQ6VXNlcjE2NDk3Mw==', - avatar_url: 'https://avatars.githubusercontent.com/u/164973?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/tahpot', - html_url: 'https://github.com/tahpot', - followers_url: 'https://api.github.com/users/tahpot/followers', - following_url: 'https://api.github.com/users/tahpot/following{/other_user}', - gists_url: 'https://api.github.com/users/tahpot/gists{/gist_id}', - starred_url: 'https://api.github.com/users/tahpot/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/tahpot/subscriptions', - organizations_url: 'https://api.github.com/users/tahpot/orgs', - repos_url: 'https://api.github.com/users/tahpot/repos', - events_url: 'https://api.github.com/users/tahpot/events{/privacy}', - received_events_url: 'https://api.github.com/users/tahpot/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjMzMzkwNjQ=', - }, - }, - }, - ] - - static webhookVerify = { - action: 'started', - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-21T19:12:53Z', - pushed_at: '2022-03-21T18:06:08Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7741, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 1, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 1, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdHQ', - repos_url: 'https://api.github.com/orgs/CrowdHQ/repos', - events_url: 'https://api.github.com/orgs/CrowdHQ/events', - hooks_url: 'https://api.github.com/orgs/CrowdHQ/hooks', - issues_url: 'https://api.github.com/orgs/CrowdHQ/issues', - members_url: 'https://api.github.com/orgs/CrowdHQ/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdHQ/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjM1ODU4MTY=', - }, - } - - static discussion = { - event: 'discussion', - created: { - action: 'created', - discussion: { - repository_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - category: { - id: 37972861, - node_id: 'DIC_kwDOGsy6M84CQ2t9', - repository_id: 449624627, - emoji: ':pray:', - name: 'Q&A', - description: 'Ask the community for help', - created_at: '2022-08-16T11:48:28.000+02:00', - updated_at: '2022-08-16T11:48:28.000+02:00', - slug: 'q-a', - is_answerable: true, - }, - answer_html_url: null, - answer_chosen_at: null, - answer_chosen_by: null, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270', - id: 4315208, - node_id: 'D_kwDOGsy6M84AQdhI', - number: 270, - title: 'test dicussion - QA', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - state: 'open', - locked: false, - comments: 0, - created_at: '2022-08-18T16:05:56Z', - updated_at: '2022-08-18T16:05:56Z', - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: 'asdasd', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/discussions/270/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/discussions/270/timeline', - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-18T16:02:43Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 24891, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 5, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 5, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - edited: { - action: 'edited', - comment: { - id: 3424357, - node_id: 'DC_kwDOGsy6M84ANEBl', - html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270#discussioncomment-3424357', - parent_id: null, - child_comment_count: 1, - repository_url: 'CrowdDotDev/crowd-postgres', - discussion_id: 4315208, - author_association: 'CONTRIBUTOR', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - created_at: '2022-08-18T16:06:04Z', - updated_at: '2022-08-19T07:14:13Z', - body: 'answer to a question - EDITED', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/3424357/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - }, - discussion: { - repository_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - category: { - id: 37972861, - node_id: 'DIC_kwDOGsy6M84CQ2t9', - repository_id: 449624627, - emoji: ':pray:', - name: 'Q&A', - description: 'Ask the community for help', - created_at: '2022-08-16T11:48:28.000+02:00', - updated_at: '2022-08-16T11:48:28.000+02:00', - slug: 'q-a', - is_answerable: true, - }, - answer_html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270#discussioncomment-3424357', - answer_chosen_at: '2022-08-18T18:41:39.000+02:00', - answer_chosen_by: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270', - id: 4315208, - node_id: 'D_kwDOGsy6M84AQdhI', - number: 270, - title: 'test dicussion - QA', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - state: 'open', - locked: false, - comments: 2, - created_at: '2022-08-18T16:05:56Z', - updated_at: '2022-08-18T16:41:39Z', - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: 'asdasd', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/discussions/270/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/discussions/270/timeline', - }, - changes: { - body: { - from: 'answer to a question', - }, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-19T06:50:13Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 25359, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 7, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 7, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - answered: { - action: 'answered', - discussion: { - repository_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - category: { - id: 37972861, - node_id: 'DIC_kwDOGsy6M84CQ2t9', - repository_id: 449624627, - emoji: ':pray:', - name: 'Q&A', - description: 'Ask the community for help', - created_at: '2022-08-16T11:48:28.000+02:00', - updated_at: '2022-08-16T11:48:28.000+02:00', - slug: 'q-a', - is_answerable: true, - }, - answer_html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270#discussioncomment-3424357', - answer_chosen_at: '2022-08-18T18:41:39.000+02:00', - answer_chosen_by: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270', - id: 4315208, - node_id: 'D_kwDOGsy6M84AQdhI', - number: 270, - title: 'test dicussion - QA', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - state: 'open', - locked: false, - comments: 2, - created_at: '2022-08-18T16:05:56Z', - updated_at: '2022-08-18T16:41:39Z', - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: 'asdasd', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/discussions/270/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/discussions/270/timeline', - }, - answer: { - id: 3424357, - node_id: 'DC_kwDOGsy6M84ANEBl', - html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270#discussioncomment-3424357', - parent_id: null, - child_comment_count: 1, - repository_url: 'CrowdDotDev/crowd-postgres', - discussion_id: 4315208, - author_association: 'CONTRIBUTOR', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - created_at: '2022-08-18T16:06:04Z', - updated_at: '2022-08-18T16:06:05Z', - body: 'answer to a question', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/3424357/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-18T16:26:28Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 24891, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 5, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 5, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - } - - static discussionComment = { - event: 'discussion_comment', - created: { - action: 'created', - comment: { - id: 3424532, - node_id: 'DC_kwDOGsy6M84ANEEU', - html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270#discussioncomment-3424532', - parent_id: 3424357, - child_comment_count: 0, - repository_url: 'CrowdDotDev/crowd-postgres', - discussion_id: 4315208, - author_association: 'CONTRIBUTOR', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - created_at: '2022-08-18T16:26:10Z', - updated_at: '2022-08-18T16:26:10Z', - body: 'a reply\r\n', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/3424532/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - }, - discussion: { - repository_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - category: { - id: 37972861, - node_id: 'DIC_kwDOGsy6M84CQ2t9', - repository_id: 449624627, - emoji: ':pray:', - name: 'Q&A', - description: 'Ask the community for help', - created_at: '2022-08-16T11:48:28.000+02:00', - updated_at: '2022-08-16T11:48:28.000+02:00', - slug: 'q-a', - is_answerable: true, - }, - answer_html_url: null, - answer_chosen_at: null, - answer_chosen_by: null, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270', - id: 4315208, - node_id: 'D_kwDOGsy6M84AQdhI', - number: 270, - title: 'test dicussion - QA', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - state: 'open', - locked: false, - comments: 2, - created_at: '2022-08-18T16:05:56Z', - updated_at: '2022-08-18T16:26:10Z', - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: 'asdasd', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/discussions/270/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/discussions/270/timeline', - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-18T16:24:41Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 24891, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 5, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 5, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - edited: { - action: 'edited', - comment: { - id: 3424357, - node_id: 'DC_kwDOGsy6M84ANEEU', - html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270#discussioncomment-3424357', - parent_id: null, - child_comment_count: 1, - repository_url: 'CrowdDotDev/crowd-postgres', - discussion_id: 4315208, - author_association: 'CONTRIBUTOR', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - created_at: '2022-08-18T16:06:04Z', - updated_at: '2022-08-19T07:14:13Z', - body: 'answer to a question - EDITED', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/3424357/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - }, - discussion: { - repository_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - category: { - id: 37972861, - node_id: 'DIC_kwDOGsy6M84CQ2t9', - repository_id: 449624627, - emoji: ':pray:', - name: 'Q&A', - description: 'Ask the community for help', - created_at: '2022-08-16T11:48:28.000+02:00', - updated_at: '2022-08-16T11:48:28.000+02:00', - slug: 'q-a', - is_answerable: true, - }, - answer_html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270#discussioncomment-3424357', - answer_chosen_at: '2022-08-18T18:41:39.000+02:00', - answer_chosen_by: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270', - id: 4315208, - node_id: 'D_kwDOGsy6M84AQdhI', - number: 270, - title: 'test dicussion - QA', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - state: 'open', - locked: false, - comments: 2, - created_at: '2022-08-18T16:05:56Z', - updated_at: '2022-08-18T16:41:39Z', - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: 'asdasd', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/discussions/270/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/discussions/270/timeline', - }, - changes: { - body: { - from: 'answer to a question', - }, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-19T06:50:13Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 25359, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 7, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 7, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - } -} diff --git a/backend/src/serverless/integrations/webhooks/__tests__/github.test.ts b/backend/src/serverless/integrations/webhooks/__tests__/github.test.ts deleted file mode 100644 index 8d8433df21..0000000000 --- a/backend/src/serverless/integrations/webhooks/__tests__/github.test.ts +++ /dev/null @@ -1,1361 +0,0 @@ -import moment from 'moment' -import IntegrationRepository from '../../../../database/repositories/integrationRepository' -import SequelizeTestUtils from '../../../../database/utils/sequelizeTestUtils' -import TestEvents from './events' -import { MemberAttributeName, PlatformType } from '@crowd/types' -import { GithubActivityType, GITHUB_GRID } from '@crowd/integrations' -import { IntegrationServiceBase } from '../../services/integrationServiceBase' -import { GithubIntegrationService } from '../../services/integrations/githubIntegrationService' -import { IStepContext } from '../../../../types/integration/stepResult' -import { getServiceLogger } from '@crowd/logging' -import { generateUUIDv1 } from '@crowd/common' - -const db = null -const installId = '23585816' - -const log = getServiceLogger() - -async function fakeContext(integration = { id: generateUUIDv1() }): Promise { - const options = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - return { - onboarding: false, - integration, - repoContext: options, - serviceContext: options, - limitCount: 0, - startTimestamp: 0, - logger: log, - pipelineData: {}, - } -} - -async function init(integration = false) { - const options = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - if (integration) { - const integration = await IntegrationRepository.create( - { - platform: PlatformType.GITHUB, - token: '', - integrationIdentifier: installId, - }, - options, - ) - - return { - tenantId: options.currentTenant.id, - integration, - } - } - return { - tenantId: options.currentTenant.id, - } -} - -describe('Github webhooks tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('Parse member tests', () => { - it('It should parse a simple member', async () => { - const member = { - login: 'joanreyero', - name: 'Joan Reyero', - url: 'https://github.com/joanreyero', - } - const context = await fakeContext() - const parsedMember = await GithubIntegrationService.parseMember(member, context) - const expected = { - username: { - [PlatformType.GITHUB]: { - username: 'joanreyero', - integrationId: context.integration.id, - }, - }, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: false, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/joanreyero', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: '', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.GITHUB]: '', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: '', - }, - }, - emails: [], - displayName: 'Joan Reyero', - } - expect(parsedMember).toStrictEqual(expected) - }) - - it('It should parse a member with Twitter', async () => { - const member = { - login: 'joanreyero', - name: 'Joan Reyero', - url: 'https://github.com/joanreyero', - twitterUsername: 'reyero', - } - const context = await fakeContext() - const parsedMember = await GithubIntegrationService.parseMember(member, context) - const expected = { - username: { - [PlatformType.GITHUB]: { - username: 'joanreyero', - integrationId: context.integration.id, - }, - // [PlatformType.TWITTER]: { - // username: 'reyero', - // integrationId: context.integration.id, - // }, - }, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: false, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/joanreyero', - // [PlatformType.TWITTER]: 'https://twitter.com/reyero', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.GITHUB]: '', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: '', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: '', - }, - }, - emails: [], - displayName: 'Joan Reyero', - } - expect(parsedMember).toStrictEqual(expected) - }) - - it('It should parse a complete member without Twitter', async () => { - const member = { - login: 'joanreyero', - name: 'Joan Reyero', - isHitable: true, - url: 'https://github.com/joanreyero', - websiteUrl: 'https://crowd.dev', - email: 'joan@crowd.dev', - bio: 'Bio goes here', - company: '@CrowdHQ ', - location: 'Cambridge, UK', - twitterUsername: 'reyero', - followers: { - totalCount: 10, - }, - } - const context = await fakeContext() - const parsedMember = await GithubIntegrationService.parseMember(member, context) - const expected = { - username: { - [PlatformType.GITHUB]: { - username: 'joanreyero', - integrationId: context.integration.id, - }, - // [PlatformType.TWITTER]: { - // username: 'reyero', - // integrationId: context.integration.id, - // }, - }, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: false, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/joanreyero', - // [PlatformType.TWITTER]: 'https://twitter.com/reyero', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://crowd.dev', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.GITHUB]: '', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Bio goes here', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Cambridge, UK', - }, - }, - reach: { [PlatformType.GITHUB]: 10 }, - emails: [], - displayName: 'Joan Reyero', - organizations: [{ name: 'crowd.dev' }], - } - expect(parsedMember).toStrictEqual(expected) - }) - }) - - describe('Issues tests', () => { - it('It should parse an issue open coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - - const issue = await GithubIntegrationService.parseWebhookIssue( - TestEvents.issues.opened, - context, - ) - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - type: GithubActivityType.ISSUE_OPENED, - timestamp: new Date(TestEvents.issues.opened.issue.created_at), - platform: PlatformType.GITHUB, - tenant: tenantId.toString(), - sourceId: TestEvents.issues.opened.issue.node_id, - sourceParentId: null, - url: TestEvents.issues.opened.issue.html_url, - title: TestEvents.issues.opened.issue.title, - body: TestEvents.issues.opened.issue.body, - channel: TestEvents.issues.opened.repository.html_url, - attributes: { - state: TestEvents.issues.opened.issue.state, - }, - score: GITHUB_GRID[GithubActivityType.ISSUE_OPENED].score, - isContribution: GITHUB_GRID[GithubActivityType.ISSUE_OPENED].isContribution, - } - expect(issue).toStrictEqual(expected) - }) - - it('It should parse an issue edited coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - - const issue = await GithubIntegrationService.parseWebhookIssue( - TestEvents.issues.edited, - context, - ) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - type: GithubActivityType.ISSUE_OPENED, - timestamp: new Date(TestEvents.issues.edited.issue.created_at), - platform: PlatformType.GITHUB, - tenant: tenantId.toString(), - sourceId: TestEvents.issues.edited.issue.node_id, - sourceParentId: null, - url: TestEvents.issues.edited.issue.html_url, - title: TestEvents.issues.edited.issue.title, - body: TestEvents.issues.edited.issue.body, - channel: TestEvents.issues.edited.repository.html_url, - attributes: { - state: TestEvents.issues.edited.issue.state, - }, - score: GITHUB_GRID[GithubActivityType.ISSUE_OPENED].score, - isContribution: GITHUB_GRID[GithubActivityType.ISSUE_OPENED].isContribution, - } - expect(issue).toStrictEqual(expected) - }) - - it('It should parse an issue reopened coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - - const issue = await GithubIntegrationService.parseWebhookIssue( - TestEvents.issues.reopened, - context, - ) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - type: GithubActivityType.ISSUE_OPENED, - timestamp: new Date(TestEvents.issues.reopened.issue.created_at), - platform: PlatformType.GITHUB, - tenant: tenantId.toString(), - sourceId: TestEvents.issues.reopened.issue.node_id, - sourceParentId: null, - url: TestEvents.issues.reopened.issue.html_url, - title: TestEvents.issues.reopened.issue.title, - body: TestEvents.issues.reopened.issue.body, - channel: TestEvents.issues.reopened.repository.html_url, - attributes: { - state: TestEvents.issues.opened.issue.state, - }, - score: GITHUB_GRID[GithubActivityType.ISSUE_OPENED].score, - isContribution: GITHUB_GRID[GithubActivityType.ISSUE_OPENED].isContribution, - } - expect(issue).toStrictEqual(expected) - }) - - it('It should parse an issue closed coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - - const issue = await GithubIntegrationService.parseWebhookIssue( - TestEvents.issues.closed, - context, - ) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - type: GithubActivityType.ISSUE_CLOSED, - timestamp: new Date(TestEvents.issues.closed.issue.closed_at), - platform: PlatformType.GITHUB, - tenant: tenantId, - sourceId: `gen-CE_${TestEvents.issues.closed.issue.node_id}_${ - TestEvents.issues.closed.sender.login - }_${new Date(TestEvents.issues.closed.issue.closed_at).toISOString()}`, - sourceParentId: TestEvents.issues.closed.issue.node_id, - url: TestEvents.issues.closed.issue.html_url, - title: '', - channel: TestEvents.issues.closed.repository.html_url, - body: '', - attributes: { - state: TestEvents.issues.closed.issue.state, - }, - score: GITHUB_GRID[GithubActivityType.ISSUE_CLOSED].score, - isContribution: GITHUB_GRID[GithubActivityType.ISSUE_CLOSED].isContribution, - } - expect(issue).toStrictEqual(expected) - }) - - it('processWebhook should not return any operations for unsupported actions', async () => { - const { integration } = await init(true) - const context = await fakeContext(integration) - - const service = new GithubIntegrationService() - - const actions = [ - 'deleted', - 'pinned', - 'unpinned', - 'assigned', - 'unassigned', - 'labeled', - 'unlabeled', - 'locked', - 'unlocked', - 'transferred', - 'milestoned', - 'demilestoned', - ] - - for (const action of actions) { - const webhook = { - payload: { - signature: '', - event: 'issues', - data: { - action, - }, - }, - } - - const result = await service.processWebhook(webhook, context) - expect(result.operations).toStrictEqual([]) - } - }) - }) - - describe('Discussion tests', () => { - it('It should parse a discussion created event coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const discussion = await GithubIntegrationService.parseWebhookDiscussion( - TestEvents.discussion.created, - context, - ) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - integrationId: integration.id, - username: 'testMember', - }, - }, - }, - username: 'testMember', - type: GithubActivityType.DISCUSSION_STARTED, - timestamp: new Date(TestEvents.discussion.created.discussion.created_at), - platform: PlatformType.GITHUB, - tenant: tenantId.toString(), - sourceId: TestEvents.discussion.created.discussion.node_id, - sourceParentId: null, - url: TestEvents.discussion.created.discussion.html_url, - title: TestEvents.discussion.created.discussion.title, - body: TestEvents.discussion.created.discussion.body, - channel: TestEvents.discussion.created.repository.html_url, - attributes: { - category: { - id: TestEvents.discussion.created.discussion.category.node_id, - isAnswerable: TestEvents.discussion.created.discussion.category.is_answerable, - name: TestEvents.discussion.created.discussion.category.name, - slug: TestEvents.discussion.created.discussion.category.slug, - emoji: TestEvents.discussion.created.discussion.category.emoji, - description: TestEvents.discussion.created.discussion.category.description, - }, - }, - score: GITHUB_GRID[GithubActivityType.DISCUSSION_STARTED].score, - isContribution: GITHUB_GRID[GithubActivityType.DISCUSSION_STARTED].isContribution, - } - - expect(discussion).toStrictEqual(expected) - }) - - it('It should parse a discussion edited event coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const discussion = await GithubIntegrationService.parseWebhookDiscussion( - TestEvents.discussion.edited, - context, - ) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - type: GithubActivityType.DISCUSSION_STARTED, - timestamp: new Date(TestEvents.discussion.edited.discussion.created_at), - platform: PlatformType.GITHUB, - tenant: tenantId.toString(), - sourceId: TestEvents.discussion.edited.discussion.node_id, - sourceParentId: null, - url: TestEvents.discussion.edited.discussion.html_url, - title: TestEvents.discussion.edited.discussion.title, - body: TestEvents.discussion.edited.discussion.body, - channel: TestEvents.discussion.edited.repository.html_url, - attributes: { - category: { - id: TestEvents.discussion.edited.discussion.category.node_id, - isAnswerable: TestEvents.discussion.edited.discussion.category.is_answerable, - name: TestEvents.discussion.edited.discussion.category.name, - slug: TestEvents.discussion.edited.discussion.category.slug, - emoji: TestEvents.discussion.edited.discussion.category.emoji, - description: TestEvents.discussion.edited.discussion.category.description, - }, - }, - score: GITHUB_GRID[GithubActivityType.DISCUSSION_STARTED].score, - isContribution: GITHUB_GRID[GithubActivityType.DISCUSSION_STARTED].isContribution, - } - expect(discussion).toStrictEqual(expected) - }) - - it('It should parse a discussion answered event coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const discussion = await GithubIntegrationService.parseWebhookDiscussion( - TestEvents.discussion.answered, - context, - ) - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - type: GithubActivityType.DISCUSSION_COMMENT, - timestamp: new Date(TestEvents.discussion.answered.answer.created_at), - platform: PlatformType.GITHUB, - tenant: tenantId.toString(), - sourceId: TestEvents.discussion.answered.answer.node_id, - sourceParentId: TestEvents.discussion.answered.discussion.node_id, - url: TestEvents.discussion.answered.answer.html_url, - body: TestEvents.discussion.answered.answer.body, - channel: TestEvents.discussion.answered.repository.html_url, - attributes: { - isSelectedAnswer: true, - }, - score: GITHUB_GRID[GithubActivityType.DISCUSSION_STARTED].score, - isContribution: GITHUB_GRID[GithubActivityType.DISCUSSION_STARTED].isContribution, - } - expect(discussion).toStrictEqual(expected) - }) - }) - - describe('Pull request tests', () => { - it('It should parse an open PR coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const pr = await GithubIntegrationService.parseWebhookPullRequest( - TestEvents.pullRequests.opened, - context, - ) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - objectMemberUsername: null, - objectMember: null, - type: GithubActivityType.PULL_REQUEST_OPENED, - timestamp: new Date(TestEvents.pullRequests.opened.pull_request.created_at), - platform: PlatformType.GITHUB, - tenant: tenantId, - sourceId: TestEvents.pullRequests.opened.pull_request.node_id, - sourceParentId: null, - url: TestEvents.pullRequests.opened.pull_request.html_url, - channel: TestEvents.pullRequests.opened.repository.html_url, - title: TestEvents.pullRequests.opened.pull_request.title, - body: TestEvents.pullRequests.opened.pull_request.body, - score: GITHUB_GRID[GithubActivityType.PULL_REQUEST_OPENED].score, - isContribution: GITHUB_GRID[GithubActivityType.PULL_REQUEST_OPENED].isContribution, - attributes: { - additions: TestEvents.pullRequests.opened.pull_request.additions, - authorAssociation: TestEvents.pullRequests.opened.pull_request.author_association, - changedFiles: TestEvents.pullRequests.opened.pull_request.changed_files, - deletions: TestEvents.pullRequests.opened.pull_request.deletions, - labels: TestEvents.pullRequests.opened.pull_request.labels, - state: TestEvents.pullRequests.opened.pull_request.state, - }, - } - expect(pr).toStrictEqual(expected) - }) - - it('It should parse an edited PR coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const pr = await GithubIntegrationService.parseWebhookPullRequest( - TestEvents.pullRequests.edited, - context, - ) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - objectMemberUsername: null, - objectMember: null, - type: GithubActivityType.PULL_REQUEST_OPENED, - timestamp: new Date(TestEvents.pullRequests.edited.pull_request.created_at), - platform: PlatformType.GITHUB, - tenant: tenantId, - sourceId: TestEvents.pullRequests.edited.pull_request.node_id, - sourceParentId: null, - url: TestEvents.pullRequests.edited.pull_request.html_url, - channel: TestEvents.pullRequests.edited.repository.html_url, - title: TestEvents.pullRequests.edited.pull_request.title, - body: TestEvents.pullRequests.edited.pull_request.body, - score: GITHUB_GRID[GithubActivityType.PULL_REQUEST_OPENED].score, - isContribution: GITHUB_GRID[GithubActivityType.PULL_REQUEST_OPENED].isContribution, - attributes: { - additions: TestEvents.pullRequests.edited.pull_request.additions, - authorAssociation: TestEvents.pullRequests.edited.pull_request.author_association, - changedFiles: TestEvents.pullRequests.edited.pull_request.changed_files, - deletions: TestEvents.pullRequests.edited.pull_request.deletions, - labels: TestEvents.pullRequests.edited.pull_request.labels.map((l) => l.name), - state: TestEvents.pullRequests.edited.pull_request.state, - }, - } - - expect(pr).toStrictEqual(expected) - }) - - it('It should parse a reopened PR coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const pr = await GithubIntegrationService.parseWebhookPullRequest( - TestEvents.pullRequests.reopened, - context, - ) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - objectMemberUsername: null, - objectMember: null, - type: GithubActivityType.PULL_REQUEST_OPENED, - timestamp: new Date(TestEvents.pullRequests.reopened.pull_request.created_at), - platform: PlatformType.GITHUB, - tenant: tenantId, - sourceId: TestEvents.pullRequests.reopened.pull_request.node_id, - sourceParentId: null, - url: TestEvents.pullRequests.reopened.pull_request.html_url, - title: TestEvents.pullRequests.reopened.pull_request.title, - body: TestEvents.pullRequests.reopened.pull_request.body, - channel: TestEvents.pullRequests.reopened.repository.html_url, - score: GITHUB_GRID[GithubActivityType.PULL_REQUEST_OPENED].score, - isContribution: GITHUB_GRID[GithubActivityType.PULL_REQUEST_OPENED].isContribution, - attributes: { - additions: TestEvents.pullRequests.reopened.pull_request.additions, - authorAssociation: TestEvents.pullRequests.reopened.pull_request.author_association, - changedFiles: TestEvents.pullRequests.reopened.pull_request.changed_files, - deletions: TestEvents.pullRequests.reopened.pull_request.deletions, - labels: TestEvents.pullRequests.reopened.pull_request.labels.map((l) => l.name), - state: TestEvents.pullRequests.reopened.pull_request.state, - }, - } - expect(pr).toStrictEqual(expected) - }) - - it('It should parse a closed PR coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const pr = await GithubIntegrationService.parseWebhookPullRequest( - TestEvents.pullRequests.closed, - context, - ) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - objectMemberUsername: null, - objectMember: null, - type: GithubActivityType.PULL_REQUEST_CLOSED, - timestamp: new Date(TestEvents.pullRequests.closed.pull_request.closed_at), - platform: PlatformType.GITHUB, - tenant: tenantId, - sourceId: `gen-CE_${TestEvents.pullRequests.closed.pull_request.node_id}_${ - TestEvents.pullRequests.closed.sender.login - }_${new Date(TestEvents.pullRequests.closed.pull_request.updated_at).toISOString()}`, - sourceParentId: TestEvents.pullRequests.closed.pull_request.node_id, - url: TestEvents.pullRequests.closed.pull_request.html_url, - title: '', - body: '', - channel: TestEvents.pullRequests.closed.repository.html_url, - score: GITHUB_GRID[GithubActivityType.PULL_REQUEST_CLOSED].score, - isContribution: GITHUB_GRID[GithubActivityType.PULL_REQUEST_CLOSED].isContribution, - attributes: { - additions: TestEvents.pullRequests.closed.pull_request.additions, - authorAssociation: TestEvents.pullRequests.closed.pull_request.author_association, - changedFiles: TestEvents.pullRequests.closed.pull_request.changed_files, - deletions: TestEvents.pullRequests.closed.pull_request.deletions, - labels: TestEvents.pullRequests.closed.pull_request.labels.map((l) => l.name), - state: TestEvents.pullRequests.closed.pull_request.state, - }, - } - expect(pr).toStrictEqual(expected) - }) - - it('It should parse an assigned PR coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const pr = await GithubIntegrationService.parseWebhookPullRequest( - TestEvents.pullRequests.assigned, - context, - ) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - objectMemberUsername: 'testMember', - objectMember: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - type: GithubActivityType.PULL_REQUEST_ASSIGNED, - timestamp: new Date(TestEvents.pullRequests.assigned.pull_request.updated_at), - platform: PlatformType.GITHUB, - tenant: tenantId, - sourceId: `gen-AE_${TestEvents.pullRequests.assigned.pull_request.node_id}_${ - TestEvents.pullRequests.assigned.sender.login - }_${TestEvents.pullRequests.assigned.assignee.login}_${new Date( - TestEvents.pullRequests.assigned.pull_request.updated_at, - ).toISOString()}`, - sourceParentId: TestEvents.pullRequests.assigned.pull_request.node_id, - url: TestEvents.pullRequests.assigned.pull_request.html_url, - title: '', - body: '', - channel: TestEvents.pullRequests.assigned.repository.html_url, - score: GITHUB_GRID[GithubActivityType.PULL_REQUEST_ASSIGNED].score, - isContribution: GITHUB_GRID[GithubActivityType.PULL_REQUEST_ASSIGNED].isContribution, - attributes: { - additions: TestEvents.pullRequests.assigned.pull_request.additions, - authorAssociation: TestEvents.pullRequests.assigned.pull_request.author_association, - changedFiles: TestEvents.pullRequests.assigned.pull_request.changed_files, - deletions: TestEvents.pullRequests.assigned.pull_request.deletions, - labels: TestEvents.pullRequests.assigned.pull_request.labels.map((l) => l.name), - state: TestEvents.pullRequests.assigned.pull_request.state, - }, - } - expect(pr).toStrictEqual(expected) - }) - - it('It should parse a review requested event coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const pr = await GithubIntegrationService.parseWebhookPullRequest( - TestEvents.pullRequests.review_requested, - context, - ) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - objectMemberUsername: 'testMember', - objectMember: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - type: GithubActivityType.PULL_REQUEST_REVIEW_REQUESTED, - timestamp: new Date(TestEvents.pullRequests.review_requested.pull_request.updated_at), - platform: PlatformType.GITHUB, - tenant: tenantId, - sourceId: `gen-RRE_${TestEvents.pullRequests.review_requested.pull_request.node_id}_${ - TestEvents.pullRequests.review_requested.sender.login - }_${TestEvents.pullRequests.review_requested.requested_reviewer.login}_${new Date( - TestEvents.pullRequests.review_requested.pull_request.updated_at, - ).toISOString()}`, - sourceParentId: TestEvents.pullRequests.review_requested.pull_request.node_id, - url: TestEvents.pullRequests.review_requested.pull_request.html_url, - title: '', - body: '', - channel: TestEvents.pullRequests.review_requested.repository.html_url, - score: GITHUB_GRID[GithubActivityType.PULL_REQUEST_REVIEW_REQUESTED].score, - isContribution: - GITHUB_GRID[GithubActivityType.PULL_REQUEST_REVIEW_REQUESTED].isContribution, - attributes: { - additions: TestEvents.pullRequests.review_requested.pull_request.additions, - authorAssociation: - TestEvents.pullRequests.review_requested.pull_request.author_association, - changedFiles: TestEvents.pullRequests.review_requested.pull_request.changed_files, - deletions: TestEvents.pullRequests.review_requested.pull_request.deletions, - labels: TestEvents.pullRequests.review_requested.pull_request.labels.map((l) => l.name), - state: TestEvents.pullRequests.review_requested.pull_request.state, - }, - } - expect(pr).toStrictEqual(expected) - }) - - it('It should parse a merged PR coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const pr = await GithubIntegrationService.parseWebhookPullRequest( - TestEvents.pullRequests.merged, - context, - ) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - objectMemberUsername: null, - objectMember: null, - type: GithubActivityType.PULL_REQUEST_MERGED, - timestamp: new Date(TestEvents.pullRequests.merged.pull_request.merged_at), - platform: PlatformType.GITHUB, - tenant: tenantId, - sourceId: `gen-ME_${TestEvents.pullRequests.merged.pull_request.node_id}_${ - TestEvents.pullRequests.merged.sender.login - }_${new Date(TestEvents.pullRequests.merged.pull_request.merged_at).toISOString()}`, - sourceParentId: TestEvents.pullRequests.merged.pull_request.node_id, - url: TestEvents.pullRequests.merged.pull_request.html_url, - title: '', - body: '', - channel: TestEvents.pullRequests.merged.repository.html_url, - score: GITHUB_GRID[GithubActivityType.PULL_REQUEST_MERGED].score, - isContribution: GITHUB_GRID[GithubActivityType.PULL_REQUEST_MERGED].isContribution, - attributes: { - additions: TestEvents.pullRequests.merged.pull_request.additions, - authorAssociation: TestEvents.pullRequests.merged.pull_request.author_association, - changedFiles: TestEvents.pullRequests.merged.pull_request.changed_files, - deletions: TestEvents.pullRequests.merged.pull_request.deletions, - labels: TestEvents.pullRequests.merged.pull_request.labels.map((l) => l.name), - state: TestEvents.pullRequests.merged.pull_request.state, - }, - } - expect(pr).toStrictEqual(expected) - }) - - it('It should parse a PR reviewed event coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const pr = await GithubIntegrationService.parseWebhookPullRequestReview( - TestEvents.pullRequestReviews.submitted, - context, - ) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - type: GithubActivityType.PULL_REQUEST_REVIEWED, - timestamp: new Date(TestEvents.pullRequestReviews.submitted.review.submitted_at), - platform: PlatformType.GITHUB, - tenant: tenantId, - sourceId: `gen-PRR_${TestEvents.pullRequestReviews.submitted.pull_request.node_id}_${ - TestEvents.pullRequestReviews.submitted.sender.login - }_${new Date(TestEvents.pullRequestReviews.submitted.review.submitted_at).toISOString()}`, - sourceParentId: TestEvents.pullRequestReviews.submitted.pull_request.node_id, - url: TestEvents.pullRequestReviews.submitted.pull_request.html_url, - title: '', - body: TestEvents.pullRequestReviews.submitted.review.body, - channel: TestEvents.pullRequestReviews.submitted.repository.html_url, - score: GITHUB_GRID[GithubActivityType.PULL_REQUEST_REVIEWED].score, - isContribution: GITHUB_GRID[GithubActivityType.PULL_REQUEST_REVIEWED].isContribution, - attributes: { - reviewState: TestEvents.pullRequestReviews.submitted.review.state.toUpperCase(), - authorAssociation: - TestEvents.pullRequestReviews.submitted.pull_request.author_association, - labels: TestEvents.pullRequestReviews.submitted.pull_request.labels.map((l) => l.name), - state: TestEvents.pullRequestReviews.submitted.pull_request.state, - }, - } - expect(pr).toStrictEqual(expected) - }) - - it('It should parse a PR review comment created event coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const pr = await GithubIntegrationService.parseWebhookPullRequestReviewThreadComment( - TestEvents.pullRequestReviewThreadComment.created, - context, - ) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - type: GithubActivityType.PULL_REQUEST_REVIEW_THREAD_COMMENT, - timestamp: new Date(TestEvents.pullRequestReviewThreadComment.created.comment.created_at), - platform: PlatformType.GITHUB, - tenant: tenantId, - sourceId: TestEvents.pullRequestReviewThreadComment.created.comment.node_id, - sourceParentId: TestEvents.pullRequestReviewThreadComment.created.pull_request.node_id, - url: TestEvents.pullRequestReviewThreadComment.created.comment.html_url, - title: '', - body: TestEvents.pullRequestReviewThreadComment.created.comment.body, - channel: TestEvents.pullRequestReviewThreadComment.created.repository.html_url, - score: GITHUB_GRID[GithubActivityType.PULL_REQUEST_REVIEW_THREAD_COMMENT].score, - isContribution: - GITHUB_GRID[GithubActivityType.PULL_REQUEST_REVIEW_THREAD_COMMENT].isContribution, - attributes: { - authorAssociation: - TestEvents.pullRequestReviewThreadComment.created.pull_request.author_association, - labels: TestEvents.pullRequestReviewThreadComment.created.pull_request.labels.map( - (l) => l.name, - ), - state: TestEvents.pullRequestReviewThreadComment.created.pull_request.state, - }, - } - expect(pr).toStrictEqual(expected) - }) - - it('processWebhook should not return any operations for unsupported actions', async () => { - const { integration } = await init(true) - const context = await fakeContext(integration) - - const service = new GithubIntegrationService() - - const actions = [ - 'auto_merge_disabled', - 'auto_merge_enabled', - 'converted_to_draft', - 'labeled', - 'locked', - 'review_request_removed', - 'synchronize', - 'unassigned', - 'unlabeled', - 'unlocked', - ] - for (const action of actions) { - const webhook = { - payload: { - signature: '', - event: 'pull_request', - data: { - action, - }, - }, - } - - const result = await service.processWebhook(webhook, context) - expect(result.operations).toStrictEqual([]) - } - }) - }) - - describe('Star tests', () => { - it('It should parse a star event coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const star = await GithubIntegrationService.parseWebhookStar(TestEvents.star.created, context) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - type: GithubActivityType.STAR, - platform: PlatformType.GITHUB, - tenant: tenantId, - timestamp: moment(TestEvents.star.created.starred_at).toDate(), - sourceId: IntegrationServiceBase.generateSourceIdHash( - 'joanreyero', - GithubActivityType.STAR, - moment(TestEvents.star.created.starred_at).unix().toString(), - PlatformType.GITHUB, - ), - sourceParentId: null, - channel: TestEvents.star.created.repository.html_url, - score: 2, - isContribution: false, - } - expect(star).toStrictEqual(expected) - }) - - it('It should parse an unstar event coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const star = await GithubIntegrationService.parseWebhookStar(TestEvents.star.deleted, context) - - const starTimestamp = star.timestamp - delete star.timestamp - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - type: GithubActivityType.UNSTAR, - platform: PlatformType.GITHUB, - tenant: tenantId, - sourceId: IntegrationServiceBase.generateSourceIdHash( - 'joanreyero', - GithubActivityType.UNSTAR, - moment(starTimestamp).unix().toString(), - PlatformType.GITHUB, - ), - sourceParentId: null, - channel: TestEvents.star.deleted.repository.html_url, - score: -2, - isContribution: false, - } - expect(star).toStrictEqual(expected) - // Check timestamp - expect(moment(starTimestamp).unix()).toBeCloseTo(moment().unix(), 3) - }) - }) - - describe('Fork tests', () => { - it('It should parse a fork event coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const fork = await GithubIntegrationService.parseWebhookFork(TestEvents.fork.created, context) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - type: GithubActivityType.FORK, - timestamp: new Date(TestEvents.fork.created.forkee.created_at), - platform: PlatformType.GITHUB, - tenant: tenantId, - sourceId: TestEvents.fork.created.forkee.node_id, - sourceParentId: null, - channel: TestEvents.fork.created.repository.html_url, - score: 4, - isContribution: false, - } - expect(fork).toStrictEqual(expected) - }) - }) - - describe('Comments tests', () => { - it('It should parse an issue comment created event coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const issue = await GithubIntegrationService.parseWebhookIssue( - TestEvents.issues.opened, - context, - ) - - const payload = TestEvents.comment.issue.created - payload.issue.node_id = issue.sourceId - const event = TestEvents.comment.event - - const comment = await GithubIntegrationService.parseWebhookComment(event, payload, context) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - type: GithubActivityType.ISSUE_COMMENT, - timestamp: new Date(TestEvents.comment.issue.created.comment.created_at), - platform: PlatformType.GITHUB, - tenant: tenantId, - sourceId: TestEvents.comment.issue.created.comment.node_id, - sourceParentId: TestEvents.comment.issue.created.issue.node_id, - url: TestEvents.comment.issue.created.comment.html_url, - body: TestEvents.comment.issue.created.comment.body, - channel: TestEvents.comment.issue.created.repository.html_url, - score: GITHUB_GRID[GithubActivityType.ISSUE_COMMENT].score, - isContribution: GITHUB_GRID[GithubActivityType.ISSUE_COMMENT].isContribution, - } - expect(comment).toStrictEqual(expected) - }) - - it('It should parse an issue comment edited event coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const issue = await GithubIntegrationService.parseWebhookIssue( - TestEvents.issues.opened, - context, - ) - - let payload = TestEvents.comment.issue.created - payload.issue.node_id = issue.sourceId - let event = TestEvents.comment.event - - await GithubIntegrationService.parseWebhookComment(event, payload, context) - - payload = TestEvents.comment.issue.edited - payload.issue.node_id = issue.sourceId - event = TestEvents.comment.event - - const comment = await GithubIntegrationService.parseWebhookComment(event, payload, context) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - type: GithubActivityType.ISSUE_COMMENT, - timestamp: new Date(TestEvents.comment.issue.edited.comment.created_at), - platform: PlatformType.GITHUB, - tenant: tenantId, - sourceId: TestEvents.comment.issue.edited.comment.node_id, - sourceParentId: TestEvents.comment.issue.edited.issue.node_id, - url: TestEvents.comment.issue.edited.comment.html_url, - body: TestEvents.comment.issue.edited.comment.body, - channel: TestEvents.comment.issue.edited.repository.html_url, - score: GITHUB_GRID[GithubActivityType.ISSUE_COMMENT].score, - isContribution: GITHUB_GRID[GithubActivityType.ISSUE_COMMENT].isContribution, - } - expect(comment).toStrictEqual(expected) - }) - - it('It should parse a pull request comment created event coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const pull = await GithubIntegrationService.parseWebhookPullRequest( - TestEvents.pullRequests.opened, - context, - ) - - const payload = TestEvents.comment.pullRequest.created - payload.issue.node_id = pull.sourceId - const event = TestEvents.comment.event - - const comment = await GithubIntegrationService.parseWebhookComment(event, payload, context) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - type: GithubActivityType.PULL_REQUEST_COMMENT, - timestamp: new Date(TestEvents.comment.pullRequest.created.comment.created_at), - platform: PlatformType.GITHUB, - tenant: tenantId, - sourceId: TestEvents.comment.pullRequest.created.comment.node_id, - sourceParentId: TestEvents.comment.pullRequest.created.issue.node_id, - url: TestEvents.comment.pullRequest.created.comment.html_url, - body: TestEvents.comment.pullRequest.created.comment.body, - channel: TestEvents.comment.pullRequest.created.repository.html_url, - score: GITHUB_GRID[GithubActivityType.ISSUE_COMMENT].score, - isContribution: GITHUB_GRID[GithubActivityType.ISSUE_COMMENT].isContribution, - } - expect(comment).toStrictEqual(expected) - }) - it('It should parse a pull request comment edited event coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const pull = await GithubIntegrationService.parseWebhookPullRequest( - TestEvents.pullRequests.opened, - context, - ) - - let payload = TestEvents.comment.pullRequest.created - payload.issue.node_id = pull.sourceId - let event = TestEvents.comment.event - - await GithubIntegrationService.parseWebhookComment(event, payload, context) - - payload = TestEvents.comment.pullRequest.edited - payload.issue.node_id = pull.sourceId - event = TestEvents.comment.event - - const comment = await GithubIntegrationService.parseWebhookComment(event, payload, context) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - type: GithubActivityType.PULL_REQUEST_COMMENT, - timestamp: new Date(TestEvents.comment.pullRequest.edited.comment.created_at), - platform: PlatformType.GITHUB, - tenant: tenantId, - sourceId: TestEvents.comment.pullRequest.edited.comment.node_id, - sourceParentId: TestEvents.comment.pullRequest.edited.issue.node_id, - url: TestEvents.comment.pullRequest.edited.comment.html_url, - body: TestEvents.comment.pullRequest.edited.comment.body, - channel: TestEvents.comment.pullRequest.edited.repository.html_url, - score: GITHUB_GRID[GithubActivityType.ISSUE_COMMENT].score, - isContribution: GITHUB_GRID[GithubActivityType.ISSUE_COMMENT].isContribution, - } - expect(comment).toStrictEqual(expected) - }) - - it('processWebhook should not return any operations for unsupported actions', async () => { - const { integration } = await init(true) - const context = await fakeContext(integration) - - const service = new GithubIntegrationService() - - const actions = ['deleted'] - for (const action of actions) { - const webhook = { - payload: { - signature: '', - event: 'issue_comment', - data: { - action, - }, - }, - } - - const result = await service.processWebhook(webhook, context) - expect(result.operations).toStrictEqual([]) - } - }) - - it('It should parse a discussion comment created event coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const discussion = await GithubIntegrationService.parseWebhookDiscussion( - TestEvents.discussion.created, - context, - ) - - const payload = TestEvents.discussionComment.created - payload.discussion.node_id = discussion.sourceId - const event = TestEvents.discussionComment.event - - const comment = await GithubIntegrationService.parseWebhookComment(event, payload, context) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - type: GithubActivityType.DISCUSSION_COMMENT, - timestamp: new Date(TestEvents.discussionComment.created.comment.created_at), - platform: PlatformType.GITHUB, - tenant: tenantId, - sourceId: TestEvents.discussionComment.created.comment.node_id, - sourceParentId: TestEvents.discussionComment.created.discussion.node_id, - url: TestEvents.discussionComment.created.comment.html_url, - body: TestEvents.discussionComment.created.comment.body, - channel: TestEvents.discussionComment.created.repository.html_url, - score: GITHUB_GRID[GithubActivityType.ISSUE_COMMENT].score, - isContribution: GITHUB_GRID[GithubActivityType.ISSUE_COMMENT].isContribution, - } - expect(comment).toStrictEqual(expected) - }) - - it('It should parse a discussion comment edited event coming from the GitHub API', async () => { - const { tenantId, integration } = await init(true) - const context = await fakeContext(integration) - const discussion = await GithubIntegrationService.parseWebhookDiscussion( - TestEvents.discussion.created, - context, - ) - - let payload = TestEvents.discussionComment.created - payload.discussion.node_id = discussion.sourceId - let event = TestEvents.discussionComment.event - - await GithubIntegrationService.parseWebhookComment(event, payload, context) - - payload = TestEvents.discussionComment.edited - payload.discussion.node_id = discussion.sourceId - event = TestEvents.discussionComment.event - - const comment = await GithubIntegrationService.parseWebhookComment(event, payload, context) - - const expected = { - member: { - username: { - [PlatformType.GITHUB]: { - username: 'testMember', - integrationId: integration.id, - }, - }, - }, - username: 'testMember', - type: GithubActivityType.DISCUSSION_COMMENT, - timestamp: new Date(TestEvents.discussionComment.edited.comment.created_at), - platform: PlatformType.GITHUB, - tenant: tenantId, - sourceId: TestEvents.discussionComment.edited.comment.node_id, - sourceParentId: TestEvents.discussionComment.edited.discussion.node_id, - url: TestEvents.discussionComment.edited.comment.html_url, - body: TestEvents.discussionComment.edited.comment.body, - channel: TestEvents.discussionComment.edited.repository.html_url, - score: GITHUB_GRID[GithubActivityType.ISSUE_COMMENT].score, - isContribution: GITHUB_GRID[GithubActivityType.ISSUE_COMMENT].isContribution, - } - expect(comment).toStrictEqual(expected) - }) - }) -}) diff --git a/backend/src/serverless/integrations/workers/sendgridWebhookWorker.ts b/backend/src/serverless/integrations/workers/sendgridWebhookWorker.ts deleted file mode 100644 index f66833339c..0000000000 --- a/backend/src/serverless/integrations/workers/sendgridWebhookWorker.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { getServiceChildLogger } from '@crowd/logging' -import { EventWebhook, EventWebhookHeader } from '@sendgrid/eventwebhook' -import { PlatformType } from '@crowd/types' -import { IS_PROD_ENV, SENDGRID_CONFIG } from '../../../conf' -import SequelizeRepository from '../../../database/repositories/sequelizeRepository' -import UserRepository from '../../../database/repositories/userRepository' -import getUserContext from '../../../database/utils/getUserContext' -import EagleEyeContentService from '../../../services/eagleEyeContentService' -import { NodeWorkerMessageBase } from '../../../types/mq/nodeWorkerMessageBase' -import { SendgridWebhookEvent, SendgridWebhookEventType } from '../../../types/webhooks' -import { NodeWorkerMessageType } from '../../types/workerTypes' -import { sendNodeWorkerMessage } from '../../utils/nodeWorkerSQS' - -const log = getServiceChildLogger('sendgridWebhookWorker') - -export default async function sendgridWebhookWorker(req) { - if (!SENDGRID_CONFIG.webhookSigningSecret) { - log.error('Sendgrid webhook signing secret is not found.') - return { - status: 400, - } - } - - if (!IS_PROD_ENV) { - log.warn('Sendgrid events will be only sent for production.') - return { - status: 200, - } - } - - const events = req.body as SendgridWebhookEvent[] - - const signature = req.headers[EventWebhookHeader.SIGNATURE().toLowerCase()] - const timestamp = req.headers[EventWebhookHeader.TIMESTAMP().toLowerCase()] - - const eventWebhookVerifier = new EventWebhook() - - const ecdsaPublicKey = eventWebhookVerifier.convertPublicKeyToECDSA( - SENDGRID_CONFIG.webhookSigningSecret, - ) - - if (!eventWebhookVerifier.verifySignature(ecdsaPublicKey, req.rawBody, signature, timestamp)) { - log.error('Sendgrid webhook cannot be verified.') - return { - status: 400, - } - } - - for (const event of events) { - if (event.sg_template_id === SENDGRID_CONFIG.templateEagleEyeDigest) { - await sendNodeWorkerMessage(event.sg_event_id, { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - event, - service: 'sendgrid-webhooks', - } as NodeWorkerMessageBase) - } - } - - return { - status: 200, - } -} - -const findPlatform = (str: string, arr: string[]): string => { - const match = arr.find((item) => str.includes(item)) - return match || null -} - -export const processSendgridWebhook = async (message: any) => { - log.info({ message }, 'Got event from sendgrid webhook!') - log.warn(message) - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - const sendgridEvent = message.event as SendgridWebhookEvent - - const user = await UserRepository.findByEmail(sendgridEvent.email, options) - - const userContext = await getUserContext(sendgridEvent.tenantId, user.id) - - switch (sendgridEvent.event) { - case SendgridWebhookEventType.DIGEST_OPENED: { - EagleEyeContentService.trackDigestEmailOpened(userContext) - break - } - case SendgridWebhookEventType.POST_CLICKED: { - const platform = findPlatform( - new URL(sendgridEvent.url).hostname, - Object.values(PlatformType), - ) - EagleEyeContentService.trackPostClicked(sendgridEvent.url, platform, userContext, 'email') - break - } - default: - log.info({ event: message.event }, 'Unsupported event') - } -} diff --git a/backend/src/serverless/integrations/workers/stripeWebhookWorker.ts b/backend/src/serverless/integrations/workers/stripeWebhookWorker.ts deleted file mode 100644 index 813d1c2f3a..0000000000 --- a/backend/src/serverless/integrations/workers/stripeWebhookWorker.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { getServiceChildLogger } from '@crowd/logging' -import { getRedisClient, RedisPubSubEmitter } from '@crowd/redis' -import moment from 'moment' -import { Stripe } from 'stripe' -import { timeout } from '@crowd/common' -import { ApiWebsocketMessage } from '@crowd/types' -import { PLANS_CONFIG, REDIS_CONFIG } from '../../../conf' -import SequelizeRepository from '../../../database/repositories/sequelizeRepository' -import Plans from '../../../security/plans' -import { NodeWorkerMessageBase } from '../../../types/mq/nodeWorkerMessageBase' -import { NodeWorkerMessageType } from '../../types/workerTypes' -import { sendNodeWorkerMessage } from '../../utils/nodeWorkerSQS' - -const log = getServiceChildLogger('stripeWebhookWorker') - -const stripe = new Stripe(PLANS_CONFIG.stripeSecretKey, { - apiVersion: '2022-08-01', - typescript: true, -}) - -export default async function stripeWebhookWorker(req) { - const sig = req.headers['stripe-signature'] - let event - - try { - event = stripe.webhooks.constructEvent(req.rawBody, sig, PLANS_CONFIG.stripWebhookSigningSecret) - await sendNodeWorkerMessage(event.id, { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - event, - service: 'stripe-webhooks', - } as NodeWorkerMessageBase) - } catch (err) { - log.error(`Webhook Error: ${err.message}`) - return { - status: 400, - } - } - - return { - status: 200, - } -} - -export const processStripeWebhook = async (message: any) => { - log.info({ message }, 'Got event from stripe webhook!') - log.warn(message) - - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - const redis = await getRedisClient(REDIS_CONFIG, true) - - const apiPubSubEmitter = new RedisPubSubEmitter( - 'api-pubsub', - redis, - (err) => { - log.error({ err }, 'Error in api-ws emitter!') - }, - log, - ) - const stripeWebhookMessage = message.event - - switch (stripeWebhookMessage.type) { - case 'checkout.session.completed': { - log.info( - { tenant: stripeWebhookMessage.data.object.client_reference_id }, - 'Processing checkout.session.complete', - ) - - // get subscription information from checkout event - const subscription = await stripe.subscriptions.retrieve( - stripeWebhookMessage.data.object.subscription, - ) - - const subscriptionEndsAt = subscription.current_period_end - const tenantId = stripeWebhookMessage.data.object.client_reference_id - - const tenant = await options.database.tenant.findByPk(tenantId) - - let productPlan - - if ((subscription as any).plan.product === PLANS_CONFIG.stripeEagleEyePlanProductId) { - productPlan = Plans.values.eagleEye - } else if ((subscription as any).plan.product === PLANS_CONFIG.stripeGrowthPlanProductId) { - productPlan = Plans.values.growth - } else { - log.error({ subscription }, `Unknown product in subscription`) - process.exit(1) - } - - if (!tenant) { - log.error({ tenantId }, 'Tenant not found!') - process.exit(1) - } else { - log.info({ tenantId }, `Tenant found - updating tenant plan to ${productPlan} plan!`) - await tenant.update({ - plan: productPlan, - isTrialPlan: false, - trialEndsAt: null, - stripeSubscriptionId: stripeWebhookMessage.data.object.subscription, - planSubscriptionEndsAt: moment(subscriptionEndsAt, 'X').toISOString(), - }) - - log.info('Emitting to redis pubsub for websocket forwarding from api..') - - // Wait few more seconds to ensure redirect is completed - await timeout(3000) - - // Send websocket message to frontend - apiPubSubEmitter.emit( - 'user', - new ApiWebsocketMessage( - 'tenant-plan-upgraded', - JSON.stringify({ - plan: productPlan, - stripeSubscriptionId: stripeWebhookMessage.data.object.subscription, - }), - undefined, - tenantId, - ), - ) - log.info('Done!') - } - - break - } - case 'invoice.payment_succeeded': { - // Since we're already updating the plan on session.completed event, - // we only need to process this event when billing_reason = `subscription_cycle` for the recurring payments. - // When subscription is newly created, billing_reason is `subscription_create` - log.info(stripeWebhookMessage.data.object.billing_reason, 'Invoice payment event') - - if (stripeWebhookMessage.data.object.billing_reason === 'subscription_cycle') { - // find tenant by stripeSubscriptionId - const tenant = await options.database.tenant.findOne({ - where: { stripeSubscriptionId: stripeWebhookMessage.data.object.subscription }, - }) - - const subscription = await stripe.subscriptions.retrieve( - stripeWebhookMessage.data.object.subscription, - ) - - await tenant.update({ - planSubscriptionEndsAt: moment(subscription.current_period_end, 'X').toISOString(), - }) - } - - break - } - default: - log.info({ event: message.event }, 'Unsupported event') - } -} diff --git a/backend/src/serverless/microservices/nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts b/backend/src/serverless/microservices/nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts deleted file mode 100644 index 55bb0d4ea3..0000000000 --- a/backend/src/serverless/microservices/nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts +++ /dev/null @@ -1,633 +0,0 @@ -import moment from 'moment' -import { RedisCache, getRedisClient } from '@crowd/redis' -import { QueryTypes } from 'sequelize' -import { convert as convertHtmlToText } from 'html-to-text' -import { getServiceChildLogger } from '@crowd/logging' -import { ActivityDisplayVariant, PlatformType } from '@crowd/types' -import getUserContext from '../../../../../database/utils/getUserContext' -import CubeJsService from '../../../../../services/cubejs/cubeJsService' -import EmailSender from '../../../../../services/emailSender' -import ConversationService from '../../../../../services/conversationService' -import { SENDGRID_CONFIG, S3_CONFIG, WEEKLY_EMAILS_CONFIG, REDIS_CONFIG } from '../../../../../conf' -import CubeJsRepository from '../../../../../cubejs/cubeJsRepository' -import { AnalyticsEmailsOutput } from '../../messageTypes' -import getStage from '../../../../../services/helpers/getStage' -import UserRepository from '../../../../../database/repositories/userRepository' -import ConversationRepository from '../../../../../database/repositories/conversationRepository' -import RecurringEmailsHistoryRepository from '../../../../../database/repositories/recurringEmailsHistoryRepository' -import { sendNodeWorkerMessage } from '../../../../utils/nodeWorkerSQS' -import { NodeWorkerMessageType } from '../../../../types/workerTypes' -import { NodeWorkerMessageBase } from '../../../../../types/mq/nodeWorkerMessageBase' -import { RecurringEmailType } from '../../../../../types/recurringEmailsHistoryTypes' -import SegmentRepository from '../../../../../database/repositories/segmentRepository' -import ActivityDisplayService from '@/services/activityDisplayService' - -const log = getServiceChildLogger('weeklyAnalyticsEmailsWorker') - -const MAX_RETRY_COUNT = 5 - -/** - * Sends weekly analytics emails of a given tenant - * to all users of the tenant. - * Data sent is for the last week. - * @param tenantId - */ -async function weeklyAnalyticsEmailsWorker(tenantId: string): Promise { - log.info(tenantId, `Processing tenant's weekly emails...`) - const response = await getAnalyticsData(tenantId) - const userContext = await getUserContext(tenantId) - - if (response.shouldRetry) { - if (WEEKLY_EMAILS_CONFIG.enabled !== 'true') { - log.info(`Weekly emails are disabled. Not retrying.`) - - return { - status: 200, - msg: `Weekly emails are disabled. Not retrying.`, - emailSent: false, - } - } - - log.error( - response.error, - 'Exception while getting analytics data. Retrying with a new message.', - ) - - // expception while getting data. send new node message and return - await sendNodeWorkerMessage(tenantId, { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - tenant: tenantId, - service: 'weekly-analytics-emails', - } as NodeWorkerMessageBase) - - return { - status: 400, - msg: `Exception while getting analytics data. Retrying with a new mq message.`, - emailSent: false, - } - } - - if (!userContext.currentUser) { - const message = `Tenant(${tenantId}) doesn't have any active users.` - log.info(message) - return { - status: 200, - msg: message, - emailSent: false, - } - } - - const { - dateTimeStartThisWeek, - dateTimeEndThisWeek, - totalMembersThisWeek, - totalMembersPreviousWeek, - activeMembersThisWeek, - activeMembersPreviousWeek, - newMembersThisWeek, - newMembersPreviousWeek, - mostActiveMembers, - totalOrganizationsThisWeek, - totalOrganizationsPreviousWeek, - activeOrganizationsThisWeek, - activeOrganizationsPreviousWeek, - newOrganizationsThisWeek, - newOrganizationsPreviousWeek, - mostActiveOrganizations, - totalActivitiesThisWeek, - totalActivitiesPreviousWeek, - newActivitiesThisWeek, - newActivitiesPreviousWeek, - topActivityTypes, - conversations, - activeTenantIntegrations, - } = response.data as any - - const rehRepository = new RecurringEmailsHistoryRepository(userContext) - - const isEmailAlreadySent = - (await rehRepository.findByWeekOfYear( - tenantId, - moment().utc().startOf('isoWeek').subtract(7, 'days').isoWeek().toString(), - RecurringEmailType.WEEKLY_ANALYTICS, - )) !== null - - if (activeTenantIntegrations.length > 0 && !isEmailAlreadySent) { - log.info(tenantId, ` has completed integrations. Eligible for weekly emails.. `) - const allTenantUsers = await UserRepository.findAllUsersOfTenant(tenantId) - - const advancedSuppressionManager = { - groupId: parseInt(SENDGRID_CONFIG.weeklyAnalyticsUnsubscribeGroupId, 10), - groupsToDisplay: [parseInt(SENDGRID_CONFIG.weeklyAnalyticsUnsubscribeGroupId, 10)], - } - - const emailSentTo: string[] = [] - - for (const user of allTenantUsers) { - if (user.email && user.emailVerified) { - const userFirstName = user.firstName ? user.firstName : user.email.split('@')[0] - - const data = { - dateRangePretty: `${dateTimeStartThisWeek.format( - 'D MMM YYYY', - )} - ${dateTimeEndThisWeek.format('D MMM YYYY')}`, - members: { - total: { - value: totalMembersThisWeek, - ...getChangeAndDirection(totalMembersThisWeek, totalMembersPreviousWeek), - }, - new: { - value: newMembersThisWeek, - ...getChangeAndDirection(newMembersThisWeek, newMembersPreviousWeek), - }, - active: { - value: activeMembersThisWeek, - ...getChangeAndDirection(activeMembersThisWeek, activeMembersPreviousWeek), - }, - mostActive: mostActiveMembers, - }, - organizations: { - total: { - value: totalOrganizationsThisWeek, - ...getChangeAndDirection(totalOrganizationsThisWeek, totalOrganizationsPreviousWeek), - }, - new: { - value: newOrganizationsThisWeek, - ...getChangeAndDirection(newOrganizationsThisWeek, newOrganizationsPreviousWeek), - }, - active: { - value: activeOrganizationsThisWeek, - ...getChangeAndDirection( - activeOrganizationsThisWeek, - activeOrganizationsPreviousWeek, - ), - }, - mostActive: mostActiveOrganizations, - }, - activities: { - total: { - value: totalActivitiesThisWeek, - ...getChangeAndDirection(totalActivitiesThisWeek, totalActivitiesPreviousWeek), - }, - new: { - value: newActivitiesThisWeek, - ...getChangeAndDirection(newActivitiesThisWeek, newActivitiesPreviousWeek), - }, - topActivityTypes, - }, - conversations, - tenant: { - name: userContext.currentTenant.name, - }, - user: { - name: userFirstName, - }, - } - - await new EmailSender(EmailSender.TEMPLATES.WEEKLY_ANALYTICS, data).sendTo( - user.email, - advancedSuppressionManager, - ) - - await new EmailSender(EmailSender.TEMPLATES.WEEKLY_ANALYTICS, data).sendTo( - 'team@crowd.dev', - advancedSuppressionManager, - ) - - emailSentTo.push(user.email) - } - } - - const reHistory = await rehRepository.create({ - tenantId, - type: RecurringEmailType.WEEKLY_ANALYTICS, - weekOfYear: dateTimeStartThisWeek.isoWeek().toString(), - emailSentAt: moment().toISOString(), - emailSentTo, - }) - - log.info({ receipt: reHistory }, `Email sent!`) - - return { status: 200, emailSent: true } - } - - if (isEmailAlreadySent) { - log.warn({ tenantId }, 'E-mail is already sent for this tenant this week. Skipping!') - } else { - log.info({ tenantId }, 'No active integrations present in the tenant. Email will not be sent.') - } - - return { - status: 200, - msg: `No active integrations present in the tenant. Email will not be sent.`, - emailSent: false, - } -} - -async function getAnalyticsData(tenantId: string) { - try { - const s3Url = `https://${ - S3_CONFIG.microservicesAssetsBucket - }-${getStage()}.s3.eu-central-1.amazonaws.com` - - const unixEpoch = moment.unix(0) - - const dateTimeEndThisWeek = moment().utc().startOf('isoWeek') - const dateTimeStartThisWeek = moment().utc().startOf('isoWeek').subtract(7, 'days') - - const dateTimeEndPreviousWeek = dateTimeStartThisWeek.clone() - const dateTimeStartPreviousWeek = dateTimeStartThisWeek.clone().subtract(7, 'days') - - const userContext = await getUserContext(tenantId) - - const cjs = new CubeJsService() - const segmentRepository = new SegmentRepository(userContext) - const subprojects = await segmentRepository.querySubprojects({}) - const segmentIds = subprojects.rows.map((subproject) => subproject.id) - // tokens should be set for each tenant - await cjs.init(tenantId, segmentIds) - - // members - const totalMembersThisWeek = await CubeJsRepository.getNewMembers( - cjs, - unixEpoch, - dateTimeEndThisWeek, - ) - - const totalMembersPreviousWeek = await CubeJsRepository.getNewMembers( - cjs, - unixEpoch, - dateTimeEndPreviousWeek, - ) - - const activeMembersThisWeek = await CubeJsRepository.getActiveMembers( - cjs, - dateTimeStartThisWeek, - dateTimeEndThisWeek, - ) - const activeMembersPreviousWeek = await CubeJsRepository.getActiveMembers( - cjs, - dateTimeStartPreviousWeek, - dateTimeEndPreviousWeek, - ) - - const newMembersThisWeek = await CubeJsRepository.getNewMembers( - cjs, - dateTimeStartThisWeek, - dateTimeEndThisWeek, - ) - const newMembersPreviousWeek = await CubeJsRepository.getNewMembers( - cjs, - dateTimeStartPreviousWeek, - dateTimeEndPreviousWeek, - ) - - const mostActiveMembers = ( - await userContext.database.sequelize.query( - ` - select - count(a.id) as "activityCount", - m."displayName" as name, - m.attributes->'avatarUrl'->>'default' as "avatarUrl" - from members m - inner join activities a on m.id = a."memberId" - where m."tenantId" = :tenantId - and a.timestamp between :startDate and :endDate - and coalesce(m.attributes->'isTeamMember'->>'default', 'false')::boolean is false - and coalesce(m.attributes->'isBot'->>'default', 'false')::boolean is false - group by m.id - order by count(a.id) desc - limit 5;`, - { - replacements: { - tenantId, - startDate: dateTimeStartThisWeek.toISOString(), - endDate: dateTimeEndThisWeek.toISOString(), - }, - type: QueryTypes.SELECT, - }, - ) - ).map((m) => { - if (!m.avatarUrl) { - m.avatarUrl = `${s3Url}/email/member-placeholder.png` - } - return m - }) - - // organizations - const totalOrganizationsThisWeek = await CubeJsRepository.getNewOrganizations( - cjs, - unixEpoch, - dateTimeEndThisWeek, - ) - const totalOrganizationsPreviousWeek = await CubeJsRepository.getNewOrganizations( - cjs, - unixEpoch, - dateTimeEndPreviousWeek, - ) - - const activeOrganizationsThisWeek = await CubeJsRepository.getActiveOrganizations( - cjs, - dateTimeStartThisWeek, - dateTimeEndThisWeek, - ) - const activeOrganizationsPreviousWeek = await CubeJsRepository.getActiveOrganizations( - cjs, - dateTimeStartPreviousWeek, - dateTimeEndPreviousWeek, - ) - - const newOrganizationsThisWeek = await CubeJsRepository.getNewOrganizations( - cjs, - dateTimeStartThisWeek, - dateTimeEndThisWeek, - ) - const newOrganizationsPreviousWeek = await CubeJsRepository.getNewOrganizations( - cjs, - dateTimeStartPreviousWeek, - dateTimeEndPreviousWeek, - ) - - const mostActiveOrganizations = ( - await userContext.database.sequelize.query( - ` - select count(a.id) as "activityCount", - o.name as name, - o.logo as "avatarUrl" - from organizations o - inner join "memberOrganizations" mo - on o.id = mo."organizationId" - and mo."deletedAt" is null - inner join members m on mo."memberId" = m.id - inner join activities a on m.id = a."memberId" - where m."tenantId" = :tenantId - and a.timestamp between :startDate and :endDate - and coalesce(m.attributes->'isTeamMember'->>'default', 'false')::boolean is false - and coalesce(m.attributes->'isBot'->>'default', 'false')::boolean is false - group by o.id - order by count(a.id) desc - limit 5;`, - { - replacements: { - tenantId, - startDate: dateTimeStartThisWeek.toISOString(), - endDate: dateTimeEndThisWeek.toISOString(), - }, - type: QueryTypes.SELECT, - }, - ) - ).map((o) => { - if (!o.avatarUrl) { - o.avatarUrl = `${s3Url}/email/organization-placeholder.png` - } - return o - }) - - // activities - const totalActivitiesThisWeek = await CubeJsRepository.getNewActivities( - cjs, - unixEpoch, - dateTimeEndThisWeek, - ) - const totalActivitiesPreviousWeek = await CubeJsRepository.getNewActivities( - cjs, - unixEpoch, - dateTimeEndPreviousWeek, - ) - - const newActivitiesThisWeek = await CubeJsRepository.getNewActivities( - cjs, - dateTimeStartThisWeek, - dateTimeEndThisWeek, - ) - const newActivitiesPreviousWeek = await CubeJsRepository.getNewActivities( - cjs, - dateTimeStartPreviousWeek, - dateTimeEndPreviousWeek, - ) - - let topActivityTypes = await userContext.database.sequelize.query( - ` - select sum(count(*)) OVER () as "totalCount", - count(*) as count, - a.type, - a.platform - from activities a - where a."tenantId" = :tenantId - and a.timestamp between :startDate and :endDate - group by a.type, a.platform - order by count(*) desc - limit 5;`, - { - replacements: { - tenantId, - startDate: dateTimeStartThisWeek.toISOString(), - endDate: dateTimeEndThisWeek.toISOString(), - }, - type: QueryTypes.SELECT, - }, - ) - - topActivityTypes = topActivityTypes.map((a) => { - const displayOptions = ActivityDisplayService.getDisplayOptions( - { - platform: a.platform, - type: a.type, - }, - SegmentRepository.getActivityTypes(userContext), - [ActivityDisplayVariant.SHORT], - ) - const prettyName: string = displayOptions.short - a.type = prettyName[0].toUpperCase() + prettyName.slice(1) - a.percentage = Number((a.count / a.totalCount) * 100).toFixed(2) - a.platformIcon = `${s3Url}/email/${a.platform}.png` - return a - }) - - // conversations - const cs = new ConversationService(userContext) - - const conversations = await Promise.all( - ( - await userContext.database.sequelize.query( - ` - select - c.id - from conversations c - join activities a on a."conversationId" = c.id - where a."tenantId" = :tenantId - and a.timestamp between :startDate and :endDate - group by c.id - order by count(a.id) desc - limit 3;`, - { - replacements: { - tenantId, - startDate: dateTimeStartThisWeek.toISOString(), - endDate: dateTimeEndThisWeek.toISOString(), - }, - type: QueryTypes.SELECT, - }, - ) - ).map(async (c) => { - const conversationLazyLoaded = await cs.findById(c.id) - - const conversationStarterActivity = conversationLazyLoaded.activities[0] - - c.conversationStartedFromNow = moment(conversationStarterActivity.timestamp).fromNow() - - const replyActivities = conversationLazyLoaded.activities.slice(1) - - c.replyCount = replyActivities.length - - c.memberCount = await ConversationRepository.getTotalMemberCount(replyActivities) - - c.platform = conversationStarterActivity.platform - - c.body = conversationStarterActivity.title - ? convertHtmlToText(conversationStarterActivity.title) - : convertHtmlToText(conversationStarterActivity.body) - - c.platformIcon = `${s3Url}/email/${conversationStarterActivity.platform}.png` - - const displayOptions = ActivityDisplayService.getDisplayOptions( - conversationStarterActivity, - SegmentRepository.getActivityTypes(userContext), - [ActivityDisplayVariant.SHORT], - ) - - let prettyChannel = conversationStarterActivity.channel - - let prettyChannelHTML = `${prettyChannel}` - - if (conversationStarterActivity.platform === PlatformType.GITHUB) { - const prettyChannelSplitted = prettyChannel.split('/') - prettyChannel = prettyChannelSplitted[prettyChannelSplitted.length - 1] - prettyChannelHTML = `${prettyChannel}` - } - - c.description = `${displayOptions.short} in ${prettyChannelHTML}` - - c.sourceLink = conversationStarterActivity.url - - c.member = conversationStarterActivity.member.displayName - - return c - }), - ) - - const activeTenantIntegrations = await userContext.database.sequelize.query( - ` - select * from integrations i - where i."tenantId" = :tenantId - and i.status = 'done' - and i."deletedAt" is null - limit 1;`, - { - replacements: { - tenantId, - }, - type: QueryTypes.SELECT, - }, - ) - - return { - shouldRetry: false, - data: { - dateTimeStartThisWeek, - dateTimeEndThisWeek, - dateTimeStartPreviousWeek, - dateTimeEndPreviousWeek, - totalMembersThisWeek, - totalMembersPreviousWeek, - activeMembersThisWeek, - activeMembersPreviousWeek, - newMembersThisWeek, - newMembersPreviousWeek, - mostActiveMembers, - totalOrganizationsThisWeek, - totalOrganizationsPreviousWeek, - activeOrganizationsThisWeek, - activeOrganizationsPreviousWeek, - newOrganizationsThisWeek, - newOrganizationsPreviousWeek, - mostActiveOrganizations, - totalActivitiesThisWeek, - totalActivitiesPreviousWeek, - newActivitiesThisWeek, - newActivitiesPreviousWeek, - topActivityTypes, - conversations, - activeTenantIntegrations, - }, - } - } catch (e) { - // check redis for retry count - const redis = await getRedisClient(REDIS_CONFIG, true) - const weeklyEmailsRetryCountsCache = new RedisCache('weeklyEmailsRetryCounts', redis, log) - const retryCount = await weeklyEmailsRetryCountsCache.get(tenantId) - - if (!retryCount) { - weeklyEmailsRetryCountsCache.set(tenantId, '0', 432000) // set the ttl for 5 days - return { - shouldRetry: true, - data: {}, - error: e, - } - } - - const parsedRetryCount = parseInt(retryCount, 10) - if (parsedRetryCount < MAX_RETRY_COUNT) { - log.info(`Current retryCount for tenant is: ${retryCount}, trying to send the e-mail again!`) - // increase retryCount and retry the email - weeklyEmailsRetryCountsCache.set(tenantId, (parsedRetryCount + 1).toString(), 432000) - return { - shouldRetry: true, - data: {}, - error: e, - } - } - - log.info( - { error: JSON.stringify(e) }, - `Retried total of ${MAX_RETRY_COUNT} times. Skipping sending e-mail!`, - ) - return { - shouldRetry: false, - data: {}, - error: e, - } - } -} - -function getChangeAndDirection(thisWeekValue: number, previousWeekValue: number) { - let changeAndDirection - - if (thisWeekValue > previousWeekValue) { - changeAndDirection = { - changeVsLastWeek: thisWeekValue - previousWeekValue, - changeVsLastWeekPercentage: Number( - ((thisWeekValue - previousWeekValue) / thisWeekValue) * 100, - ).toFixed(2), - changeVsLastWeekDerivative: 'increasing', - } - } else if (thisWeekValue === previousWeekValue) { - changeAndDirection = { - changeVsLastWeek: 0, - changeVsLastWeekPercentage: 0, - changeVsLastWeekDerivative: 'equal', - } - } else { - changeAndDirection = { - changeVsLastWeek: previousWeekValue - thisWeekValue, - changeVsLastWeekPercentage: Number( - ((previousWeekValue - thisWeekValue) / previousWeekValue) * 100, - ).toFixed(2), - changeVsLastWeekDerivative: 'decreasing', - } - } - - return changeAndDirection -} - -export { weeklyAnalyticsEmailsWorker } diff --git a/backend/src/serverless/microservices/nodejs/automation/workers/__tests__/newActivityWorker.test.ts b/backend/src/serverless/microservices/nodejs/automation/workers/__tests__/newActivityWorker.test.ts deleted file mode 100644 index cb5a2bde69..0000000000 --- a/backend/src/serverless/microservices/nodejs/automation/workers/__tests__/newActivityWorker.test.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { generateUUIDv4 as uuid } from '@crowd/common' -import { - AutomationData, - AutomationState, - AutomationTrigger, - AutomationType, - NewActivitySettings, -} from '../../../../../../types/automationTypes' -import { PlatformType } from '@crowd/types' -import { shouldProcessActivity } from '../newActivityWorker' - -function createAutomationData(settings: NewActivitySettings): AutomationData { - return { - id: uuid(), - name: 'Activity test', - state: AutomationState.ACTIVE, - trigger: AutomationTrigger.NEW_ACTIVITY, - settings, - type: AutomationType.WEBHOOK, - createdAt: new Date().toISOString(), - tenantId: '321', - lastExecutionAt: null, - lastExecutionError: null, - lastExecutionState: null, - } -} - -describe('New Activity Automation Worker tests', () => { - it('Should process an activity that matches settings', async () => { - const automation = createAutomationData({ - platforms: [PlatformType.DEVTO], - types: ['comment'], - keywords: ['Crowd.dev'], - teamMemberActivities: false, - }) - - const activity = { - id: '1234', - type: 'comment', - platform: PlatformType.DEVTO, - body: 'Crowd.dev is awesome!', - member: { - attributes: {}, - }, - } - - expect(await shouldProcessActivity(activity, automation)).toBeTruthy() - }) - - it("Shouldn't process an activity that belongs to a team member", async () => { - const automation = createAutomationData({ - platforms: [PlatformType.DEVTO], - types: ['comment'], - keywords: ['Crowd.dev'], - teamMemberActivities: true, - }) - - const activity = { - id: '1234', - type: 'comment', - platform: PlatformType.DEVTO, - member: { - attributes: { - isTeamMember: { - default: true, - custom: true, - }, - isBot: { - default: false, - }, - }, - }, - - body: 'Crowd.dev all awesome!', - } - - expect(await shouldProcessActivity(activity, automation)).toBeTruthy() - }) - - it("Shouldn't process an activity which platform does not match", async () => { - const automation = createAutomationData({ - platforms: [PlatformType.DEVTO], - types: ['comment'], - keywords: ['Crowd.dev'], - teamMemberActivities: false, - }) - - const activity = { - id: '1234', - type: 'comment', - platform: PlatformType.DISCORD, - body: 'Crowd.dev is awesome!', - } - - expect(await shouldProcessActivity(activity, automation)).toBeFalsy() - }) - - it("Shouldn't process an activity which type does not match", async () => { - const automation = createAutomationData({ - platforms: [PlatformType.DEVTO], - types: ['comment'], - keywords: ['Crowd.dev'], - teamMemberActivities: false, - }) - - const activity = { - id: '1234', - type: 'follow', - platform: PlatformType.DEVTO, - body: 'Crowd.dev is awesome!', - } - - expect(await shouldProcessActivity(activity, automation)).toBeFalsy() - }) - - it("Shouldn't process an activity which keyword does not match", async () => { - const automation = createAutomationData({ - platforms: [PlatformType.DEVTO], - types: ['comment'], - keywords: ['Crowd.dev'], - teamMemberActivities: false, - }) - - const activity = { - id: '1234', - type: 'comment', - platform: PlatformType.DEVTO, - body: 'We are all awesome!', - } - - expect(await shouldProcessActivity(activity, automation)).toBeFalsy() - }) - - it("Shouldn't process an activity that belongs to a bot", async () => { - const automation = createAutomationData({ - platforms: [PlatformType.DEVTO], - types: ['comment'], - keywords: ['Crowd.dev'], - teamMemberActivities: false, - }) - - const activity = { - id: '1234', - type: 'comment', - platform: PlatformType.DEVTO, - member: { - attributes: { - isTeamMember: { - default: false, - custom: true, - }, - isBot: { - default: true, - }, - }, - }, - body: 'Crowd.dev all awesome!', - } - - expect(await shouldProcessActivity(activity, automation)).toBeFalsy() - }) - - it("Shouldn't process an activity that belongs to a team member", async () => { - const automation = createAutomationData({ - platforms: [PlatformType.DEVTO], - types: ['comment'], - keywords: ['Crowd.dev'], - teamMemberActivities: false, - }) - - const activity = { - id: '1234', - type: 'comment', - platform: PlatformType.DEVTO, - member: { - attributes: { - isTeamMember: { - default: true, - custom: true, - }, - isBot: { - default: false, - }, - }, - }, - body: 'Crowd.dev all awesome!', - } - - expect(await shouldProcessActivity(activity, automation)).toBeFalsy() - }) -}) diff --git a/backend/src/serverless/microservices/nodejs/automation/workers/__tests__/newMemberWorker.test.ts b/backend/src/serverless/microservices/nodejs/automation/workers/__tests__/newMemberWorker.test.ts deleted file mode 100644 index c30dbe132a..0000000000 --- a/backend/src/serverless/microservices/nodejs/automation/workers/__tests__/newMemberWorker.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { generateUUIDv4 as uuid } from '@crowd/common' -import { - AutomationData, - AutomationState, - AutomationTrigger, - AutomationType, - NewMemberSettings, -} from '../../../../../../types/automationTypes' -import { PlatformType } from '@crowd/types' -import { shouldProcessMember } from '../newMemberWorker' - -function createAutomationData(settings: NewMemberSettings): AutomationData { - return { - id: uuid(), - name: 'Member test', - state: AutomationState.ACTIVE, - trigger: AutomationTrigger.NEW_MEMBER, - settings, - type: AutomationType.WEBHOOK, - createdAt: new Date().toISOString(), - tenantId: '321', - lastExecutionAt: null, - lastExecutionError: null, - lastExecutionState: null, - } -} - -describe('New Member Automation Worker tests', () => { - it('Should process a worker that matches settings', async () => { - const automation = createAutomationData({ - platforms: [PlatformType.DISCORD], - }) - - const member = { - id: '1234', - username: { - [PlatformType.DISCORD]: 'discordUsername', - }, - } - - expect(await shouldProcessMember(member, automation)).toBeTruthy() - }) - - it("Shouldn't process a worker that does not match settings", async () => { - const automation = createAutomationData({ - platforms: [PlatformType.DEVTO], - }) - - const member = { - id: '1234', - username: { - [PlatformType.DISCORD]: 'discordUsername', - }, - } - - expect(await shouldProcessMember(member, automation)).toBeFalsy() - }) -}) diff --git a/backend/src/serverless/microservices/nodejs/automation/workers/newActivityWorker.ts b/backend/src/serverless/microservices/nodejs/automation/workers/newActivityWorker.ts deleted file mode 100644 index 83f7f9d5f9..0000000000 --- a/backend/src/serverless/microservices/nodejs/automation/workers/newActivityWorker.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { getServiceChildLogger } from '@crowd/logging' -import getUserContext from '../../../../../database/utils/getUserContext' -import ActivityRepository from '../../../../../database/repositories/activityRepository' -import AutomationRepository from '../../../../../database/repositories/automationRepository' -import { - AutomationData, - AutomationState, - AutomationTrigger, - AutomationType, - NewActivitySettings, -} from '../../../../../types/automationTypes' -import { sendWebhookProcessRequest } from './util' -import { prepareMemberPayload } from './newMemberWorker' -import AutomationExecutionRepository from '../../../../../database/repositories/automationExecutionRepository' -import SequelizeRepository from '../../../../../database/repositories/sequelizeRepository' -import MemberRepository from '../../../../../database/repositories/memberRepository' - -const log = getServiceChildLogger('newActivityWorker') - -/** - * Helper function to check whether a single activity should be processed by automation - * @param activity Activity data - * @param automation {AutomationData} Automation data - */ -export const shouldProcessActivity = async ( - activity: any, - automation: AutomationData, -): Promise => { - const settings = automation.settings as NewActivitySettings - - let process = true - - // check whether activity type matches - if (settings.types && settings.types.length > 0) { - if (!settings.types.includes(activity.type)) { - log.warn( - `Ignoring automation ${automation.id} - Activity ${activity.id} type '${ - activity.type - }' does not match automation setting types: [${settings.types.join(', ')}]`, - ) - process = false - } - } - - // check whether activity platform matches - if (process && settings.platforms && settings.platforms.length > 0) { - if (!settings.platforms.includes(activity.platform)) { - log.warn( - `Ignoring automation ${automation.id} - Activity ${activity.id} platform '${ - activity.platform - }' does not match automation setting platforms: [${settings.platforms.join(', ')}]`, - ) - process = false - } - } - - // check whether activity content contains any of the keywords - if (process && settings.keywords && settings.keywords.length > 0) { - const body = (activity.body as string).toLowerCase() - if (!settings.keywords.some((keyword) => body.includes(keyword.trim().toLowerCase()))) { - log.warn( - `Ignoring automation ${automation.id} - Activity ${ - activity.id - } content does not match automation setting keywords: [${settings.keywords.join(', ')}]`, - ) - process = false - } - } - - if ( - process && - !settings.teamMemberActivities && - activity.member.attributes.isTeamMember && - activity.member.attributes.isTeamMember.default - ) { - log.warn( - `Ignoring automation ${automation.id} - Activity ${activity.id} belongs to a team member!`, - ) - process = false - } - - if (activity?.member?.attributes?.isBot && activity?.member?.attributes?.isBot.default) { - log.warn( - `Ignoring automation ${automation.id} - Activity ${activity.id} belongs to a bot, cannot be processed automaticaly!`, - ) - process = false - } - - if (process) { - const userContext = await SequelizeRepository.getDefaultIRepositoryOptions() - const repo = new AutomationExecutionRepository(userContext) - - const hasAlreadyBeenTriggered = await repo.hasAlreadyBeenTriggered(automation.id, activity.id) - if (hasAlreadyBeenTriggered) { - log.warn( - `Ignoring automation ${automation.id} - Activity ${activity.id} was already processed!`, - ) - process = false - } - } - - return process -} - -/** - * Return a cleaned up copy of the activity that contains only data that is relevant for automation. - * - * @param activity Activity data as it came from the repository layer - * @returns a cleaned up payload to use with automation - */ -export const prepareActivityPayload = (activity: any): any => { - const copy = { ...activity } - - delete copy.importHash - delete copy.updatedAt - delete copy.updatedById - delete copy.deletedAt - if (copy.member) { - copy.member = prepareMemberPayload(copy.member) - } - if (copy.parent) { - copy.parent = prepareActivityPayload(copy.parent) - } - - return copy -} - -/** - * Check whether this activity matches any automations for tenant. - * If so emit automation process messages to NodeJS microservices SQS queue. - * - * @param tenantId tenant unique ID - * @param activityId activity unique ID - * @param activityData activity data - */ -export default async (tenantId: string, activityId: string, segmentId: string): Promise => { - const userContext = await getUserContext(tenantId, null, [segmentId]) - - try { - // check if relevant automations exists in this tenant - const automations = await new AutomationRepository(userContext).findAll({ - trigger: AutomationTrigger.NEW_ACTIVITY, - state: AutomationState.ACTIVE, - }) - - if (automations.length > 0) { - log.info(`Found ${automations.length} automations to process!`) - let activity = await ActivityRepository.findById(activityId, userContext) - - if (activity.member?.id) { - const member = await MemberRepository.findById(activity.member.id, userContext) - activity = { - ...activity, - member, - engagement: member?.score || 0, - } - } - - for (const automation of automations) { - if (await shouldProcessActivity(activity, automation)) { - log.info(`Activity ${activity.id} is being processed by automation ${automation.id}!`) - - switch (automation.type) { - case AutomationType.WEBHOOK: - await sendWebhookProcessRequest( - tenantId, - automation, - activity.id, - prepareActivityPayload(activity), - AutomationType.WEBHOOK, - ) - break - case AutomationType.SLACK: - await sendWebhookProcessRequest( - tenantId, - automation, - activity.id, - prepareActivityPayload(activity), - AutomationType.SLACK, - ) - break - default: - log.error(`ERROR: Automation type '${automation.type}' is not supported!`) - } - } - } - } - } catch (error) { - log.error(error, 'Error while processing new activity automation trigger!') - throw error - } -} diff --git a/backend/src/serverless/microservices/nodejs/automation/workers/newMemberWorker.ts b/backend/src/serverless/microservices/nodejs/automation/workers/newMemberWorker.ts deleted file mode 100644 index f1c6c38b5e..0000000000 --- a/backend/src/serverless/microservices/nodejs/automation/workers/newMemberWorker.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { getServiceChildLogger } from '@crowd/logging' -import AutomationExecutionRepository from '../../../../../database/repositories/automationExecutionRepository' -import AutomationRepository from '../../../../../database/repositories/automationRepository' -import MemberRepository from '../../../../../database/repositories/memberRepository' -import SequelizeRepository from '../../../../../database/repositories/sequelizeRepository' -import getUserContext from '../../../../../database/utils/getUserContext' -import { - AutomationData, - AutomationState, - AutomationTrigger, - AutomationType, - NewMemberSettings, -} from '../../../../../types/automationTypes' -import { sendWebhookProcessRequest } from './util' - -const log = getServiceChildLogger('newMemberWorker') - -/** - * Helper function to check whether a single member should be processed by automation - * @param member Member data - * @param automation {AutomationData} Automation data - */ -export const shouldProcessMember = async ( - member: any, - automation: AutomationData, -): Promise => { - const settings = automation.settings as NewMemberSettings - - let process = true - - // check whether member platforms matches - if (settings.platforms && settings.platforms.length > 0) { - const platforms = Object.keys(member.username) - if (!platforms.some((platform) => settings.platforms.includes(platform))) { - log.warn( - `Ignoring automation ${automation.id} - Member ${ - member.id - } platforms do not include any of automation setting platforms: [${settings.platforms.join( - ', ', - )}]`, - ) - process = false - } - } - - if (process) { - const userContext = await SequelizeRepository.getDefaultIRepositoryOptions() - const repo = new AutomationExecutionRepository(userContext) - - const hasAlreadyBeenTriggered = await repo.hasAlreadyBeenTriggered(automation.id, member.id) - if (hasAlreadyBeenTriggered) { - log.warn(`Ignoring automation ${automation.id} - Member ${member.id} was already processed!`) - process = false - } - } - - return process -} - -/** - * Return a cleaned up copy of the member that contains only data that is relevant for automation. - * - * @param member Member data as it came from the repository layer - * @returns a cleaned up payload to use with automation - */ -export const prepareMemberPayload = (member: any): any => { - const copy = { ...member } - - delete copy.importHash - delete copy.signals - delete copy.type - delete copy.score - delete copy.updatedAt - delete copy.updatedById - delete copy.deletedAt - - return copy -} -/** - * Check whether this member matches any automations for tenant. - * If so emit automation process messages to NodeJS microservices SQS queue. - * - * @param tenantId tenant unique ID - * @param memberId tenant member ID - * @param memberData community member data - */ -export default async (tenantId: string, memberId: string, segmentId: string): Promise => { - const userContext = await getUserContext(tenantId, null, [segmentId]) - - try { - // check if relevant automation exists in this tenant - const automations = await new AutomationRepository(userContext).findAll({ - trigger: AutomationTrigger.NEW_MEMBER, - state: AutomationState.ACTIVE, - }) - - if (automations.length > 0) { - log.info(`Found ${automations.length} automations to process!`) - - const member = await MemberRepository.findById(memberId, userContext) - - for (const automation of automations) { - if (await shouldProcessMember(member, automation)) { - log.info(`Member ${member.id} is being processed by automation ${automation.id}!`) - - switch (automation.type) { - case AutomationType.WEBHOOK: - await sendWebhookProcessRequest( - tenantId, - automation, - member.id, - prepareMemberPayload(member), - AutomationType.WEBHOOK, - ) - break - case AutomationType.SLACK: - await sendWebhookProcessRequest( - tenantId, - automation, - member.id, - prepareMemberPayload(member), - AutomationType.SLACK, - ) - break - default: - log.error(`ERROR: Automation type '${automation.type}' is not supported!`) - } - } - } - } - } catch (error) { - log.error(error, 'Error while processing new member automation trigger!') - throw error - } -} diff --git a/backend/src/serverless/microservices/nodejs/automation/workers/slack/newActivityBlocks.ts b/backend/src/serverless/microservices/nodejs/automation/workers/slack/newActivityBlocks.ts deleted file mode 100644 index 125b9891f8..0000000000 --- a/backend/src/serverless/microservices/nodejs/automation/workers/slack/newActivityBlocks.ts +++ /dev/null @@ -1,219 +0,0 @@ -import htmlToMrkdwn from 'html-to-mrkdwn-ts' -import { integrationLabel, integrationProfileUrl } from '@crowd/types' -import { API_CONFIG } from '../../../../../../conf' - -const defaultAvatarUrl = - 'https://uploads-ssl.webflow.com/635150609746eee5c60c4aac/6502afc9d75946873c1efa93_image%20(292).png' - -const computeEngagementLevel = (score) => { - if (score <= 1) { - return 'Silent' - } - if (score <= 3) { - return 'Quiet' - } - if (score <= 6) { - return 'Engaged' - } - if (score <= 8) { - return 'Fan' - } - if (score <= 10) { - return 'Ultra' - } - return '' -} - -const replacements: Record = { - '/images/integrations/linkedin-reactions/like.svg': ':thumbsup:', - '/images/integrations/linkedin-reactions/maybe.svg': ':thinking_face:', - '/images/integrations/linkedin-reactions/praise.svg': ':clap:', - '/images/integrations/linkedin-reactions/appreciation.svg': ':heart_hands:', - '/images/integrations/linkedin-reactions/empathy.svg': ':heart:', - '/images/integrations/linkedin-reactions/entertainment.svg': ':laughing:', - '/images/integrations/linkedin-reactions/interest.svg': ':bulb:', - 'href="/': `href="${API_CONFIG.frontendUrl}/`, -} - -const replaceHeadline = (text) => { - Object.keys(replacements).forEach((key) => { - text = text.replaceAll(key, replacements[key]) - }) - return text -} - -const truncateText = (text: string, characters: number = 60): string => { - if (text.length > characters) { - return `${text.substring(0, characters)}...` - } - return text -} - -export const newActivityBlocks = (activity) => { - // Which platform identities are displayed as buttons and which ones go to menu - let buttonPlatforms = ['github', 'twitter', 'linkedin'] - - const display = htmlToMrkdwn(replaceHeadline(activity.display.default)) - const reach = activity.member.reach?.[activity.platform] || activity.member.reach?.total - - const { member } = activity - const memberProperties = [] - if (member.attributes.jobTitle?.default) { - memberProperties.push(`*💼 Job title:* ${member.attributes.jobTitle?.default}`) - } - if (member.organizations.length > 0) { - const orgs = member.organizations.map( - (org) => - `<${`${API_CONFIG.frontendUrl}/organizations/${org.id}`}|${org.name || org.displayName}>`, - ) - memberProperties.push(`*🏢 Organization:* ${orgs.join(' | ')}`) - } - if (reach > 0) { - memberProperties.push(`*👥 Reach:* ${reach} followers`) - } - if (member.attributes?.location?.default) { - memberProperties.push(`*📍 Location:* ${member.attributes?.location?.default}`) - } - if (member.emails.length > 0) { - const [email] = member.emails - memberProperties.push(`*✉️ Email:* `) - } - const engagementLevel = computeEngagementLevel(activity.member.score || activity.engagement) - if (engagementLevel.length > 0) { - memberProperties.push(`*📊 Engagement level:* ${engagementLevel}`) - } - if (activity.member.activeOn) { - const platforms = activity.member.activeOn - .map((platform) => integrationLabel[platform] || platform) - .join(' | ') - memberProperties.push(`*💬 Active on:* ${platforms}`) - } - - const profiles = Object.keys(member.username) - .map((p) => { - const username = (member.username?.[p] || []).length > 0 ? member.username[p][0] : null - const url = - member.attributes?.url?.[p] || (username && integrationProfileUrl[p](username)) || null - return { - platform: p, - url, - } - }) - .filter((p) => !!p.url) - - if (!buttonPlatforms.includes(activity.platform)) { - buttonPlatforms = [activity.platform, ...buttonPlatforms] - } - - const buttonProfiles = buttonPlatforms - .map((platform) => profiles.find((profile) => profile.platform === platform)) - .filter((profiles) => !!profiles) - - const menuProfiles = profiles.filter((profile) => !buttonPlatforms.includes(profile.platform)) - - return { - blocks: [ - { - type: 'section', - text: { - type: 'mrkdwn', - text: `*<${API_CONFIG.frontendUrl}/contacts/${activity.member.id}|${activity.member.displayName}>* *${display.text}*`, - }, - ...(activity.url - ? { - accessory: { - type: 'button', - text: { - type: 'plain_text', - text: `:arrow_upper_right: ${ - activity.platform !== 'other' - ? `Open on ${integrationLabel[activity.platform]}` - : 'Open link' - }`, - emoji: true, - }, - url: activity.url, - }, - } - : {}), - }, - ...(activity.title || activity.body - ? [ - { - type: 'section', - text: { - type: 'mrkdwn', - text: `>${ - activity.title && activity.title !== activity.display.default - ? `*${truncateText(htmlToMrkdwn(activity.title).text, 120).replaceAll( - '\n', - '\n>', - )}* \n>` - : '' - }${truncateText(htmlToMrkdwn(activity.body).text, 260).replaceAll('\n', '\n>')}`, - }, - }, - ] - : []), - ...(memberProperties.length > 0 - ? [ - { - type: 'divider', - }, - { - type: 'section', - text: { - type: 'mrkdwn', - text: memberProperties.join('\n'), - }, - accessory: { - type: 'image', - image_url: member.attributes?.avatarUrl?.default ?? defaultAvatarUrl, - alt_text: 'computer thumbnail', - }, - }, - ] - : []), - { - type: 'actions', - elements: [ - { - type: 'button', - text: { - type: 'plain_text', - text: 'View in crowd.dev', - emoji: true, - }, - url: `${API_CONFIG.frontendUrl}/contacts/${member.id}`, - }, - ...(buttonProfiles || []) - .map(({ platform, url }) => ({ - type: 'button', - text: { - type: 'plain_text', - text: `${integrationLabel[platform] ?? platform} profile`, - emoji: true, - }, - url, - })) - .filter((action) => !!action.url), - ...(menuProfiles.length > 0 - ? [ - { - type: 'overflow', - options: menuProfiles.map(({ platform, url }) => ({ - text: { - type: 'plain_text', - text: `${integrationLabel[platform] ?? platform} profile`, - emoji: true, - }, - url, - })), - }, - ] - : []), - ], - }, - ], - } -} diff --git a/backend/src/serverless/microservices/nodejs/automation/workers/slack/newMemberBlocks.ts b/backend/src/serverless/microservices/nodejs/automation/workers/slack/newMemberBlocks.ts deleted file mode 100644 index 80c3c0ecd7..0000000000 --- a/backend/src/serverless/microservices/nodejs/automation/workers/slack/newMemberBlocks.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { integrationLabel, integrationProfileUrl } from '@crowd/types' -import { API_CONFIG } from '../../../../../../conf' - -const defaultAvatarUrl = - 'https://uploads-ssl.webflow.com/635150609746eee5c60c4aac/6502afc9d75946873c1efa93_image%20(292).png' - -export const newMemberBlocks = (member) => { - // Which platform identities are displayed as buttons and which ones go to menu - let buttonPlatforms = ['github', 'twitter', 'linkedin'] - - const platforms = member.activeOn - const reach = - platforms && platforms.length > 0 ? member.reach?.[platforms[0]] : member.reach?.total - const details = [] - if (member.attributes.jobTitle?.default) { - details.push(`*💼 Job title:* ${member.attributes.jobTitle?.default}`) - } - if (member.organizations.length > 0) { - const orgs = member.organizations.map( - (org) => - `<${`${API_CONFIG.frontendUrl}/organizations/${org.id}`}|${org.name || org.displayName}>`, - ) - details.push(`*🏢 Organization:* ${orgs.join(' | ')}`) - } - if (reach > 0) { - details.push(`*👥 Reach:* ${reach} followers`) - } - if (member.attributes?.location?.default) { - details.push(`*📍 Location:* ${member.attributes?.location?.default}`) - } - if (member.emails.length > 0) { - const [email] = member.emails - details.push(`*✉️ Email:* `) - } - const profiles = Object.keys(member.username) - .map((p) => { - const username = (member.username?.[p] || []).length > 0 ? member.username[p][0] : null - const url = - member.attributes?.url?.[p] || (username && integrationProfileUrl[p](username)) || null - return { - platform: p, - url, - } - }) - .filter((p) => !!p.url) - - if (platforms.length > 0 && !buttonPlatforms.includes(platforms[0])) { - buttonPlatforms = [platforms[0], ...buttonPlatforms] - } - - const buttonProfiles = buttonPlatforms - .map((platform) => profiles.find((profile) => profile.platform === platform)) - .filter((profiles) => !!profiles) - - const menuProfiles = profiles.filter((profile) => !buttonPlatforms.includes(profile.platform)) - - return { - blocks: [ - { - type: 'header', - text: { - type: 'plain_text', - text: member.displayName, - emoji: true, - }, - }, - ...(platforms && platforms.length > 0 - ? [ - { - type: 'context', - elements: [ - { - type: 'mrkdwn', - text: `Joined your community on *${ - integrationLabel[platforms[0]] || platforms[0] - }*`, - }, - ], - }, - ] - : []), - ...(details.length > 0 - ? [ - { - type: 'divider', - }, - { - type: 'section', - text: { - type: 'mrkdwn', - text: details.length > 0 ? details.join('\n') : '\n', - }, - accessory: { - type: 'image', - image_url: member.attributes?.avatarUrl?.default ?? defaultAvatarUrl, - alt_text: 'computer thumbnail', - }, - }, - { - type: 'divider', - }, - ] - : []), - { - type: 'actions', - elements: [ - { - type: 'button', - text: { - type: 'plain_text', - text: 'View in crowd.dev', - emoji: true, - }, - url: `${API_CONFIG.frontendUrl}/contacts/${member.id}`, - }, - ...(buttonProfiles || []) - .map(({ platform, url }) => ({ - type: 'button', - text: { - type: 'plain_text', - text: `${integrationLabel[platform] ?? platform} profile`, - emoji: true, - }, - url, - })) - .filter((action) => !!action.url), - ...(menuProfiles.length > 0 - ? [ - { - type: 'overflow', - options: menuProfiles.map(({ platform, url }) => ({ - text: { - type: 'plain_text', - text: `${integrationLabel[platform] ?? platform} profile`, - emoji: true, - }, - url, - })), - }, - ] - : []), - ], - }, - ], - } -} diff --git a/backend/src/serverless/microservices/nodejs/automation/workers/slackWorker.ts b/backend/src/serverless/microservices/nodejs/automation/workers/slackWorker.ts deleted file mode 100644 index 70334fbd65..0000000000 --- a/backend/src/serverless/microservices/nodejs/automation/workers/slackWorker.ts +++ /dev/null @@ -1,100 +0,0 @@ -import request from 'superagent' -import { getServiceChildLogger } from '@crowd/logging' -import getUserContext from '../../../../../database/utils/getUserContext' -import AutomationRepository from '../../../../../database/repositories/automationRepository' -import { AutomationExecutionState } from '../../../../../types/automationTypes' -import AutomationExecutionService from '../../../../../services/automationExecutionService' -import SequelizeRepository from '../../../../../database/repositories/sequelizeRepository' -import SettingsRepository from '../../../../../database/repositories/settingsRepository' -import { newMemberBlocks } from './slack/newMemberBlocks' -import { newActivityBlocks } from './slack/newActivityBlocks' - -const log = getServiceChildLogger('webhookWorker') - -/** - * Actually fire the webhook with the relevant payload - * - * @param tenantId tenant unique ID - * @param automationId automation unique ID (or undefined) - * @param automationData automation data (or undefined) - * @param eventId trigger event unique ID - * @param payload payload to send - */ -export default async ( - tenantId: string, - automationId: string, - automationData: any, - eventId: string, - payload: any, -): Promise => { - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - const tenantSettings = await SettingsRepository.getTenantSettings(tenantId, options) - const userContext = await getUserContext(tenantId) - - const automationExecutionService = new AutomationExecutionService(userContext) - - const automation = - automationData !== undefined - ? automationData - : await new AutomationRepository(userContext).findById(automationId) - - log.info(`Firing slack automation ${automation.id} for event ${eventId}!`) - - let slackMessage = null - let success = false - try { - if (automation.trigger === 'new_member') { - slackMessage = { - text: `${payload.displayName} has joined your community!`, - ...newMemberBlocks(payload), - } - } else if (automation.trigger === 'new_activity') { - slackMessage = { - text: ':satellite_antenna: New activity', - ...newActivityBlocks(payload), - } - } else { - log.warn(`Error no slack handler for automation trigger ${automation.trigger}!`) - return - } - - const result = await request.post(tenantSettings.dataValues.slackWebHook).send(slackMessage) - - success = true - log.debug(`Slack response code ${result.statusCode}!`) - } catch (err) { - log.warn(`Error while firing slack automation ${automation.id} for event ${eventId}!`) - - let error: any - - if (err.status === 404) { - error = { - type: 'connect', - message: `Could not access slack workspace!`, - } - } else { - error = { - type: 'connect', - } - } - - await automationExecutionService.create({ - automation, - eventId, - payload: slackMessage, - state: AutomationExecutionState.ERROR, - error, - }) - - throw err - } - - if (success) { - await automationExecutionService.create({ - automation, - eventId, - payload: slackMessage, - state: AutomationExecutionState.SUCCESS, - }) - } -} diff --git a/backend/src/serverless/microservices/nodejs/automation/workers/util.ts b/backend/src/serverless/microservices/nodejs/automation/workers/util.ts deleted file mode 100644 index b9873d557b..0000000000 --- a/backend/src/serverless/microservices/nodejs/automation/workers/util.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NodeWorkerMessageType } from '../../../../types/workerTypes' -import { sendNodeWorkerMessage } from '../../../../utils/nodeWorkerSQS' -import { AutomationType } from '../../../../../types/automationTypes' -import { NodeWorkerMessageBase } from '../../../../../types/mq/nodeWorkerMessageBase' - -export const sendWebhookProcessRequest = async ( - tenant: string, - automation: any, - eventId: string, - payload: any, - type: AutomationType = AutomationType.WEBHOOK, -): Promise => { - const event = { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - service: 'automation-process', - automationType: type, - tenant, - automation, - eventId, - payload, - } - await sendNodeWorkerMessage(tenant, event as NodeWorkerMessageBase) -} diff --git a/backend/src/serverless/microservices/nodejs/automation/workers/webhookWorker.ts b/backend/src/serverless/microservices/nodejs/automation/workers/webhookWorker.ts deleted file mode 100644 index 12645c3605..0000000000 --- a/backend/src/serverless/microservices/nodejs/automation/workers/webhookWorker.ts +++ /dev/null @@ -1,100 +0,0 @@ -import request from 'superagent' -import { getServiceChildLogger } from '@crowd/logging' -import getUserContext from '../../../../../database/utils/getUserContext' -import AutomationRepository from '../../../../../database/repositories/automationRepository' -import { AutomationExecutionState, WebhookSettings } from '../../../../../types/automationTypes' -import AutomationExecutionService from '../../../../../services/automationExecutionService' - -const log = getServiceChildLogger('webhookWorker') - -/** - * Actually fire the webhook with the relevant payload - * - * @param tenantId tenant unique ID - * @param automationId automation unique ID (or undefined) - * @param automationData automation data (or undefined) - * @param eventId trigger event unique ID - * @param payload payload to send - */ -export default async ( - tenantId: string, - automationId: string, - automationData: any, - eventId: string, - payload: any, -): Promise => { - const userContext = await getUserContext(tenantId) - const automationExecutionService = new AutomationExecutionService(userContext) - - const automation = - automationData !== undefined - ? automationData - : await new AutomationRepository(userContext).findById(automationId) - const settings = automation.settings as WebhookSettings - - const now = new Date() - log.info(`Firing automation ${automation.id} for event ${eventId} to url '${settings.url}'!`) - const eventPayload = { - eventId, - eventType: automation.trigger, - eventExecutedAt: now.toISOString(), - eventPayload: payload, - } - - let success = false - try { - const result = await request - .post(settings.url) - .send(eventPayload) - .set('User-Agent', 'Crowd.dev Automations Executor') - .set('X-CrowdDotDev-Event-Type', automation.trigger) - .set('X-CrowdDotDev-Event-ID', eventId) - - success = true - log.debug(`Webhook response code ${result.statusCode}!`) - } catch (err) { - log.warn( - `Error while firing webhook automation ${automation.id} for event ${eventId} to url '${settings.url}'!`, - ) - - let error: any - - if (err.syscall && err.code) { - error = { - type: 'network', - message: `Could not access ${settings.url}!`, - } - } else if (err.status) { - error = { - type: 'http_status', - message: `POST @ ${settings.url} returned ${err.statusCode} - ${err.statusMessage}!`, - body: err.res !== undefined ? err.res.body : undefined, - } - } else { - error = { - type: 'unknown', - message: err.message, - errorObject: err, - } - } - - await automationExecutionService.create({ - automation, - eventId, - payload: eventPayload, - state: AutomationExecutionState.ERROR, - error, - }) - - throw err - } - - if (success) { - await automationExecutionService.create({ - automation, - eventId, - payload: eventPayload, - state: AutomationExecutionState.SUCCESS, - }) - } -} diff --git a/backend/src/serverless/microservices/nodejs/bulk-enrichment/bulkEnrichmentWorker.ts b/backend/src/serverless/microservices/nodejs/bulk-enrichment/bulkEnrichmentWorker.ts deleted file mode 100644 index 07e5d20dc7..0000000000 --- a/backend/src/serverless/microservices/nodejs/bulk-enrichment/bulkEnrichmentWorker.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { getRedisClient, RedisCache } from '@crowd/redis' -import { getSecondsTillEndOfMonth } from '../../../../utils/timing' -import { REDIS_CONFIG } from '../../../../conf' -import getUserContext from '../../../../database/utils/getUserContext' -import MemberEnrichmentService from '../../../../services/premium/enrichment/memberEnrichmentService' -import { FeatureFlagRedisKey } from '../../../../types/common' - -/** - * Sends weekly analytics emails of a given tenant - * to all users of the tenant. - * Data sent is for the last week. - * @param tenantId - */ -async function bulkEnrichmentWorker( - tenantId: string, - memberIds: string[], - segmentIds: string[], - notifyFrontend: boolean, - skipCredits: boolean, -) { - const userContext = await getUserContext(tenantId, null, segmentIds) - - const memberEnrichmentService = new MemberEnrichmentService(userContext) - - const { enrichedMemberCount } = await memberEnrichmentService.bulkEnrich( - memberIds, - notifyFrontend, - ) - - const failedEnrichmentRequests = memberIds.length - enrichedMemberCount - - // if skipCredits is true, no need to check or deduct credits - if (!skipCredits) { - if (failedEnrichmentRequests > 0) { - const redis = await getRedisClient(REDIS_CONFIG, true) - - // get redis cache that stores memberEnrichmentCount - const memberEnrichmentCountCache = new RedisCache( - FeatureFlagRedisKey.MEMBER_ENRICHMENT_COUNT, - redis, - userContext.log, - ) - - // get current enrichment count of tenant from redis - const memberEnrichmentCount = await memberEnrichmentCountCache.get( - userContext.currentTenant.id, - ) - - // calculate remaining seconds for the end of the month, to set TTL for redis keys - const secondsRemainingUntilEndOfMonth = getSecondsTillEndOfMonth() - - if (!memberEnrichmentCount) { - await memberEnrichmentCountCache.set( - userContext.currentTenant.id, - '0', - secondsRemainingUntilEndOfMonth, - ) - } else { - // Before sending the queue message, we increase the memberEnrichmentCount with all member Ids that are sent, - // assuming that we'll be able to enrich all. - // If any of enrichments failed, we should add these credits back, reducing memberEnrichmentCount - await memberEnrichmentCountCache.set( - userContext.currentTenant.id, - (parseInt(memberEnrichmentCount, 10) - failedEnrichmentRequests).toString(), - secondsRemainingUntilEndOfMonth, - ) - } - } - } -} -export { bulkEnrichmentWorker } diff --git a/backend/src/serverless/microservices/nodejs/bulk-enrichment/bulkOrganizationEnrichmentWorker.ts b/backend/src/serverless/microservices/nodejs/bulk-enrichment/bulkOrganizationEnrichmentWorker.ts deleted file mode 100644 index d516493783..0000000000 --- a/backend/src/serverless/microservices/nodejs/bulk-enrichment/bulkOrganizationEnrichmentWorker.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { getRedisClient, RedisCache } from '@crowd/redis' -import { getSecondsTillEndOfMonth } from '../../../../utils/timing' -import { ORGANIZATION_ENRICHMENT_CONFIG, REDIS_CONFIG } from '../../../../conf' -import getUserContext from '../../../../database/utils/getUserContext' -import { PLAN_LIMITS } from '../../../../feature-flags/isFeatureEnabled' -import OrganizationEnrichmentService from '../../../../services/premium/enrichment/organizationEnrichmentService' -import { FeatureFlag, FeatureFlagRedisKey } from '../../../../types/common' - -export async function BulkorganizationEnrichmentWorker( - tenantId: string, - maxEnrichLimit: number = 0, - verbose: boolean = false, - includeOrgsActiveLastYear: boolean = false, -) { - const userContext = await getUserContext(tenantId) - const redis = await getRedisClient(REDIS_CONFIG, true) - const organizationEnrichmentCountCache = new RedisCache( - FeatureFlagRedisKey.ORGANIZATION_ENRICHMENT_COUNT, - redis, - userContext.log, - ) - const usedEnrichmentCount = parseInt( - (await organizationEnrichmentCountCache.get(userContext.currentTenant.id)) ?? '0', - 10, - ) - - // Discard limits and credits if maxEnrichLimit is provided - const skipCredits = maxEnrichLimit > 0 - - const remainderEnrichmentLimit = skipCredits - ? maxEnrichLimit // Use maxEnrichLimit as the limit if provided - : PLAN_LIMITS[userContext.currentTenant.plan][FeatureFlag.ORGANIZATION_ENRICHMENT] - - usedEnrichmentCount - - let enrichedOrgs = [] - if (remainderEnrichmentLimit > 0) { - const enrichmentService = new OrganizationEnrichmentService({ - options: userContext, - apiKey: ORGANIZATION_ENRICHMENT_CONFIG.apiKey, - tenantId, - limit: remainderEnrichmentLimit, - }) - enrichedOrgs = await enrichmentService.enrichOrganizationsAndSignalDone( - includeOrgsActiveLastYear, - verbose, - ) - } - - if (!skipCredits) { - await organizationEnrichmentCountCache.set( - userContext.currentTenant.id, - (usedEnrichmentCount + enrichedOrgs.length).toString(), - getSecondsTillEndOfMonth(), - ) - } -} diff --git a/backend/src/serverless/microservices/nodejs/csv-export/csvExportWorker.ts b/backend/src/serverless/microservices/nodejs/csv-export/csvExportWorker.ts deleted file mode 100644 index 0d1e77eee2..0000000000 --- a/backend/src/serverless/microservices/nodejs/csv-export/csvExportWorker.ts +++ /dev/null @@ -1,144 +0,0 @@ -import moment from 'moment' -import { parseAsync } from 'json2csv' -import { HttpRequest } from '@aws-sdk/protocol-http' -import { S3RequestPresigner } from '@aws-sdk/s3-request-presigner' -import { Hash } from '@aws-sdk/hash-node' -import { parseUrl } from '@aws-sdk/url-parser' -import { formatUrl } from '@aws-sdk/util-format-url' -import { getServiceChildLogger } from '@crowd/logging' -import getUserContext from '../../../../database/utils/getUserContext' -import EmailSender from '../../../../services/emailSender' -import { S3_CONFIG } from '../../../../conf' -import { BaseOutput, ExportableEntity } from '../messageTypes' -import getStage from '../../../../services/helpers/getStage' -import { s3 } from '../../../../services/aws' -import MemberService from '../../../../services/memberService' -import UserRepository from '../../../../database/repositories/userRepository' - -const log = getServiceChildLogger('csvExportWorker') - -/** - * Sends weekly analytics emails of a given tenant - * to all users of the tenant. - * Data sent is for the last week. - * @param tenantId - */ -async function csvExportWorker( - entity: ExportableEntity, - userId: string, - tenantId: string, - segmentIds: string[], - criteria: any, -): Promise { - const fields = [ - 'id', - 'username', - 'displayName', - 'emails', - 'score', - 'joinedAt', - 'activeOn', - 'identities', - 'tags', - 'notes', - 'organizations', - 'activityCount', - 'lastActive', - 'reach', - 'averageSentiment', - 'score', - 'attributes', - ] - - const opts = { fields } - - // get the data without limits - const userContext = await getUserContext(tenantId, null, segmentIds) - - let data = null - - switch (entity) { - case ExportableEntity.MEMBERS: { - const memberService = new MemberService(userContext) - data = await memberService.queryForCsv(criteria) - break - } - default: - throw new Error(`Unrecognized exportable entity ${entity}`) - } - - if (!data || !data.rows) { - const message = `Unable to retrieve data to export as CSV, exiting..` - log.error(message) - return { - status: 400, - msg: message, - } - } - - const csv = await parseAsync(data.rows, opts) - - const key = `csv-exports/${moment().format('YYYY-MM-DD')}_${entity}_${tenantId}.csv` - - log.info({ tenantId, entity }, `Uploading csv to s3..`) - const privateObjectUrl = await uploadToS3(csv, key) - log.info({ tenantId, entity }, 'CSV uploaded successfully.') - - log.info({ tenantId, entity }, `Generating pre-signed url..`) - const url = await getPresignedUrl(privateObjectUrl) - log.info({ tenantId, entity, url }, `Url generated successfully.`) - - log.info({ tenantId, entity }, `Sending e-mail with pre-signed url..`) - const user = await UserRepository.findById(userId, userContext) - - await new EmailSender(EmailSender.TEMPLATES.CSV_EXPORT, { link: url }).sendTo(user.email) - - log.info({ tenantId, entity, email: user.email }, `CSV export e-mail with download link sent.`) - - return { - status: 200, - msg: `CSV export e-mail sent!`, - } -} - -async function uploadToS3(csv: any, key: string): Promise { - try { - await s3 - .putObject({ - Bucket: `${S3_CONFIG.microservicesAssetsBucket}-${getStage()}`, - Key: key, - Body: csv, - }) - .promise() - - return `https://${S3_CONFIG.microservicesAssetsBucket}-${getStage()}.s3.${ - S3_CONFIG.aws.region - }.amazonaws.com/${key}` - } catch (error) { - log.error(error, 'Error on uploading CSV file!') - throw error - } -} - -async function getPresignedUrl(objectUrl: string): Promise { - try { - const awsS3ObjectUrl = parseUrl(objectUrl) - - const presigner = new S3RequestPresigner({ - credentials: { - accessKeyId: S3_CONFIG.aws.accessKeyId, - secretAccessKey: S3_CONFIG.aws.secretAccessKey, - }, - region: S3_CONFIG.aws.region, - sha256: Hash.bind(null, 'sha256'), - }) - - const url = formatUrl(await presigner.presign(new HttpRequest(awsS3ObjectUrl))) - return url - } catch (error) { - log.error(error, 'Error on creating pre-signed url!') - throw error - } -} - -export { csvExportWorker } diff --git a/backend/src/serverless/microservices/nodejs/csv-export/test.json b/backend/src/serverless/microservices/nodejs/csv-export/test.json deleted file mode 100644 index 4573399e35..0000000000 --- a/backend/src/serverless/microservices/nodejs/csv-export/test.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "node_microservice", - "service": "csv-export", - "tenant": "9182fb91-ff30-402a-a3ab-eba45dc35508", - "user": "f47e240c-8229-4642-87fa-03aedb65b6d4", - "entity": "members", - "criteria": { - "filter": {}, - "orderBy": "", - "limit": 0, - "offset": 0 - } -} diff --git a/backend/src/serverless/microservices/nodejs/eagle-eye-email-digest/eagleEyeEmailDigestWorker.ts b/backend/src/serverless/microservices/nodejs/eagle-eye-email-digest/eagleEyeEmailDigestWorker.ts deleted file mode 100644 index cb6470a9b7..0000000000 --- a/backend/src/serverless/microservices/nodejs/eagle-eye-email-digest/eagleEyeEmailDigestWorker.ts +++ /dev/null @@ -1,75 +0,0 @@ -import moment from 'moment-timezone' -import { getServiceChildLogger } from '@crowd/logging' -import { S3_CONFIG } from '../../../../conf' -import RecurringEmailsHistoryRepository from '../../../../database/repositories/recurringEmailsHistoryRepository' -import SequelizeRepository from '../../../../database/repositories/sequelizeRepository' -import TenantUserRepository from '../../../../database/repositories/tenantUserRepository' -import getUserContext from '../../../../database/utils/getUserContext' -import EagleEyeContentService from '../../../../services/eagleEyeContentService' -import EagleEyeSettingsService from '../../../../services/eagleEyeSettingsService' -import EmailSender from '../../../../services/emailSender' -import getStage from '../../../../services/helpers/getStage' -import { RecurringEmailType } from '../../../../types/recurringEmailsHistoryTypes' - -const log = getServiceChildLogger('eagleEyeEmailDigestWorker') - -async function eagleEyeEmailDigestWorker(userId: string, tenantId: string): Promise { - const s3Url = `https://${ - S3_CONFIG.microservicesAssetsBucket - }-${getStage()}.s3.eu-central-1.amazonaws.com` - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - - const tenantUser = await TenantUserRepository.findByTenantAndUser(tenantId, userId, options) - - if (moment(tenantUser.settings.eagleEye.emailDigest.nextEmailAt) > moment()) { - log.info( - 'nextEmailAt is already updated. Email is already sent. Exiting without sending the email.', - ) - return - } - - const userContext = await getUserContext(tenantId, userId) - - const eagleEyeContentService = new EagleEyeContentService(userContext) - const content = (await eagleEyeContentService.search(true)).slice(0, 10).map((c: any) => { - c.platformIcon = `${s3Url}/email/${c.platform}.png` - c.post.thumbnail = null - return c - }) - - await new EmailSender( - EmailSender.TEMPLATES.EAGLE_EYE_DIGEST, - { - content, - frequency: tenantUser.settings.eagleEye.emailDigest.frequency, - date: moment().format('D MMM YYYY'), - }, - tenantId, - ).sendTo(tenantUser.settings.eagleEye.emailDigest.email) - - const rehRepository = new RecurringEmailsHistoryRepository(userContext) - - const reHistory = await rehRepository.create({ - tenantId: userContext.currentTenant.id, - type: RecurringEmailType.EAGLE_EYE_DIGEST, - emailSentAt: moment().toISOString(), - emailSentTo: [tenantUser.settings.eagleEye.emailDigest.email], - }) - - // update nextEmailAt - const nextEmailAt = EagleEyeSettingsService.getNextEmailDigestDate( - tenantUser.settings.eagleEye.emailDigest, - ) - const updateSettings = tenantUser.settings.eagleEye - updateSettings.emailDigest.nextEmailAt = nextEmailAt - - await TenantUserRepository.updateEagleEyeSettings( - userContext.currentUser.id, - updateSettings, - userContext, - ) - - log.info({ receipt: reHistory }) -} - -export { eagleEyeEmailDigestWorker } diff --git a/backend/src/serverless/microservices/nodejs/integration-data-checker/integrationDataCheckerSettings.ts b/backend/src/serverless/microservices/nodejs/integration-data-checker/integrationDataCheckerSettings.ts deleted file mode 100644 index df5e598c34..0000000000 --- a/backend/src/serverless/microservices/nodejs/integration-data-checker/integrationDataCheckerSettings.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { IntegrationDataCheckerSettings } from './integrationDataCheckerTypes' - -// Enum -export enum IntegrationDataCheckerSettingsType { - REGULAR = 'regular', - PLATFORM_SPECIFIC = 'platformSpecific', -} - -export const integrationDataCheckerSettings: IntegrationDataCheckerSettings[] = [ - // Check that all new integrations received data in the first two hours - { - timeSinceLastData: '2 hours', - onlyNewIntegrations: true, - actions: { sendSlackAlert: true, changeStatus: false }, - // actions: { sendSlackAlert: true, changeStatus: true }, - type: IntegrationDataCheckerSettingsType.REGULAR, - }, - // Check that Slack and Discord integrations have message activities in the first 2 hours - { - timeSinceLastData: '2 hours', - onlyNewIntegrations: true, - actions: { sendSlackAlert: true, changeStatus: false }, - // actions: { sendSlackAlert: true, changeStatus: true }, - type: IntegrationDataCheckerSettingsType.PLATFORM_SPECIFIC, - activityPlatformsAndType: { - platforms: ['slack', 'discord'], - type: 'message', - }, - }, - // Check that each integration is actually getting data every in the last 3 days - // { - // timeSinceLastData: '3 days', - // onlyNewIntegrations: false, - // actions: { - // sendSlackAlert: true, - // changeStatus: false, - // }, - // type: IntegrationDataCheckerSettingsType.REGULAR, - // }, -] diff --git a/backend/src/serverless/microservices/nodejs/integration-data-checker/integrationDataCheckerTypes.ts b/backend/src/serverless/microservices/nodejs/integration-data-checker/integrationDataCheckerTypes.ts deleted file mode 100644 index 85f220bd7c..0000000000 --- a/backend/src/serverless/microservices/nodejs/integration-data-checker/integrationDataCheckerTypes.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type IntegrationDataCheckerSettings = { - timeSinceLastData: string - onlyNewIntegrations?: boolean - type: 'regular' | 'platformSpecific' - activityPlatformsAndType?: { - platforms: string[] - type: string - } - actions: IntegrationDataCheckerActions -} - -export type IntegrationDataCheckerActions = { - sendSlackAlert?: boolean - changeStatus?: boolean -} diff --git a/backend/src/serverless/microservices/nodejs/integration-data-checker/integrationDataCheckerWorker.ts b/backend/src/serverless/microservices/nodejs/integration-data-checker/integrationDataCheckerWorker.ts deleted file mode 100644 index 1be97fcb44..0000000000 --- a/backend/src/serverless/microservices/nodejs/integration-data-checker/integrationDataCheckerWorker.ts +++ /dev/null @@ -1,132 +0,0 @@ -import moment from 'moment' -import { getServiceChildLogger } from '@crowd/logging' -import { sendSlackAlert, SlackAlertTypes } from '@crowd/alerting' -import getUserContext from '../../../../database/utils/getUserContext' -import IntegrationService from '../../../../services/integrationService' -import ActivityService from '../../../../services/activityService' -import { - integrationDataCheckerSettings, - IntegrationDataCheckerSettingsType, -} from './integrationDataCheckerSettings' -import { IRepositoryOptions } from '../../../../database/repositories/IRepositoryOptions' -import { IntegrationDataCheckerSettings } from './integrationDataCheckerTypes' -import { SLACK_ALERTING_CONFIG } from '../../../../conf' - -const log = getServiceChildLogger('integrationDataCheckerWorker') - -async function integrationDataCheckerWorker(integrationId, tenantId): Promise { - const userContext: IRepositoryOptions = await getUserContext(tenantId) - const integrationService = new IntegrationService(userContext) - const integration = await integrationService.findById(integrationId) - - if (integration) { - await checkIntegrationForAllSettings(integration, userContext) - } -} - -/** - * Check if the integration has data, platform-agnostic. - * Each setting will contain a timeframe that we want, and some instructions of what to do. - * If there have not been any activities in the timeframe, we will act accordingly. - * @param integration The integration to check - * @param userContext User context - */ -async function checkIntegrationForAllSettings(integration, userContext: IRepositoryOptions) { - const activityService = new ActivityService(userContext) - for (const settings of integrationDataCheckerSettings) { - // This is moment() - the time. For example, moment().subtract(1, 'hour') is 1 hour ago. - const timestampSinceLastData = generateDate(settings.timeSinceLastData) - - if (shouldCheckThisIntegration(integration, settings, timestampSinceLastData)) { - if ( - !(settings.type === IntegrationDataCheckerSettingsType.PLATFORM_SPECIFIC) || - settings.activityPlatformsAndType?.platforms.includes(integration.platform) - ) { - const activityCount = ( - await activityService.findAndCountAll({ - filter: { - platform: integration.platform, - createdAt: { - gte: timestampSinceLastData, - }, - ...(settings.type === IntegrationDataCheckerSettingsType.PLATFORM_SPECIFIC && { - type: settings.activityPlatformsAndType.type, - }), - }, - limit: 1, - }) - ).count - - if (!activityCount) { - await changeStatusAction(settings, integration, userContext) - await sendSlackAlertAction(settings, integration, userContext) - break - } - } - } - } -} - -function shouldCheckThisIntegration( - integration: any, - settings: IntegrationDataCheckerSettings, - timestampSinceLastData: moment.Moment, -) { - // We always should be only checking integrations that have been created before the time we want to check. - // Otherwise, it is never relevant. - if (integration.createdAt < timestampSinceLastData) { - // Either we do not care about it being only new integrations, - // or the integration's createdAt is before the time we want to check + the reset frequency. - return ( - !settings.onlyNewIntegrations || - integration.createdAt > timestampSinceLastData.subtract(1, 'hour') - ) - } - return false -} - -async function changeStatusAction( - settings: IntegrationDataCheckerSettings, - integration, - userContext: IRepositoryOptions, -) { - if (settings.actions.changeStatus) { - const integrationService = new IntegrationService(userContext) - await integrationService.update(integration.id, { - status: 'no-data', - }) - } -} - -async function sendSlackAlertAction( - settings: IntegrationDataCheckerSettings, - integration, - userContext: IRepositoryOptions, -) { - return sendSlackAlert({ - slackURL: SLACK_ALERTING_CONFIG.url, - alertType: SlackAlertTypes.DATA_CHECKER, - integration, - userContext, - log, - frameworkVersion: 'old', - settings, - }) -} - -function generateDate(timeframe) { - const now = moment() - if (timeframe.includes('hour')) { - // Parse int will actually work. 2 hours => 2, 1 day => 1, etc. - const hours = parseInt(timeframe, 10) - return now.subtract(hours, 'hours') - } - if (timeframe.includes('day')) { - const days = parseInt(timeframe, 10) - return now.subtract(days, 'days') - } - log.error('Invalid timeframe', timeframe) - throw new Error('Invalid timeframe') -} - -export { integrationDataCheckerWorker } diff --git a/backend/src/serverless/microservices/nodejs/integration-data-checker/refreshSampleDataWorker.ts b/backend/src/serverless/microservices/nodejs/integration-data-checker/refreshSampleDataWorker.ts deleted file mode 100644 index bd700ab732..0000000000 --- a/backend/src/serverless/microservices/nodejs/integration-data-checker/refreshSampleDataWorker.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { QueryTypes } from 'sequelize' -import { API_CONFIG, SAMPLE_DATA_CONFIG } from '../../../../conf' -import getUserContext from '../../../../database/utils/getUserContext' - -async function refreshSampleDataWorker(): Promise { - // This is only needed for hosted edition - if (API_CONFIG.edition === 'crowd-hosted') { - const tenantId = SAMPLE_DATA_CONFIG.tenantId - const userContext = await getUserContext(SAMPLE_DATA_CONFIG.tenantId) - const updateDays = 1 // Every day we need to refresh - - // These are all the tables that have columns that need to be updated - const tables = [ - { name: 'activities', columns: ['createdAt', 'timestamp'] }, - { name: 'members', columns: ['joinedAt', 'createdAt'] }, - { name: 'notes', columns: ['createdAt'] }, - { name: 'conversations', columns: ['createdAt'] }, - { name: 'organizations', columns: ['createdAt'] }, - { name: 'tags', columns: ['createdAt'] }, - ] - - // We are using a direct query because it is very specific functionality. - // There is no point creating repository methods. - for (const table of tables) { - for (const column of table.columns) { - const query = ` - UPDATE ${table.name} - SET "${column}" = "${column}" + INTERVAL '${updateDays} days' - WHERE "tenantId" = '${tenantId}'; - ` - await userContext.database.sequelize.query(query, { type: QueryTypes.UPDATE }) - } - } - } -} - -export { refreshSampleDataWorker } diff --git a/backend/src/serverless/microservices/nodejs/merge-suggestions/mergeSuggestionsWorker.ts b/backend/src/serverless/microservices/nodejs/merge-suggestions/mergeSuggestionsWorker.ts deleted file mode 100644 index 295b2ef6c3..0000000000 --- a/backend/src/serverless/microservices/nodejs/merge-suggestions/mergeSuggestionsWorker.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { getOpensearchClient } from '@crowd/opensearch' -import { getServiceChildLogger } from '@crowd/logging' -import { OrganizationMergeSuggestionType } from '@crowd/types' -import getUserContext from '../../../../database/utils/getUserContext' -import MemberService from '../../../../services/memberService' -import { IRepositoryOptions } from '../../../../database/repositories/IRepositoryOptions' -import { - IMemberMergeSuggestionsType, - IMemberMergeSuggestion, -} from '../../../../database/repositories/types/memberTypes' -import SegmentService from '../../../../services/segmentService' -import OrganizationService from '@/services/organizationService' -import { OPENSEARCH_CONFIG } from '@/conf' - -const log = getServiceChildLogger('mergeSuggestionsWorker') - -async function mergeSuggestionsWorker(tenantId): Promise { - const userContext: IRepositoryOptions = await getUserContext(tenantId) - const segmentService = new SegmentService(userContext) - const { rows: segments } = await segmentService.querySubprojects({}) - userContext.currentSegments = segments - userContext.opensearch = getOpensearchClient(OPENSEARCH_CONFIG) - - log.info(`Generating organization merge suggestions for tenant ${tenantId}!`) - - const organizationService = new OrganizationService(userContext) - await organizationService.generateMergeSuggestions(OrganizationMergeSuggestionType.BY_IDENTITY) - - log.info(`Done generating organization merge suggestions for tenant ${tenantId}!`) - - log.info(`Generating member merge suggestions for tenant ${tenantId}!`) - - const memberService = new MemberService(userContext) - // Splitting these because in the near future we will be treating them differently - const byUsername: IMemberMergeSuggestion[] = await memberService.getMergeSuggestions( - IMemberMergeSuggestionsType.USERNAME, - ) - await memberService.addToMerge(byUsername) - const byEmail: IMemberMergeSuggestion[] = await memberService.getMergeSuggestions( - IMemberMergeSuggestionsType.EMAIL, - ) - await memberService.addToMerge(byEmail) - const bySimilarity: IMemberMergeSuggestion[] = await memberService.getMergeSuggestions( - IMemberMergeSuggestionsType.SIMILARITY, - ) - await memberService.addToMerge(bySimilarity) - log.info(`Done generating member merge suggestions for tenant ${tenantId}!`) -} - -export { mergeSuggestionsWorker } diff --git a/backend/src/serverless/microservices/nodejs/messageTypes.ts b/backend/src/serverless/microservices/nodejs/messageTypes.ts deleted file mode 100644 index 69cfabea29..0000000000 --- a/backend/src/serverless/microservices/nodejs/messageTypes.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { AutomationTrigger, AutomationType } from '../../../types/automationTypes' - -export type BaseNodeMicroserviceMessage = { - service: string - tenant?: string -} - -export type AutomationMessage = BaseNodeMicroserviceMessage & { - trigger: AutomationTrigger -} - -export type CsvExportMessage = BaseNodeMicroserviceMessage & { - entity: ExportableEntity - user: string - segmentIds: string[] - criteria: any -} - -export type EagleEyeEmailDigestMessage = BaseNodeMicroserviceMessage & { - user: string -} - -export type IntegrationDataCheckerMessage = BaseNodeMicroserviceMessage & { - integrationId: string - tenantId: string -} - -export type ActivityAutomationData = { - activityId: string -} - -export type NewActivityAutomationMessage = BaseNodeMicroserviceMessage & - ActivityAutomationData & { - segmentId: string - } - -export type MemberAutomationData = { - memberId: string -} - -export type NewMemberAutomationMessage = BaseNodeMicroserviceMessage & - MemberAutomationData & { - segmentId: string - } - -export type ProcessAutomationMessage = BaseNodeMicroserviceMessage & { - automationType: AutomationType -} - -export type ProcessWebhookAutomationMessage = BaseNodeMicroserviceMessage & { - automationId?: string - automation?: any - eventId: string - payload: any -} - -export type NodeMicroserviceMessage = - | BaseNodeMicroserviceMessage - | AutomationMessage - | NewActivityAutomationMessage - | NewMemberAutomationMessage - | ProcessAutomationMessage - | ProcessWebhookAutomationMessage - -export type BaseOutput = { status: number; msg?: string } - -export interface AnalyticsEmailsOutput extends BaseOutput { - emailSent: boolean -} - -export enum ExportableEntity { - MEMBERS = 'members', -} - -export type BulkEnrichMessage = { - service: string - tenant: string - memberIds: string[] - segmentIds: string[] - notifyFrontend: boolean - skipCredits: boolean -} - -export type OrganizationBulkEnrichMessage = { - service: string - tenantId: string - maxEnrichLimit: number -} diff --git a/backend/src/serverless/microservices/nodejs/workDispatcher.ts b/backend/src/serverless/microservices/nodejs/workDispatcher.ts deleted file mode 100644 index b9ac1241f9..0000000000 --- a/backend/src/serverless/microservices/nodejs/workDispatcher.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { NodeMicroserviceMessage } from './messageTypes' -import workerFactory from './workerFactory' -import { NodeWorkerMessageBase } from '../../../types/mq/nodeWorkerMessageBase' - -export const processNodeMicroserviceMessage = async (msg: NodeWorkerMessageBase): Promise => { - const microserviceMsg = msg as any as NodeMicroserviceMessage - await workerFactory(microserviceMsg) -} diff --git a/backend/src/serverless/microservices/nodejs/workerFactory.ts b/backend/src/serverless/microservices/nodejs/workerFactory.ts deleted file mode 100644 index 1a88e4c88a..0000000000 --- a/backend/src/serverless/microservices/nodejs/workerFactory.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* eslint-disable no-case-declarations */ -import { Edition } from '@crowd/types' -import { weeklyAnalyticsEmailsWorker } from './analytics/workers/weeklyAnalyticsEmailsWorker' -import { - AutomationMessage, - CsvExportMessage, - NewActivityAutomationMessage, - NewMemberAutomationMessage, - NodeMicroserviceMessage, - ProcessAutomationMessage, - ProcessWebhookAutomationMessage, - BulkEnrichMessage, - EagleEyeEmailDigestMessage, - IntegrationDataCheckerMessage, - OrganizationBulkEnrichMessage, -} from './messageTypes' -import { AutomationTrigger, AutomationType } from '../../../types/automationTypes' -import newActivityWorker from './automation/workers/newActivityWorker' -import newMemberWorker from './automation/workers/newMemberWorker' -import webhookWorker from './automation/workers/webhookWorker' -import slackWorker from './automation/workers/slackWorker' -import { csvExportWorker } from './csv-export/csvExportWorker' -import { processStripeWebhook } from '../../integrations/workers/stripeWebhookWorker' -import { processSendgridWebhook } from '../../integrations/workers/sendgridWebhookWorker' -import { bulkEnrichmentWorker } from './bulk-enrichment/bulkEnrichmentWorker' -import { eagleEyeEmailDigestWorker } from './eagle-eye-email-digest/eagleEyeEmailDigestWorker' -import { integrationDataCheckerWorker } from './integration-data-checker/integrationDataCheckerWorker' -import { refreshSampleDataWorker } from './integration-data-checker/refreshSampleDataWorker' -import { mergeSuggestionsWorker } from './merge-suggestions/mergeSuggestionsWorker' -import { BulkorganizationEnrichmentWorker } from './bulk-enrichment/bulkOrganizationEnrichmentWorker' -import { API_CONFIG } from '../../../conf' - -/** - * Worker factory for spawning different microservices - * according to event.service - * @param event - * @returns worker function promise - */ - -async function workerFactory(event: NodeMicroserviceMessage): Promise { - const { service, tenant } = event as any - switch (service.toLowerCase()) { - case 'stripe-webhooks': - return processStripeWebhook(event) - case 'sendgrid-webhooks': - return processSendgridWebhook(event) - case 'weekly-analytics-emails': - return weeklyAnalyticsEmailsWorker(tenant) - case 'eagle-eye-email-digest': - const eagleEyeDigestMessage = event as EagleEyeEmailDigestMessage - return eagleEyeEmailDigestWorker(eagleEyeDigestMessage.user, eagleEyeDigestMessage.tenant) - case 'integration-data-checker': - const integrationDataCheckerMessage = event as IntegrationDataCheckerMessage - return integrationDataCheckerWorker( - integrationDataCheckerMessage.integrationId, - integrationDataCheckerMessage.tenantId, - ) - case 'merge-suggestions': - return mergeSuggestionsWorker(tenant) - - case 'refresh-sample-data': - return refreshSampleDataWorker() - - case 'csv-export': - const csvExportMessage = event as CsvExportMessage - return csvExportWorker( - csvExportMessage.entity, - csvExportMessage.user, - tenant, - csvExportMessage.segmentIds, - csvExportMessage.criteria, - ) - case 'bulk-enrich': - const bulkEnrichMessage = event as BulkEnrichMessage - return bulkEnrichmentWorker( - bulkEnrichMessage.tenant, - bulkEnrichMessage.memberIds, - bulkEnrichMessage.segmentIds, - bulkEnrichMessage.notifyFrontend, - bulkEnrichMessage.skipCredits, - ) - case 'enrich-organizations': { - const bulkEnrichMessage = event as OrganizationBulkEnrichMessage - return BulkorganizationEnrichmentWorker( - bulkEnrichMessage.tenantId, - bulkEnrichMessage.maxEnrichLimit, - ) - } - case 'automation-process': - if (API_CONFIG.edition === Edition.LFX) { - return {} - } - const automationProcessRequest = event as ProcessAutomationMessage - - switch (automationProcessRequest.automationType) { - case AutomationType.WEBHOOK: - const webhookProcessRequest = event as ProcessWebhookAutomationMessage - return webhookWorker( - tenant, - webhookProcessRequest.automationId, - webhookProcessRequest.automation, - webhookProcessRequest.eventId, - webhookProcessRequest.payload, - ) - case AutomationType.SLACK: - const slackProcessRequest = event as ProcessWebhookAutomationMessage - return slackWorker( - tenant, - slackProcessRequest.automationId, - slackProcessRequest.automation, - slackProcessRequest.eventId, - slackProcessRequest.payload, - ) - default: - throw new Error(`Invalid automation type ${automationProcessRequest.automationType}!`) - } - - case 'automation': - if (API_CONFIG.edition === Edition.LFX) { - return {} - } - const automationRequest = event as AutomationMessage - - switch (automationRequest.trigger) { - case AutomationTrigger.NEW_ACTIVITY: - const newActivityAutomationRequest = event as NewActivityAutomationMessage - return newActivityWorker( - tenant, - newActivityAutomationRequest.activityId, - newActivityAutomationRequest.segmentId, - ) - case AutomationTrigger.NEW_MEMBER: - const newMemberAutomationRequest = event as NewMemberAutomationMessage - return newMemberWorker( - tenant, - newMemberAutomationRequest.memberId, - newMemberAutomationRequest.segmentId, - ) - default: - throw new Error(`Invalid automation trigger ${automationRequest.trigger}!`) - } - default: - throw new Error(`Invalid microservice ${service}`) - } -} - -export default workerFactory diff --git a/backend/src/serverless/microservices/python/.dockerignore b/backend/src/serverless/microservices/python/.dockerignore deleted file mode 100644 index 1ca06247f7..0000000000 --- a/backend/src/serverless/microservices/python/.dockerignore +++ /dev/null @@ -1,12 +0,0 @@ -**/venv* -**/node_modules -**/.serverless -Dockerfile -**/.git -**/.webpack -**/.serverless -**/.cubestore -**/.idea -**/.vscode -**/.env -**/.env.* \ No newline at end of file diff --git a/backend/src/serverless/microservices/python/.gitignore b/backend/src/serverless/microservices/python/.gitignore deleted file mode 100644 index d37254d0a4..0000000000 --- a/backend/src/serverless/microservices/python/.gitignore +++ /dev/null @@ -1,39 +0,0 @@ -.idea - -**/.env* -!**/.env*.dist -!**/.env*.base - -**/*-venv -**/venv -**/venv* - -**/__pycache__ -**/*.egg-info - -crowd-*/dist -crowd-*/build - -**/build/ - -**/node_modules -**/build -**/.vscode -**/.DS_Store - -**/.vscode* -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -*.pem - -**/.pytest_cache* - -**/.serverless -**/psycopg2 - -docker-compose.yaml diff --git a/backend/src/serverless/microservices/python/.pre-commit-config.yaml b/backend/src/serverless/microservices/python/.pre-commit-config.yaml deleted file mode 100644 index d0c13b0cda..0000000000 --- a/backend/src/serverless/microservices/python/.pre-commit-config.yaml +++ /dev/null @@ -1,9 +0,0 @@ -repos: - - repo: https://github.com/ambv/black - rev: 22.3.0 - hooks: - - id: black - - repo: https://gitlab.com/pycqa/flake8 - rev: 4.0.1 - hooks: - - id: flake8 diff --git a/backend/src/serverless/microservices/python/Dockerfile.kube b/backend/src/serverless/microservices/python/Dockerfile.kube deleted file mode 100644 index c590d0050b..0000000000 --- a/backend/src/serverless/microservices/python/Dockerfile.kube +++ /dev/null @@ -1,20 +0,0 @@ -FROM python:3.8-bullseye - -RUN apt install -y --no-install-recommends gcc - -WORKDIR /var/task - -COPY ./requirements.txt ./requirements.dev.txt ./ -COPY ./crowd-backend/setup.py ./crowd-backend/ -COPY ./crowd-members-score/setup.py ./crowd-members-score/ - -RUN python -m venv --copies ./venv -RUN ./venv/bin/pip install psycopg2-binary && \ -./venv/bin/pip install -r requirements.txt && \ -./venv/bin/pip install -r requirements.dev.txt - -RUN ./venv/bin/pip list - -COPY . . - -ENTRYPOINT ["./start-python-worker.sh"] diff --git a/backend/src/serverless/microservices/python/README.md b/backend/src/serverless/microservices/python/README.md deleted file mode 100644 index 7e65a6cefb..0000000000 --- a/backend/src/serverless/microservices/python/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# Crowd.dev Monorepository - -This is the monorepository for most software related to crowd.dev. - -Currently, all python software is contained in the crowd namespace, and the modules are in subfolders named accordingly. -E.g. the internally used github api resides in [crowd-github-api](crowd-github-api) and provides its functionality in a -`github_api` folder under the `crowd` - -The `requirements.txt` contains dev requirements for formatting, linting, testing and `pre-commit`-hooks. -The modules specify their own requirements in their `setup.py's` `install_requires`. - -## Setup - -In order to start developing, the environment needs to be prepared and some hooks need to be configured. - -### Requirements - -- Python 3.8 - -### Steps - -- Clone this repo and `cd` into it -- Create a virtual environment with `python -m venv venv-crowd` -- Activate it on unix/Windows with Git Bash `source venv-crowd/bin/activate` or on Windows `venv-crowd\Scripts\activate.bat` -- Install all crowd modules in editable mode: `pip install -r requirements.internal.txt` -- Install the dev modules `pip install -r requirements.txt` -- Setup the pre-commit hook for formatting and linting `pre-commit install` -- Setup the pre-push testing hook on Unix `make` or on Unix/Windows `cp hooks/pre-push .git/hooks` -- TODO setup ENV variables -- verify that everything works with running `pytest` diff --git a/backend/src/serverless/microservices/python/conftest.py b/backend/src/serverless/microservices/python/conftest.py deleted file mode 100644 index 4bd7b8b12f..0000000000 --- a/backend/src/serverless/microservices/python/conftest.py +++ /dev/null @@ -1,39 +0,0 @@ -import pytest -import os - -from crowd.backend.repository import Repository -from sqlalchemy import create_engine -from sqlalchemy.exc import SQLAlchemyError - - -def is_db_up(url): - """Checks the connection to test db, and if successfull closes the connection""" - try: - engine = create_engine( - url, echo=False, execution_options={"postgresql_readonly": True, "postgresql_deferrable": True} - ) - engine.connect() - engine.dispose() - return True - except SQLAlchemyError: - return False - - -@pytest.fixture(scope="session") -def api(docker_services): - """Ensure that the db is responsive and return a reference to the repository class""" - - docker_services.wait_until_responsive( - timeout=300.0, pause=0.1, check=lambda: is_db_up("postgresql://postgres:example@localhost:5433/crowd-web") - ) - tenant_id = "f5c97d75-b919-4be6-9e57-b851efb336a1" - try: - api = Repository(tenant_id, "postgresql://postgres:example@localhost:5433/crowd-web") - yield api - finally: - api.session.close() - - -@pytest.fixture(scope="session") -def docker_compose_file(pytestconfig): - return os.path.join(str(pytestconfig.rootdir), "docker-compose-test.yaml") diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/controllers/__init__.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/controllers/__init__.py deleted file mode 100644 index eb74dbb811..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/controllers/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .base_controller import BaseController # noqa -from .members_controller import MembersController # noqa -from ..infrastructure.config import KUBE_MODE - -# TODO-kube -if not KUBE_MODE: - import dotenv # noqa - found = dotenv.find_dotenv(".env") - dotenv.load_dotenv(found) \ No newline at end of file diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/controllers/base_controller.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/controllers/base_controller.py deleted file mode 100644 index a22e073cdc..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/controllers/base_controller.py +++ /dev/null @@ -1,28 +0,0 @@ -from crowd.backend.repository import Repository -from crowd.backend.infrastructure import DbOperationsSQS - - -class BaseController(object): - """ - Base controller class. It has functions and variables common for all controllers. - This class is to be used as a parent for a specific controller only. - """ - - def __init__(self, tenant_id, repository=False, test=False): - """ - Function to initialise the controller. - - Args: - tenant_id (UUID): the tenant ID in Crowd web - repository (Repository, optional): the repository to use for transactions. Defaults to False. - test (bool, optional): Whether we are in test mode. Defaults to False. - """ - self.tenant_id = tenant_id - # If the Repository was not sent, initialise one. - if not repository: - self.repository = Repository(tenant_id=tenant_id, test=test) - else: - self.repository = repository - - self.test = test - self.sqs = DbOperationsSQS() diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/controllers/members_controller.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/controllers/members_controller.py deleted file mode 100644 index 9b01df06b5..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/controllers/members_controller.py +++ /dev/null @@ -1,44 +0,0 @@ -from crowd.backend.models import Member -from crowd.backend.repository import Repository -from crowd.backend.controllers import BaseController -from crowd.backend.infrastructure.logging import get_logger -from uuid import UUID -from crowd.backend.enums import Operations -from crowd.backend.repository.keys import DBKeys as dbk - -logger = get_logger(__name__) - - -class MembersController(BaseController): - """ - Controller for members in Crowd.dev. - It can add or update members, detect members to merge and do the merge, and mark members as not merging. - - Args: - BaseController (BaseController): parent BaseController class. - """ - - def __init__(self, tenant_id: "UUID", repository: "Repository" = False, test: "bool" = False) -> "None": - super().__init__(tenant_id, repository=repository, test=test) - - def update_members_to_merge(self, to_merge, send=True): - """ - Function to update members to merge - - Args: - to_merge ([tuple]): list of tuples - """ - return self.sqs.send_message(self.tenant_id, Operations.UPDATE_MEMBERS_TO_MERGE, to_merge, send) - - def update(self, updates, send=True): - """ - Function to update the integrations - - Args: - updates ([{id, update}]): list of dicts with id and corresponding update - """ - if type(updates) is not list: - updates = [ - updates, - ] - return self.sqs.send_message(self.tenant_id, Operations.UPDATE_MEMBERS, updates, send) diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/controllers/test_controllers.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/controllers/test_controllers.py deleted file mode 100644 index 99887dfba5..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/controllers/test_controllers.py +++ /dev/null @@ -1,106 +0,0 @@ -from crowd.backend.controllers import ActivitiesController -from crowd.backend.controllers import MembersController -from crowd.backend.controllers import IntegrationsController -from crowd.backend.repository import Repository - - -def test_upsert_member(api: "Repository"): - members_controller = MembersController(api.tenant_id, api) - member_to_upsert = { - "id": "c5c1e44d-86d7-40b7-80ef-55fc281620ca", - "username": '{"apis": "jacqueline_love", "discord": "jacqueline_love", "crowdUsername": "jacqueline_love"}', - "type": "member", - "info": "{}", - "platform": "github", - "crowdInfo": '{"discord": {"name": "Jacqueline Love", "sample": True}}', - "email": "jacqueline.love@gmail.com", - "score": 10, - "bio": "", - "organisation": "", - "location": "", - "signals": None, - "joinedAt": "2021-09-13T17:27:36.421Z", - "importHash": None, - "createdAt": "2022-02-28T12:52:12.598Z", - "updatedAt": "2022-02-28T12:52:12.598Z", - "deletedAt": None, - "tenantId": "49e74479-63c3-46c0-89f5-b31700f36d80", - "createdById": "0772da68-f887-4ef9-9dbd-a67495a8ab9f", - "updatedById": "0772da68-f887-4ef9-9dbd-a67495a8ab9f", - } - result = members_controller.upsert([member_to_upsert], send=False) - assert result == 1 - - -def test_update_members_to_merge(api: "Repository"): - """Tests updating the members to merge""" - members_controller = MembersController(api.tenant_id, api) - members_to_merge = ({"id": "160f1462-7df1-4bc0-bc16-8b357608725c"}, {"id": "c5c1e44d-86d7-40b7-80ef-55fc281620ca"}) - result = members_controller.update_members_to_merge([members_to_merge], send=False) - assert result == 1 - - -def test_update_members(api: "Repository"): - """Tests updating a members using the members_controller""" - members_controller = MembersController(api.tenant_id, api) - updates = {"id": "160f1462-7df1-4bc0-bc16-8b357608725c", "update": {"organisation": "crowd.dev"}} - result = members_controller.update([updates], send=False) - assert result == 1 - - -def test_add_activity_with_member(api: "Repository"): - """Tests adding an activity with a Member""" - activities_controller = ActivitiesController(api.tenant_id, api) - - activity = { - "id": "097240d7-2bd9-4296-89a9-f697222cf98a", - "type": "issue-comment", - "timestamp": "2021-01-11T10:40:01.000Z", - "platform": "github", - "info": "{}", - "crowdInfo": '{"url": "test", "body": "test", "repo": "test", "title": "Client returns different total number of entities in a non-deterministic way", "parent_url": "htest"}', # noqa - "isContribution": True, - "score": 3, - "sourceId": None, - "importHash": None, - "createdAt": "2022-02-28T12:54:27.761Z", - "updatedAt": "2022-02-28T12:54:27.761Z", - "deletedAt": None, - "member": { - "id": "c5c1e44d-86d7-40b7-80ef-55fc281620ca", - "username": '{"apis": "jacqueline_love", "discord": "jacqueline_love", "crowdUsername": "jacqueline_love"}', - "type": "member", - "info": "{}", - "crowdInfo": '{"discord": {"name": "Jacqueline Love", "sample": True}}', - "email": "jacqueline.love@gmail.com", - "score": 10, - "bio": "", - "organisation": "", - "location": "", - "signals": None, - "joinedAt": "2021-09-13T17:27:36.421Z", - "importHash": None, - "createdAt": "2022-02-28T12:52:12.598Z", - "updatedAt": "2022-02-28T12:52:12.598Z", - "deletedAt": None, - "tenantId": "49e74479-63c3-46c0-89f5-b31700f36d80", - "createdById": "0772da68-f887-4ef9-9dbd-a67495a8ab9f", - "updatedById": "0772da68-f887-4ef9-9dbd-a67495a8ab9f", - }, - "parentId": None, - "tenantId": "2d19aa2c-2ae5-48d0-b501-ef07271879c9", - "createdById": "0d03387c-afe6-4519-a2e6-70f174a55390", - "updatedById": "0d03387c-afe6-4519-a2e6-70f174a55390", - } - - result = activities_controller.add_activity_with_member([activity], send=False) - assert result == 1 - - -def test_update_integrations(api: "Repository"): - """Tests updating an integration""" - integrations_controller = IntegrationsController(api.tenant_id, api) - - updates = {"id": "6aa8b316-3386-401b-9fa9-dda6ab1f3649", "update": {"status": "in-progress"}} - result = integrations_controller.update([updates], send=False) - assert result == 1 diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/enums/__init__.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/enums/__init__.py deleted file mode 100644 index 9cbf7bd42a..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/enums/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .operations import Operations # noqa -from .services import Services # noqs diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/enums/operations.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/enums/operations.py deleted file mode 100644 index 6bf7a7a34c..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/enums/operations.py +++ /dev/null @@ -1,11 +0,0 @@ -from enum import Enum - - -class Operations(Enum): - UPDATE_MEMBERS: str = "update_members" - UPSERT_MEMBERS: str = "upsert_members" - UPDATE_MEMBERS_TO_MERGE: str = "update_members_to_merge" - UPSERT_ACTIVITIES_WITH_MEMBERS: str = "upsert_activities_with_members" - UPDATE_INTEGRATIONS: str = "update_integrations" - UPDATE_WIDGETS: str = "update_widgets" - UPDATE_MICROSERVICES: str = "update_microservices" diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/enums/services.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/enums/services.py deleted file mode 100644 index 3be1bbddf0..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/enums/services.py +++ /dev/null @@ -1,5 +0,0 @@ -from enum import Enum - - -class Services(Enum): - MEMBERS_SCORE = "members_score" diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/infrastructure/__init__.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/infrastructure/__init__.py deleted file mode 100644 index 64f06dadc3..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/infrastructure/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from .config import KUBE_MODE - -# TODO-kube -if not KUBE_MODE: - import dotenv - found = dotenv.find_dotenv(".env") - dotenv.load_dotenv(found) - -from .sqs import SQS # noqa -from .db_operations_sqs import DbOperationsSQS # noqa -from .services_sqs import ServicesSQS # noqa diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/infrastructure/config.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/infrastructure/config.py deleted file mode 100644 index 7503c680b8..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/infrastructure/config.py +++ /dev/null @@ -1,47 +0,0 @@ -import os - -# TODO-kube -KUBE_MODE = os.environ.get("KUBE_MODE") is not None - -IS_TEST_ENV = os.environ.get("SERVICE_ENV") == "test" -IS_DEV_ENV = os.environ.get("SERVICE_ENV") == "development" or \ - os.environ.get("SERVICE_ENV") == "docker" or \ - os.environ.get("SERVICE_ENV") is None -IS_PROD_ENV = os.environ.get("SERVICE_ENV") == "production" -IS_STAGING_ENV = os.environ.get("SERVICE_ENV") == "staging" - -SERVICE = os.environ.get("SERVICE") - -LOG_LEVEL = os.environ.get("LOG_LEVEL") or "INFO" -if LOG_LEVEL == "TRACE": - LOG_LEVEL = "DEBUG" -elif LOG_LEVEL == "FATAL": - LOG_LEVEL = "CRITICAL" -elif LOG_LEVEL == "WARN": - LOG_LEVEL = "WARNING" - -# SQS Settings -NODEJS_WORKER_QUEUE = os.environ.get("CROWD_SQS_NODEJS_WORKER_QUEUE") -PYTHON_WORKER_QUEUE = os.environ.get("CROWD_SQS_PYTHON_WORKER_QUEUE") -SQS_HOST = os.environ.get("CROWD_SQS_HOST") -SQS_PORT = os.environ.get("CROWD_SQS_PORT") -SQS_ENDPOINT_URL = os.environ.get("CROWD_SQS_ENDPOINT") -SQS_ACCESS_KEY_ID = os.environ.get("CROWD_SQS_AWS_ACCESS_KEY_ID") -SQS_SECRET_ACCESS_KEY = os.environ.get("CROWD_SQS_AWS_SECRET_ACCESS_KEY") -SQS_REGION = os.environ.get("CROWD_SQS_AWS_REGION") - -# DB Settings - -if "CROWD_DB_PYTHON_WORKER_USERNAME" in os.environ: - DB_USERNAME = os.environ.get("CROWD_DB_PYTHON_WORKER_USERNAME") - DB_PASSWORD = os.environ.get("CROWD_DB_PYTHON_WORKER_PASSWORD") -elif "CROWD_DB_USERNAME" in os.environ: - DB_USERNAME = os.environ.get("CROWD_DB_USERNAME") - DB_PASSWORD = os.environ.get("CROWD_DB_PASSWORD") -else: - raise Exception("No database credentials configured!") - -DB_DATABASE = os.environ.get("CROWD_DB_DATABASE") -DB_HOST = os.environ.get("CROWD_DB_READ_HOST") -DB_PORT = os.environ.get("CROWD_DB_PORT") -DB_URL = f'postgresql://{DB_USERNAME}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_DATABASE}' diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/infrastructure/db_operations_sqs.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/infrastructure/db_operations_sqs.py deleted file mode 100644 index 1ce25e5a76..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/infrastructure/db_operations_sqs.py +++ /dev/null @@ -1,90 +0,0 @@ -from crowd.backend.infrastructure import SQS -from crowd.backend.infrastructure.logging import get_logger -from crowd.backend.enums import Operations -import os -from uuid import uuid1 as uuid -from functools import reduce -import json - -from crowd.backend.infrastructure.config import KUBE_MODE, NODEJS_WORKER_QUEUE - -logger = get_logger(__name__) - - -class DbOperationsSQS(SQS): - def __init__(self): - # TODO-kube - if KUBE_MODE: - db_operations_sqs_url = NODEJS_WORKER_QUEUE - else: - db_operations_sqs_url = os.environ.get("DB_OPERATIONS_SQS_URL") - super().__init__(db_operations_sqs_url) - - @staticmethod - def validate_update(records): - out = [] - for record in records: - if "id" not in record: - raise ValueError(f"Missing id in {record} Expected: 'id': , 'update': ") - if "update" not in record: - raise ValueError(f"Missing value in {record} Expected: 'id': , 'update': ") - record["id"] = str(record["id"]) - out.append(record) - - return out - - def send_message(self, tenant_id, operation, records, send=True): - """ - Send a message to the SQS queue that will trigger Write operations - - Args: - tenant_id (str): tenant id - operation (Operation): An operation from crowd.sqs_api.operations - records ([dict]): list of records to be added or updated - """ - tenant_id = str(tenant_id) - - if records: - message_id = f"{tenant_id}-{operation.value}-" - if operation == Operations.UPDATE_INTEGRATIONS: - DbOperationsSQS.validate_update(records) - deduplication_id = DbOperationsSQS.make_id() - message_id = message_id + deduplication_id - - elif operation == Operations.UPDATE_MEMBERS: - DbOperationsSQS.validate_update(records) - deduplication_id = DbOperationsSQS.make_id() - message_id = message_id + deduplication_id - - elif operation == Operations.UPSERT_ACTIVITIES_WITH_MEMBERS: - platform = records[0]["platform"] - type = records[0]["type"] - deduplication_id = message_id + platform + "-" + type - deduplication_id = DbOperationsSQS.make_id() - - elif operation == Operations.UPDATE_MEMBERS_TO_MERGE: - deduplication_id = DbOperationsSQS.make_id() - message_id = message_id + deduplication_id - - elif operation == Operations.UPSERT_MEMBERS: - deduplication_id = DbOperationsSQS.make_id() - platform = records[0]["platform"] - type = records[0]["type"] - elif operation == Operations.UPDATE_MICROSERVICES: - deduplication_id = DbOperationsSQS.make_id() - - else: - return None - - chuncked = [records[i : i + 5] for i in range(0, len(records), 5)] - - for chunk in chuncked: - body = dict(tenant_id=tenant_id, operation=operation.value, records=chunk) - # TODO-kube - if KUBE_MODE: - body["type"] = "db_operations" - if send: - super().send_message(body, message_id, DbOperationsSQS.make_id()) - else: - return 1 - return None diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/infrastructure/logging.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/infrastructure/logging.py deleted file mode 100644 index 5528e73ee4..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/infrastructure/logging.py +++ /dev/null @@ -1,32 +0,0 @@ -import logging -from datetime import datetime - -from pythonjsonlogger import jsonlogger - -from crowd.backend.infrastructure.config import SERVICE, LOG_LEVEL - -class CustomJsonFormatter(jsonlogger.JsonFormatter): - def add_fields(self, log_record, record, message_dict): - super(CustomJsonFormatter, self).add_fields(log_record, record, message_dict) - log_record['name'] = SERVICE - if not log_record.get('timestamp'): - now = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ') - log_record['timestamp'] = now - if log_record.get('level'): - log_record['level'] = log_record['level'].upper() - else: - log_record['level'] = record.levelname - - -def get_logger(name): - logger = logging.getLogger(f"{SERVICE}/{name}") - logger.setLevel(LOG_LEVEL.upper()) - - if not logger.handlers: - logHandler = logging.StreamHandler() - formatter = CustomJsonFormatter('%(timestamp)s %(level)s %(name)s %(message)s') - logHandler.setFormatter(formatter) - logger.addHandler(logHandler) - logger.propagate = False - - return logger \ No newline at end of file diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/infrastructure/services_sqs.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/infrastructure/services_sqs.py deleted file mode 100644 index 0d229d2b5a..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/infrastructure/services_sqs.py +++ /dev/null @@ -1,47 +0,0 @@ -from crowd.backend.infrastructure import SQS -from crowd.backend.infrastructure.logging import get_logger -from crowd.backend.enums import Services -import os - -from crowd.backend.infrastructure.config import KUBE_MODE, PYTHON_WORKER_QUEUE -from crowd.backend.models import microservice - -logger = get_logger(__name__) - - -class ServicesSQS(SQS): - def __init__(self): - # TODO-kube - if KUBE_MODE: - url = PYTHON_WORKER_QUEUE - else: - url = os.environ.get("PYTHON_MICROSERVICES_SQS_URL") - super().__init__(url) - - def send_message(self, tenant_id, microservice_id, service, params=None, send=True): - """ - Send a message to the SQS queue that will trigger services - - Args: - tenant_id (str): tenant id - microservicei_id (str): micrservice id - service (Service): An valid service to activate - params: (dict): params to send to the service - """ - if not params: - params = {} - - tenant_id = str(tenant_id) - if service in Services._value2member_map_: # This checks in the service is in the enum - message_id = f"{tenant_id}-{service}" - deduplication_id = ServicesSQS.make_id() - - else: - raise Exception(f"Service {service} not supported") - - body = dict(tenant=tenant_id, microservice_id=microservice_id, service=service, params=params) - - if send: - return super().send_message(body, message_id, deduplication_id) - else: - return 1 diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/infrastructure/sqs.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/infrastructure/sqs.py deleted file mode 100644 index 201c24053b..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/infrastructure/sqs.py +++ /dev/null @@ -1,136 +0,0 @@ -import boto3 -import os -from uuid import uuid1 as uuid -import json -from crowd.backend.infrastructure.logging import get_logger - -from crowd.backend.infrastructure.config import KUBE_MODE, IS_DEV_ENV, SQS_ENDPOINT_URL, SQS_REGION, \ - SQS_SECRET_ACCESS_KEY, SQS_ACCESS_KEY_ID - -logger = get_logger(__name__) - - -def string_converter(o): - """ - Function that converts object to string - This will be used when converting to Json, to convert non serializable attributes - """ - return o.__str__() - - -class SQS: - """ - Class to handle SQS requests. Can send and recieve messages. - """ - - def __init__(self, sqs_url): - """ - Initialise class to handle SQS requests. - - Args: - sqs_url (str): SQS url. - """ - self.sqs_url = sqs_url - # Otherwise from the environment files. - - # TODO-kube - if KUBE_MODE: - if SQS_ENDPOINT_URL: - self.sqs = boto3.client("sqs", - endpoint_url=SQS_ENDPOINT_URL, - region_name=SQS_REGION, - aws_secret_access_key=SQS_SECRET_ACCESS_KEY, - aws_access_key_id=SQS_ACCESS_KEY_ID) - else: - self.sqs = boto3.client("sqs", - region_name=SQS_REGION, - aws_secret_access_key=SQS_SECRET_ACCESS_KEY, - aws_access_key_id=SQS_ACCESS_KEY_ID) - else: - if os.environ.get("NODE_ENV") == "development": - self.sqs = boto3.client( - "sqs", - region_name="eu-central-1", - aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID_CROWD"), - aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY_CROWD"), - endpoint_url=f'{os.environ.get("LOCALSTACK_HOSTNAME")}:{os.environ.get("LOCALSTACK_PORT")}' - ) - else: - self.sqs = boto3.client( - "sqs", - region_name="eu-central-1", - aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID_CROWD"), - aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY_CROWD"), - ) - - def send_message(self, body, id, deduplicationId, attributes=None): - """ - Sent a message to the queue - - Args: - body (dict): the body of the message. - id (str): id of the message group - attributes (dict, optional): attributes for the message. Defaults to {}. - - Returns: - [type]: [description] - """ - - if not attributes: - attributes = {} - - if type(body) is not str: - body = json.dumps(body, default=string_converter) - return self.sqs.send_message( - QueueUrl=self.sqs_url, - MessageAttributes=attributes, - MessageBody=body, - MessageGroupId=id, - MessageDeduplicationId=deduplicationId, - ) - - def receive_message(self, delete=True, wait_time_seconds=0, visibility_timeout=60): - """ - Receive a message from the queue. - - Args: - delete (bool, optional): delete after receiving. Defaults to True. - wait_time_seconds (int, optional): how long should the request wait for a queue message. - visibility_timeout (int, optional): how long should the message be invisible to other receivers - - Returns: - dict: The fetched message. If no messages, returns None. - """ - response = self.sqs.receive_message( - QueueUrl=self.sqs_url, - MaxNumberOfMessages=1, - MessageAttributeNames=["All"], - VisibilityTimeout=visibility_timeout, - WaitTimeSeconds=wait_time_seconds, - ) - - if "Messages" in response.keys(): - - message = response["Messages"][0] - receipt_handle = message["ReceiptHandle"] - - if delete: - # Delete received message from queue - self.sqs.delete_message(QueueUrl=self.sqs_url, ReceiptHandle=receipt_handle) - return message - - return None - - def delete_message(self, receipt_handle): - """ - Delete a message from the queue. - Args: - receipt_handle: (string, required): receipt handle from the SQS message - - Returns: None - """ - self.sqs.delete_message(QueueUrl=self.sqs_url, ReceiptHandle=receipt_handle) - - @staticmethod - def make_id(): - return str(uuid()) diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/__init__.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/__init__.py deleted file mode 100644 index 987f1d3dd0..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -from .activity import Activity # noqa -from .member import Member # noqa -from .tenant import Tenant -from .microservice import Microservice -from .integration import Integration -from ..infrastructure import KUBE_MODE - -# from .repo import Repo # noqa -# TODO-kube -if not KUBE_MODE: - import dotenv # noqa - found = dotenv.find_dotenv(".env") - found_base = dotenv.find_dotenv(".env") - dotenv.load_dotenv(found) - dotenv.load_dotenv(found_base) \ No newline at end of file diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/activity.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/activity.py deleted file mode 100644 index aa7816fb9b..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/activity.py +++ /dev/null @@ -1,57 +0,0 @@ -from sqlalchemy import Column, String, DateTime, ForeignKey, Boolean, Integer -from .base import Base -from sqlalchemy.orm import relationship, validates -from sqlalchemy.dialects.postgresql import JSONB - - -class Activity(Base): - """ - Activity model - - Args: - Base (Base): Parent class - """ - - __tablename__ = "activities" # Table name in database - # __table_args__ = {'extend_existing': True} - - id = Column(String, primary_key=True) - type = Column(String, nullable=False) - timestamp = Column(DateTime, nullable=False) - platform = Column(String, nullable=False) - info = Column(JSONB, default={}) - crowdInfo = Column(JSONB, default={}) - isContribution = Column(Boolean, nullable=False, default=False) - score = Column(Integer, default=2) - sourceId = Column(String) - sourceParentId = Column(String) - importHash = Column(String, nullable=False) - createdAt = Column(DateTime) - updatedAt = Column(DateTime) - deletedAt = Column(DateTime) - - memberId = Column(String, ForeignKey("members.id"), nullable=False) - parentMember = relationship("Member", back_populates="activities") - - parentId = Column(String, ForeignKey("activities.id")) - parent = relationship("Activity") - - tenantId = Column(String, ForeignKey("tenants.id"), nullable=False) - parentTenant = relationship("Tenant", back_populates="activities") - - # validation - @validates("type") - def validate_type(self, key, value): - assert value != "" - return value - - @validates("platform") - def validate_platform(self, key, value): - assert value != "" - return value - - @validates("importHash") - def validate_importHash(self, key, value): - if value is not None: - assert len(value) <= 255 - return value diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/base.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/base.py deleted file mode 100644 index 860e54258a..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/base.py +++ /dev/null @@ -1,3 +0,0 @@ -from sqlalchemy.ext.declarative import declarative_base - -Base = declarative_base() diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/integration.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/integration.py deleted file mode 100644 index dbdc505c07..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/integration.py +++ /dev/null @@ -1,38 +0,0 @@ -from sqlalchemy import Column, String, DateTime, Integer, Text, ForeignKey -from .base import Base -from sqlalchemy.orm import relationship, validates -from sqlalchemy.dialects.postgresql import JSONB - - -class Integration(Base): - """ - Integration model - - Args: - Base (Base): Parent class - """ - - __tablename__ = "integrations" # Table name in database - - id = Column(String, primary_key=True) - platform = Column(Text) - status = Column(Text) - limitCount = Column(Integer) - limitLastResetAt = Column(DateTime) - token = Column(Text) - refreshToken = Column(Text) - settings = Column(JSONB) - integrationIdentifier = Column(Text) - importHash = Column(String, nullable=True) - createdAt = Column(DateTime) - updatedAt = Column(DateTime) - deletedAt = Column(DateTime) - - tenantId = Column(String, ForeignKey("tenants.id"), nullable=False) - parentTenant = relationship("Tenant", back_populates="integrations") - - @validates("importHash") - def validate_importHash(self, key, value): - if value is not None: - assert len(value) <= 255 - return value diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/member.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/member.py deleted file mode 100644 index 0891ab9ca3..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/member.py +++ /dev/null @@ -1,73 +0,0 @@ -from sqlalchemy import Column, String, DateTime, ForeignKey, Integer, Table, ARRAY -from .base import Base -from sqlalchemy.orm import relationship, validates -from sqlalchemy.dialects.postgresql import JSONB, UUID - - -association_noMerge_table = Table( - "memberNoMerge", - Base.metadata, - Column("memberId", UUID(as_uuid=True), ForeignKey("members.id"), primary_key=True), -) - -association_toMerge_table = Table( - "memberToMerge", - Base.metadata, - Column("memberId", UUID(as_uuid=True), ForeignKey("members.id"), primary_key=True), -) - - -class Member(Base): - """ - Member model - - Args: - Base (Base): Parent class - """ - - __tablename__ = "members" # Table name in database - - id = Column(String, primary_key=True) - displayName = Column(String, nullable=False) - displayName = Column(String, nullable=True) - attributes = Column(JSONB, default={}) - emails = Column(ARRAY(String)) - score = Column(Integer, default=-1) - joinedAt = Column(DateTime, nullable=False) - importHash = Column(String, nullable=True) - createdAt = Column(DateTime) - updatedAt = Column(DateTime) - deletedAt = Column(DateTime) - - tenantId = Column(String, ForeignKey("tenants.id"), nullable=False) - parentTenant = relationship("Tenant", back_populates="members") - - # relationships - activities = relationship("Activity", back_populates="parentMember", lazy="dynamic") - - noMerge = relationship("Member", secondary=association_noMerge_table, back_populates="noMerge") - - toMerge = relationship("Member", secondary=association_toMerge_table, back_populates="toMerge") - - # validation - @validates("username") - def validate_username(self, key, value): - assert value != "" - return value - - @validates("type") - def validate_type(self, key, value): - assert value != "" - assert value == "member" - return value - - @validates("joinedAt") - def validate_joinedAt(self, key, value): - assert value != "" - return value - - @validates("importHash") - def validate_importHash(self, key, value): - if value is not None: - assert len(value) <= 255 - return value diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/microservice.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/microservice.py deleted file mode 100644 index ea51f76c11..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/microservice.py +++ /dev/null @@ -1,38 +0,0 @@ -from sqlalchemy import Column, String, ForeignKey, Boolean, DateTime -from .base import Base -from sqlalchemy.orm import relationship, validates -from sqlalchemy.dialects.postgresql import JSONB - - -class Microservice(Base): - """ - Activity model - - Args: - Base (Base): Parent class - """ - - __tablename__ = "microservices" # Table name in database - # __table_args__ = {'extend_existing': True} - - id = Column(String, primary_key=True) - init = Column(Boolean, nullable=False) - running = Column(Boolean, nullable=False) - type = Column(String, nullable=False) - variant = Column(String, nullable=False) - settings = Column(JSONB) - importHash = Column(String, nullable=True) - createdAt = Column(DateTime) - updatedAt = Column(DateTime) - - tenantId = Column(String, ForeignKey("tenants.id"), nullable=False) - parentTenant = relationship("Tenant", back_populates="microservices") - - createdById = Column(String, ForeignKey("users.id")) - updatedById = Column(String, ForeignKey("users.id")) - - @validates("importHash") - def validate_importHash(self, key, value): - if value is not None: - assert len(value) <= 255 - return value diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/tenant.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/tenant.py deleted file mode 100644 index b454609039..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/models/tenant.py +++ /dev/null @@ -1,60 +0,0 @@ -from sqlalchemy import Column, String, DateTime -from .base import Base -from sqlalchemy.orm import relationship, validates - - -class Tenant(Base): - """ - Tenant model - - Args: - Base (Base): Parent class - """ - - __tablename__ = "tenants" # Table name in database - id = Column(String, primary_key=True) - name = Column(String, nullable=False) - url = Column(String, nullable=False) - plan = Column(String, nullable=False) - planStatus = Column(String, nullable=False, default="active") - planStripeCustomerId = Column(String) - planUserId = Column(String) - createdAt = Column(DateTime) - updatedAt = Column(DateTime) - deletedAt = Column(DateTime) - createdById = Column(String) - updatedById = Column(String) - - # relationships - - activities = relationship("Activity", back_populates="parentTenant") - members = relationship("Member", back_populates="parentTenant") - microservices = relationship("Microservice", back_populates="parentTenant") - integrations = relationship("Integration", back_populates="parentTenant") - - # validation - @validates("name") - def validate_name(self, key, value): - assert value != "" - assert value is not None - assert len(value) <= 255 - return value - - @validates("url") - def validate_url(self, key, value): - assert value != "" - assert value is not None - assert len(value) <= 50 - return value - - @validates("planStatus") - def validate_planStatus(self, key, value): - assert value != "" - assert value in ["active", "cancel_at_period_end", "error"] - return value - - @validates("planStripeCustomerId") - def validate_planStripeCustomerId(self, key, value): - assert value is not None - assert len(value) <= 255 - return value diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/repository/__init__.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/repository/__init__.py deleted file mode 100644 index 3bb026c019..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/repository/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .repository import Repository # noqa -from ..infrastructure import KUBE_MODE - -# TODO-kube -if not KUBE_MODE: - import dotenv - found = dotenv.find_dotenv(".env") - found_base = dotenv.find_dotenv(".env.base") - dotenv.load_dotenv(found) - dotenv.load_dotenv(found_base) diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/repository/keys.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/repository/keys.py deleted file mode 100644 index cd3709a7e9..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/repository/keys.py +++ /dev/null @@ -1,44 +0,0 @@ -class DBKeys: - INFO = "info" - CROWD_INFO = "crowdInfo" - URL = "url" - COMMITS = "commits" - ISSUES = "issues" - PULLS = "pulls" - SUBS = "subscribers" - TIMESTAMP = "timestamp" - NAME = "name" - USERNAME = "username" - STARS = "stargazers" - NETWORK = "network" - FORKS = "forks" - GITHUB_ACTIONS = "actions" - ACTIONS = "actions" - PLATFORM = "platform" - TYPE = "type" - FOLLOWERS = "followers" - FOLLOWING = "following" - PROJECTS = "projects" - PROJECT = "project" - TENANT = "tenantId" - IS_CONTRIBUTION = "isContribution" - DELETE_STARS = "delete-stargazers" - DELETE_SUBS = "delete-subscribers" - MEMBERS = "members" - MEMBER = "member" - SCORE = "score" - EMAILS = "emails" - TO_MERGE = "membersToMerge" - CROWD_USERNAME = "crowdUsername" - NO_MERGE = "noMerge" - TAGS = "tags" - JOINED_AT = "joinedAt" - ORGANISATION = "organisation" - LOCATION = "location" - BIO = "bio" - SIGNAL = "signal" - TOKEN = "token" - INTEGRATION_IDENTIFIER = "integrationIdentifier" - SOURCE_ID = "sourceId" - SOURCE_PARENT_ID = "sourceParentId" - TEAM_MEMBER = "team" diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/repository/repository.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/repository/repository.py deleted file mode 100644 index e49b65f1dc..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/repository/repository.py +++ /dev/null @@ -1,272 +0,0 @@ -from crowd.backend.infrastructure.config import KUBE_MODE, DB_URL -from crowd.backend.infrastructure.logging import get_logger -from crowd.backend.repository.keys import DBKeys as dbk -import dns -import os -from jmespath import search -from sqlalchemy.orm import sessionmaker -from sqlalchemy import create_engine -from crowd.backend.models.base import Base -from crowd.backend.models import Member -from crowd.backend.models import Activity -from crowd.backend.models import Tenant -from crowd.backend.models import Microservice -import uuid -import json - -from datetime import timedelta -from sqlalchemy import desc, asc - -logger = get_logger(__name__) - -_ = dns.version.version - - -class Repository(object): - """ - Class for interacting with the database. - """ - - def __init__(self, tenant_id="", db_url=False, test=False, send=True): - """ - Initialiser function for the the db repository. - - Args: - tenant_id (str, optional): the tenant ID. Defaults to ''. - db_url (bool, optional): the database url, otherwise it will be - obtain from env variables. Defaults to False. - test (bool, optional): whether we are in test mode. Defaults to - False. send (bool, optional): whether to save the documents. Defaults - to True. - """ - self.test = test - - if db_url: - self.db_url = db_url - else: - # TODO-kube - if KUBE_MODE: - self.db_url = DB_URL - else: - if test: - self.db_url = os.environ.get("DB_URL_TEST") - else: - username = os.environ.get("DATABASE_USERNAME") - password = os.environ.get("DATABASE_PASSWORD") - database = os.environ.get("DATABASE_DATABASE") - host = os.environ.get("DATABASE_HOST_READ") - self.db_url = f'postgresql://{username}:{password}@{host}/{database}' - - # self.db_url = 'postgresql://postgres:example@localhost:5432/crowd-web' - - self.engine = create_engine( - self.db_url, pool_pre_ping=True, echo=False, execution_options={"postgresql_readonly": True, "postgresql_deferrable": True}, - connect_args={ - "keepalives": 1, - "keepalives_idle": 30, - "keepalives_interval": 10, - "keepalives_count": 5, - } - ) - - Base.metadata.create_all(self.engine, checkfirst=True) - self.Session = sessionmaker(bind=self.engine) - - self.tenant_id = tenant_id - self.send = send - - def _validate_tenant_id(self): - """ - Check if a tenant ID is valid - - Raises: - BaseException: raise an error if the tenant id was not found - - Returns: - bool: true if it was found - """ - if not self.find_by_id(Tenant, self.tenant_id) and not self.test: - raise BaseException("Invalid tenant id") - return True - - def set_tenant_id(self, tenant_id): - self.tenant_id = tenant_id - - def find_in_table(self, table, query, many=False): - """ - Find a document in a collection - - Args: - table (Base): class of the entity - query (dict): query to search by. Example: {'firstname':'Duncan', 'lastname':'Iain'} - many (bool): whether to return many (defaults to False) - - Returns: - dict: document - """ - - with self.Session() as session: - search_query = session.query(table) - for attr, value in query.items(): - # Check if query is nested - nested_count = attr.count(".") - # If nested - if nested_count > 0: - attributes = attr.split(".") - nested_attributes = tuple(attributes[1:]) - # Define nested expression - expr = getattr(table, attributes[0])[nested_attributes] - # Execute search_query - search_query = search_query.filter(expr == json.dumps(value)) - else: - search_query = search_query.filter(getattr(table, attr) == value) - - if many: - return search_query.all() - return search_query.first() - - def find_by_id(self, table, id): - """ - Find by id - - Args: - table (Base): class of the entity - id (str): the id of the document - - Returns: - dict: the document - """ - - with self.Session() as session: - return session.query(table).get(id) - - def find_all_usernames(self): - with self.engine.connect() as con: - return con.execute( - f"""select m."id", mw."username", m."displayName", m."emails" - from "members" m - inner join "memberActivityAggregatesMVs" mw on m.id = mw.id - where m."tenantId" = '{self.tenant_id}'""" - ).fetchall() - - def find_all( - self, table, ignore_tenant: "bool" = False, query: "dict" = None, order: "dict" = None - ) -> "list[dict]": - """ - Find all the documents in a collection - - Args: - table (Base): class of the entity - ignore_tenant (bool, optional): whether to filter by tenant. Never set to True in production. - Defaults to False. - query (dict): The query dictionary - order (dict) - - Returns: - [type]: [description] - """ - if not query: - query = {} - - if not ignore_tenant: - query = { - **query, - **{dbk.TENANT: uuid.UUID(self.tenant_id)}, - } - - with self.Session() as session: - search_query = session.query(table) - for attr, value in query.items(): - # Check if query is nested - nested_count = attr.count(".") - # If nested - if nested_count > 0: - attributes = attr.split(".") - nested_attributes = tuple(attributes[1:]) - # Define nested expression - expr = getattr(table, attributes[0])[nested_attributes] - # Execute search_query - search_query = search_query.filter(expr == json.dumps(value)) - else: - search_query = search_query.filter(getattr(table, attr) == value) - - if order: - for key, value in order.items(): - if value: - search_query = search_query.order_by(asc(key)) - else: - search_query = search_query.order_by(desc(key)) - - return search_query.all() - - def find_activities(self, search_filters=None): - if not search_filters: - search_filters = {} - return self.find_in_table(Activity, search_filters, many=True) - - def count(self, table, search_filters=None): - if not search_filters: - search_filters = {} - - search_filters[dbk.TENANT] = uuid.UUID(self.tenant_id) - - with self.Session() as session: - search_query = session.query(table) - for attr, value in search_filters.items(): - # Check if query is nested - nested_count = attr.count(".") - # If nested - if nested_count > 0: - attributes = attr.split(".") - nested_attributes = tuple(attributes[1:]) - # Define nested expression - expr = getattr(table, attributes[0])[nested_attributes] - # Execute query - search_query = search_query.filter(expr == json.dumps(value)) - else: - search_query = search_query.filter(getattr(table, attr) == value) - - return search_query.count() - - def find_available_microservices(self, service): - """ - Function to get microservices of type service that are not running - - Args: - service (str): - """ - - return self.find_in_table(Microservice, {"type": service, "running": False}, many=True) - - def find_new_members(self, microservice, query: "dict" = None) -> "list[dict]": - """ - Find all the documents in a collection - - Args: - table (Base): class of the entity - - Returns: - Array: Members - """ - if not query: - query = {} - - query = { - **query, - **{dbk.TENANT: uuid.UUID(self.tenant_id)}, - } - - with self.Session() as session: - search_query = session.query(Member) - - # Filter with query - for attr, value in query.items(): - search_query = search_query.filter(getattr(Member, attr) == value) - - # Find members that are new - # We use a security padding of 5 minutes - search_query = search_query.filter( - Member.createdAt >= (microservice.updatedAt - timedelta(minutes=5)) - ).order_by(Member.createdAt.desc()) - - return search_query.all() diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/repository/test_repository.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/repository/test_repository.py deleted file mode 100644 index b0bedf1dfc..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/repository/test_repository.py +++ /dev/null @@ -1,97 +0,0 @@ -from crowd.backend.models.integration import Integration -from crowd.backend.repository import Repository -from crowd.backend.models.activity import Activity -from crowd.backend.models.member import Member -import uuid - - -def test_find_in_table(api: "Repository"): - """Tests the find in table function on an Activity of type pull_request-closed""" - result = api.find_in_table(Activity, {"type": "pull_request-closed"}) - assert result.id == uuid.UUID("51feeeee-202b-4271-8ef5-2ba8bef973d3") - - -def test_find_by_id(api: "Repository"): - """Tests the find by id function for a Member""" - result = api.find_by_id(Member, "26f6d9ed-cf73-4dad-80f2-7f6e23a38370") - assert result.username == {"github": "0x161e-swei", "crowdUsername": "0x161e-swei"} - - -def test_find_by_id_empty(api: "Repository"): - """Tests the find by id function on an unexistant Member""" - result = api.find_by_id(Member, uuid.UUID("123e4567-e89b-12d3-a456-426614174000")) - assert result is None - - -def test_find_all(api: "Repository"): - """Tests the find by id function for a Member""" - result = api.find_all(Member) - assert len(result) > 0 - assert type(result[0]) == Member - - -def test_find_all_single_target(api: "Repository"): - """Tests the find all function for a Member and using github username""" - result = api.find_all(Member, query={"username.github": "cjqpker"}) - assert len(result) == 1 - assert type(result[0]) == Member - - -def test_find_members(api: "Repository"): - """Tests the find members function""" - result = api.find_members("ThomasPluck") - assert len(result) > 0 - assert type(result[0]) == Member - assert result[0].username["crowdUsername"] == "ThomasPluck" - - -def test_count(api: "Repository"): - """Tests the count function for Members that have their slack timzezone in Europe/Amsterdam""" - result = api.count(Member, {"crowdInfo.slack.timezone": "Europe/Amsterdam"}) - assert result == 2 - - -def test_validate_tenant_id(api: "Repository"): - """Tests the validate tenant_id function""" - result = api._validate_tenant_id() - assert result is True - - -def test_find_active_widgets(api: "Repository"): - """Tests finding active widgets of type benchmark""" - result = api.find_active_widgets("benchmark") - assert len(result) == 1 - assert result[0].type == "benchmark" - assert result[0].settings is not None - - -def test_find_activities(api: "Repository"): - """ "Tests finding activities of type "joined_community""" - result = api.find_activities({"type": "joined_community"}) - - assert type(result[0]) == Activity - assert result[0].type == "joined_community" - assert len(result) == 3753 - - -def test_find_integration_by_platform(api: "Repository"): - """ "Tests finding integration by crowd platform""" - result = api.find_integration_by_platform("crowd") - - assert type(result) == Integration - assert result.platform == "crowd" - - -def test_find_integration_by_identifier(api: "Repository"): - """ "Tests finding integration by github identification""" - result = api.find_integration_by_identifier("github", "23253548") - - assert type(result) == Integration - - -def test_find_community_member_order_by_createdAt(api: "Repository"): - """Tests finding members and ordering them with descending createdAt dates""" - - members = api.find_all(Member, query={"type": "member"}, order={Member.createdAt: False}) - - assert members[0].createdAt >= members[len(members) - 1].createdAt diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/repository/test_sample.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/repository/test_sample.py deleted file mode 100644 index 087bb5f24b..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/repository/test_sample.py +++ /dev/null @@ -1,48 +0,0 @@ -from crowd.backend.repository import Repository -from crowd.backend.models.activity import Activity -from crowd.backend.models.member import Member -import uuid - - -def test_find_in_table(api: "Repository"): - result = api.find_in_table(Activity, {"type": "pull_request-closed"}) - assert result.id == uuid.UUID("51feeeee-202b-4271-8ef5-2ba8bef973d3") - - -def test_find_by_id(api: "Repository"): - result = api.find_by_id(Member, "26f6d9ed-cf73-4dad-80f2-7f6e23a38370") - assert result.username == {"github": "0x161e-swei", "crowdUsername": "0x161e-swei"} - - -def test_find_by_id_empty(api: "Repository"): - result = api.find_by_id(Member, uuid.UUID("123e4567-e89b-12d3-a456-426614174000")) - assert result is None - - -def test_find_all(api: "Repository"): - result = api.find_all(Member) - assert len(result) > 0 - assert type(result[0]) == Member - - -def test_find_all_single_target(api: "Repository"): - result = api.find_all(Member, query={"username.github": "cjqpker"}) - assert len(result) == 1 - assert type(result[0]) == Member - - -def test_find_members(api: "Repository"): - result = api.find_members("ThomasPluck") - assert len(result) > 0 - assert type(result[0]) == Member - assert result[0].username["crowdUsername"] == "ThomasPluck" - - -def test_count(api: "Repository"): - result = api.count(Member, {"crowdInfo.slack.timezone": "Europe/Amsterdam"}) - assert result == 2 - - -def test_validate_tenant_id(api: "Repository"): - result = api._validate_tenant_id() - assert result is True diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/utils/coordinator/__init__.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/utils/coordinator/__init__.py deleted file mode 100644 index b9ce97dd1f..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/utils/coordinator/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .base_coordinator import base_coordinator # noqa diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/utils/coordinator/base_coordinator.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/utils/coordinator/base_coordinator.py deleted file mode 100644 index 4c5982c48f..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/utils/coordinator/base_coordinator.py +++ /dev/null @@ -1,21 +0,0 @@ -from crowd.backend.repository import Repository -from crowd.backend.models.tenant import Tenant -from crowd.backend.infrastructure import ServicesSQS - - -def base_coordinator(service, tenants=None): - """ - Coordinator function handler that gets all the tenants and sends an SQS message to the worker for each tenant - Args: - service (str): The service to be processed - Returns: - (str): Success message - """ - # Getting all available microservices of type service - microservices = Repository().find_available_microservices(service) - - sqs_sender = ServicesSQS() - for microservice in microservices: - sqs_sender.send_message(microservice.tenantId, microservice.id, service) - - return f"{len(microservices)} microservices sent to {service} queue" diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/utils/datetime/__init__.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/utils/datetime/__init__.py deleted file mode 100644 index c0eb47562f..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/utils/datetime/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .crowd_date_time import CrowdDateTime # noqa diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/utils/datetime/crowd_date_time.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/utils/datetime/crowd_date_time.py deleted file mode 100644 index 9d6e17246b..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/utils/datetime/crowd_date_time.py +++ /dev/null @@ -1,24 +0,0 @@ -from datetime import datetime -import pytz - - -class CrowdDateTime: - @staticmethod - def now(): - return datetime.now().replace(tzinfo=pytz.utc) - - @staticmethod - def format(date): - date = date.replace(tzinfo=pytz.utc) - return date - - @staticmethod - def date_time(*args): - return CrowdDateTime.format(datetime(*args)) - - @staticmethod - def from_time_stamp(ts): - if type(ts) == str: - if "." in ts: - ts = ts[: ts.find(".")] - return CrowdDateTime.format(datetime.fromtimestamp(int(ts))) diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/utils/grid/__init__.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/utils/grid/__init__.py deleted file mode 100644 index 8e1148d88c..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/utils/grid/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .grid import GithubGrid # noqa diff --git a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/utils/grid/grid.py b/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/utils/grid/grid.py deleted file mode 100644 index 7b1bf028e6..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/crowd/backend/utils/grid/grid.py +++ /dev/null @@ -1,29 +0,0 @@ -from crowd.backend.infrastructure.logging import get_logger - -logger = get_logger(__name__) - -class BaseGrid: - - default = 2 - - @classmethod - def get_score(cls, action): - action = action.replace("-", "_") - if action in cls.__dict__: - return cls.__dict__[action] - return BaseGrid.default - - -class GithubGrid(BaseGrid): - issues_opened = 8 - issues_closed = 6 - issue_comment = 6 - pull_request_opened = 10 - pull_request_closed = 8 - pull_request_comment = 6 - commit_comment = 3 - star = 2 - unstar = -2 - fork = 4 - - diff --git a/backend/src/serverless/microservices/python/crowd-backend/setup.py b/backend/src/serverless/microservices/python/crowd-backend/setup.py deleted file mode 100644 index 78b9eb8744..0000000000 --- a/backend/src/serverless/microservices/python/crowd-backend/setup.py +++ /dev/null @@ -1,18 +0,0 @@ -import io -import os - -from setuptools import setup, find_namespace_packages - - -def read(rel_path): - here = os.path.abspath(os.path.dirname(__file__)) - with io.open(os.path.join(here, rel_path), "r") as fp: - return fp.read() - - -setup( - name="crowd-backend", - packages=find_namespace_packages(include=["crowd.*"]), - install_requires=["pyjwt", "python-dotenv", "requests", "cryptography == 3.4.7", - "python-dateutil", "pytz", "SQLAlchemy==1.4.46", "dnspython==2.2.1", "boto3"], -) diff --git a/backend/src/serverless/microservices/python/crowd-members-score/crowd/members_score/__init__.py b/backend/src/serverless/microservices/python/crowd-members-score/crowd/members_score/__init__.py deleted file mode 100644 index 32e49c996b..0000000000 --- a/backend/src/serverless/microservices/python/crowd-members-score/crowd/members_score/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .members_score import MembersScore # noqa -from .worker import members_score_worker # noqa \ No newline at end of file diff --git a/backend/src/serverless/microservices/python/crowd-members-score/crowd/members_score/members_score.py b/backend/src/serverless/microservices/python/crowd-members-score/crowd/members_score/members_score.py deleted file mode 100644 index ebd116ef9d..0000000000 --- a/backend/src/serverless/microservices/python/crowd-members-score/crowd/members_score/members_score.py +++ /dev/null @@ -1,235 +0,0 @@ -import decimal -from crowd.backend.infrastructure.logging import get_logger -from crowd.backend.repository import Repository -from crowd.backend.repository.keys import DBKeys as dbk -from datetime import datetime -from dateutil import parser -from crowd.backend.controllers import MembersController -from crowd.backend.models import Member, Tenant -import time -from crowd.backend.utils.datetime import CrowdDateTime as cdt -from sklearn.cluster import KMeans -import numpy as np - -logger = get_logger(__name__) - - -class MembersScore: - def __init__(self, tenant_id, repository=False, test=False, send=True): - - self.tenant_id = tenant_id - - if not repository: - self.repository = Repository(tenant_id=self.tenant_id, test=test) - else: - self.repository = repository - - self.fetch_scores() - self.team_members = [ - member.id for member in self.repository.find_all(Member, query={"attributes.isTeamMember.default": True}) - ] - - self.send = send - - self.original_scores = {} - self.scores = {} - - def fetch_scores(self): - """ - This function accesses the database and fetches the mean scores for each member for the last year - - The sql query selects all members for a tenant, - and joins it with a table containing every single day for the past year. - This resulting table is then used to calculate the monthly mean score of - engagement for each member for the past year. - The results of this query should be a table where each row contains a member and his/her engagement - for each month of the past year. - """ - with self.repository.engine.connect() as con: - - id = self.repository.tenant_id - - self.mean_scores = con.execute( - f'select "memberId", avg(number_daily_activities) as average_daily_activities, avg(summed_daily_score) as summed_daily_score, coalesce(stddev(number_daily_activities),0), coalesce(stddev(summed_daily_score),0) ,extract(month from MyJoinDate) as month, extract(year from MyJoinDate) as year\ - from (\ - select FullDates."memberId", FullDates.MyJoinDate, coalesce(sum(e), 0) as number_daily_activities, coalesce(sum(s), 0) as summed_daily_score from \ - (\ - select "memberId", AllDays.MyJoinDate, coalesce(sum(e), 0) as number_daily_activities, coalesce(sum(s), 0) as summed_daily_score\ - from\ - (SELECT date_trunc(\'day\', dd):: date as MyJoinDate\ - FROM generate_series\ - ( (now() - INTERVAL \'364 DAY\')::timestamp\ - , (now())::timestamp\ - , \'1 day\'::interval) dd\ - ) AllDays\ - cross join ( select "memberId", count(*) as e, sum(score) as s, date("timestamp") as "timestamp" \ - from public.activities where "activities"."tenantId" = CAST(\'{id}\' as uuid) \ - group by "memberId", date("timestamp") ) U\ - group by "memberId", Alldays.MyJoinDate order by Alldays.MyJoinDate ASC\ - ) FullDates \ - left join (select "memberId" as cm_id, count(*) as e, sum(score) as s, date("timestamp") as "timestamp" \ - from public.activities where "activities"."tenantId" = CAST(\'{id}\' as uuid) \ - group by "memberId", date("timestamp")) T on T."cm_id"=FullDates."memberId" and T."timestamp" = FullDates.MyJoinDate\ - group by FullDates."memberId", FullDates.MyJoinDate order by FullDates.MyJoinDate asc\ - ) Daily group by "memberId", extract(month from MyJoinDate), extract(year from MyJoinDate)' - ).fetchall() - - def _calculate_months(self, date): - """ - Calculate time difference - - Args: - date (datime.datetime): the date to calculate months from - """ - now = cdt.format(datetime.now()) - date = cdt.format(date) - diff = now - date - return (diff.days) / 30 - - def calculate_member_score(self, i, row): - - result = 0 - - k = 10 - m = 13 # Number of months to take into account - - average_monthly_score = row[2] - - current_month = datetime.now().month - current_day = datetime.now().day - - stddev_score_activities = row[4] - month = int(row[5]) - year = int(row[6]) - - if month == current_month: - average_monthly_score = average_monthly_score * decimal.Decimal(current_day / 30) - - sm = float(average_monthly_score) / float(1 + stddev_score_activities) - - time_from_month = self._calculate_months(datetime.strptime(f"{int(year)}-{int(month)}", "%Y-%m")) - - result = ((0.9**time_from_month) * sm) * (k / m) - - return result - - def _member_lookalike_score(self, lookalikes): - out = {} - for member in lookalikes: - score = 0 - if dbk.ACTIONS in member.crowdInfo.get("github", {}): - for action in member.crowdInfo["github"][dbk.ACTIONS]: - timestamp = timestamp = parser.parse(action["timestamp"]) - timestamp = timestamp.replace(tzinfo=None) - score += (0.9 ** self._calculate_months(timestamp)) * action["score"] - - twitter = "twitter" in member.username - email = member.email - if email and twitter: - score *= 4 - elif email or twitter: - score *= 2.5 - out[member.id] = round(score, 2) - return out - - def _member_scores_(self, members): - """ - Calculate the raw score for all members based on the activities they performed. - Loop through the list of all activities and add the score of the activity weighted by the time since the activity - to the score of the member. - """ - - mean_scores = self.mean_scores - - scores = {} - for i, row in enumerate(mean_scores): - - member_id = row[0] - - # Checking that member is not team member - # if "team" not in member.crowdInfo: - if member_id not in self.team_members: - if member_id not in scores: - scores[member_id] = 0 - - scores[member_id] += self.calculate_member_score(i, row) - else: - scores[member_id] = -1 - - return scores - - def normalise(self, scores): - """ - Normalise the scores of all members based on the median raw score of all members. - """ - - # Getting a list of members who have 0 engagement - # And a list of raw scores for noramlization - inactive_members = [k for k, v in scores.items() if v == 0] - - active_members_scores = {k: v for k, v in scores.items() if v != 0} - - active_members_raw_scores = [active_members_scores[x] for x in active_members_scores.keys()] - - if len(active_members_scores) == 0: - return active_members_scores - - # Initialize the k means cluster - if len(active_members_raw_scores) < 10: - k = len(active_members_raw_scores) - else: - k = 10 - kmeans = KMeans(n_clusters=k, random_state=0) - # Fit predict on our scores - normalized_scores = kmeans.fit_predict(np.array(active_members_raw_scores).reshape(-1, 1)) - - ord_idx = np.argsort(kmeans.cluster_centers_.flatten()) - - cntrs = np.zeros_like(normalized_scores) - 1 - for i in np.arange(k): - cntrs[normalized_scores == ord_idx[i]] = i - - normalized_scores = cntrs - - # Assigning inactive member engagement level - for key in inactive_members: - scores[key] = 0 - - # Assigning other engagement level - i = 0 - for key in active_members_scores: - scores[key] = normalized_scores[i] + 1 - i += 1 - return scores - - def main(self): - # Keeping track of time for lambda timeout - start = time.time() - members = self.repository.find_all(Member, query={}) - - for member in members: - self.original_scores[member.id] = member.score - - self.scores = self._member_scores_(members) - - # Take care of case where tenant doesn't have activities - if len(self.scores) == 0: - return {} - - scores_to_update = self.normalise(self.scores) - - changed = 0 - - members_controller = MembersController(self.tenant_id, repository=self.repository) - - for n, member_id in enumerate(scores_to_update): - if time.time() - start > 800: - break - # We only update the score if it has changed - if scores_to_update[member_id] != self.original_scores.get(member_id, -2): - changed += 1 - members_controller.update( - [{"id": str(member_id), "update": {dbk.SCORE: scores_to_update[member_id]}}], send=self.send - ) - - return scores_to_update diff --git a/backend/src/serverless/microservices/python/crowd-members-score/crowd/members_score/test_members_score.py b/backend/src/serverless/microservices/python/crowd-members-score/crowd/members_score/test_members_score.py deleted file mode 100644 index e1399feeb7..0000000000 --- a/backend/src/serverless/microservices/python/crowd-members-score/crowd/members_score/test_members_score.py +++ /dev/null @@ -1,57 +0,0 @@ -from crowd.backend.repository import Repository -from crowd.members_score import MembersScore - - -def test_calculate_member_score(api: "Repository"): - - api.set_tenant_id("f5c97d75-b919-4be6-9e57-b851efb336a1") - members_score = MembersScore(api.tenant_id, api, send=False) - - updates = members_score.main() - - assert len(updates) == 207 - assert type(updates) == dict - - -def test_calculate_tenant_with_less_than_10_members(api: "Repository"): - - api.set_tenant_id("f6ea695e-cd8d-437b-acaa-474c53d76b05") - members_score = MembersScore(api.tenant_id, api, send=False) - - updates = members_score.main() - - assert len(updates) == 4 - assert type(updates) == dict - - -def test_check_updates_when_scores_dont_change(api: "Repository"): - - id = "f6ea695e-cd8d-437b-acaa-474c53d76b05" - api.set_tenant_id(id) - members_score = MembersScore(api.tenant_id, api, send=False) - - updates = members_score.main() - - with api.engine.connect() as con: - number_of_members = con.execute( - f'select count(*) from "members" cm where "tenantId" = \'{id}\'' - ).fetchall()[0][0] - - # Check that if length of updates is less than the number of members - assert len(updates) < number_of_members - assert type(updates) == dict - - -def test_check_specific_member_scores(api: "Repository"): - - id = "b044af41-657a-4925-9541-cf8dfbdc687b" - api.set_tenant_id(id) - members_score = MembersScore(api.tenant_id, api, send=False) - - updates = members_score.main() - - updates_str = {str(k): v for k, v in updates.items()} - - assert updates_str["f97995cd-6400-49e9-84a6-6ef9f38ffbf6"] == 6 - assert updates_str["f2e355ed-3a45-4b63-b228-59ee7aeafe0c"] == 7 - assert updates_str["bc6665c0-203c-4d9c-b95f-07877df7f9be"] == 1 diff --git a/backend/src/serverless/microservices/python/crowd-members-score/crowd/members_score/worker.py b/backend/src/serverless/microservices/python/crowd-members-score/crowd/members_score/worker.py deleted file mode 100644 index 1a00d35dee..0000000000 --- a/backend/src/serverless/microservices/python/crowd-members-score/crowd/members_score/worker.py +++ /dev/null @@ -1,5 +0,0 @@ -from crowd.members_score import MembersScore - - -def members_score_worker(tenant_id): - MembersScore(tenant_id).main() diff --git a/backend/src/serverless/microservices/python/crowd-members-score/setup.py b/backend/src/serverless/microservices/python/crowd-members-score/setup.py deleted file mode 100644 index 80f6070d15..0000000000 --- a/backend/src/serverless/microservices/python/crowd-members-score/setup.py +++ /dev/null @@ -1,20 +0,0 @@ -import io -import os - -from setuptools import setup, find_namespace_packages - - -def read(rel_path): - here = os.path.abspath(os.path.dirname(__file__)) - with io.open(os.path.join(here, rel_path), "r") as fp: - return fp.read() - - -setup( - name="crowd-member-metrics", - packages=find_namespace_packages(include=["crowd.*"]), - install_requires=[ - "python-dateutil", - "scikit-learn", - ], -) diff --git a/backend/src/serverless/microservices/python/hooks/pre-push b/backend/src/serverless/microservices/python/hooks/pre-push deleted file mode 100644 index f168e4c349..0000000000 --- a/backend/src/serverless/microservices/python/hooks/pre-push +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -unameOut="$(uname -s)" -case "${unameOut}" in - Linux*) machine=Linux;; - Darwin*) machine=Mac;; - CYGWIN*) machine=Cygwin;; - MINGW*) machine=MinGw;; - MSYS_NT*) machine=Windows;; - *) machine="UNKNOWN:${unameOut}" -esac - - -case "$machine" in - Windows) - source venv-crowd/Scripts/activate - ;; - MinGw) - source venv-crowd/Scripts/activate - ;; - *) - source venv-crowd/bin/activate - ;; -esac - -pytest diff --git a/backend/src/serverless/microservices/python/pyproject.toml b/backend/src/serverless/microservices/python/pyproject.toml deleted file mode 100644 index 014f377e3d..0000000000 --- a/backend/src/serverless/microservices/python/pyproject.toml +++ /dev/null @@ -1,9 +0,0 @@ -[tool.black] -line-length = 120 -exclude = ''' -/( - \.git - | \.venv - | \.?venv.* -)/ -''' diff --git a/backend/src/serverless/microservices/python/pytest.ini b/backend/src/serverless/microservices/python/pytest.ini deleted file mode 100644 index 239a35b573..0000000000 --- a/backend/src/serverless/microservices/python/pytest.ini +++ /dev/null @@ -1,7 +0,0 @@ -[pytest] -env_files = - .env - .env -log_cli=true -log_level=INFO - diff --git a/backend/src/serverless/microservices/python/python_worker.py b/backend/src/serverless/microservices/python/python_worker.py deleted file mode 100644 index 861fb8bd20..0000000000 --- a/backend/src/serverless/microservices/python/python_worker.py +++ /dev/null @@ -1,40 +0,0 @@ -import json - -from crowd.backend.enums import Services -from crowd.backend.infrastructure import SQS -from crowd.backend.infrastructure.config import PYTHON_WORKER_QUEUE -from crowd.backend.infrastructure.logging import get_logger -from crowd.backend.utils.coordinator import base_coordinator -from crowd.members_score import members_score_worker - -logger = get_logger(__name__) - -sqs = SQS(PYTHON_WORKER_QUEUE) - -logger.info(f"Listening for messages on: {PYTHON_WORKER_QUEUE}") - -while True: - msg = sqs.receive_message(delete=False, wait_time_seconds=15) - if msg is not None: - msg_receipt = msg['ReceiptHandle'] - - body = json.loads(msg['Body']) - msg_type = body.get('type', '') - service = body.get('service', '') - tenant_id = body.get('tenant', '') - microservice_id = body.get('microservice_id', '') - member = body.get('member', '') - params = body.get('params', None) - - if service == Services.MEMBERS_SCORE.value: - sqs.delete_message(msg_receipt) - logger.info("triggering members_score") - members_score_worker(tenant_id) - - elif msg_type == Services.MEMBERS_SCORE.value: - sqs.delete_message(msg_receipt) - logger.info("triggering members_score coordinator") - base_coordinator(str(Services.MEMBERS_SCORE.value)) - - else: - logger.error(f"Error while processing a queue message! Unrecognized message format: {body}") diff --git a/backend/src/serverless/microservices/python/requirements.dev.txt b/backend/src/serverless/microservices/python/requirements.dev.txt deleted file mode 100644 index e5b6e390d1..0000000000 --- a/backend/src/serverless/microservices/python/requirements.dev.txt +++ /dev/null @@ -1,8 +0,0 @@ -black~=22.3.0 -flake8~=4.0.1 -pre-commit~=2.15.0 -pytest~=6.2.5 -pytest-docker~=0.12.0 -pytest-dotenv~=0.5.2 -pytest-mock~=3.6.1 -psycopg2~=2.9.3 \ No newline at end of file diff --git a/backend/src/serverless/microservices/python/requirements.txt b/backend/src/serverless/microservices/python/requirements.txt deleted file mode 100644 index a0a88fff80..0000000000 --- a/backend/src/serverless/microservices/python/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ --e ./crowd-backend --e ./crowd-members-score -python-json-logger \ No newline at end of file diff --git a/backend/src/serverless/microservices/python/start-python-worker.sh b/backend/src/serverless/microservices/python/start-python-worker.sh deleted file mode 100755 index 4814f166f7..0000000000 --- a/backend/src/serverless/microservices/python/start-python-worker.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -CLI_HOME="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -$CLI_HOME/venv/bin/python -u python_worker.py diff --git a/backend/src/serverless/types/workerTypes.ts b/backend/src/serverless/types/workerTypes.ts deleted file mode 100644 index 9ab75a0917..0000000000 --- a/backend/src/serverless/types/workerTypes.ts +++ /dev/null @@ -1,17 +0,0 @@ -export enum NodeWorkerMessageType { - INTEGRATION_CHECK = 'integration_check', - INTEGRATION_PROCESS = 'integration_process', - NODE_MICROSERVICE = 'node_microservice', - DB_OPERATIONS = 'db_operations', - PROCESS_WEBHOOK = 'process_webhook', -} - -export enum PythonWorkerMessageType { - MEMBERS_SCORE = 'members_score', -} - -export interface PythonWorkerMessage { - type: PythonWorkerMessageType - member?: string - tenant?: string -} diff --git a/backend/src/serverless/utils/nodeWorkerSQS.ts b/backend/src/serverless/utils/nodeWorkerSQS.ts deleted file mode 100644 index b8a69518d1..0000000000 --- a/backend/src/serverless/utils/nodeWorkerSQS.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { MessageBodyAttributeMap } from 'aws-sdk/clients/sqs' -import moment from 'moment' -import { getServiceChildLogger } from '@crowd/logging' -import { NodeWorkerMessageBase } from '../../types/mq/nodeWorkerMessageBase' -import { IS_TEST_ENV, SQS_CONFIG } from '../../conf' -import { sendMessage } from '../../utils/sqs' -import { NodeWorkerMessageType } from '../types/workerTypes' -import { AutomationTrigger } from '../../types/automationTypes' -import { ExportableEntity } from '../microservices/nodejs/messageTypes' - -const log = getServiceChildLogger('nodeWorkerSQS') - -// 15 minute limit for delaying is max for SQS -const limitSeconds = 15 * 60 - -export const sendNodeWorkerMessage = async ( - tenantId: string, - body: NodeWorkerMessageBase, - delaySeconds?: number, - targetQueueUrl?: string, -): Promise => { - if (IS_TEST_ENV) { - return - } - - // we can only delay for 15 minutes then we have to re-delay message - let attributes: MessageBodyAttributeMap - let delay: number - let delayed = false - if (delaySeconds) { - if (delaySeconds > limitSeconds) { - // delay for 15 minutes and add the remaineder to the attributes - const remainedSeconds = delaySeconds - limitSeconds - attributes = { - tenantId: { - DataType: 'String', - StringValue: tenantId, - }, - remainingDelaySeconds: { - DataType: 'Number', - StringValue: `${remainedSeconds}`, - }, - } - - if (targetQueueUrl) { - attributes.targetQueueUrl = { DataType: 'String', StringValue: targetQueueUrl } - } - delay = limitSeconds - } else { - attributes = { - tenantId: { - DataType: 'String', - StringValue: tenantId, - }, - } - if (targetQueueUrl) { - attributes.targetQueueUrl = { DataType: 'String', StringValue: targetQueueUrl } - } - delay = delaySeconds - } - - delayed = true - } - - const now = moment().valueOf() - - const params = { - QueueUrl: delayed ? SQS_CONFIG.nodejsWorkerDelayableQueue : SQS_CONFIG.nodejsWorkerQueue, - MessageGroupId: delayed ? undefined : `${now}`, - MessageDeduplicationId: delayed ? undefined : `${tenantId}-${now}`, - MessageBody: JSON.stringify(body), - MessageAttributes: attributes, - DelaySeconds: delay, - } - - log.debug( - { - messageType: body.type, - body, - }, - 'Sending nodejs-worker sqs message!', - ) - await sendMessage(params) -} - -export const sendNewActivityNodeSQSMessage = async ( - tenant: string, - activityId: string, - segmentId: string, -): Promise => { - const payload = { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - tenant, - activityId, - segmentId, - trigger: AutomationTrigger.NEW_ACTIVITY, - service: 'automation', - } - await sendNodeWorkerMessage(tenant, payload as NodeWorkerMessageBase) -} - -export const sendNewMemberNodeSQSMessage = async ( - tenant: string, - memberId: string, - segmentId: string, -): Promise => { - const payload = { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - tenant, - memberId, - segmentId, - trigger: AutomationTrigger.NEW_MEMBER, - service: 'automation', - } - await sendNodeWorkerMessage(tenant, payload as NodeWorkerMessageBase) -} - -export const sendExportCSVNodeSQSMessage = async ( - tenant: string, - user: string, - entity: ExportableEntity, - segmentIds: string[], - criteria: any, -): Promise => { - const payload = { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - service: 'csv-export', - user, - tenant, - entity, - criteria, - segmentIds, - } - await sendNodeWorkerMessage(tenant, payload as NodeWorkerMessageBase) -} - -export const sendBulkEnrichMessage = async ( - tenant: string, - memberIds: string[], - segmentIds: string[], - notifyFrontend: boolean = true, - skipCredits: boolean = false, -): Promise => { - const payload = { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - service: 'bulk-enrich', - memberIds, - tenant, - segmentIds, - notifyFrontend, - skipCredits, - } - await sendNodeWorkerMessage(tenant, payload as NodeWorkerMessageBase) -} diff --git a/backend/src/serverless/utils/pythonWorkerSQS.ts b/backend/src/serverless/utils/pythonWorkerSQS.ts deleted file mode 100644 index 91768f3c6c..0000000000 --- a/backend/src/serverless/utils/pythonWorkerSQS.ts +++ /dev/null @@ -1,27 +0,0 @@ -import moment from 'moment' -import { sqs } from '../../services/aws' -import { IS_TEST_ENV, KUBE_MODE, SQS_CONFIG } from '../../conf' -import { PythonWorkerMessage } from '../types/workerTypes' - -export const sendPythonWorkerMessage = async ( - tenantId: string, - body: PythonWorkerMessage, -): Promise => { - if (IS_TEST_ENV) { - return - } - - // TODO-kube - if (!KUBE_MODE) { - throw new Error("Can't send python-worker SQS message when not in kube mode!") - } - - await sqs - .sendMessage({ - QueueUrl: SQS_CONFIG.pythonWorkerQueue, - MessageGroupId: tenantId, - MessageDeduplicationId: `${tenantId}-${moment().valueOf()}`, - MessageBody: JSON.stringify(body), - }) - .promise() -} diff --git a/backend/src/serverless/utils/queueService.ts b/backend/src/serverless/utils/queueService.ts new file mode 100644 index 0000000000..8e751a0bca --- /dev/null +++ b/backend/src/serverless/utils/queueService.ts @@ -0,0 +1,58 @@ +import { + DataSinkWorkerEmitter, + IntegrationRunWorkerEmitter, + IntegrationStreamWorkerEmitter, + SearchSyncWorkerEmitter, +} from '@crowd/common_services' +import { getServiceChildLogger } from '@crowd/logging' +import { IQueue, QueueFactory } from '@crowd/queue' + +import { QUEUE_CONFIG } from '../../conf' + +const log = getServiceChildLogger('service.queue') + +let queueClient: IQueue +export const QUEUE_CLIENT = (): IQueue => { + if (queueClient) return queueClient + + // TODO: will be bound to an environment variable + queueClient = QueueFactory.createQueueService(QUEUE_CONFIG) + return queueClient +} + +let runWorkerEmitter: IntegrationRunWorkerEmitter +export const getIntegrationRunWorkerEmitter = async (): Promise => { + if (runWorkerEmitter) return runWorkerEmitter + + runWorkerEmitter = new IntegrationRunWorkerEmitter(QUEUE_CLIENT(), log) + await runWorkerEmitter.init() + return runWorkerEmitter +} + +let streamWorkerEmitter: IntegrationStreamWorkerEmitter +export const getIntegrationStreamWorkerEmitter = + async (): Promise => { + if (streamWorkerEmitter) return streamWorkerEmitter + + streamWorkerEmitter = new IntegrationStreamWorkerEmitter(QUEUE_CLIENT(), log) + await streamWorkerEmitter.init() + return streamWorkerEmitter + } + +let searchSyncWorkerEmitter: SearchSyncWorkerEmitter +export const getSearchSyncWorkerEmitter = async (): Promise => { + if (searchSyncWorkerEmitter) return searchSyncWorkerEmitter + + searchSyncWorkerEmitter = new SearchSyncWorkerEmitter(QUEUE_CLIENT(), log) + await searchSyncWorkerEmitter.init() + return searchSyncWorkerEmitter +} + +let dataSinkWorkerEmitter: DataSinkWorkerEmitter +export const getDataSinkWorkerEmitter = async (): Promise => { + if (dataSinkWorkerEmitter) return dataSinkWorkerEmitter + + dataSinkWorkerEmitter = new DataSinkWorkerEmitter(QUEUE_CLIENT(), log) + await dataSinkWorkerEmitter.init() + return dataSinkWorkerEmitter +} diff --git a/backend/src/serverless/utils/serviceSQS.ts b/backend/src/serverless/utils/serviceSQS.ts deleted file mode 100644 index 4608978b9b..0000000000 --- a/backend/src/serverless/utils/serviceSQS.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - IntegrationRunWorkerEmitter, - IntegrationStreamWorkerEmitter, - IntegrationSyncWorkerEmitter, - SearchSyncWorkerEmitter, - SqsClient, - getSqsClient, -} from '@crowd/sqs' -import { getServiceChildLogger } from '@crowd/logging' -import { getServiceTracer } from '@crowd/tracing' -import { SQS_CONFIG } from '../../conf' - -const tracer = getServiceTracer() -const log = getServiceChildLogger('service.sqs') - -let sqsClient: SqsClient -const getClient = (): SqsClient => { - if (sqsClient) return sqsClient - - const config = SQS_CONFIG - sqsClient = getSqsClient({ - region: config.aws.region, - host: config.host, - port: config.port, - accessKeyId: config.aws.accessKeyId, - secretAccessKey: config.aws.secretAccessKey, - }) - return sqsClient -} - -let runWorkerEmitter: IntegrationRunWorkerEmitter -export const getIntegrationRunWorkerEmitter = async (): Promise => { - if (runWorkerEmitter) return runWorkerEmitter - - runWorkerEmitter = new IntegrationRunWorkerEmitter(getClient(), tracer, log) - await runWorkerEmitter.init() - return runWorkerEmitter -} - -let streamWorkerEmitter: IntegrationStreamWorkerEmitter -export const getIntegrationStreamWorkerEmitter = - async (): Promise => { - if (streamWorkerEmitter) return streamWorkerEmitter - - streamWorkerEmitter = new IntegrationStreamWorkerEmitter(getClient(), tracer, log) - await streamWorkerEmitter.init() - return streamWorkerEmitter - } - -let searchSyncWorkerEmitter: SearchSyncWorkerEmitter -export const getSearchSyncWorkerEmitter = async (): Promise => { - if (searchSyncWorkerEmitter) return searchSyncWorkerEmitter - - searchSyncWorkerEmitter = new SearchSyncWorkerEmitter(getClient(), tracer, log) - await searchSyncWorkerEmitter.init() - return searchSyncWorkerEmitter -} - -let integrationSyncWorkerEmitter: IntegrationSyncWorkerEmitter -export const getIntegrationSyncWorkerEmitter = async (): Promise => { - if (integrationSyncWorkerEmitter) return integrationSyncWorkerEmitter - - integrationSyncWorkerEmitter = new IntegrationSyncWorkerEmitter(getClient(), tracer, log) - await integrationSyncWorkerEmitter.init() - return integrationSyncWorkerEmitter -} diff --git a/backend/src/services/IServiceOptions.ts b/backend/src/services/IServiceOptions.ts index a35e82fff6..e35a36c4d4 100644 --- a/backend/src/services/IServiceOptions.ts +++ b/backend/src/services/IServiceOptions.ts @@ -1,6 +1,8 @@ +import { DbConnection } from '@crowd/data-access-layer/src/database' import { Logger } from '@crowd/logging' import { RedisClient } from '@crowd/redis' -import { SegmentData } from '../types/segmentTypes' +import { Client as TemporalClient } from '@crowd/temporal' +import { SegmentData } from '@crowd/types' export interface IServiceOptions { log: Logger @@ -11,4 +13,7 @@ export interface IServiceOptions { database: any redis: RedisClient transaction?: any + temporal: TemporalClient + productDb: DbConnection + profileSql?: boolean } diff --git a/backend/src/services/MergeActionsService.ts b/backend/src/services/MergeActionsService.ts new file mode 100644 index 0000000000..889962c041 --- /dev/null +++ b/backend/src/services/MergeActionsService.ts @@ -0,0 +1,46 @@ +import { findEntityMergeActions } from '@crowd/data-access-layer/src/mergeActions/repo' +import { LoggerBase } from '@crowd/logging' + +import SequelizeRepository from '@/database/repositories/sequelizeRepository' + +import { IServiceOptions } from './IServiceOptions' + +export default class MergeActionsService extends LoggerBase { + options: IServiceOptions + + constructor(options: IServiceOptions) { + super(options.log) + this.options = options + } + + async query(args) { + const qx = SequelizeRepository.getQueryExecutor(this.options) + + const filters = { + state: args.state, + limit: args.limit, + offset: args.offset, + } + + const results = await findEntityMergeActions(qx, args.entityId, args.type, filters) + + return results.map((result) => ({ + primaryId: result.primaryId, + secondaryId: result.secondaryId, + state: result.state, + // derive operation type from step and if step is null, default to merge + operationType: result.step ? MergeActionsService.getOperationType(result.step) : 'unknown', + })) + } + + static getOperationType(step: string) { + if (step.startsWith('merge')) { + return 'merge' + } + if (step.startsWith('unmerge')) { + return 'unmerge' + } + + throw new Error(`Unrecognized merge action step: ${step}`) + } +} diff --git a/backend/src/services/__tests__/activityService.test.ts b/backend/src/services/__tests__/activityService.test.ts deleted file mode 100644 index 5c44e9afda..0000000000 --- a/backend/src/services/__tests__/activityService.test.ts +++ /dev/null @@ -1,3193 +0,0 @@ -import { v4 as uuid } from 'uuid' - -import SequelizeTestUtils from '../../database/utils/sequelizeTestUtils' -import MemberService from '../memberService' -import ActivityService from '../activityService' -import MemberRepository from '../../database/repositories/memberRepository' -import ActivityRepository from '../../database/repositories/activityRepository' -import ConversationService from '../conversationService' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import { MemberAttributeName, PlatformType } from '@crowd/types' -import SettingsRepository from '../../database/repositories/settingsRepository' -import ConversationSettingsRepository from '../../database/repositories/conversationSettingsRepository' -import MemberAttributeSettingsService from '../memberAttributeSettingsService' -import { IServiceOptions } from '../../services/IServiceOptions' -import { GITHUB_MEMBER_ATTRIBUTES, TWITTER_MEMBER_ATTRIBUTES } from '@crowd/integrations' -import { populateSegments, switchSegments } from '../../database/utils/segmentTestUtils' -import SegmentRepository from '../../database/repositories/segmentRepository' -import OrganizationRepository from '../../database/repositories/organizationRepository' -import { SegmentStatus } from '../../types/segmentTypes' -import OrganizationService from '../organizationService' -import MemberSegmentAffiliationRepository from '../../database/repositories/memberSegmentAffiliationRepository' -import SegmentService from '../segmentService' -import { SegmentData } from '../../types/segmentTypes' -import MemberAffiliationService from '../memberAffiliationService' - -const db = null -const searchEngine = null - -describe('ActivityService tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('upsert method', () => { - it('Should create non existent activity with no parent', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: 'Body', - title: 'Title', - url: 'URL', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - attributes: { - replies: 12, - }, - sourceId: '#sourceId', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - } - - const activityCreated = await new ActivityService(mockIRepositoryOptions).upsert(activity) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - activityCreated.createdAt = activityCreated.createdAt.toISOString().split('T')[0] - activityCreated.updatedAt = activityCreated.updatedAt.toISOString().split('T')[0] - delete activityCreated.member - delete activityCreated.objectMember - - const expectedActivityCreated = { - id: activityCreated.id, - attributes: activity.attributes, - type: 'activity', - timestamp: new Date('2020-05-27T15:13:30Z'), - platform: PlatformType.GITHUB, - isContribution: true, - score: 1, - username: 'test', - objectMemberUsername: null, - memberId: memberCreated.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - channel: null, - body: 'Body', - title: 'Title', - url: 'URL', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - tasks: [], - parent: null, - parentId: null, - conversationId: null, - sourceId: activity.sourceId, - sourceParentId: null, - display: { - default: activityCreated.type, - short: activityCreated.type, - channel: '', - }, - organizationId: null, - organization: null, - } - - expect(activityCreated).toStrictEqual(expectedActivityCreated) - }) - - it('Should create non existent activity with parent', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const activity1 = { - type: 'question', - timestamp: '2020-05-27T15:13:30Z', - username: 'test', - member: memberCreated.id, - platform: 'non-existing-platform', - body: 'What is love?', - isContribution: true, - score: 1, - sourceId: 'sourceId#1', - } - - const activityCreated1 = await new ActivityService(mockIRepositoryOptions).upsert(activity1) - - const activity2 = { - type: 'answer', - timestamp: '2020-05-28T15:13:30Z', - platform: 'non-existing-platform', - body: 'Baby dont hurt me', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 2, - sourceId: 'sourceId#2', - sourceParentId: activityCreated1.sourceId, - } - - const activityCreated2 = await new ActivityService(mockIRepositoryOptions).upsert(activity2) - - // Since an activity with a parent is created, a Conversation entity should be created at this point - // with both parent and the child activities. Try finding it using the slug - - const conversationCreated = await new ConversationService( - mockIRepositoryOptions, - ).findAndCountAll({ slug: 'what-is-love' }) - - delete activityCreated2.member - delete activityCreated2.parent - delete activityCreated2.objectMember - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - activityCreated2.createdAt = activityCreated2.createdAt.toISOString().split('T')[0] - activityCreated2.updatedAt = activityCreated2.updatedAt.toISOString().split('T')[0] - - const expectedActivityCreated = { - id: activityCreated2.id, - body: activity2.body, - type: activity2.type, - channel: null, - attributes: {}, - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.42, - }, - url: null, - title: null, - timestamp: new Date(activity2.timestamp), - platform: activity2.platform, - isContribution: activity2.isContribution, - score: activity2.score, - username: 'test', - objectMemberUsername: null, - memberId: memberCreated.id, - objectMemberId: null, - tasks: [], - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parentId: activityCreated1.id, - sourceParentId: activity1.sourceId, - sourceId: activity2.sourceId, - conversationId: conversationCreated.rows[0].id, - display: { - default: activity2.type, - short: activity2.type, - channel: '', - }, - organizationId: null, - organization: null, - } - - expect(activityCreated2).toStrictEqual(expectedActivityCreated) - }) - - it('Should update already existing activity succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const activity1 = { - type: 'question', - timestamp: '2020-05-27T15:13:30Z', - username: 'test', - member: memberCreated.id, - body: 'What is love?', - title: 'Song', - platform: 'non-existing-platform', - attributes: { - nested_1: { - attribute_1: '1', - nested_2: { - attribute_2: '2', - attribute_array: [1, 2, 3], - }, - }, - }, - isContribution: true, - score: 1, - sourceId: '#sourceId1', - } - - const activityCreated1 = await new ActivityService(mockIRepositoryOptions).upsert(activity1) - - const activity2 = { - type: 'question', - timestamp: '2020-05-27T15:13:30Z', - username: 'test', - member: memberCreated.id, - platform: 'non-existing-platform', - body: 'Test', - attributes: { - nested_1: { - attribute_1: '1', - nested_2: { - attribute_2: '5', - attribute_3: 'test', - attribute_array: [3, 4, 5], - }, - }, - one: 'Baby dont hurt me', - two: 'Dont hurt me', - three: 'No more', - }, - isContribution: false, - score: 2, - sourceId: '#sourceId1', - } - - const activityUpserted = await new ActivityService(mockIRepositoryOptions).upsert(activity2) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - activityUpserted.createdAt = activityUpserted.createdAt.toISOString().split('T')[0] - activityUpserted.updatedAt = activityUpserted.updatedAt.toISOString().split('T')[0] - - // delete models before expect because we already have ids (memberId, parentId) - delete activityUpserted.member - delete activityUpserted.parent - delete activityUpserted.objectMember - - const attributesExpected = { - ...activity1.attributes, - ...activity2.attributes, - } - - attributesExpected.nested_1.nested_2.attribute_array = [1, 2, 3, 4, 5] - - const expectedActivityCreated = { - id: activityCreated1.id, - attributes: attributesExpected, - type: activity2.type, - timestamp: new Date(activity2.timestamp), - platform: activity2.platform, - isContribution: activity2.isContribution, - score: activity2.score, - title: activity1.title, - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.42, - }, - url: null, - body: activity2.body, - channel: null, - username: 'test', - objectMemberUsername: null, - memberId: memberCreated.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tasks: [], - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parentId: null, - sourceParentId: null, - sourceId: activity1.sourceId, - conversationId: null, - display: { - default: activity2.type, - short: activity2.type, - channel: '', - }, - organizationId: null, - organization: null, - } - - expect(activityUpserted).toStrictEqual(expectedActivityCreated) - }) - - it('Should create various conversations successfully with given parent-child relationships of activities [ascending timestamp order]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberService = new MemberService(mockIRepositoryOptions) - const activityService = new ActivityService(mockIRepositoryOptions) - - const member1Created = await memberService.upsert({ - username: { - [PlatformType.DISCORD]: 'test', - }, - platform: PlatformType.DISCORD, - joinedAt: '2020-05-27T15:13:30Z', - }) - const member2Created = await memberService.upsert({ - username: { - [PlatformType.DISCORD]: 'test2', - }, - platform: PlatformType.DISCORD, - joinedAt: '2020-05-27T15:13:30Z', - }) - - // Simulate a reply chain in discord - - const activity1 = { - type: 'message', - timestamp: '2020-05-27T15:13:30Z', - username: 'test', - member: member1Created.id, - platform: PlatformType.DISCORD, - body: 'What is love?', - isContribution: true, - score: 1, - sourceId: 'sourceId#1', - } - - let activityCreated1 = await activityService.upsert(activity1) - - const activity2 = { - type: 'message', - timestamp: '2020-05-28T15:14:30Z', - platform: PlatformType.DISCORD, - body: 'Baby dont hurt me', - isContribution: true, - username: 'test2', - member: member2Created.id, - score: 2, - sourceId: 'sourceId#2', - sourceParentId: activityCreated1.sourceId, - } - - const activityCreated2 = await activityService.upsert(activity2) - - const activity3 = { - type: 'message', - timestamp: '2020-05-28T15:15:30Z', - platform: PlatformType.DISCORD, - body: 'Dont hurt me', - isContribution: true, - username: 'test', - member: member1Created.id, - score: 2, - sourceId: 'sourceId#3', - sourceParentId: activityCreated2.sourceId, - } - - const activityCreated3 = await activityService.upsert(activity3) - - const activity4 = { - type: 'message', - timestamp: '2020-05-28T15:16:30Z', - platform: PlatformType.DISCORD, - body: 'No more', - isContribution: true, - username: 'test2', - member: member2Created.id, - score: 2, - sourceId: 'sourceId#4', - sourceParentId: activityCreated3.sourceId, - } - - const activityCreated4 = await activityService.upsert(activity4) - - // Get the conversation using slug (generated using the chain starter activity attributes.body) - const conversationCreated = ( - await new ConversationService(mockIRepositoryOptions).findAndCountAll({ - slug: 'what-is-love', - }) - ).rows[0] - - // We have to get activity1 again because conversation creation happens - // after creation of the first activity that has a parent (activity2) - activityCreated1 = await activityService.findById(activityCreated1.id) - - // All activities (including chain starter) should belong to the same conversation - expect(activityCreated1.conversationId).toStrictEqual(conversationCreated.id) - expect(activityCreated2.conversationId).toStrictEqual(conversationCreated.id) - expect(activityCreated3.conversationId).toStrictEqual(conversationCreated.id) - expect(activityCreated4.conversationId).toStrictEqual(conversationCreated.id) - - // Emulate a thread in discord - - const activity5 = { - type: 'message', - timestamp: '2020-05-28T15:17:30Z', - platform: PlatformType.DISCORD, - body: 'Never gonna give you up', - isContribution: true, - username: 'test', - member: member1Created.id, - score: 2, - sourceId: 'sourceId#5', - } - let activityCreated5 = await activityService.upsert(activity5) - - const activity6 = { - type: 'message', - timestamp: '2020-05-28T15:18:30Z', - platform: PlatformType.DISCORD, - body: 'Never gonna let you down', - isContribution: true, - username: 'test2', - member: member2Created.id, - score: 2, - sourceId: 'sourceId#6', - sourceParentId: activityCreated5.sourceId, - } - const activityCreated6 = await activityService.upsert(activity6) - - const activity7 = { - type: 'message', - timestamp: '2020-05-28T15:19:30Z', - platform: PlatformType.DISCORD, - body: 'Never gonna run around and desert you', - isContribution: true, - username: 'test', - member: member1Created.id, - score: 2, - sourceId: 'sourceId#7', - sourceParentId: activityCreated5.sourceId, - } - const activityCreated7 = await activityService.upsert(activity7) - - const conversationCreated2 = ( - await new ConversationService(mockIRepositoryOptions).findAndCountAll({ - slug: 'never-gonna-give-you-up', - }) - ).rows[0] - - activityCreated5 = await activityService.findById(activityCreated5.id) - - // All activities (including thread starter) should belong to the same conversation - expect(activityCreated5.conversationId).toStrictEqual(conversationCreated2.id) - expect(activityCreated6.conversationId).toStrictEqual(conversationCreated2.id) - expect(activityCreated7.conversationId).toStrictEqual(conversationCreated2.id) - }) - - it('Should keep old timestamp', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberService = new MemberService(mockIRepositoryOptions) - const activityService = new ActivityService(mockIRepositoryOptions) - - const cm = await memberService.upsert({ - username: { - [PlatformType.DISCORD]: 'test', - }, - platform: PlatformType.DISCORD, - }) - - const activity1 = { - type: 'message', - timestamp: '2020-05-27T15:13:30Z', - username: 'test', - member: cm.id, - platform: PlatformType.DISCORD, - sourceId: 'sourceId#1', - } - - const activityCreated1 = await activityService.upsert(activity1) - - const activity2 = { - type: 'message', - timestamp: '2022-05-27T15:13:30Z', - username: 'test', - member: cm.id, - platform: PlatformType.DISCORD, - sourceId: 'sourceId#1', - body: 'What is love?', - } - - const activityCreated2 = await activityService.upsert(activity2) - - expect(activityCreated2.timestamp).toStrictEqual(activityCreated1.timestamp) - expect(activityCreated2.body).toBe(activity2.body) - }) - - it('Should keep isMainBranch as true', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberService = new MemberService(mockIRepositoryOptions) - const activityService = new ActivityService(mockIRepositoryOptions) - - const cm = await memberService.upsert({ - username: { - [PlatformType.DISCORD]: 'test', - }, - platform: PlatformType.DISCORD, - }) - - const activity1 = { - type: 'message', - timestamp: '2020-05-27T15:13:30Z', - username: 'test', - member: cm.id, - platform: PlatformType.DISCORD, - sourceId: 'sourceId#1', - attributes: { - isMainBranch: true, - other: 'other', - }, - } - - await activityService.upsert(activity1) - - const activity2 = { - type: 'message', - timestamp: '2022-05-27T15:13:30Z', - username: 'test', - member: cm.id, - platform: PlatformType.DISCORD, - sourceId: 'sourceId#1', - body: 'What is love?', - attributes: { - isMainBranch: false, - other2: 'other2', - }, - } - - const activityCreated2 = await activityService.upsert(activity2) - - expect(activityCreated2.attributes).toStrictEqual({ - isMainBranch: true, - other: 'other', - other2: 'other2', - }) - }) - - it('Should create various conversations successfully with given parent-child relationships of activities [descending timestamp order]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberService = new MemberService(mockIRepositoryOptions) - const activityService = new ActivityService(mockIRepositoryOptions) - - const member1Created = await memberService.upsert({ - username: { - [PlatformType.DISCORD]: 'test', - }, - platform: PlatformType.DISCORD, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const member2Created = await memberService.upsert({ - username: { - [PlatformType.DISCORD]: 'test2', - }, - platform: PlatformType.DISCORD, - joinedAt: '2020-05-27T15:13:30Z', - }) - - // Simulate a reply chain in discord in reverse order (child activities come first) - - const activity4 = { - type: 'message', - timestamp: '2020-05-28T15:16:30Z', - platform: PlatformType.DISCORD, - body: 'No more', - isContribution: true, - username: 'test2', - member: member2Created.id, - score: 2, - sourceId: 'sourceId#4', - sourceParentId: 'sourceId#3', - } - - let activityCreated4 = await activityService.upsert(activity4) - - const activity3 = { - type: 'message', - timestamp: '2020-05-28T15:15:30Z', - platform: PlatformType.DISCORD, - body: 'Dont hurt me', - isContribution: true, - username: 'test', - member: member1Created.id, - score: 2, - sourceId: 'sourceId#3', - sourceParentId: 'sourceId#2', - } - - let activityCreated3 = await activityService.upsert(activity3) - - const activity2 = { - type: 'message', - timestamp: '2020-05-28T15:14:30Z', - platform: PlatformType.DISCORD, - body: 'Baby dont hurt me', - isContribution: true, - username: 'test2', - member: member2Created.id, - score: 2, - sourceId: 'sourceId#2', - sourceParentId: 'sourceId#1', - } - - let activityCreated2 = await activityService.upsert(activity2) - - const activity1 = { - type: 'message', - timestamp: '2020-05-27T15:13:30Z', - username: 'test', - member: member1Created.id, - platform: PlatformType.DISCORD, - body: 'What is love?', - isContribution: true, - score: 1, - sourceId: 'sourceId#1', - } - - // main parent activity that starts the reply chain - let activityCreated1 = await activityService.upsert(activity1) - - // get activities again - activityCreated1 = await activityService.findById(activityCreated1.id) - activityCreated2 = await activityService.findById(activityCreated2.id) - activityCreated3 = await activityService.findById(activityCreated3.id) - activityCreated4 = await activityService.findById(activityCreated4.id) - - // expect parentIds - expect(activityCreated4.parentId).toBe(activityCreated3.id) - expect(activityCreated3.parentId).toBe(activityCreated2.id) - expect(activityCreated2.parentId).toBe(activityCreated1.id) - - // Get the conversation using slug (generated using the chain starter activity attributes.body -last added activityCreated1-) - const conversationCreated = ( - await new ConversationService(mockIRepositoryOptions).findAndCountAll({ - slug: 'what-is-love', - }) - ).rows[0] - - // All activities (including chain starter) should belong to the same conversation - expect(activityCreated1.conversationId).toStrictEqual(conversationCreated.id) - expect(activityCreated2.conversationId).toStrictEqual(conversationCreated.id) - expect(activityCreated3.conversationId).toStrictEqual(conversationCreated.id) - expect(activityCreated4.conversationId).toStrictEqual(conversationCreated.id) - - // Simulate a thread in reverse order - - const activity6 = { - type: 'message', - timestamp: '2020-05-28T15:18:30Z', - platform: PlatformType.DISCORD, - body: 'Never gonna let you down', - isContribution: true, - username: 'test2', - member: member2Created.id, - score: 2, - sourceId: 'sourceId#6', - sourceParentId: 'sourceId#5', - } - let activityCreated6 = await activityService.upsert(activity6) - - const activity7 = { - type: 'message', - timestamp: '2020-05-28T15:19:30Z', - platform: PlatformType.DISCORD, - body: 'Never gonna run around and desert you', - - isContribution: true, - username: 'test', - member: member1Created.id, - score: 2, - sourceId: 'sourceId#7', - sourceParentId: 'sourceId#5', - } - let activityCreated7 = await activityService.upsert(activity7) - - const activity5 = { - type: 'message', - timestamp: '2020-05-28T15:17:30Z', - platform: PlatformType.DISCORD, - body: 'Never gonna give you up', - isContribution: true, - username: 'test', - member: member1Created.id, - score: 2, - sourceId: 'sourceId#5', - } - let activityCreated5 = await activityService.upsert(activity5) - - const conversationCreated2 = ( - await new ConversationService(mockIRepositoryOptions).findAndCountAll({ - slug: 'never-gonna-give-you-up', - }) - ).rows[0] - - // get activities again - activityCreated5 = await activityService.findById(activityCreated5.id) - activityCreated6 = await activityService.findById(activityCreated6.id) - activityCreated7 = await activityService.findById(activityCreated7.id) - - // expect parentIds - expect(activityCreated6.parentId).toBe(activityCreated5.id) - expect(activityCreated7.parentId).toBe(activityCreated5.id) - - expect(activityCreated5.conversationId).toStrictEqual(conversationCreated2.id) - expect(activityCreated6.conversationId).toStrictEqual(conversationCreated2.id) - expect(activityCreated7.conversationId).toStrictEqual(conversationCreated2.id) - - // Add some more childs to the conversation1 and conversation2 - // After setting child-parent in reverse order, we're now adding - // some more childiren in normal order - - // add a new reply to the chain-starter activity - const activity8 = { - type: 'message', - timestamp: '2020-05-28T15:21:30Z', - platform: PlatformType.DISCORD, - body: 'additional reply to the reply chain', - isContribution: true, - username: 'test2', - member: member2Created.id, - score: 2, - sourceId: 'sourceId#8', - sourceParentId: 'sourceId#1', - } - - const activityCreated8 = await activityService.upsert(activity8) - - expect(activityCreated8.parentId).toBe(activityCreated1.id) - expect(activityCreated8.conversationId).toStrictEqual(conversationCreated.id) - - // add a new activity to the thread - const activity9 = { - type: 'message', - timestamp: '2020-05-28T15:35:30Z', - platform: PlatformType.DISCORD, - body: 'additional message to the thread', - isContribution: true, - username: 'test2', - member: member2Created.id, - score: 2, - sourceId: 'sourceId#9', - sourceParentId: 'sourceId#5', - } - - const activityCreated9 = await activityService.upsert(activity9) - - expect(activityCreated9.parentId).toBe(activityCreated5.id) - expect(activityCreated9.conversationId).toStrictEqual(conversationCreated2.id) - }) - - // Tests for checking channel logic when creating activity - // Settings should get updated only when a new channel is sent alog while creating activity. - it('Should create an activity with a channel which is not present in settings and add it to settings', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test1', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: 'Body', - title: 'Title', - url: 'URL', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - channel: 'TestChannel', - attributes: { - replies: 12, - }, - sourceId: '#sourceId', - isContribution: true, - username: 'test1', - member: memberCreated.id, - score: 1, - } - - await new ActivityService(mockIRepositoryOptions).upsert(activity) - const segmentRepository = new SegmentRepository(mockIRepositoryOptions) - const activityChannels = await segmentRepository.fetchTenantActivityChannels() - expect(activityChannels[activity.platform].includes(activity.channel)).toBe(true) - }) - - it('Should not create a duplicate channel when a channel is present in settings', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test1', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: 'Body', - title: 'Title', - url: 'URL', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - channel: 'TestChannel', - attributes: { - replies: 12, - }, - sourceId: '#sourceId', - isContribution: true, - username: 'test1', - member: memberCreated.id, - score: 1, - } - - await new ActivityService(mockIRepositoryOptions).upsert(activity) - let settings = await SettingsRepository.findOrCreateDefault({}, mockIRepositoryOptions) - const activity1 = { - type: 'activity1', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: 'Body', - title: 'Title', - url: 'URL', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - channel: 'TestChannel', - attributes: { - replies: 12, - }, - sourceId: '#sourceId', - isContribution: true, - member: memberCreated.id, - score: 1, - } - - await new ActivityService(mockIRepositoryOptions).upsert(activity) - const segmentRepository = new SegmentRepository(mockIRepositoryOptions) - const activityChannels = await segmentRepository.fetchTenantActivityChannels() - expect(activityChannels[activity1.platform].length).toBe(1) - }) - }) - - describe('createWithMember method', () => { - it('Create an activity with given member [no parent activity]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService( - mockIRepositoryOptions, - ) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - const member = { - username: { - [PlatformType.GITHUB]: 'anil_github', - }, - email: 'lala@l.com', - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - [PlatformType.TWITTER]: 'https://twitter.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - organisation: 'Crowd', - joinedAt: '2020-05-27T15:13:30Z', - } - - const data = { - member, - body: 'Description\nThis pull request adds a new Dashboard and related widgets. This work will probably have to be revisited as soon as possible since a lot of decisions were made, without having too much time to think about different outcomes/possibilities. We rushed these changes so that we can demo a working dashboard to YC and to our Investors.\nChanges Proposed\n\nUpdate Chart.js\nAdd two different type of widgets (number and graph)\nRemove older/default widgets from dashboard and add our own widgets\nHide some items from the menu\nAdd all widget infrastructure (actions, services, etc) to integrate with the backend\nAdd a few more CSS tweaks\n\nScreenshots', - title: 'Dashboard widgets and some other tweaks/adjustments', - url: 'https://github.com/CrowdDevHQ/crowd-web/pull/16', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - sentiment: 0.98, - label: 'positive', - }, - channel: 'https://github.com/CrowdDevHQ/crowd-web', - timestamp: '2021-09-30T14:20:27.000Z', - type: 'pull_request-closed', - isContribution: true, - platform: PlatformType.GITHUB, - score: 4, - sourceId: '#sourceId1', - } - - const activityWithMember = await new ActivityService(mockIRepositoryOptions).createWithMember( - data, - ) - - delete activityWithMember.member - delete activityWithMember.display - delete activityWithMember.objectMember - - activityWithMember.createdAt = activityWithMember.createdAt.toISOString().split('T')[0] - activityWithMember.updatedAt = activityWithMember.updatedAt.toISOString().split('T')[0] - - const memberFound = await MemberRepository.findById( - activityWithMember.memberId, - mockIRepositoryOptions, - ) - - const expectedActivityCreated = { - id: activityWithMember.id, - type: data.type, - body: data.body, - title: data.title, - url: data.url, - channel: data.channel, - sentiment: data.sentiment, - attributes: {}, - timestamp: new Date(data.timestamp), - platform: data.platform, - isContribution: data.isContribution, - score: data.score, - username: 'anil_github', - objectMemberUsername: null, - memberId: memberFound.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tasks: [], - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parentId: null, - parent: null, - sourceParentId: null, - sourceId: data.sourceId, - conversationId: null, - organizationId: null, - organization: null, - } - - expect(activityWithMember).toStrictEqual(expectedActivityCreated) - }) - - it('Create an activity with given member [with parent activity, upsert member, new activity] [parent first, child later]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService( - mockIRepositoryOptions, - ) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - const member = { - username: 'anil_github', - email: 'lala@l.com', - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - [PlatformType.TWITTER]: 'https://twitter.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - organisation: 'Crowd', - joinedAt: '2020-05-27T15:13:30Z', - } - - const data = { - member, - body: 'Description\nThis pull request adds a new Dashboard and related widgets. This work will probably have to be revisited as soon as possible since a lot of decisions were made, without having too much time to think about different outcomes/possibilities. We rushed these changes so that we can demo a working dashboard to YC and to our Investors.\nChanges Proposed\n\nUpdate Chart.js\nAdd two different type of widgets (number and graph)\nRemove older/default widgets from dashboard and add our own widgets\nHide some items from the menu\nAdd all widget infrastructure (actions, services, etc) to integrate with the backend\nAdd a few more CSS tweaks\n\nScreenshots', - title: 'Dashboard widgets and some other tweaks/adjustments', - url: 'https://github.com/CrowdDevHQ/crowd-web/pull/16', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - timestamp: '2021-09-30T14:20:27.000Z', - type: 'pull_request-closed', - isContribution: true, - platform: PlatformType.GITHUB, - score: 4, - sourceId: '#sourceId1', - } - - const activityWithMember1 = await new ActivityService( - mockIRepositoryOptions, - ).createWithMember(data) - - const data2 = { - member, - body: 'Description\nMinor pull request that fixes the order by Score and # of activities in the members list page', - title: 'Add order by score and # of activities', - url: 'https://github.com/CrowdDevHQ/crowd-web/pull/30', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - timestamp: '2021-11-30T14:20:27.000Z', - type: 'pull_request-open', - isContribution: true, - platform: PlatformType.GITHUB, - score: 4, - sourceId: '#sourceId2', - sourceParentId: data.sourceId, - } - - const activityWithMember2 = await new ActivityService( - mockIRepositoryOptions, - ).createWithMember(data2) - - // Since an activity with a parent is created, a Conversation entity should be created at this point - // with both parent and the child activities. Try finding it using the slug (slug is generated using parent.attributes.body) - - const conversationCreated = await new ConversationService( - mockIRepositoryOptions, - ).findAndCountAll({ slug: 'description-this-pull-request-adds-a-new-dashboard-and-related' }) - - // delete models before expect because we already have ids (memberId, parentId) - delete activityWithMember2.member - delete activityWithMember2.parent - delete activityWithMember2.display - delete activityWithMember2.objectMember - - activityWithMember2.createdAt = activityWithMember2.createdAt.toISOString().split('T')[0] - activityWithMember2.updatedAt = activityWithMember2.updatedAt.toISOString().split('T')[0] - - const memberFound = await MemberRepository.findById( - activityWithMember1.memberId, - mockIRepositoryOptions, - ) - - const expectedActivityCreated = { - id: activityWithMember2.id, - body: data2.body, - title: data2.title, - url: data2.url, - channel: data2.channel, - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.42, - }, - attributes: {}, - type: data2.type, - timestamp: new Date(data2.timestamp), - platform: data2.platform, - tasks: [], - isContribution: data2.isContribution, - score: data2.score, - username: 'anil_github', - objectMemberUsername: null, - memberId: memberFound.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parentId: activityWithMember1.id, - sourceParentId: data2.sourceParentId, - sourceId: data2.sourceId, - conversationId: conversationCreated.rows[0].id, - organizationId: null, - organization: null, - } - - expect(activityWithMember2).toStrictEqual(expectedActivityCreated) - }) - - it('Create an activity with given member [with parent activity, upsert member, new activity] [child first, parent later]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const activityService = new ActivityService(mockIRepositoryOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService( - mockIRepositoryOptions, - ) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - const member = { - username: 'anil_github', - email: 'lala@l.com', - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - [PlatformType.TWITTER]: 'https://twitter.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - organisation: 'Crowd', - joinedAt: '2020-05-27T15:13:30Z', - } - - const dataChild = { - member, - body: 'Description\nMinor pull request that fixes the order by Score and # of activities in the members list page', - title: 'Add order by score and # of activities', - url: 'https://github.com/CrowdDevHQ/crowd-web/pull/30', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - timestamp: '2021-11-30T14:20:27.000Z', - type: 'pull_request-open', - isContribution: true, - platform: PlatformType.GITHUB, - score: 4, - sourceParentId: '#sourceId1', - sourceId: '#childSourceId', - } - - let activityWithMemberChild = await activityService.createWithMember(dataChild) - - const dataParent = { - member, - body: 'Description\nThis pull request adds a new Dashboard and related widgets. This work will probably have to be revisited as soon as possible since a lot of decisions were made, without having too much time to think about different outcomes/possibilities. We rushed these changes so that we can demo a working dashboard to YC and to our Investors.\nChanges Proposed\n\nUpdate Chart.js\nAdd two different type of widgets (number and graph)\nRemove older/default widgets from dashboard and add our own widgets\nHide some items from the menu\nAdd all widget infrastructure (actions, services, etc) to integrate with the backend\nAdd a few more CSS tweaks\n\nScreenshots', - title: 'Dashboard widgets and some other tweaks/adjustments', - url: 'https://github.com/CrowdDevHQ/crowd-web/pull/16', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - timestamp: '2021-09-30T14:20:27.000Z', - type: 'pull_request-closed', - isContribution: true, - platform: PlatformType.GITHUB, - score: 4, - sourceId: dataChild.sourceParentId, - } - - let activityWithMemberParent = await activityService.createWithMember(dataParent) - - // after creating parent, conversation should be started - const conversationCreated = await new ConversationService( - mockIRepositoryOptions, - ).findAndCountAll({ slug: 'description-this-pull-request-adds-a-new-dashboard-and-related' }) - - // get child and parent activity again - activityWithMemberChild = await activityService.findById(activityWithMemberChild.id) - activityWithMemberParent = await activityService.findById(activityWithMemberParent.id) - - // delete models before expect because we already have ids (memberId, parentId) - delete activityWithMemberChild.member - delete activityWithMemberChild.parent - delete activityWithMemberChild.display - delete activityWithMemberChild.objectMember - delete activityWithMemberParent.member - delete activityWithMemberParent.parent - delete activityWithMemberParent.display - delete activityWithMemberParent.objectMember - - activityWithMemberChild.createdAt = activityWithMemberChild.createdAt - .toISOString() - .split('T')[0] - activityWithMemberChild.updatedAt = activityWithMemberChild.updatedAt - .toISOString() - .split('T')[0] - activityWithMemberParent.createdAt = activityWithMemberParent.createdAt - .toISOString() - .split('T')[0] - activityWithMemberParent.updatedAt = activityWithMemberParent.updatedAt - .toISOString() - .split('T')[0] - - const memberFound = await MemberRepository.findById( - activityWithMemberChild.memberId, - mockIRepositoryOptions, - ) - - const expectedParentActivityCreated = { - id: activityWithMemberParent.id, - body: dataParent.body, - title: dataParent.title, - url: dataParent.url, - channel: dataParent.channel, - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.42, - }, - attributes: {}, - type: dataParent.type, - timestamp: new Date(dataParent.timestamp), - platform: dataParent.platform, - isContribution: dataParent.isContribution, - tasks: [], - score: dataParent.score, - username: 'anil_github', - objectMemberUsername: null, - memberId: memberFound.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parentId: null, - sourceParentId: null, - sourceId: dataParent.sourceId, - conversationId: conversationCreated.rows[0].id, - organizationId: null, - organization: null, - } - - expect(activityWithMemberParent).toStrictEqual(expectedParentActivityCreated) - - const expectedChildActivityCreated = { - id: activityWithMemberChild.id, - body: dataChild.body, - title: dataChild.title, - url: dataChild.url, - channel: dataChild.channel, - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.42, - }, - attributes: {}, - type: dataChild.type, - timestamp: new Date(dataChild.timestamp), - platform: dataChild.platform, - isContribution: dataChild.isContribution, - score: dataChild.score, - username: 'anil_github', - objectMemberUsername: null, - memberId: memberFound.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - tasks: [], - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parentId: activityWithMemberParent.id, - sourceParentId: dataChild.sourceParentId, - sourceId: dataChild.sourceId, - conversationId: conversationCreated.rows[0].id, - organizationId: null, - organization: null, - } - - expect(activityWithMemberChild).toStrictEqual(expectedChildActivityCreated) - }) - - it(`Should respect the affiliation settings when setting an activity's organization with multiple member organizations`, async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const segmentRepo = new SegmentRepository(mockIRepositoryOptions) - - const segment1 = await segmentRepo.create({ - name: 'Crowd.dev - Segment1', - url: '', - parentName: 'Crowd.dev - Segment1', - grandparentName: 'Crowd.dev - Segment1', - slug: 'crowd.dev-1', - parentSlug: 'crowd.dev-1', - grandparentSlug: 'crowd.dev-1', - status: SegmentStatus.ACTIVE, - sourceId: null, - sourceParentId: null, - }) - - const segment2 = await segmentRepo.create({ - name: 'Crowd.dev - Segment2', - url: '', - parentName: 'Crowd.dev - Segment2', - grandparentName: 'Crowd.dev - Segment2', - slug: 'crowd.dev-2', - parentSlug: 'crowd.dev-2', - grandparentSlug: 'crowd.dev-2', - status: SegmentStatus.ACTIVE, - sourceId: null, - sourceParentId: null, - }) - - await populateSegments(mockIRepositoryOptions) - const org1 = await OrganizationRepository.create( - { - displayName: 'tesla', - }, - mockIRepositoryOptions, - ) - - const org2 = await OrganizationRepository.create( - { - displayName: 'crowd.dev', - }, - mockIRepositoryOptions, - ) - - const member = { - username: { - [PlatformType.GITHUB]: 'anil_github', - }, - organizations: [org1, org2], - affiliations: [ - { - segmentId: segment1.id, - organizationId: org2.id, - dateStart: '2021-09-01', - }, - { - segmentId: segment2.id, - organizationId: null, - dateStart: '2021-09-01', - }, - ], - } - - const data = { - member, - timestamp: '2021-09-30T14:20:27.000Z', - type: 'pull_request-closed', - platform: PlatformType.GITHUB, - sourceId: '#sourceId1', - } - - switchSegments(mockIRepositoryOptions, [segment1]) - - let activityWithMember = await new ActivityService(mockIRepositoryOptions).createWithMember( - data, - ) - - let activity = await ActivityRepository.findById( - activityWithMember.id, - mockIRepositoryOptions, - ) - - // org2 should be set as organization because it's in member's affiliated organizations - expect(activity.organization.name).toEqual(org2.name) - - // add another activity to segment2 for the same member - switchSegments(mockIRepositoryOptions, [segment2]) - - data.sourceId = '#sourceId2' - data.member = member // createWithMember modifies member, reset it - - activityWithMember = await new ActivityService(mockIRepositoryOptions).createWithMember(data) - - activity = await ActivityRepository.findById(activityWithMember.id, mockIRepositoryOptions) - - // this member had a null affiliation(meaning no organizations should be set) in segment 2 - expect(activity.organization).toBeNull() - }) - - describe('Member tests in createWithMember', () => { - it('Should set the joinedAt to the time of the activity when the member does not exist', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService( - mockIRepositoryOptions, - ) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - const member = { - username: { - [PlatformType.GITHUB]: 'anil_github', - }, - email: 'lala@l.com', - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - [PlatformType.TWITTER]: 'https://twitter.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - organisation: 'Crowd', - joinedAt: '2020-05-27T15:13:30Z', - } - - const data = { - member, - body: 'Description\nThis pull request adds a new Dashboard and related widgets. This work will probably have to be revisited as soon as possible since a lot of decisions were made, without having too much time to think about different outcomes/possibilities. We rushed these changes so that we can demo a working dashboard to YC and to our Investors.\nChanges Proposed\n\nUpdate Chart.js\nAdd two different type of widgets (number and graph)\nRemove older/default widgets from dashboard and add our own widgets\nHide some items from the menu\nAdd all widget infrastructure (actions, services, etc) to integrate with the backend\nAdd a few more CSS tweaks\n\nScreenshots', - title: 'Dashboard widgets and some other tweaks/adjustments', - url: 'https://github.com/CrowdDevHQ/crowd-web/pull/16', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - timestamp: '2021-09-30T14:20:27.000Z', - type: 'pull_request-closed', - isContribution: true, - platform: PlatformType.GITHUB, - score: 4, - sourceId: '#sourceId1', - } - - const activityWithMember = await new ActivityService( - mockIRepositoryOptions, - ).createWithMember(data) - - delete activityWithMember.member - delete activityWithMember.display - delete activityWithMember.objectMember - - activityWithMember.createdAt = activityWithMember.createdAt.toISOString().split('T')[0] - activityWithMember.updatedAt = activityWithMember.updatedAt.toISOString().split('T')[0] - - const memberFound = await MemberRepository.findById( - activityWithMember.memberId, - mockIRepositoryOptions, - ) - - const expectedActivityCreated = { - id: activityWithMember.id, - body: data.body, - title: data.title, - url: data.url, - channel: data.channel, - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.42, - }, - attributes: {}, - type: data.type, - timestamp: new Date(data.timestamp), - platform: data.platform, - isContribution: data.isContribution, - score: data.score, - username: 'anil_github', - objectMemberUsername: null, - memberId: memberFound.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - tasks: [], - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parentId: null, - parent: null, - sourceParentId: null, - sourceId: data.sourceId, - conversationId: null, - organizationId: null, - organization: null, - } - - expect(activityWithMember).toStrictEqual(expectedActivityCreated) - expect(memberFound.joinedAt).toStrictEqual(expectedActivityCreated.timestamp) - expect(memberFound.username).toStrictEqual({ - [PlatformType.GITHUB]: ['anil_github'], - }) - }) - - it('Should replace joinedAt when activity ts is earlier than existing joinedAt', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService( - mockIRepositoryOptions, - ) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - const member = { - username: { - [PlatformType.GITHUB]: 'anil_github', - }, - displayName: 'Anil', - email: 'lala@l.com', - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - [PlatformType.TWITTER]: 'https://twitter.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - organisation: 'Crowd', - joinedAt: '2022-05-27T15:13:30Z', - } - - await MemberRepository.create(member, mockIRepositoryOptions) - - const data = { - member, - body: 'Description\nThis pull request adds a new Dashboard and related widgets. This work will probably have to be revisited as soon as possible since a lot of decisions were made, without having too much time to think about different outcomes/possibilities. We rushed these changes so that we can demo a working dashboard to YC and to our Investors.\nChanges Proposed\n\nUpdate Chart.js\nAdd two different type of widgets (number and graph)\nRemove older/default widgets from dashboard and add our own widgets\nHide some items from the menu\nAdd all widget infrastructure (actions, services, etc) to integrate with the backend\nAdd a few more CSS tweaks\n\nScreenshots', - title: 'Dashboard widgets and some other tweaks/adjustments', - url: 'https://github.com/CrowdDevHQ/crowd-web/pull/16', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - timestamp: '2021-09-30T14:20:27.000Z', - type: 'pull_request-closed', - isContribution: true, - platform: PlatformType.GITHUB, - score: 4, - sourceId: '#sourceId1', - } - - const activityWithMember = await new ActivityService( - mockIRepositoryOptions, - ).createWithMember(data) - - delete activityWithMember.member - delete activityWithMember.display - delete activityWithMember.objectMember - - activityWithMember.createdAt = activityWithMember.createdAt.toISOString().split('T')[0] - activityWithMember.updatedAt = activityWithMember.updatedAt.toISOString().split('T')[0] - - const memberFound = await MemberRepository.findById( - activityWithMember.memberId, - mockIRepositoryOptions, - ) - - const expectedActivityCreated = { - id: activityWithMember.id, - body: data.body, - title: data.title, - url: data.url, - channel: data.channel, - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - sentiment: 0.42, - mixed: 0.42, - label: 'positive', - }, - attributes: {}, - type: data.type, - timestamp: new Date(data.timestamp), - platform: data.platform, - isContribution: data.isContribution, - score: data.score, - username: 'anil_github', - objectMemberUsername: null, - memberId: memberFound.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tasks: [], - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parentId: null, - parent: null, - sourceId: data.sourceId, - sourceParentId: null, - conversationId: null, - organizationId: null, - organization: null, - } - - expect(activityWithMember).toStrictEqual(expectedActivityCreated) - expect(memberFound.joinedAt).toStrictEqual(expectedActivityCreated.timestamp) - expect(memberFound.username).toStrictEqual({ - [PlatformType.GITHUB]: ['anil_github'], - }) - }) - - it('Should not replace joinedAt when activity ts is later than existing joinedAt', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService( - mockIRepositoryOptions, - ) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - const member = { - username: { - [PlatformType.GITHUB]: 'anil_github', - }, - displayName: 'Anil', - email: 'lala@l.com', - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - [PlatformType.TWITTER]: 'https://twitter.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - organisation: 'Crowd', - joinedAt: '2020-05-27T15:13:30Z', - } - - await MemberRepository.create(member, mockIRepositoryOptions) - - const data = { - member, - body: 'Description\nThis pull request adds a new Dashboard and related widgets. This work will probably have to be revisited as soon as possible since a lot of decisions were made, without having too much time to think about different outcomes/possibilities. We rushed these changes so that we can demo a working dashboard to YC and to our Investors.\nChanges Proposed\n\nUpdate Chart.js\nAdd two different type of widgets (number and graph)\nRemove older/default widgets from dashboard and add our own widgets\nHide some items from the menu\nAdd all widget infrastructure (actions, services, etc) to integrate with the backend\nAdd a few more CSS tweaks\n\nScreenshots', - title: 'Dashboard widgets and some other tweaks/adjustments', - url: 'https://github.com/CrowdDevHQ/crowd-web/pull/16', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - timestamp: '2021-09-30T14:20:27.000Z', - type: 'pull_request-closed', - isContribution: true, - platform: PlatformType.GITHUB, - score: 4, - sourceId: '#sourceId1', - } - - const activityWithMember = await new ActivityService( - mockIRepositoryOptions, - ).createWithMember(data) - - delete activityWithMember.member - delete activityWithMember.display - delete activityWithMember.objectMember - - activityWithMember.createdAt = activityWithMember.createdAt.toISOString().split('T')[0] - activityWithMember.updatedAt = activityWithMember.updatedAt.toISOString().split('T')[0] - - const memberFound = await MemberRepository.findById( - activityWithMember.memberId, - mockIRepositoryOptions, - ) - - const expectedActivityCreated = { - id: activityWithMember.id, - body: data.body, - title: data.title, - url: data.url, - channel: data.channel, - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.42, - }, - attributes: {}, - type: data.type, - timestamp: new Date(data.timestamp), - platform: data.platform, - isContribution: data.isContribution, - score: data.score, - username: 'anil_github', - objectMemberUsername: null, - memberId: memberFound.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tasks: [], - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parentId: null, - parent: null, - sourceId: data.sourceId, - sourceParentId: null, - conversationId: null, - organizationId: null, - organization: null, - } - - expect(activityWithMember).toStrictEqual(expectedActivityCreated) - expect(memberFound.joinedAt).toStrictEqual(new Date('2020-05-27T15:13:30Z')) - expect(memberFound.username).toStrictEqual({ - [PlatformType.GITHUB]: ['anil_github'], - }) - }) - - it('It should replace joinedAt if the original was in year 1970', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - await populateSegments(mockIRepositoryOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService( - mockIRepositoryOptions, - ) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - const member = { - username: { - [PlatformType.GITHUB]: 'anil_github', - }, - displayName: 'Anil', - email: 'lala@l.com', - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - [PlatformType.TWITTER]: 'https://twitter.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Computer Science', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Istanbul', - }, - }, - organisation: 'Crowd', - joinedAt: new Date('1970-01-01T00:00:00Z'), - } - - await MemberRepository.create(member, mockIRepositoryOptions) - - const data = { - member, - body: 'Description\nThis pull request adds a new Dashboard and related widgets. This work will probably have to be revisited as soon as possible since a lot of decisions were made, without having too much time to think about different outcomes/possibilities. We rushed these changes so that we can demo a working dashboard to YC and to our Investors.\nChanges Proposed\n\nUpdate Chart.js\nAdd two different type of widgets (number and graph)\nRemove older/default widgets from dashboard and add our own widgets\nHide some items from the menu\nAdd all widget infrastructure (actions, services, etc) to integrate with the backend\nAdd a few more CSS tweaks\n\nScreenshots', - title: 'Dashboard widgets and some other tweaks/adjustments', - state: 'merged', - url: 'https://github.com/CrowdDevHQ/crowd-web/pull/16', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - timestamp: '2021-09-30T14:20:27.000Z', - type: 'pull_request-closed', - isContribution: true, - platform: PlatformType.GITHUB, - score: 4, - sourceId: '#sourceId1', - } - - const activityWithMember = await new ActivityService( - mockIRepositoryOptions, - ).createWithMember(data) - - delete activityWithMember.member - delete activityWithMember.display - delete activityWithMember.objectMember - - activityWithMember.createdAt = activityWithMember.createdAt.toISOString().split('T')[0] - activityWithMember.updatedAt = activityWithMember.updatedAt.toISOString().split('T')[0] - - const memberFound = await MemberRepository.findById( - activityWithMember.memberId, - mockIRepositoryOptions, - ) - - const expectedActivityCreated = { - id: activityWithMember.id, - body: data.body, - title: data.title, - url: data.url, - channel: data.channel, - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.42, - }, - attributes: {}, - type: data.type, - timestamp: new Date(data.timestamp), - platform: data.platform, - isContribution: data.isContribution, - score: data.score, - username: 'anil_github', - objectMemberUsername: null, - memberId: memberFound.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tasks: [], - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parentId: null, - parent: null, - sourceId: data.sourceId, - sourceParentId: null, - conversationId: null, - organizationId: null, - organization: null, - } - - expect(activityWithMember).toStrictEqual(expectedActivityCreated) - expect(memberFound.joinedAt).toStrictEqual(expectedActivityCreated.timestamp) - expect(memberFound.username).toStrictEqual({ - [PlatformType.GITHUB]: ['anil_github'], - }) - }) - - it('Should respect joinedAt when an existing activity comes in with a different timestamp', async () => { - // This can happen in cases like the Twitter integration. - // For follow activities, if we are onboarding we set the timestamp to 1970, - // but if we are not onboarding, we set the timestamp to the current time. - // This can cause having 2 activities with different timestamps, but the same sourceId. - // The joinedAt should stay untouched in this case. - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService( - mockIRepositoryOptions, - ) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - const data = { - member: { - username: 'anil', - }, - timestamp: '1970-01-01T00:00:00.000Z', - type: 'follow', - platform: PlatformType.TWITTER, - sourceId: '#sourceId1', - } - - const activityWithMember = await new ActivityService( - mockIRepositoryOptions, - ).createWithMember(data) - - const data2 = { - member: { - username: 'anil', - }, - timestamp: '2021-09-30T14:20:27.000Z', - type: 'follow', - platform: PlatformType.TWITTER, - sourceId: '#sourceId1', - } - // Upsert the same activity with a different timestamp - await new ActivityService(mockIRepositoryOptions).createWithMember(data2) - - const memberFound = await MemberRepository.findById( - activityWithMember.memberId, - mockIRepositoryOptions, - ) - // The joinedAt should stay untouched - expect(memberFound.joinedAt).toStrictEqual(new Date('1970-01-01T00:00:00.000Z')) - }) - }) - }) - - describe('addToConversation method', () => { - it('Should create a new conversation and add the activities in, when parent and child has no conversation', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const activityService = new ActivityService(mockIRepositoryOptions) - - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const activityParent = { - type: 'activity', - timestamp: '2020-05-27T14:13:30Z', - platform: PlatformType.GITHUB, - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - let activityParentCreated = await ActivityRepository.create( - activityParent, - mockIRepositoryOptions, - ) - - const activityChild = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - parent: activityParentCreated.id, - sourceId: '#sourceId2', - } - - let activityChildCreated = await ActivityRepository.create( - activityChild, - mockIRepositoryOptions, - ) - - const transaction = await SequelizeRepository.createTransaction(mockIRepositoryOptions) - - await activityService.addToConversation( - activityChildCreated.id, - activityParentCreated.id, - transaction, - ) - - await SequelizeRepository.commitTransaction(transaction) - - const conversationCreated = ( - await new ConversationService(mockIRepositoryOptions).findAndCountAll({ - slug: 'some-parent-activity', - }) - ).rows[0] - - // get activities again - activityChildCreated = await activityService.findById(activityChildCreated.id) - activityParentCreated = await activityService.findById(activityParentCreated.id) - - // activities should belong to the newly created conversation - expect(activityChildCreated.conversationId).toBe(conversationCreated.id) - expect(activityParentCreated.conversationId).toBe(conversationCreated.id) - }) - - it('Should add the child activity to parents conversation, when parent already has a conversation', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const activityService = new ActivityService(mockIRepositoryOptions) - const conversationService = new ConversationService(mockIRepositoryOptions) - - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const conversation = await conversationService.create({ - slug: 'some-slug', - title: 'some title', - }) - - const activityParent = { - type: 'activity', - timestamp: '2020-05-27T14:13:30Z', - platform: PlatformType.GITHUB, - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - conversationId: conversation.id, - sourceId: '#sourceId1', - } - - const activityParentCreated = await ActivityRepository.create( - activityParent, - mockIRepositoryOptions, - ) - - const activityChild = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - parent: activityParentCreated.id, - sourceId: '#sourceId2', - } - - let activityChildCreated = await ActivityRepository.create( - activityChild, - mockIRepositoryOptions, - ) - - const transaction = await SequelizeRepository.createTransaction(mockIRepositoryOptions) - - await activityService.addToConversation( - activityChildCreated.id, - activityParentCreated.id, - transaction, - ) - - await SequelizeRepository.commitTransaction(transaction) - - // get child activity again - activityChildCreated = await activityService.findById(activityChildCreated.id) - - // child should be added to already existing conservation - expect(activityChildCreated.conversationId).toBe(conversation.id) - expect(activityParentCreated.conversationId).toBe(conversation.id) - }) - - it('Should add the parent activity to childs conversation and update conversation [published=false] title&slug, when child already has a conversation', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const activityService = new ActivityService(mockIRepositoryOptions) - const conversationService = new ConversationService(mockIRepositoryOptions) - - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - let conversation = await conversationService.create({ - slug: 'some-slug', - title: 'some title', - }) - - const activityParent = { - type: 'activity', - timestamp: '2020-05-27T14:13:30Z', - platform: PlatformType.GITHUB, - channel: 'https://github.com/CrowdDevHQ/crowd-web', - body: 'Some Parent Activity', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - let activityParentCreated = await ActivityRepository.create( - activityParent, - mockIRepositoryOptions, - ) - - const activityChild = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - parent: activityParentCreated.id, - conversationId: conversation.id, - sourceId: '#sourceId2', - } - - const activityChildCreated = await ActivityRepository.create( - activityChild, - mockIRepositoryOptions, - ) - - const transaction = await SequelizeRepository.createTransaction(mockIRepositoryOptions) - - await activityService.addToConversation( - activityChildCreated.id, - activityParentCreated.id, - transaction, - ) - - await SequelizeRepository.commitTransaction(transaction) - - // get the conversation again - conversation = await conversationService.findById(conversation.id) - - // conversation should be updated with newly added parents body - expect(conversation.title).toBe('Some Parent Activity') - expect(conversation.slug).toBe('some-parent-activity') - - // get parent activity again - activityParentCreated = await activityService.findById(activityParentCreated.id) - - // parent should be added to the conversation - expect(activityChildCreated.conversationId).toBe(conversation.id) - expect(activityParentCreated.conversationId).toBe(conversation.id) - }) - - it('Should add the parent activity to childs conversation and NOT update conversation [published=true] title&slug, when child already has a conversation', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const activityService = new ActivityService(mockIRepositoryOptions) - const conversationService = new ConversationService(mockIRepositoryOptions) - - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - let conversation = await conversationService.create({ - slug: 'some-slug', - title: 'some title', - published: true, - }) - - const activityParent = { - type: 'activity', - timestamp: '2020-05-27T14:13:30Z', - platform: PlatformType.GITHUB, - body: 'Some Parent Activity', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - let activityParentCreated = await ActivityRepository.create( - activityParent, - mockIRepositoryOptions, - ) - - const activityChild = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: 'Here', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - parent: activityParentCreated.id, - conversationId: conversation.id, - sourceId: '#sourceId2', - } - - const activityChildCreated = await ActivityRepository.create( - activityChild, - mockIRepositoryOptions, - ) - - const transaction = await SequelizeRepository.createTransaction(mockIRepositoryOptions) - - await activityService.addToConversation( - activityChildCreated.id, - activityParentCreated.id, - transaction, - ) - - await SequelizeRepository.commitTransaction(transaction) - - // get the conversation again - conversation = await conversationService.findById(conversation.id) - - // conversation fields should NOT be updated because it's already published - expect(conversation.title).toBe('some title') - expect(conversation.slug).toBe('some-slug') - - // get parent activity again - activityParentCreated = await activityService.findById(activityParentCreated.id) - - // parent should be added to the conversation - expect(activityChildCreated.conversationId).toBe(conversation.id) - expect(activityParentCreated.conversationId).toBe(conversation.id) - }) - - it('Should always auto-publish when conversationSettings.autoPublish.status is set to all', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const activityService = new ActivityService(mockIRepositoryOptions) - await SettingsRepository.findOrCreateDefault( - { website: 'https://some-website' }, - mockIRepositoryOptions, - ) - await ConversationSettingsRepository.findOrCreateDefault( - { - autoPublish: { - status: 'all', - }, - }, - mockIRepositoryOptions, - ) - - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const activityParent = { - type: 'activity', - timestamp: '2020-05-27T14:13:30Z', - platform: PlatformType.GITHUB, - body: 'Some Parent Activity', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - let activityParentCreated = await ActivityRepository.create( - activityParent, - mockIRepositoryOptions, - ) - - const activityChild = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: 'Here', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - parent: activityParentCreated.id, - sourceId: '#sourceId2', - } - - let activityChildCreated = await ActivityRepository.create( - activityChild, - mockIRepositoryOptions, - ) - - const transaction = await SequelizeRepository.createTransaction(mockIRepositoryOptions) - - await activityService.addToConversation( - activityChildCreated.id, - activityParentCreated.id, - transaction, - ) - - const conversationCreated = ( - await new ConversationService({ - ...mockIRepositoryOptions, - transaction, - } as IServiceOptions).findAndCountAll({ - slug: 'some-parent-activity', - }) - ).rows[0] - - await SequelizeRepository.commitTransaction(transaction) - - // get activities again - activityChildCreated = await activityService.findById(activityChildCreated.id) - activityParentCreated = await activityService.findById(activityParentCreated.id) - - // activities should belong to the newly created conversation - expect(activityChildCreated.conversationId).toBe(conversationCreated.id) - expect(activityParentCreated.conversationId).toBe(conversationCreated.id) - - expect(conversationCreated.published).toStrictEqual(true) - }) - - it('Should never auto-publish when conversationSettings.autoPublish.status is set to disabled', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const activityService = new ActivityService(mockIRepositoryOptions) - await SettingsRepository.findOrCreateDefault( - { website: 'https://some-website' }, - mockIRepositoryOptions, - ) - await ConversationSettingsRepository.findOrCreateDefault( - { - autoPublish: { - status: 'disabled', - }, - }, - mockIRepositoryOptions, - ) - - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const activityParent = { - type: 'activity', - timestamp: '2020-05-27T14:13:30Z', - platform: PlatformType.GITHUB, - body: 'Some Parent Activity', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - let activityParentCreated = await ActivityRepository.create( - activityParent, - mockIRepositoryOptions, - ) - - const activityChild = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: 'Here', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - parent: activityParentCreated.id, - sourceId: '#sourceId2', - } - - let activityChildCreated = await ActivityRepository.create( - activityChild, - mockIRepositoryOptions, - ) - - const transaction = await SequelizeRepository.createTransaction(mockIRepositoryOptions) - - await activityService.addToConversation( - activityChildCreated.id, - activityParentCreated.id, - transaction, - ) - - const conversationCreated = ( - await new ConversationService({ - ...mockIRepositoryOptions, - transaction, - } as IServiceOptions).findAndCountAll({ - slug: 'some-parent-activity', - }) - ).rows[0] - - await SequelizeRepository.commitTransaction(transaction) - - // get activities again - activityChildCreated = await activityService.findById(activityChildCreated.id) - activityParentCreated = await activityService.findById(activityParentCreated.id) - - // activities should belong to the newly created conversation - expect(activityChildCreated.conversationId).toBe(conversationCreated.id) - expect(activityParentCreated.conversationId).toBe(conversationCreated.id) - - expect(conversationCreated.published).toStrictEqual(false) - }) - - it('Should auto-publish when conversationSettings.autoPublish.status is set to custom and rules match', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const activityService = new ActivityService(mockIRepositoryOptions) - await SettingsRepository.findOrCreateDefault( - { website: 'https://some-website' }, - mockIRepositoryOptions, - ) - await ConversationSettingsRepository.findOrCreateDefault( - { - autoPublish: { - status: 'custom', - channelsByPlatform: { - [PlatformType.GITHUB]: ['crowd-web'], - }, - }, - }, - mockIRepositoryOptions, - ) - - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const activityParent = { - type: 'activity', - timestamp: '2020-05-27T14:13:30Z', - platform: PlatformType.GITHUB, - body: 'Some Parent Activity', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - let activityParentCreated = await ActivityRepository.create( - activityParent, - mockIRepositoryOptions, - ) - - const activityChild = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: 'Here', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - parent: activityParentCreated.id, - sourceId: '#sourceId2', - } - - let activityChildCreated = await ActivityRepository.create( - activityChild, - mockIRepositoryOptions, - ) - - const transaction = await SequelizeRepository.createTransaction(mockIRepositoryOptions) - - await activityService.addToConversation( - activityChildCreated.id, - activityParentCreated.id, - transaction, - ) - - const conversationCreated = ( - await new ConversationService({ - ...mockIRepositoryOptions, - transaction, - } as IServiceOptions).findAndCountAll({ - slug: 'some-parent-activity', - }) - ).rows[0] - - await SequelizeRepository.commitTransaction(transaction) - - // get activities again - activityChildCreated = await activityService.findById(activityChildCreated.id) - activityParentCreated = await activityService.findById(activityParentCreated.id) - - // activities should belong to the newly created conversation - expect(activityChildCreated.conversationId).toBe(conversationCreated.id) - expect(activityParentCreated.conversationId).toBe(conversationCreated.id) - - expect(conversationCreated.published).toStrictEqual(true) - }) - - it("Should not auto-publish when conversationSettings.autoPublish.status is set to custom and rules don't match", async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const activityService = new ActivityService(mockIRepositoryOptions) - await SettingsRepository.findOrCreateDefault( - { website: 'https://some-website' }, - mockIRepositoryOptions, - ) - await ConversationSettingsRepository.findOrCreateDefault( - { - autoPublish: { - status: 'custom', - channelsByPlatform: { - [PlatformType.GITHUB]: ['a-different-test-channel'], - }, - }, - }, - mockIRepositoryOptions, - ) - - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const activityParent = { - type: 'activity', - timestamp: '2020-05-27T14:13:30Z', - platform: PlatformType.GITHUB, - body: 'Some Parent Activity', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - let activityParentCreated = await ActivityRepository.create( - activityParent, - mockIRepositoryOptions, - ) - - const activityChild = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: 'Here', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - parent: activityParentCreated.id, - sourceId: '#sourceId2', - } - - let activityChildCreated = await ActivityRepository.create( - activityChild, - mockIRepositoryOptions, - ) - - const transaction = await SequelizeRepository.createTransaction(mockIRepositoryOptions) - - await activityService.addToConversation( - activityChildCreated.id, - activityParentCreated.id, - transaction, - ) - - const conversations = await new ConversationService({ - ...mockIRepositoryOptions, - transaction, - } as IServiceOptions).findAndCountAll({ - slug: 'some-parent-activity', - }) - - const conversationCreated = conversations.rows[0] - - await SequelizeRepository.commitTransaction(transaction) - - // get activities again - activityChildCreated = await activityService.findById(activityChildCreated.id) - activityParentCreated = await activityService.findById(activityParentCreated.id) - - // activities should belong to the newly created conversation - expect(activityChildCreated.conversationId).toBe(conversationCreated.id) - expect(activityParentCreated.conversationId).toBe(conversationCreated.id) - - expect(conversationCreated.published).toStrictEqual(false) - }) - }) - - describe('affiliations', () => { - let options - let activityService: ActivityService - let memberService - let organizationService - let segmentService: SegmentService - let memberAffiliationService: MemberAffiliationService - let memberSegmentAffiliationRepository: MemberSegmentAffiliationRepository - - let defaultActivity - let defaultMember - - beforeEach(async () => { - options = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(options) - - activityService = new ActivityService(options) - memberService = new MemberService(options) - organizationService = new OrganizationService(options) - segmentService = new SegmentService(options) - memberAffiliationService = new MemberAffiliationService(options) - memberSegmentAffiliationRepository = new MemberSegmentAffiliationRepository(options) - - defaultActivity = { - type: 'question', - timestamp: '2020-05-27T15:13:30Z', - username: 'test', - platform: PlatformType.GITHUB, - } - defaultMember = { - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - } - }) - - async function createMember(data = {}) { - return await memberService.upsert({ - ...defaultMember, - username: { - [PlatformType.GITHUB]: uuid(), - }, - ...data, - }) - } - - async function createActivity(memberId, data: any = {}) { - const activity = await activityService.upsert({ - ...defaultActivity, - sourceId: uuid(), - member: memberId, - ...data, - }) - - if (data.organizationId) { - await ActivityRepository.update( - activity.id, - { - organizationId: data.organizationId, - }, - options, - ) - return await activityService.findById(activity.id) - } - - return activity - } - - async function findActivity(id) { - return await activityService.findById(id) - } - - async function createOrg(name, data = {}) { - return await organizationService.createOrUpdate({ - identities: [ - { - name, - platform: 'crowd', - }, - ], - ...data, - }) - } - - async function createSegment(slug, data = {}) { - const db1 = await SequelizeTestUtils.getDatabase(db) - const tenant = options.currentTenant - try { - const segment = ( - await db1.segment.create({ - url: tenant.url, - name: slug, - parentName: tenant.name, - grandparentName: tenant.name, - slug: slug, - parentSlug: 'default', - grandparentSlug: 'default', - status: SegmentStatus.ACTIVE, - description: null, - sourceId: null, - sourceParentId: null, - tenantId: tenant.id, - }) - ).get({ plain: true }) - return segment - } catch (error) { - console.log(error) - throw error - } - } - - async function addWorkExperience(memberId, orgId, data = {}) { - return await MemberRepository.createOrUpdateWorkExperience( - { - memberId, - organizationId: orgId, - updateAffiliation: false, - source: 'test', - ...data, - }, - options, - ) - } - - describe('new activities', () => { - it('Should affiliate nothing if member has no organizations and no affiliations', async () => { - const member = await createMember() - const activity = await createActivity(member.id) - - expect(activity.organization).toBeNull() - }) - - it('Should affiliate work experience if member has organizations', async () => { - const member = await createMember() - const organization = await createOrg('hello') - await addWorkExperience(member.id, organization.id, { - dateStart: '2020-01-01', - }) - const activity = await createActivity(member.id, { - timestamp: '2023-01-01', - }) - - expect(activity.organization.id).toBe(organization.id) - }) - - it('Should affiliate with matching work experience if activity is from the past', async () => { - const member = await createMember() - const org1 = await createOrg('org1') - const org2 = await createOrg('org2') - - await addWorkExperience(member.id, org1.id, { - dateStart: '2020-01-01', - dateEnd: '2020-02-01', - }) - await addWorkExperience(member.id, org2.id, { - dateStart: '2020-02-01', - }) - - const activity = await createActivity(member.id, { - timestamp: '2020-01-15', - }) - - expect(activity.organization.id).toBe(org1.id) - }) - - it('Should affiliate with most recent open work experience if member has multiple organizations', async () => { - const member = await createMember() - const org1 = await createOrg('org1') - const org2 = await createOrg('org2') - - await addWorkExperience(member.id, org1.id, { - dateStart: '2020-01-01', - }) - await addWorkExperience(member.id, org2.id, { - dateStart: '2020-02-01', - }) - - const activity = await createActivity(member.id, { - timestamp: '2020-03-01', - }) - - expect(activity.organization.id).toBe(org2.id) - }) - - it('Should affiliate with most recent open work experience, even if there is a more recent closed one', async () => { - const member = await createMember() - const org1 = await createOrg('org1') - const org2 = await createOrg('org2') - - await addWorkExperience(member.id, org1.id, { - dateStart: '2020-01-01', - }) - await addWorkExperience(member.id, org2.id, { - dateStart: '2020-02-01', - dateEnd: '2020-03-01', - }) - - const activity = await createActivity(member.id) - - expect(activity.organization.id).toBe(org1.id) - }) - - it('Should affiliate with manual affiliation if member has organizations and affiliations', async () => { - const member = await createMember() - const org1 = await createOrg('org1') - const org2 = await createOrg('org2') - - await addWorkExperience(member.id, org1.id, { - dateStart: '2020-01-01', - }) - - await memberSegmentAffiliationRepository.createOrUpdate({ - memberId: member.id, - segmentId: options.currentSegments[0].id, - organizationId: org2.id, - dateStart: '2020-02-01', - }) - - const activity = await createActivity(member.id) - - expect(activity.organization.id).toBe(org2.id) - }) - - it('Should affiliate to invidiual if member has organizations and affiliations', async () => { - const member = await createMember() - const org1 = await createOrg('org1') - - await addWorkExperience(member.id, org1.id, { - dateStart: '2020-01-01', - }) - - await memberSegmentAffiliationRepository.createOrUpdate({ - memberId: member.id, - segmentId: options.currentSegments[0].id, - organizationId: null, - dateStart: '2020-02-01', - }) - - const activity = await createActivity(member.id) - - expect(activity.organization).toBeNull() - }) - - it('Should not affiliate if there are no relevant manual affiliations', async () => { - const member = await createMember() - const org1 = await createOrg('org1') - const segment1 = await createSegment('segment1') - - await memberSegmentAffiliationRepository.createOrUpdate({ - memberId: member.id, - segmentId: segment1.id, - organizationId: org1.id, - }) - - const activity = await createActivity(member.id) - - expect(activity.organization).toBeNull() - }) - }) - - describe('existing activities', () => { - it('Should clear affiliation if there is a manual individual affiliation', async () => { - const member = await createMember() - const org1 = await createOrg('org1') - const segment1 = await createSegment('segment1') - - await memberSegmentAffiliationRepository.createOrUpdate({ - memberId: member.id, - segmentId: segment1.id, - organizationId: null, - }) - - let activity = await createActivity(member.id, { - organizationId: org1.id, - }) - - await memberAffiliationService.updateAffiliation(member.id) - - activity = await findActivity(activity.id) - - expect(activity.organization).toBeNull() - }) - - it('Should affiliate activities', async () => { - const member = await createMember() - - let activity1 = await createActivity(member.id) - let activity2 = await createActivity(member.id) - - const org = await createOrg('org') - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - }) - - await memberAffiliationService.updateAffiliation(member.id) - - activity1 = await findActivity(activity1.id) - activity2 = await findActivity(activity2.id) - - expect(activity1.organization.id).toBe(org.id) - expect(activity2.organization.id).toBe(org.id) - }) - - it('Should only affiliate activities of specific member', async () => { - const member1 = await createMember() - const member2 = await createMember() - - let activity1 = await createActivity(member1.id) - let activity2 = await createActivity(member2.id) - - const org1 = await createOrg('org1') - await addWorkExperience(member1.id, org1.id, { - dateStart: '2020-01-01', - }) - const org2 = await createOrg('org2') - await addWorkExperience(member2.id, org2.id, { - dateStart: '2020-01-01', - }) - - await memberAffiliationService.updateAffiliation(member1.id) - - activity2 = await findActivity(activity2.id) - - expect(activity2.organization).toBeNull() - }) - - it('Should affiliate with matching recent work experience', async () => { - const member = await createMember() - - let activity = await createActivity(member.id, { - timestamp: '2020-01-01', - }) - - const org1 = await createOrg('org1') - await addWorkExperience(member.id, org1.id) - const org2 = await createOrg('org2') - await addWorkExperience(member.id, org2.id, { - dateStart: '2023-01-01', - }) - const org3 = await createOrg('org2') - await addWorkExperience(member.id, org3.id, { - dateStart: '2019-01-01', - dateEnd: '2022-01-01', - }) - - await memberAffiliationService.updateAffiliation(member.id) - - activity = await findActivity(activity.id) - - expect(activity.organization.id).toBe(org3.id) - }) - - it('Should affiliate first created org to past activities', async () => { - const member = await createMember() - - let activity = await createActivity(member.id, { - timestamp: '2022-05-01', - }) - - const org1 = await createOrg('org1') - await addWorkExperience(member.id, org1.id) - const org2 = await createOrg('org2') - await addWorkExperience(member.id, org2.id, { - dateStart: '2023-01-01', - }) - const org3 = await createOrg('org2') - await addWorkExperience(member.id, org3.id, { - dateStart: '2019-01-01', - dateEnd: '2022-01-01', - }) - - await memberAffiliationService.updateAffiliation(member.id) - - activity = await findActivity(activity.id) - - expect(activity.organization.id).toBe(org1.id) - }) - - it('Should prefer manual affiliation over work experience', async () => { - const member = await createMember() - - let activity = await createActivity(member.id, { - timestamp: '2020-05-01', - }) - - const org1 = await createOrg('org1') - await addWorkExperience(member.id, org1.id, { - dateStart: '2020-01-01', - }) - - const org2 = await createOrg('org2') - await memberSegmentAffiliationRepository.createOrUpdate({ - memberId: member.id, - segmentId: options.currentSegments[0].id, - organizationId: org2.id, - dateStart: '2020-01-01', - }) - - await memberAffiliationService.updateAffiliation(member.id) - - activity = await findActivity(activity.id) - - expect(activity.organization.id).toBe(org2.id) - }) - - it('Should prefer manual individual affiliation over work experience', async () => { - const member = await createMember() - - let activity = await createActivity(member.id, { - timestamp: '2020-05-01', - }) - - const org1 = await createOrg('org1') - await addWorkExperience(member.id, org1.id, { - dateStart: '2020-01-01', - }) - - await memberSegmentAffiliationRepository.createOrUpdate({ - memberId: member.id, - segmentId: options.currentSegments[0].id, - organizationId: null, - dateStart: '2020-01-01', - }) - - await memberAffiliationService.updateAffiliation(member.id) - - activity = await findActivity(activity.id) - - expect(activity.organization).toBeNull() - }) - - it('Should trigger affiliation update when changing member organizations', async () => { - const member = await createMember() - - let activity = await createActivity(member.id, { - timestamp: '2020-05-01', - }) - - const org1 = await createOrg('org1') - await addWorkExperience(member.id, org1.id, { - dateStart: '2020-01-01', - updateAffiliation: true, - }) - - activity = await findActivity(activity.id) - - expect(activity.organization.id).toBe(org1.id) - }) - - it('Should trigger affiliation update when changing member manual affiliations', async () => { - const member = await createMember() - - let activity = await createActivity(member.id) - - const org1 = await createOrg('org1') - - await memberSegmentAffiliationRepository.createOrUpdate({ - memberId: member.id, - segmentId: options.currentSegments[0].id, - organizationId: org1.id, - }) - - activity = await findActivity(activity.id) - - expect(activity.organization.id).toBe(org1.id) - }) - }) - - it('Should filter by organization based on organizationId', async () => { - const member = await createMember() - - const org1 = await createOrg('org1') - await addWorkExperience(member.id, org1.id) - - const org2 = await createOrg('org2') - await addWorkExperience(member.id, org2.id) - - let activity1 = await createActivity(member.id, { - organizationId: org1.id, - }) - let activity2 = await createActivity(member.id, { - organizationId: org2.id, - }) - - const { rows } = await activityService.query({ - filter: { - organizations: [org1.id], - }, - }) - - expect(rows.length).toBe(1) - }) - }) -}) diff --git a/backend/src/services/__tests__/eagleEyeContentService.test.ts b/backend/src/services/__tests__/eagleEyeContentService.test.ts deleted file mode 100644 index 9170b4a645..0000000000 --- a/backend/src/services/__tests__/eagleEyeContentService.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import SequelizeTestUtils from '../../database/utils/sequelizeTestUtils' -import { EagleEyeActionType, EagleEyeContent } from '../../types/eagleEyeTypes' -import EagleEyeContentService from '../eagleEyeContentService' - -const db = null - -describe('EagleEyeContentService tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('upsert method', () => { - it('Should create or update a single content using URL field', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const content: EagleEyeContent = { - platform: 'reddit', - url: 'https://some-post-url', - post: { - title: 'post title', - body: 'post body', - }, - postedAt: '2020-05-27T15:13:30Z', - tenantId: mockIRepositoryOptions.currentTenant.id, - actions: [ - { - type: EagleEyeActionType.BOOKMARK, - timestamp: '2022-06-27T14:13:30Z', - }, - ], - } - - const service = new EagleEyeContentService(mockIRepositoryOptions) - const c1 = await service.upsert(content) - - let contents = await service.query({}) - - expect(contents.count).toBe(1) - expect(contents.rows).toStrictEqual([c1]) - - // upsert previous url with some new fields - const contentWithSameUrl: EagleEyeContent = { - platform: 'reddit', - url: 'https://some-post-url', - post: { - title: 'a brand new post title', - body: 'better post body', - }, - postedAt: '2020-05-27T15:13:30Z', - tenantId: mockIRepositoryOptions.currentTenant.id, - } - - const c1Upserted = await service.upsert(contentWithSameUrl) - - contents = await service.query({}) - expect(contents.count).toBe(1) - expect(contents.rows).toStrictEqual([c1Upserted]) - expect(c1Upserted.id).toEqual(c1.id) - expect(contents.rows[0].post).toStrictEqual(contentWithSameUrl.post) - }) - }) -}) diff --git a/backend/src/services/__tests__/integrationService.test.ts b/backend/src/services/__tests__/integrationService.test.ts deleted file mode 100644 index cc8d13e8b6..0000000000 --- a/backend/src/services/__tests__/integrationService.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import SequelizeTestUtils from '../../database/utils/sequelizeTestUtils' -import IntegrationService from '../integrationService' -import { PlatformType } from '@crowd/types' - -const db = null - -describe('IntegrationService tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('createOrUpdate', () => { - it('Should create a new integration because platform does not exist yet', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - const integrationService = new IntegrationService(mockIServiceOptions) - - const integrationToCreate = { - platform: PlatformType.GITHUB, - token: '1234', - integrationIdentifier: '1234', - status: 'in-progress', - } - - let integrations = await integrationService.findAndCountAll({}) - expect(integrations.count).toEqual(0) - - await integrationService.createOrUpdate(integrationToCreate) - integrations = await integrationService.findAndCountAll({}) - - expect(integrations.count).toEqual(1) - }) - - it('Should update existing integration if platform already exists', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - const integrationService = new IntegrationService(mockIServiceOptions) - - const integrationToCreate = { - platform: PlatformType.GITHUB, - token: '1234', - integrationIdentifier: '1234', - status: 'in-progress', - } - - await integrationService.createOrUpdate(integrationToCreate) - let integrations = await integrationService.findAndCountAll({}) - expect(integrations.count).toEqual(1) - expect(integrations.rows[0].status).toEqual('in-progress') - - const integrationToUpdate = { - platform: PlatformType.GITHUB, - token: '1234', - integrationIdentifier: '1234', - status: 'done', - } - - await integrationService.createOrUpdate(integrationToUpdate) - integrations = await integrationService.findAndCountAll({}) - expect(integrations.count).toEqual(1) - expect(integrations.rows[0].status).toEqual('done') - }) - }) - - describe('Find all active integrations tests', () => { - it('Should find an empty list when there are no integrations', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - expect( - (await new IntegrationService(mockIServiceOptions).getAllActiveIntegrations()).count, - ).toBe(0) - }) - - it('Should return n for n active integrations', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - await new IntegrationService(mockIServiceOptions).createOrUpdate({ - platform: PlatformType.SLACK, - status: 'done', - }) - expect( - (await new IntegrationService(mockIServiceOptions).getAllActiveIntegrations()).count, - ).toBe(1) - - await new IntegrationService(mockIServiceOptions).createOrUpdate({ - platform: PlatformType.GITHUB, - status: 'done', - }) - expect( - (await new IntegrationService(mockIServiceOptions).getAllActiveIntegrations()).count, - ).toBe(2) - }) - - it('Should return n for n active integrations when there are other integrations', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - await new IntegrationService(mockIServiceOptions).createOrUpdate({ - platform: PlatformType.SLACK, - status: 'done', - }) - await new IntegrationService(mockIServiceOptions).createOrUpdate({ - platform: PlatformType.DISCORD, - status: 'in-progress', - }) - - expect( - (await new IntegrationService(mockIServiceOptions).getAllActiveIntegrations()).count, - ).toBe(1) - - await new IntegrationService(mockIServiceOptions).createOrUpdate({ - platform: PlatformType.GITHUB, - status: 'done', - }) - expect( - (await new IntegrationService(mockIServiceOptions).getAllActiveIntegrations()).count, - ).toBe(2) - }) - }) -}) diff --git a/backend/src/services/__tests__/memberAttributeSettingsService.test.ts b/backend/src/services/__tests__/memberAttributeSettingsService.test.ts deleted file mode 100644 index 974382fba8..0000000000 --- a/backend/src/services/__tests__/memberAttributeSettingsService.test.ts +++ /dev/null @@ -1,905 +0,0 @@ -/* eslint @typescript-eslint/no-unused-vars: 0 */ - -import { - DEVTO_MEMBER_ATTRIBUTES, - DISCORD_MEMBER_ATTRIBUTES, - GITHUB_MEMBER_ATTRIBUTES, - SLACK_MEMBER_ATTRIBUTES, - TWITTER_MEMBER_ATTRIBUTES, -} from '@crowd/integrations' -import SequelizeTestUtils from '../../database/utils/sequelizeTestUtils' -import Error400 from '../../errors/Error400' -import MemberAttributeSettingsService from '../memberAttributeSettingsService' -import { MemberAttributeType } from '@crowd/types' -import { RedisCache, getRedisClient } from '@crowd/redis' -import { REDIS_CONFIG } from '../../conf' -import { getServiceLogger } from '@crowd/logging' - -const log = getServiceLogger() - -let cache: RedisCache | undefined = undefined -const clearRedisCache = async () => { - if (!cache) { - const redis = await getRedisClient(REDIS_CONFIG) - cache = new RedisCache('memberAttributes', redis, log) - } - - await cache.deleteAll() -} - -const db = null -describe('MemberAttributeSettingService tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - await clearRedisCache() - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('createPredefined tests', () => { - it('Should create predefined github attributes', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attributes = (await as.createPredefined(GITHUB_MEMBER_ATTRIBUTES)).map((attribute) => { - attribute.createdAt = (attribute.createdAt as any).toISOString().split('T')[0] - attribute.updatedAt = (attribute.updatedAt as any).toISOString().split('T')[0] - return attribute - }) - - const [ - isHireableCreated, - urlCreated, - websiteUrlCreated, - bioCreated, - companyCreated, - locationCreated, - ] = attributes - - const [isHireable, url, websiteUrl, bio, company, location] = GITHUB_MEMBER_ATTRIBUTES - - const expected = [ - { - id: isHireableCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: isHireable.show, - type: isHireable.type, - canDelete: isHireable.canDelete, - name: isHireable.name, - label: isHireable.label, - }, - { - id: urlCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: url.show, - type: url.type, - canDelete: url.canDelete, - name: url.name, - label: url.label, - }, - { - id: websiteUrlCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: websiteUrl.show, - type: websiteUrl.type, - canDelete: websiteUrl.canDelete, - name: websiteUrl.name, - label: websiteUrl.label, - }, - { - id: bioCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: bio.show, - type: bio.type, - canDelete: bio.canDelete, - name: bio.name, - label: bio.label, - }, - { - id: companyCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: company.show, - type: company.type, - canDelete: company.canDelete, - name: company.name, - label: company.label, - }, - { - id: locationCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: location.show, - type: location.type, - canDelete: location.canDelete, - name: location.name, - label: location.label, - }, - ] - - expect(attributes).toEqual(expected) - }) - it('Should create predefined discord attributes', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attributes = (await as.createPredefined(DISCORD_MEMBER_ATTRIBUTES)).map((attribute) => { - attribute.createdAt = (attribute.createdAt as any).toISOString().split('T')[0] - attribute.updatedAt = (attribute.updatedAt as any).toISOString().split('T')[0] - return attribute - }) - - const [idCreated, avatarUrlCreated] = attributes - - const [id, avatarUrl] = DISCORD_MEMBER_ATTRIBUTES - - const expected = [ - { - id: idCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: id.show, - type: id.type, - canDelete: id.canDelete, - name: id.name, - label: id.label, - }, - { - id: avatarUrlCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: avatarUrl.show, - type: avatarUrl.type, - canDelete: avatarUrl.canDelete, - name: avatarUrl.name, - label: avatarUrl.label, - }, - ] - - expect(attributes).toEqual(expected) - }) - - it('Should create predefined devto attributes', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attributes = (await as.createPredefined(DEVTO_MEMBER_ATTRIBUTES)).map((attribute) => { - attribute.createdAt = (attribute.createdAt as any).toISOString().split('T')[0] - attribute.updatedAt = (attribute.updatedAt as any).toISOString().split('T')[0] - return attribute - }) - - const [idCreated, urlCreated, nameCreated, bioCreated, locationCreated] = attributes - - const [id, url, name, bio, location] = DEVTO_MEMBER_ATTRIBUTES - - const expected = [ - { - id: idCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: id.show, - type: id.type, - canDelete: id.canDelete, - name: id.name, - label: id.label, - }, - { - id: urlCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: url.show, - type: url.type, - canDelete: url.canDelete, - name: url.name, - label: url.label, - }, - { - id: nameCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: name.show, - type: name.type, - canDelete: name.canDelete, - name: name.name, - label: name.label, - }, - { - id: bioCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: bio.show, - type: bio.type, - canDelete: bio.canDelete, - name: bio.name, - label: bio.label, - }, - { - id: locationCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: location.show, - type: location.type, - canDelete: location.canDelete, - name: location.name, - label: location.label, - }, - ] - - expect(attributes).toEqual(expected) - }) - it('Should create predefined twitter attributes', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attributes = (await as.createPredefined(TWITTER_MEMBER_ATTRIBUTES)).map((attribute) => { - attribute.createdAt = (attribute.createdAt as any).toISOString().split('T')[0] - attribute.updatedAt = (attribute.updatedAt as any).toISOString().split('T')[0] - return attribute - }) - - const [idCreated, avatarUrlCreated, urlCreated, bioCreated, locationCreated] = attributes - - const [id, avatarUrl, url, bio, location] = TWITTER_MEMBER_ATTRIBUTES - - const expected = [ - { - id: idCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: id.show, - type: id.type, - canDelete: id.canDelete, - name: id.name, - label: id.label, - }, - { - id: avatarUrlCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: avatarUrl.show, - type: avatarUrl.type, - canDelete: avatarUrl.canDelete, - name: avatarUrl.name, - label: avatarUrl.label, - }, - { - id: urlCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: url.show, - type: url.type, - canDelete: url.canDelete, - name: url.name, - label: url.label, - }, - { - id: bioCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: bio.show, - type: bio.type, - canDelete: bio.canDelete, - name: bio.name, - label: bio.label, - }, - { - id: locationCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: location.show, - type: location.type, - canDelete: location.canDelete, - name: location.name, - label: location.label, - }, - ] - - expect(attributes).toEqual(expected) - }) - it('Should create predefined slack attributes', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attributes = (await as.createPredefined(SLACK_MEMBER_ATTRIBUTES)).map((attribute) => { - attribute.createdAt = (attribute.createdAt as any).toISOString().split('T')[0] - attribute.updatedAt = (attribute.updatedAt as any).toISOString().split('T')[0] - return attribute - }) - - const [idCreated, avatarUrlCreated, jobTitleCreated, timezoneCreated] = attributes - - const [id, avatarUrl, jobTitle, timezone] = SLACK_MEMBER_ATTRIBUTES - - const expected = [ - { - id: idCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: id.show, - type: id.type, - canDelete: id.canDelete, - name: id.name, - label: id.label, - }, - { - id: avatarUrlCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: avatarUrl.show, - type: avatarUrl.type, - canDelete: avatarUrl.canDelete, - name: avatarUrl.name, - label: avatarUrl.label, - }, - { - id: jobTitleCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: jobTitle.show, - type: jobTitle.type, - canDelete: jobTitle.canDelete, - name: jobTitle.name, - label: jobTitle.label, - }, - { - id: timezoneCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: timezone.show, - type: timezone.type, - canDelete: timezone.canDelete, - name: timezone.name, - label: timezone.label, - }, - ] - - expect(attributes).toEqual(expected) - }) - it('Should accept duplicate attributes from different platforms without an exception', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attributes = await as.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - const attributes2 = (await as.createPredefined(DEVTO_MEMBER_ATTRIBUTES)).map((attribute) => { - attribute.createdAt = (attribute.createdAt as any).toISOString().split('T')[0] - attribute.updatedAt = (attribute.updatedAt as any).toISOString().split('T')[0] - return attribute - }) - - // create predefined method should still return shared attributes `url` and `id` - const [ - idCreatedTwitter, - _avatarUrlCreated, - urlCreatedTwitter, - bioCreatedTwitter, - locationCreatedTwitter, - ] = attributes - - const [ - _idCreatedDevTo, - _urlCreatedDevTo, - nameCreatedDevTo, - _bioCreatedDevTo, - _locationCreatedDevTo, - ] = attributes2 - - const [id, url, name, bio, location] = DEVTO_MEMBER_ATTRIBUTES - console.log('urlCreatedTwitter', urlCreatedTwitter.id) - console.log('urlCreatedDevTo', _urlCreatedDevTo.id) - console.log('bioCreatedTwitter', bioCreatedTwitter.id) - console.log('bioCreatedDevTo', _bioCreatedDevTo.id) - console.log('locationCreatedTwitter', locationCreatedTwitter.id) - console.log('locationCreatedDevTo', _locationCreatedDevTo.id) - const expected = [ - { - id: idCreatedTwitter.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: id.show, - type: id.type, - canDelete: id.canDelete, - name: id.name, - label: id.label, - }, - { - id: _urlCreatedDevTo.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: url.show, - type: url.type, - canDelete: url.canDelete, - name: url.name, - label: url.label, - }, - { - id: nameCreatedDevTo.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: name.show, - type: name.type, - canDelete: name.canDelete, - name: name.name, - label: name.label, - }, - { - id: _bioCreatedDevTo.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: bio.show, - type: bio.type, - canDelete: bio.canDelete, - name: bio.name, - label: bio.label, - }, - { - id: _locationCreatedDevTo.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: location.show, - type: location.type, - canDelete: location.canDelete, - name: location.name, - label: location.label, - }, - ] - - expect(attributes2).toEqual(expected) - - // find all attributes: url, name, id, imgUrl should be present - const allAttributes = await as.findAndCountAll({}) - - expect(allAttributes.count).toBe(6) - const allAttributeNames = allAttributes.rows.map((attribute) => attribute.name) - - expect(allAttributeNames).toEqual(['name', 'url', 'bio', 'location', 'avatarUrl', 'sourceId']) - }) - }) - describe('create tests', () => { - it('Should add single attribute to member attributes - all fields', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attribute1 = { - name: 'att1', - label: 'attribute 1', - type: MemberAttributeType.BOOLEAN, - canDelete: true, - show: true, - } - - const attributeCreated = await as.create(attribute1) - - const attributeExpected = { - ...attributeCreated, - options: [], - name: attribute1.name, - label: attribute1.label, - type: attribute1.type, - canDelete: attribute1.canDelete, - show: attribute1.show, - } - - expect(attributeCreated).toStrictEqual(attributeExpected) - }) - - it('Should create a multi-select field with options', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attribute1 = { - name: 'att1', - label: 'attribute 1', - type: MemberAttributeType.MULTI_SELECT, - options: ['option1', 'option2'], - canDelete: true, - show: true, - } - - const attributeCreated = await as.create(attribute1) - - const attributeExpected = { - ...attributeCreated, - options: ['option1', 'option2'], - name: attribute1.name, - label: attribute1.label, - type: attribute1.type, - canDelete: attribute1.canDelete, - show: attribute1.show, - } - - expect(attributeCreated).toStrictEqual(attributeExpected) - }) - - it('Should add single attribute to member attributes - without default fields', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attribute1 = { - name: 'att1', - label: 'attribute 1', - type: MemberAttributeType.BOOLEAN, - } - - const attributeCreated = await as.create(attribute1) - - // canDelete and show should be true by default - const attributeExpected = { - ...attributeCreated, - options: [], - name: attribute1.name, - label: attribute1.label, - type: attribute1.type, - canDelete: true, - show: true, - } - - expect(attributeCreated).toStrictEqual(attributeExpected) - }) - - it('Should add single attribute to member attributes - without default fields and name', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attribute1 = { - label: 'an attribute with multiple words', - type: MemberAttributeType.BOOLEAN, - } - - const attributeCreated = await as.create(attribute1) - - // name should be generated from the label - const attributeExpected = { - ...attributeCreated, - options: [], - name: 'anAttributeWithMultipleWords', - label: attribute1.label, - type: attribute1.type, - canDelete: true, - show: true, - } - - expect(attributeCreated).toStrictEqual(attributeExpected) - }) - }) - - describe('destroyAll tests', () => { - it('Should remove a single attribute succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attribute = await as.create({ - name: 'att1', - label: 'attribute 1', - type: MemberAttributeType.BOOLEAN, - canDelete: true, - show: true, - }) - - await as.destroyAll([attribute.id]) - - const allAttributes = await as.findAndCountAll({}) - - expect(allAttributes.count).toBe(0) - expect(allAttributes.rows).toStrictEqual([]) - }) - - it('Should remove multiple existing attributes successfully, and should silently accept non existing names and keep the canDelete=false attributes intact', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attribute1 = await as.create({ - name: 'att1', - label: 'attribute 1', - type: MemberAttributeType.BOOLEAN, - canDelete: true, - show: true, - }) - - const attribute2 = await as.create({ - name: 'att2', - label: 'attribute 2', - type: MemberAttributeType.STRING, - canDelete: false, - show: true, - }) - - const attribute3 = await as.create({ - name: 'att3', - label: 'attribute 3', - type: MemberAttributeType.EMAIL, - canDelete: true, - show: false, - }) - - await as.destroyAll([attribute1.id, attribute2.id, attribute3.id]) - - const allAttributes = await as.findAndCountAll({}) - - expect(allAttributes.count).toBe(1) - expect(allAttributes.rows).toStrictEqual([attribute2]) - }) - }) - - describe('update tests', () => { - it(`Should throw typesNotMatching 400 error when updating an existing attribute's type to another value`, async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attribute = await as.create({ - name: 'attribute 1', - label: 'attribute 1', - type: MemberAttributeType.BOOLEAN, - canDelete: true, - show: true, - }) - - await expect(() => - as.update(attribute.id, { - name: attribute.name, - label: 'some other label', - type: MemberAttributeType.STRING, - }), - ).rejects.toThrowError( - new Error400('en', 'settings.memberAttributes.errors.typesNotMatching', attribute.name), - ) - }) - - it(`Should throw canDeleteReadonly 400 error when updating an existing attribute's canDelete field to another value`, async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attribute = await as.create({ - name: 'attribute 1', - label: 'attribute 1', - type: MemberAttributeType.BOOLEAN, - canDelete: true, - show: true, - }) - - await expect(() => - as.update(attribute.id, { - canDelete: false, - show: true, - }), - ).rejects.toThrowError( - new Error400('en', 'settings.memberAttributes.errors.canDeleteReadonly', attribute.name), - ) - }) - - it(`Should should update other cases successfully`, async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attribute = await as.create({ - name: 'attribute 1', - label: 'attribute 1', - type: MemberAttributeType.BOOLEAN, - canDelete: true, - show: true, - }) - - const attribute1Update = { - name: attribute.name, - label: 'some other label', - type: attribute.type, - canDelete: true, - show: false, - } - - const updatedAttribute = await as.update(attribute.id, attribute1Update) - - const expectedAttribute = { - ...updatedAttribute, - name: attribute.name, - label: attribute1Update.label, - type: attribute.type, - canDelete: attribute.canDelete, - show: attribute1Update.show, - } - - expect(updatedAttribute).toStrictEqual(expectedAttribute) - }) - }) - - describe('isCorrectType tests', () => { - it(`Should check various types and values successfully`, async () => { - const isCorrectType = MemberAttributeSettingsService.isCorrectType - - // boolean - expect(isCorrectType(true, MemberAttributeType.BOOLEAN)).toBeTruthy() - expect(isCorrectType(false, MemberAttributeType.BOOLEAN)).toBeTruthy() - expect(isCorrectType('true', MemberAttributeType.BOOLEAN)).toBeTruthy() - expect(isCorrectType('false', MemberAttributeType.BOOLEAN)).toBeTruthy() - - expect(isCorrectType(5, MemberAttributeType.BOOLEAN)).toBeFalsy() - expect(isCorrectType('someString', MemberAttributeType.BOOLEAN)).toBeFalsy() - expect(isCorrectType({}, MemberAttributeType.BOOLEAN)).toBeFalsy() - expect(isCorrectType([], MemberAttributeType.BOOLEAN)).toBeFalsy() - - // string - expect(isCorrectType('', MemberAttributeType.STRING)).toBeTruthy() - expect(isCorrectType('someString', MemberAttributeType.STRING)).toBeTruthy() - - expect(isCorrectType(5, MemberAttributeType.STRING)).toBeFalsy() - expect(isCorrectType(true, MemberAttributeType.STRING)).toBeFalsy() - expect(isCorrectType({}, MemberAttributeType.STRING)).toBeFalsy() - - // date - expect(isCorrectType('2022-05-10', MemberAttributeType.DATE)).toBeTruthy() - expect(isCorrectType('2022-06-15T00:00:00', MemberAttributeType.DATE)).toBeTruthy() - expect(isCorrectType('2022-07-14T00:00:00Z', MemberAttributeType.DATE)).toBeTruthy() - - expect(isCorrectType(5, MemberAttributeType.DATE)).toBeFalsy() - expect(isCorrectType('someString', MemberAttributeType.DATE)).toBeFalsy() - expect(isCorrectType('', MemberAttributeType.DATE)).toBeFalsy() - expect(isCorrectType(true, MemberAttributeType.DATE)).toBeFalsy() - expect(isCorrectType({}, MemberAttributeType.DATE)).toBeFalsy() - expect(isCorrectType([], MemberAttributeType.DATE)).toBeFalsy() - - // email - expect(isCorrectType('anil@crowd.dev', MemberAttributeType.EMAIL)).toBeTruthy() - expect(isCorrectType('anil+123@crowd.dev', MemberAttributeType.EMAIL)).toBeTruthy() - - expect(isCorrectType(15, MemberAttributeType.EMAIL)).toBeFalsy() - expect(isCorrectType('', MemberAttributeType.EMAIL)).toBeFalsy() - expect(isCorrectType('someString', MemberAttributeType.EMAIL)).toBeFalsy() - expect(isCorrectType(true, MemberAttributeType.EMAIL)).toBeFalsy() - expect(isCorrectType({}, MemberAttributeType.EMAIL)).toBeFalsy() - expect(isCorrectType([], MemberAttributeType.EMAIL)).toBeFalsy() - - // number - expect(isCorrectType(100, MemberAttributeType.NUMBER)).toBeTruthy() - expect(isCorrectType(5.123, MemberAttributeType.NUMBER)).toBeTruthy() - expect(isCorrectType(0.000001, MemberAttributeType.NUMBER)).toBeTruthy() - expect(isCorrectType(0, MemberAttributeType.NUMBER)).toBeTruthy() - expect(isCorrectType('125', MemberAttributeType.NUMBER)).toBeTruthy() - - expect(isCorrectType('', MemberAttributeType.NUMBER)).toBeFalsy() - expect(isCorrectType('someString', MemberAttributeType.NUMBER)).toBeFalsy() - expect(isCorrectType(true, MemberAttributeType.NUMBER)).toBeFalsy() - expect(isCorrectType({}, MemberAttributeType.NUMBER)).toBeFalsy() - expect(isCorrectType([], MemberAttributeType.NUMBER)).toBeFalsy() - - // multiselect - expect( - isCorrectType(['a', 'b', 'c'], MemberAttributeType.MULTI_SELECT, { - options: ['a', 'b', 'c', 'd'], - }), - ).toBeTruthy() - expect( - isCorrectType([], MemberAttributeType.MULTI_SELECT, { options: ['a', 'b', 'c', 'd'] }), - ).toBeTruthy() - expect( - isCorrectType(['a'], MemberAttributeType.MULTI_SELECT, { options: ['a', 'b', 'c', 'd'] }), - ).toBeTruthy() - expect( - isCorrectType(['a', '42'], MemberAttributeType.MULTI_SELECT, { - options: ['a', 'b', 'c', 'd'], - }), - ).toBeFalsy() - expect( - isCorrectType('a', MemberAttributeType.MULTI_SELECT, { options: ['a', 'b', 'c'] }), - ).toBeFalsy() - expect( - isCorrectType(5, MemberAttributeType.MULTI_SELECT, { options: ['a', 'b', 'c'] }), - ).toBeFalsy() - }) - }) -}) diff --git a/backend/src/services/__tests__/memberService.test.ts b/backend/src/services/__tests__/memberService.test.ts deleted file mode 100644 index 9783d70fe4..0000000000 --- a/backend/src/services/__tests__/memberService.test.ts +++ /dev/null @@ -1,3308 +0,0 @@ -import SequelizeTestUtils from '../../database/utils/sequelizeTestUtils' -import MemberService from '../memberService' -import MemberRepository from '../../database/repositories/memberRepository' -import ActivityRepository from '../../database/repositories/activityRepository' -import TagRepository from '../../database/repositories/tagRepository' -import Error404 from '../../errors/Error404' -import Error400 from '../../errors/Error400' -import { MemberAttributeName, MemberAttributeType, PlatformType } from '@crowd/types' -import OrganizationRepository from '../../database/repositories/organizationRepository' -import TaskRepository from '../../database/repositories/taskRepository' -import NoteRepository from '../../database/repositories/noteRepository' -import MemberAttributeSettingsService from '../memberAttributeSettingsService' -import SettingsRepository from '../../database/repositories/settingsRepository' -import OrganizationService from '../organizationService' -import Plans from '../../security/plans' -import { generateUUIDv1 } from '@crowd/common' -import lodash from 'lodash' -import { - DEVTO_MEMBER_ATTRIBUTES, - DISCORD_MEMBER_ATTRIBUTES, - GITHUB_MEMBER_ATTRIBUTES, - SLACK_MEMBER_ATTRIBUTES, - TWITTER_MEMBER_ATTRIBUTES, -} from '@crowd/integrations' - -const db = null - -describe('MemberService tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('upsert method', () => { - it('Should throw 400 error when platform does not exist in member data', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const mas = new MemberAttributeSettingsService(mockIServiceOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const member1 = { - username: { - [PlatformType.GITHUB]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - emails: ['lala@l.com'], - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - joinedAt: '2020-05-28T15:13:30Z', - } - - await expect(() => - new MemberService(mockIServiceOptions).upsert(member1), - ).rejects.toThrowError(new Error400()) - }) - - it('Should create non existent member - attributes with matching platform', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const mas = new MemberAttributeSettingsService(mockIServiceOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await mas.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - await mas.createPredefined(DISCORD_MEMBER_ATTRIBUTES) - - const member1 = { - username: 'anil', - platform: PlatformType.GITHUB, - emails: ['lala@l.com'], - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - [PlatformType.TWITTER]: 'https://some-twitter-url', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.TWITTER]: '#twitterId', - [PlatformType.DISCORD]: '#discordId', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://some-image-url', - }, - }, - joinedAt: '2020-05-28T15:13:30Z', - } - - // Save some attributes since they get modified in the upsert function - const { platform, username, attributes } = member1 - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - username: { - [platform]: [username], - }, - displayName: username, - attributes: { - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.DISCORD]: attributes[MemberAttributeName.SOURCE_ID][PlatformType.DISCORD], - [PlatformType.TWITTER]: attributes[MemberAttributeName.SOURCE_ID][PlatformType.TWITTER], - default: attributes[MemberAttributeName.SOURCE_ID][PlatformType.TWITTER], - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: - attributes[MemberAttributeName.AVATAR_URL][PlatformType.TWITTER], - default: attributes[MemberAttributeName.AVATAR_URL][PlatformType.TWITTER], - }, - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - default: attributes[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.URL][PlatformType.GITHUB], - [PlatformType.TWITTER]: attributes[MemberAttributeName.URL][PlatformType.TWITTER], - default: attributes[MemberAttributeName.URL][PlatformType.TWITTER], - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - default: attributes[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.BIO][PlatformType.GITHUB], - default: attributes[MemberAttributeName.BIO][PlatformType.GITHUB], - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.LOCATION][PlatformType.GITHUB], - default: attributes[MemberAttributeName.LOCATION][PlatformType.GITHUB], - }, - }, - emails: member1.emails, - score: member1.score, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - organizations: [ - { - displayName: 'l.com', - id: memberCreated.organizations[0].id, - memberOrganizations: { - memberId: memberCreated.id, - organizationId: memberCreated.organizations[0].id, - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - }, - ], - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - reach: { total: -1 }, - joinedAt: new Date('2020-05-28T15:13:30Z'), - lastEnriched: null, - enrichedBy: [], - contributions: null, - affiliations: [], - manuallyCreated: false, - } - - expect(memberCreated).toStrictEqual(memberExpected) - }) - - it('Should create non existent member - object type username', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - const mas = new MemberAttributeSettingsService(mockIServiceOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - [PlatformType.TWITTER]: 'anil_twitter', - }, - platform: PlatformType.GITHUB, - emails: ['lala@l.com'], - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - joinedAt: '2020-05-28T15:13:30Z', - } - - // Save some attributes since they get modified in the upsert function - const { username, attributes } = member1 - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - username: { - [PlatformType.GITHUB]: ['anil'], - [PlatformType.TWITTER]: ['anil_twitter'], - }, - displayName: 'anil', - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - default: attributes[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.URL][PlatformType.GITHUB], - default: attributes[MemberAttributeName.URL][PlatformType.GITHUB], - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - default: attributes[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.BIO][PlatformType.GITHUB], - default: attributes[MemberAttributeName.BIO][PlatformType.GITHUB], - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.LOCATION][PlatformType.GITHUB], - default: attributes[MemberAttributeName.LOCATION][PlatformType.GITHUB], - }, - }, - emails: member1.emails, - score: member1.score, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - lastEnriched: null, - organizations: [ - { - displayName: 'l.com', - id: memberCreated.organizations[0].id, - memberOrganizations: { - memberId: memberCreated.id, - organizationId: memberCreated.organizations[0].id, - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - }, - ], - enrichedBy: [], - contributions: null, - reach: { total: -1 }, - joinedAt: new Date('2020-05-28T15:13:30Z'), - affiliations: [], - manuallyCreated: false, - } - - expect(memberCreated).toStrictEqual(memberExpected) - }) - - it('Should create non existent member - reach as number', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: 'anil', - platform: PlatformType.GITHUB, - emails: ['lala@l.com'], - score: 10, - attributes: {}, - reach: 10, - bio: 'Computer Science', - joinedAt: '2020-05-28T15:13:30Z', - location: 'Istanbul', - } - - // Save some attributes since they get modified in the upsert function - const { platform, username } = member1 - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - username: { - [platform]: [username], - }, - displayName: username, - attributes: {}, - emails: member1.emails, - lastEnriched: null, - organizations: [ - { - displayName: 'l.com', - id: memberCreated.organizations[0].id, - memberOrganizations: { - memberId: memberCreated.id, - organizationId: memberCreated.organizations[0].id, - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - }, - ], - enrichedBy: [], - contributions: null, - score: member1.score, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - reach: { total: 10, [PlatformType.GITHUB]: 10 }, - joinedAt: new Date('2020-05-28T15:13:30Z'), - affiliations: [], - manuallyCreated: false, - } - - expect(memberCreated).toStrictEqual(memberExpected) - }) - - it('Should create non existent member - reach as object, platform in object', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: 'anil', - platform: PlatformType.GITHUB, - emails: ['lala@l.com'], - score: 10, - reach: { [PlatformType.GITHUB]: 10, [PlatformType.TWITTER]: 10 }, - bio: 'Computer Science', - joinedAt: '2020-05-28T15:13:30Z', - location: 'Istanbul', - } - - // Save some attributes since they get modified in the upsert function - const { platform, username } = member1 - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - username: { - [platform]: [username], - }, - displayName: username, - attributes: {}, - lastEnriched: null, - organizations: [ - { - displayName: 'l.com', - id: memberCreated.organizations[0].id, - memberOrganizations: { - memberId: memberCreated.id, - organizationId: memberCreated.organizations[0].id, - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - }, - ], - enrichedBy: [], - contributions: null, - emails: member1.emails, - score: member1.score, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - reach: { total: 20, [PlatformType.GITHUB]: 10, [PlatformType.TWITTER]: 10 }, - joinedAt: new Date('2020-05-28T15:13:30Z'), - affiliations: [], - manuallyCreated: false, - } - - expect(memberCreated).toStrictEqual(memberExpected) - }) - - it('Should create non existent member - reach as object, platform not in object', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: 'anil', - platform: PlatformType.GITHUB, - emails: ['lala@l.com'], - score: 10, - reach: { [PlatformType.DISCORD]: 10, [PlatformType.TWITTER]: 10 }, - bio: 'Computer Science', - joinedAt: '2020-05-28T15:13:30Z', - location: 'Istanbul', - } - - // Save some attributes since they get modified in the upsert function - const { platform, username } = member1 - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - username: { - [platform]: [username], - }, - displayName: username, - attributes: {}, - emails: member1.emails, - score: member1.score, - lastEnriched: null, - organizations: [ - { - displayName: 'l.com', - id: memberCreated.organizations[0].id, - memberOrganizations: { - memberId: memberCreated.id, - organizationId: memberCreated.organizations[0].id, - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - }, - ], - enrichedBy: [], - contributions: null, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - reach: { total: 20, [PlatformType.DISCORD]: 10, [PlatformType.TWITTER]: 10 }, - joinedAt: new Date('2020-05-28T15:13:30Z'), - affiliations: [], - manuallyCreated: false, - } - - expect(memberCreated).toStrictEqual(memberExpected) - }) - - it('Should create non existent member - organization as name, no enrichment', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: 'anil', - platform: PlatformType.GITHUB, - emails: ['lala@gmail.com'], - score: 10, - attributes: {}, - reach: 10, - bio: 'Computer Science', - organizations: ['crowd.dev'], - joinedAt: '2020-05-28T15:13:30Z', - location: 'Istanbul', - } - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const organization = (await OrganizationRepository.findAndCountAll({}, mockIServiceOptions)) - .rows[0] - - const foundMember = await MemberRepository.findById(memberCreated.id, mockIServiceOptions) - - const o1 = foundMember.organizations[0].get({ plain: true }) - delete o1.createdAt - delete o1.updatedAt - - expect(o1).toStrictEqual({ - id: organization.id, - displayName: 'crowd.dev', - github: null, - location: null, - website: null, - description: null, - emails: null, - phoneNumbers: null, - logo: null, - memberOrganizations: { - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - tags: null, - twitter: null, - linkedin: null, - crunchbase: null, - employees: null, - revenueRange: null, - importHash: null, - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - isTeamOrganization: false, - type: null, - ticker: null, - size: null, - naics: null, - lastEnrichedAt: null, - industry: null, - headline: null, - geoLocation: null, - founded: null, - employeeCountByCountry: null, - address: null, - profiles: null, - attributes: {}, - manuallyCreated: false, - affiliatedProfiles: null, - allSubsidiaries: null, - alternativeDomains: null, - alternativeNames: null, - averageEmployeeTenure: null, - averageTenureByLevel: null, - averageTenureByRole: null, - directSubsidiaries: null, - employeeChurnRate: null, - employeeCountByMonth: null, - employeeGrowthRate: null, - employeeCountByMonthByLevel: null, - employeeCountByMonthByRole: null, - gicsSector: null, - grossAdditionsByMonth: null, - grossDeparturesByMonth: null, - ultimateParent: null, - immediateParent: null, - }) - }) - - it('Should create non existent member - organization as object, no enrichment', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: 'anil', - platform: PlatformType.GITHUB, - emails: ['lala@gmail.com'], - score: 10, - attributes: {}, - reach: 10, - bio: 'Computer Science', - organizations: [{ name: 'crowd.dev', url: 'https://crowd.dev', description: 'Here' }], - joinedAt: '2020-05-28T15:13:30Z', - location: 'Istanbul', - } - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const organization = (await OrganizationRepository.findAndCountAll({}, mockIServiceOptions)) - .rows[0] - - const foundMember = await MemberRepository.findById(memberCreated.id, mockIServiceOptions) - - const o1 = foundMember.organizations[0].get({ plain: true }) - delete o1.createdAt - delete o1.updatedAt - - expect(o1).toStrictEqual({ - id: organization.id, - displayName: 'crowd.dev', - github: null, - location: null, - website: null, - description: 'Here', - emails: null, - phoneNumbers: null, - logo: null, - memberOrganizations: { - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - tags: null, - twitter: null, - linkedin: null, - crunchbase: null, - employees: null, - revenueRange: null, - importHash: null, - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - isTeamOrganization: false, - type: null, - ticker: null, - size: null, - naics: null, - lastEnrichedAt: null, - industry: null, - headline: null, - geoLocation: null, - founded: null, - employeeCountByCountry: null, - address: null, - profiles: null, - attributes: {}, - manuallyCreated: false, - affiliatedProfiles: null, - allSubsidiaries: null, - alternativeDomains: null, - alternativeNames: null, - averageEmployeeTenure: null, - averageTenureByLevel: null, - averageTenureByRole: null, - directSubsidiaries: null, - employeeChurnRate: null, - employeeCountByMonth: null, - employeeGrowthRate: null, - employeeCountByMonthByLevel: null, - employeeCountByMonthByRole: null, - gicsSector: null, - grossAdditionsByMonth: null, - grossDeparturesByMonth: null, - ultimateParent: null, - immediateParent: null, - }) - }) - - it('Should create non existent member - organization as id, no enrichment', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const oCreated = await new OrganizationService(mockIServiceOptions).createOrUpdate({ - identities: [ - { - name: 'crowd.dev', - platform: 'crowd', - }, - ], - }) - - const member1 = { - username: 'anil', - platform: PlatformType.GITHUB, - emails: ['lala@gmail.com'], - score: 10, - attributes: {}, - reach: 10, - bio: 'Computer Science', - organizations: [oCreated.id], - joinedAt: '2020-05-28T15:13:30Z', - location: 'Istanbul', - } - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const organization = (await OrganizationRepository.findAndCountAll({}, mockIServiceOptions)) - .rows[0] - - const foundMember = await MemberRepository.findById(memberCreated.id, mockIServiceOptions) - - const o1 = foundMember.organizations[0].get({ plain: true }) - delete o1.createdAt - delete o1.updatedAt - - expect(o1).toStrictEqual({ - id: organization.id, - displayName: 'crowd.dev', - github: null, - location: null, - website: null, - description: null, - emails: null, - phoneNumbers: null, - logo: null, - memberOrganizations: { - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - tags: null, - twitter: null, - linkedin: null, - crunchbase: null, - employees: null, - revenueRange: null, - importHash: null, - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - isTeamOrganization: false, - type: null, - ticker: null, - size: null, - naics: null, - lastEnrichedAt: null, - industry: null, - headline: null, - geoLocation: null, - founded: null, - employeeCountByCountry: null, - address: null, - profiles: null, - attributes: {}, - manuallyCreated: false, - affiliatedProfiles: null, - allSubsidiaries: null, - alternativeDomains: null, - alternativeNames: null, - averageEmployeeTenure: null, - averageTenureByLevel: null, - averageTenureByRole: null, - directSubsidiaries: null, - employeeChurnRate: null, - employeeCountByMonth: null, - employeeGrowthRate: null, - employeeCountByMonthByLevel: null, - employeeCountByMonthByRole: null, - gicsSector: null, - grossAdditionsByMonth: null, - grossDeparturesByMonth: null, - ultimateParent: null, - immediateParent: null, - }) - }) - - it('Should create non existent member - organization with enrichment', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions( - db, - Plans.values.growth, - ) - - const member1 = { - username: 'anil', - platform: PlatformType.GITHUB, - emails: ['lala@gmail.com'], - score: 10, - attributes: {}, - reach: 10, - bio: 'Computer Science', - organizations: [{ name: 'crowd.dev', url: 'https://crowd.dev', description: 'Here' }], - joinedAt: '2020-05-28T15:13:30Z', - location: 'Istanbul', - } - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const organization = (await OrganizationRepository.findAndCountAll({}, mockIServiceOptions)) - .rows[0] - - const foundMember = await MemberRepository.findById(memberCreated.id, mockIServiceOptions) - - const o1 = foundMember.organizations[0].get({ plain: true }) - delete o1.createdAt - delete o1.updatedAt - - expect(o1).toStrictEqual({ - id: organization.id, - displayName: 'crowd.dev', - github: null, - location: null, - website: null, - description: - 'Understand, grow, and engage your developer community with zero hassle. With crowd.dev, you can build developer communities that drive your business forward.', - emails: ['hello@crowd.dev', 'jonathan@crowd.dev', 'careers@crowd.dev'], - phoneNumbers: ['+42 424242'], - logo: 'https://logo.clearbit.com/crowd.dev', - memberOrganizations: { - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - tags: [], - twitter: { - id: '1362101830923259908', - bio: 'Community-led Growth for Developer-first Companies.\nJoin our private beta. 👇', - site: 'https://t.co/GRLDhqFWk4', - avatar: 'https://pbs.twimg.com/profile_images/1419741008716251141/6exZe94-_normal.jpg', - handle: 'CrowdDotDev', - location: '🌍 remote', - followers: 107, - following: 0, - }, - linkedin: { - handle: 'company/crowddevhq', - }, - crunchbase: { - handle: null, - }, - employees: 5, - revenueRange: { - max: 1, - min: 0, - }, - importHash: null, - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - isTeamOrganization: false, - type: null, - ticker: null, - size: null, - naics: null, - lastEnrichedAt: null, - industry: null, - headline: null, - geoLocation: null, - founded: null, - employeeCountByCountry: null, - address: null, - profiles: null, - attributes: {}, - manuallyCreated: false, - affiliatedProfiles: null, - allSubsidiaries: null, - alternativeDomains: null, - alternativeNames: null, - averageEmployeeTenure: null, - averageTenureByLevel: null, - averageTenureByRole: null, - directSubsidiaries: null, - employeeChurnRate: null, - employeeCountByMonth: null, - employeeGrowthRate: null, - employeeCountByMonthByLevel: null, - employeeCountByMonthByRole: null, - gicsSector: null, - grossAdditionsByMonth: null, - grossDeparturesByMonth: null, - ultimateParent: null, - immediateParent: null, - }) - }) - - it('Should update existent member succesfully - simple', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const mas = new MemberAttributeSettingsService(mockIServiceOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const member1 = { - username: 'anil', - emails: ['lala@l.com'], - platform: PlatformType.GITHUB, - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - joinedAt: '2020-05-28T15:13:30Z', - } - - const member1Username = member1.username - const attributes = member1.attributes - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const member2 = { - username: 'anil', - emails: ['test@gmail.com', 'test2@yahoo.com'], - platform: PlatformType.GITHUB, - location: 'Ankara', - } - - const memberUpdated = await new MemberService(mockIServiceOptions).upsert(member2) - - memberUpdated.createdAt = memberUpdated.createdAt.toISOString().split('T')[0] - memberUpdated.updatedAt = memberUpdated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - username: { - [PlatformType.GITHUB]: [member1Username], - }, - displayName: member1Username, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - default: attributes[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.URL][PlatformType.GITHUB], - default: attributes[MemberAttributeName.URL][PlatformType.GITHUB], - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - default: attributes[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.BIO][PlatformType.GITHUB], - default: attributes[MemberAttributeName.BIO][PlatformType.GITHUB], - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.LOCATION][PlatformType.GITHUB], - default: attributes[MemberAttributeName.LOCATION][PlatformType.GITHUB], - }, - }, - lastEnriched: null, - organizations: [ - { - displayName: 'l.com', - id: memberCreated.organizations[0].id, - memberOrganizations: { - memberId: memberCreated.id, - organizationId: memberCreated.organizations[0].id, - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - }, - ], - enrichedBy: [], - contributions: null, - emails: ['lala@l.com', 'test@gmail.com', 'test2@yahoo.com'], - score: member1.score, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - joinedAt: new Date('2020-05-28T15:13:30Z'), - reach: { total: -1 }, - affiliations: [], - manuallyCreated: false, - } - - expect(memberUpdated).toStrictEqual(memberExpected) - }) - - it('Should update existent member successfully - attributes with matching platform', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const mas = new MemberAttributeSettingsService(mockIServiceOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await mas.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - const member1 = { - username: 'anil', - emails: ['lala@l.com'], - platform: PlatformType.GITHUB, - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - }, - }, - joinedAt: '2020-05-28T15:13:30Z', - } - - const member1Username = member1.username - const attributes1 = member1.attributes - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const member2 = { - username: 'anil', - platform: PlatformType.GITHUB, - attributes: { - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - [MemberAttributeName.URL]: { - [PlatformType.TWITTER]: 'https://twitter-url', - }, - }, - } - - const attributes2 = member2.attributes - - const memberUpdated = await new MemberService(mockIServiceOptions).upsert(member2) - - memberUpdated.createdAt = memberUpdated.createdAt.toISOString().split('T')[0] - memberUpdated.updatedAt = memberUpdated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - username: { - [PlatformType.GITHUB]: [member1Username], - }, - displayName: member1Username, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: - attributes1[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - default: attributes1[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: attributes1[MemberAttributeName.URL][PlatformType.GITHUB], - [PlatformType.TWITTER]: attributes2[MemberAttributeName.URL][PlatformType.TWITTER], - default: attributes2[MemberAttributeName.URL][PlatformType.TWITTER], - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: - attributes2[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - default: attributes2[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: attributes2[MemberAttributeName.BIO][PlatformType.GITHUB], - default: attributes2[MemberAttributeName.BIO][PlatformType.GITHUB], - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: attributes2[MemberAttributeName.LOCATION][PlatformType.GITHUB], - default: attributes2[MemberAttributeName.LOCATION][PlatformType.GITHUB], - }, - }, - lastEnriched: null, - organizations: [ - { - displayName: 'l.com', - id: memberCreated.organizations[0].id, - memberOrganizations: { - memberId: memberCreated.id, - organizationId: memberCreated.organizations[0].id, - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - }, - ], - enrichedBy: [], - contributions: null, - emails: member1.emails, - score: member1.score, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - joinedAt: new Date('2020-05-28T15:13:30Z'), - reach: { total: -1 }, - affiliations: [], - manuallyCreated: false, - } - - expect(memberUpdated).toStrictEqual(memberExpected) - }) - - it('Should update existent member succesfully - object type username', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const mas = new MemberAttributeSettingsService(mockIServiceOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const member1 = { - username: 'anil', - emails: ['lala@l.com'], - platform: PlatformType.GITHUB, - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - joinedAt: '2020-05-28T15:13:30Z', - } - - const member1Username = member1.username - const attributes = member1.attributes - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const member2 = { - username: { - [PlatformType.GITHUB]: 'anil', - [PlatformType.TWITTER]: 'anil_twitter', - [PlatformType.DISCORD]: 'anil_discord', - }, - platform: PlatformType.GITHUB, - } - - const memberUpdated = await new MemberService(mockIServiceOptions).upsert(member2) - - memberUpdated.createdAt = memberUpdated.createdAt.toISOString().split('T')[0] - memberUpdated.updatedAt = memberUpdated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - username: { - [PlatformType.GITHUB]: ['anil'], - [PlatformType.TWITTER]: ['anil_twitter'], - [PlatformType.DISCORD]: ['anil_discord'], - }, - displayName: 'anil', - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - default: attributes[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.URL][PlatformType.GITHUB], - default: attributes[MemberAttributeName.URL][PlatformType.GITHUB], - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - default: attributes[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.BIO][PlatformType.GITHUB], - default: attributes[MemberAttributeName.BIO][PlatformType.GITHUB], - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.LOCATION][PlatformType.GITHUB], - default: attributes[MemberAttributeName.LOCATION][PlatformType.GITHUB], - }, - }, - emails: member1.emails, - lastEnriched: null, - organizations: [ - { - displayName: 'l.com', - id: memberCreated.organizations[0].id, - memberOrganizations: { - memberId: memberCreated.id, - organizationId: memberCreated.organizations[0].id, - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - }, - ], - enrichedBy: [], - contributions: null, - score: member1.score, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - joinedAt: new Date('2020-05-28T15:13:30Z'), - reach: { total: -1 }, - affiliations: [], - manuallyCreated: false, - } - - expect(memberUpdated).toStrictEqual(memberExpected) - }) - - it('Should throw 400 error when given platform does not match with username object ', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - const mas = new MemberAttributeSettingsService(mockIServiceOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const member1 = { - username: 'anil', - emails: ['lala@l.com'], - platform: PlatformType.GITHUB, - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - joinedAt: '2020-05-28T15:13:30Z', - } - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const member2 = { - username: { - [PlatformType.GITHUB]: 'anil', - [PlatformType.TWITTER]: 'anil_twitter', - [PlatformType.DISCORD]: 'anil_discord', - }, - platform: PlatformType.SLACK, - } - - await expect(() => - new MemberService(mockIServiceOptions).upsert(member2), - ).rejects.toThrowError(new Error400()) - }) - - it('Should update existent member succesfully - JSON fields', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - const mas = new MemberAttributeSettingsService(mockIServiceOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await mas.createPredefined(DEVTO_MEMBER_ATTRIBUTES) - - const member1 = { - username: 'anil', - platform: PlatformType.TWITTER, - emails: ['lala@l.com'], - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - joinedAt: '2020-05-28T15:13:30Z', - } - - const member1Username = member1.username - const attributes1 = member1.attributes - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const member2 = { - username: 'anil', - platform: PlatformType.TWITTER, - joinedAt: '2020-05-28T15:13:30Z', - location: 'Ankara', - attributes: { - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.DEVTO]: '#someDevtoId', - [PlatformType.SLACK]: '#someSlackId', - }, - [MemberAttributeName.NAME]: { - [PlatformType.DEVTO]: 'Michael Scott', - }, - [MemberAttributeName.URL]: { - [PlatformType.DEVTO]: 'https://some-devto-url', - }, - }, - } - - const attributes2 = member2.attributes - - const memberUpdated = await new MemberService(mockIServiceOptions).upsert(member2) - - memberUpdated.createdAt = memberUpdated.createdAt.toISOString().split('T')[0] - memberUpdated.updatedAt = memberUpdated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - joinedAt: new Date('2020-05-28T15:13:30Z'), - username: { - [PlatformType.TWITTER]: [member1Username], - }, - displayName: member1Username, - attributes: { - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.DEVTO]: attributes2[MemberAttributeName.SOURCE_ID][PlatformType.DEVTO], - [PlatformType.SLACK]: attributes2[MemberAttributeName.SOURCE_ID][PlatformType.SLACK], - default: attributes2[MemberAttributeName.SOURCE_ID][PlatformType.DEVTO], - }, - [MemberAttributeName.NAME]: { - [PlatformType.DEVTO]: attributes2[MemberAttributeName.NAME][PlatformType.DEVTO], - default: attributes2[MemberAttributeName.NAME][PlatformType.DEVTO], - }, - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: - attributes1[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - default: attributes1[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: attributes1[MemberAttributeName.URL][PlatformType.GITHUB], - [PlatformType.DEVTO]: attributes2[MemberAttributeName.URL][PlatformType.DEVTO], - default: attributes1[MemberAttributeName.URL][PlatformType.GITHUB], - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: - attributes1[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - default: attributes1[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: attributes1[MemberAttributeName.BIO][PlatformType.GITHUB], - default: attributes1[MemberAttributeName.BIO][PlatformType.GITHUB], - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: attributes1[MemberAttributeName.LOCATION][PlatformType.GITHUB], - default: attributes1[MemberAttributeName.LOCATION][PlatformType.GITHUB], - }, - }, - emails: member1.emails, - lastEnriched: null, - organizations: [ - { - displayName: 'l.com', - id: memberCreated.organizations[0].id, - memberOrganizations: { - memberId: memberCreated.id, - organizationId: memberCreated.organizations[0].id, - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - }, - ], - enrichedBy: [], - contributions: null, - score: member1.score, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - reach: { total: -1 }, - affiliations: [], - manuallyCreated: false, - } - - expect(memberUpdated).toStrictEqual(memberExpected) - }) - - it('Should update existent member succesfully - reach from default to complete - sending number', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: 'anil', - platform: PlatformType.GITHUB, - joinedAt: '2020-05-28T15:13:30Z', - } - - const member1Username = member1.username - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const member2 = { - username: 'anil', - platform: PlatformType.GITHUB, - reach: 10, - } - - const memberUpdated = await new MemberService(mockIServiceOptions).upsert(member2) - - memberUpdated.createdAt = memberUpdated.createdAt.toISOString().split('T')[0] - memberUpdated.updatedAt = memberUpdated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - joinedAt: new Date('2020-05-28T15:13:30Z'), - username: { - [PlatformType.GITHUB]: [member1Username], - }, - displayName: member1Username, - lastEnriched: null, - organizations: [], - enrichedBy: [], - contributions: null, - reach: { total: 10, [PlatformType.GITHUB]: 10 }, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - score: -1, - emails: [], - attributes: {}, - affiliations: [], - manuallyCreated: false, - } - - expect(memberUpdated).toStrictEqual(memberExpected) - }) - - it('Should update existent member succesfully - reach from default to complete - sending platform', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: 'anil', - type: 'member', - platform: PlatformType.GITHUB, - joinedAt: '2020-05-28T15:13:30Z', - } - - const member1Username = member1.username - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const member2 = { - username: 'anil', - platform: PlatformType.GITHUB, - reach: { [PlatformType.GITHUB]: 10 }, - } - - const memberUpdated = await new MemberService(mockIServiceOptions).upsert(member2) - - memberUpdated.createdAt = memberUpdated.createdAt.toISOString().split('T')[0] - memberUpdated.updatedAt = memberUpdated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - joinedAt: new Date('2020-05-28T15:13:30Z'), - username: { - [PlatformType.GITHUB]: [member1Username], - }, - lastEnriched: null, - organizations: [], - enrichedBy: [], - contributions: null, - displayName: member1Username, - reach: { total: 10, [PlatformType.GITHUB]: 10 }, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - score: -1, - emails: [], - attributes: {}, - affiliations: [], - manuallyCreated: false, - } - - expect(memberUpdated).toStrictEqual(memberExpected) - }) - - it('Should update existent member succesfully - complex reach update from object', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: 'anil', - type: 'member', - platform: PlatformType.GITHUB, - joinedAt: '2020-05-28T15:13:30Z', - reach: { [PlatformType.TWITTER]: 10, linkedin: 10, total: 20 }, - } - - const member1Username = member1.username - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const member2 = { - username: 'anil', - platform: PlatformType.GITHUB, - reach: { [PlatformType.GITHUB]: 15, linkedin: 11 }, - } - - const memberUpdated = await new MemberService(mockIServiceOptions).upsert(member2) - - memberUpdated.createdAt = memberUpdated.createdAt.toISOString().split('T')[0] - memberUpdated.updatedAt = memberUpdated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - joinedAt: new Date('2020-05-28T15:13:30Z'), - username: { - [PlatformType.GITHUB]: [member1Username], - }, - lastEnriched: null, - organizations: [], - enrichedBy: [], - contributions: null, - displayName: member1Username, - reach: { total: 36, [PlatformType.GITHUB]: 15, linkedin: 11, [PlatformType.TWITTER]: 10 }, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - score: -1, - emails: [], - attributes: {}, - affiliations: [], - manuallyCreated: false, - } - - expect(memberUpdated).toStrictEqual(memberExpected) - }) - - it('Should update existent member succesfully - complex reach update from number', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: 'anil', - type: 'member', - platform: PlatformType.GITHUB, - joinedAt: '2020-05-28T15:13:30Z', - reach: { [PlatformType.TWITTER]: 10, linkedin: 10, total: 20 }, - } - - const member1Username = member1.username - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const member2 = { - username: 'anil', - platform: PlatformType.GITHUB, - reach: 30, - } - - const memberUpdated = await new MemberService(mockIServiceOptions).upsert(member2) - - memberUpdated.createdAt = memberUpdated.createdAt.toISOString().split('T')[0] - memberUpdated.updatedAt = memberUpdated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - joinedAt: new Date('2020-05-28T15:13:30Z'), - username: { - [PlatformType.GITHUB]: [member1Username], - }, - displayName: member1Username, - lastEnriched: null, - organizations: [], - enrichedBy: [], - contributions: null, - reach: { total: 50, [PlatformType.GITHUB]: 30, linkedin: 10, [PlatformType.TWITTER]: 10 }, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - score: -1, - emails: [], - attributes: {}, - affiliations: [], - manuallyCreated: false, - } - - expect(memberUpdated).toStrictEqual(memberExpected) - }) - }) - - describe('update method', () => { - it('Should update existent member succesfully - removing identities with simple string format', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: 'anil', - type: 'member', - platform: PlatformType.GITHUB, - joinedAt: '2020-05-28T15:13:30Z', - } - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - const toUpdate = { - username: 'anil_new', - platform: PlatformType.GITHUB, - } - - const memberUpdated = await new MemberService(mockIServiceOptions).update( - memberCreated.id, - toUpdate, - ) - - expect(memberUpdated.username[PlatformType.GITHUB]).toStrictEqual(['anil_new']) - }) - - it('Should update existent member succesfully - removing identities with simple identity format', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: { - [PlatformType.GITHUB]: { - username: 'anil', - }, - }, - platform: PlatformType.GITHUB, - type: 'member', - joinedAt: '2020-05-28T15:13:30Z', - } - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - const toUpdate = { - username: { - [PlatformType.GITHUB]: { - username: 'anil_new', - }, - }, - platform: PlatformType.GITHUB, - } - - const memberUpdated = await new MemberService(mockIServiceOptions).update( - memberCreated.id, - toUpdate, - ) - - expect(memberUpdated.username[PlatformType.GITHUB]).toStrictEqual(['anil_new']) - }) - - it('Should update existent member succesfully - removing identities with array identity format', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: { - [PlatformType.GITHUB]: [ - { - username: 'anil', - }, - ], - }, - platform: PlatformType.GITHUB, - type: 'member', - joinedAt: '2020-05-28T15:13:30Z', - } - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - const toUpdate = { - username: { - [PlatformType.GITHUB]: [ - { - username: 'anil_new', - }, - { - username: 'anil_new2', - }, - ], - }, - platform: PlatformType.GITHUB, - } - - const memberUpdated = await new MemberService(mockIServiceOptions).update( - memberCreated.id, - toUpdate, - ) - - expect(memberUpdated.username[PlatformType.GITHUB]).toStrictEqual(['anil_new', 'anil_new2']) - }) - }) - - describe('merge method', () => { - it('Should merge', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mas = new MemberAttributeSettingsService(mockIRepositoryOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await mas.createPredefined(DISCORD_MEMBER_ATTRIBUTES) - await mas.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - await mas.createPredefined(SLACK_MEMBER_ATTRIBUTES) - - const memberService = new MemberService(mockIRepositoryOptions) - - let t1 = await TagRepository.create({ name: 'tag1' }, mockIRepositoryOptions) - let t2 = await TagRepository.create({ name: 'tag2' }, mockIRepositoryOptions) - let t3 = await TagRepository.create({ name: 'tag3' }, mockIRepositoryOptions) - - let o1 = await OrganizationRepository.create({ displayName: 'org1' }, mockIRepositoryOptions) - let o2 = await OrganizationRepository.create({ displayName: 'org2' }, mockIRepositoryOptions) - let o3 = await OrganizationRepository.create({ displayName: 'org3' }, mockIRepositoryOptions) - - let task1 = await TaskRepository.create({ name: 'task1' }, mockIRepositoryOptions) - let task2 = await TaskRepository.create({ name: 'task2' }, mockIRepositoryOptions) - let task3 = await TaskRepository.create({ name: 'task3' }, mockIRepositoryOptions) - - let note1 = await NoteRepository.create({ body: 'note1' }, mockIRepositoryOptions) - let note2 = await NoteRepository.create({ body: 'note2' }, mockIRepositoryOptions) - let note3 = await NoteRepository.create({ body: 'note3' }, mockIRepositoryOptions) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - }, - displayName: 'Anil', - emails: ['anil+1@crowd.dev', 'anil+2@crowd.dev'], - joinedAt: '2021-05-27T15:14:30Z', - attributes: {}, - tags: [t1.id, t2.id], - organizations: [o1.id, o2.id], - tasks: [task1.id, task2.id], - notes: [note1.id, note2.id], - } - - const member2 = { - username: { - [PlatformType.DISCORD]: 'anil', - }, - emails: ['anil+1@crowd.dev', 'anil+3@crowd.dev'], - displayName: 'Anil', - joinedAt: '2021-05-30T15:14:30Z', - attributes: { - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Crowd.dev', - default: 'Crowd.dev', - }, - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.DISCORD]: '#discordId', - default: '#discordId', - }, - }, - tags: [t2.id, t3.id], - organizations: [o2.id, o3.id], - tasks: [task2.id, task3.id], - notes: [note2.id, note3.id], - } - - const member3 = { - username: { - [PlatformType.TWITTER]: 'anil', - }, - displayName: 'Anil', - joinedAt: '2021-05-30T15:14:30Z', - attributes: { - [MemberAttributeName.URL]: { - [PlatformType.TWITTER]: 'https://a-twitter-url', - default: 'https://a-twitter-url', - }, - }, - } - const member4 = { - username: { - [PlatformType.SLACK]: 'testt', - }, - displayName: 'Member 4', - joinedAt: '2021-05-30T15:14:30Z', - attributes: { - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.SLACK]: '#slackId', - default: '#slackId', - }, - }, - } - - const returnedMember1 = await MemberRepository.create(member1, mockIRepositoryOptions) - const returnedMember2 = await MemberRepository.create(member2, mockIRepositoryOptions) - const returnedMember3 = await MemberRepository.create(member3, mockIRepositoryOptions) - const returnedMember4 = await MemberRepository.create(member4, mockIRepositoryOptions) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - body: 'Here', - }, - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - isContribution: true, - username: 'anil', - member: returnedMember2.id, - score: 1, - sourceId: '#sourceId1', - } - - let activityCreated = await ActivityRepository.create(activity, mockIRepositoryOptions) - - // toMerge[1] = [(1,2),(1,4)] toMerge[2] = [(2,1)] toMerge[4] = [(4,1)] - // noMerge[2] = [3] - await MemberRepository.addToMerge( - [{ members: [returnedMember1.id, returnedMember2.id], similarity: null }], - mockIRepositoryOptions, - ) - await MemberRepository.addToMerge( - [{ members: [returnedMember1.id, returnedMember4.id], similarity: null }], - mockIRepositoryOptions, - ) - await MemberRepository.addToMerge( - [{ members: [returnedMember2.id, returnedMember1.id], similarity: null }], - mockIRepositoryOptions, - ) - await MemberRepository.addToMerge( - [{ members: [returnedMember4.id, returnedMember1.id], similarity: null }], - mockIRepositoryOptions, - ) - - await MemberRepository.addNoMerge( - returnedMember2.id, - returnedMember3.id, - mockIRepositoryOptions, - ) - - const response = await memberService.merge(returnedMember1.id, returnedMember2.id) - - const mergedMember = await MemberRepository.findById( - response.mergedId, - mockIRepositoryOptions, - ) - - // Sequelize returns associations as array of models, we need to get plain objects - mergedMember.activities = mergedMember.activities.map((i) => i.get({ plain: true })) - mergedMember.tags = mergedMember.tags.map((i) => i.get({ plain: true })) - mergedMember.organizations = mergedMember.organizations.map((i) => - SequelizeTestUtils.objectWithoutKey(i.get({ plain: true }), ['memberOrganizations']), - ) - mergedMember.tasks = mergedMember.tasks.map((i) => i.get({ plain: true })) - mergedMember.notes = mergedMember.notes.map((i) => i.get({ plain: true })) - - // get the created activity again, it's member should be updated after merge - activityCreated = await ActivityRepository.findById( - activityCreated.id, - mockIRepositoryOptions, - ) - - // we don't need activity.member because we're already expecting member->activities - activityCreated = SequelizeTestUtils.objectWithoutKey(activityCreated, [ - 'member', - 'objectMember', - 'parent', - 'tasks', - 'display', - 'organization', - ]) - - // get previously created tags - t1 = await TagRepository.findById(t1.id, mockIRepositoryOptions) - t2 = await TagRepository.findById(t2.id, mockIRepositoryOptions) - t3 = await TagRepository.findById(t3.id, mockIRepositoryOptions) - - // get previously created organizations - o1 = await OrganizationRepository.findById(o1.id, mockIRepositoryOptions) - o2 = await OrganizationRepository.findById(o2.id, mockIRepositoryOptions) - o3 = await OrganizationRepository.findById(o3.id, mockIRepositoryOptions) - - // get previously created tasks - task1 = await TaskRepository.findById(task1.id, mockIRepositoryOptions) - task2 = await TaskRepository.findById(task2.id, mockIRepositoryOptions) - task3 = await TaskRepository.findById(task3.id, mockIRepositoryOptions) - - // get previously created notes - note1 = await NoteRepository.findById(note1.id, mockIRepositoryOptions) - note2 = await NoteRepository.findById(note2.id, mockIRepositoryOptions) - note3 = await NoteRepository.findById(note3.id, mockIRepositoryOptions) - - // remove tags->member relations as well (we should be only checking 1-deep relations) - t1 = SequelizeTestUtils.objectWithoutKey(t1, 'members') - t2 = SequelizeTestUtils.objectWithoutKey(t2, 'members') - t3 = SequelizeTestUtils.objectWithoutKey(t3, 'members') - - // remove organizations->member relations as well (we should be only checking 1-deep relations) - o1 = SequelizeTestUtils.objectWithoutKey(o1, [ - 'memberCount', - 'joinedAt', - 'activityCount', - 'memberOrganizations', - ]) - o2 = SequelizeTestUtils.objectWithoutKey(o2, [ - 'memberCount', - 'joinedAt', - 'activityCount', - 'memberOrganizations', - ]) - o3 = SequelizeTestUtils.objectWithoutKey(o3, [ - 'memberCount', - 'joinedAt', - 'activityCount', - 'memberOrganizations', - ]) - - // remove tasks->member and tasks->activity tasks->assignees relations as well (we should be only checking 1-deep relations) - task1 = SequelizeTestUtils.objectWithoutKey(task1, ['members', 'activities', 'assignees']) - task2 = SequelizeTestUtils.objectWithoutKey(task2, ['members', 'activities', 'assignees']) - task3 = SequelizeTestUtils.objectWithoutKey(task3, ['members', 'activities', 'assignees']) - - // remove notes->member relations as well (we should be only checking 1-deep relations) - note1 = SequelizeTestUtils.objectWithoutKey(note1, ['members', 'createdBy']) - note2 = SequelizeTestUtils.objectWithoutKey(note2, ['members', 'createdBy']) - note3 = SequelizeTestUtils.objectWithoutKey(note3, ['members', 'createdBy']) - - mergedMember.updatedAt = mergedMember.updatedAt.toISOString().split('T')[0] - - const expectedMember = { - id: returnedMember1.id, - username: { - [PlatformType.GITHUB]: ['anil'], - [PlatformType.DISCORD]: ['anil'], - }, - lastEnriched: null, - enrichedBy: [], - contributions: null, - displayName: 'Anil', - identities: [PlatformType.GITHUB, PlatformType.DISCORD], - activities: [activityCreated], - attributes: { - ...member1.attributes, - ...member2.attributes, - }, - activeOn: [activityCreated.platform], - activityTypes: [`${activityCreated.platform}:${activityCreated.type}`], - emails: ['anil+1@crowd.dev', 'anil+2@crowd.dev', 'anil+3@crowd.dev'], - score: -1, - importHash: null, - createdAt: returnedMember1.createdAt, - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - joinedAt: new Date(member1.joinedAt), - reach: { total: -1 }, - tags: [t1, t2, t3], - tasks: [task1, task2, task3], - notes: [note1, note2, note3], - organizations: [ - SequelizeTestUtils.objectWithoutKey(o1, [ - 'activeOn', - 'identities', - 'lastActive', - 'segments', - 'weakIdentities', - ]), - SequelizeTestUtils.objectWithoutKey(o2, [ - 'activeOn', - 'identities', - 'lastActive', - 'segments', - 'weakIdentities', - ]), - SequelizeTestUtils.objectWithoutKey(o3, [ - 'activeOn', - 'identities', - 'lastActive', - 'segments', - 'weakIdentities', - ]), - ], - noMerge: [returnedMember3.id], - toMerge: [returnedMember4.id], - activityCount: 1, - activeDaysCount: 1, - averageSentiment: activityCreated.sentiment.sentiment, - lastActive: activityCreated.timestamp, - lastActivity: activityCreated, - numberOfOpenSourceContributions: 0, - affiliations: [], - manuallyCreated: false, - } - - expect( - mergedMember.tasks.sort((a, b) => { - const nameA = a.name.toLowerCase() - const nameB = b.name.toLowerCase() - if (nameA < nameB) { - return -1 - } - if (nameA > nameB) { - return 1 - } - return 0 - }), - ).toEqual( - expectedMember.tasks.sort((a, b) => { - const nameA = a.name.toLowerCase() - const nameB = b.name.toLowerCase() - if (nameA < nameB) { - return -1 - } - if (nameA > nameB) { - return 1 - } - return 0 - }), - ) - - expect( - mergedMember.organizations.sort((a, b) => { - const nameA = a.displayName.toLowerCase() - const nameB = b.displayName.toLowerCase() - if (nameA < nameB) { - return -1 - } - if (nameA > nameB) { - return 1 - } - return 0 - }), - ).toEqual( - expectedMember.organizations.sort((a, b) => { - const nameA = a.displayName.toLowerCase() - const nameB = b.displayName.toLowerCase() - if (nameA < nameB) { - return -1 - } - if (nameA > nameB) { - return 1 - } - return 0 - }), - ) - delete mergedMember.tasks - delete expectedMember.tasks - delete mergedMember.organizations - delete expectedMember.organizations - - expect(mergedMember).toStrictEqual(expectedMember) - }) - - it('Should catch when two members are the same', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mas = new MemberAttributeSettingsService(mockIRepositoryOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const memberService = new MemberService(mockIRepositoryOptions) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - }, - displayName: 'Anil', - joinedAt: '2021-05-27T15:14:30Z', - attributes: {}, - } - - const memberCreated = await MemberRepository.create(member1, mockIRepositoryOptions) - const mergeOutput = await memberService.merge(memberCreated.id, memberCreated.id) - - expect(mergeOutput).toStrictEqual({ status: 203, mergedId: memberCreated.id }) - - const found = await memberService.findById(memberCreated.id) - expect(found).toStrictEqual(memberCreated) - }) - }) - - describe('addToNoMerge method', () => { - it('Should add two members to their respective noMerges, these members should be excluded from toMerges respectively', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mas = new MemberAttributeSettingsService(mockIRepositoryOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await mas.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - await mas.createPredefined(DISCORD_MEMBER_ATTRIBUTES) - - const memberService = new MemberService(mockIRepositoryOptions) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - }, - displayName: 'Anil', - joinedAt: '2021-05-27T15:14:30Z', - attributes: {}, - } - - const member2 = { - username: { - [PlatformType.DISCORD]: 'anil', - }, - displayName: 'Anil', - joinedAt: '2021-05-30T15:14:30Z', - attributes: { - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.DISCORD]: '#discordId', - default: '#discordId', - }, - }, - } - - const member3 = { - username: { - [PlatformType.TWITTER]: 'anil', - }, - displayName: 'Anil', - joinedAt: '2021-05-30T15:14:30Z', - attributes: { - [MemberAttributeName.URL]: { - [PlatformType.TWITTER]: 'https://a-twitter-url', - default: 'https://a-twitter-url', - }, - }, - } - - let returnedMember1 = await MemberRepository.create(member1, mockIRepositoryOptions) - let returnedMember2 = await MemberRepository.create(member2, mockIRepositoryOptions) - let returnedMember3 = await MemberRepository.create(member3, mockIRepositoryOptions) - - // toMerge[1] = [(1,2),(1,3)] toMerge[2] = [(2,1),(2,3)] toMerge[3] = [(3,1),(3,2)] - await MemberRepository.addToMerge( - [{ members: [returnedMember1.id, returnedMember2.id], similarity: null }], - mockIRepositoryOptions, - ) - await MemberRepository.addToMerge( - [{ members: [returnedMember2.id, returnedMember1.id], similarity: null }], - mockIRepositoryOptions, - ) - - await MemberRepository.addToMerge( - [{ members: [returnedMember1.id, returnedMember3.id], similarity: null }], - mockIRepositoryOptions, - ) - await MemberRepository.addToMerge( - [{ members: [returnedMember3.id, returnedMember1.id], similarity: null }], - mockIRepositoryOptions, - ) - await MemberRepository.addToMerge( - [{ members: [returnedMember2.id, returnedMember3.id], similarity: null }], - mockIRepositoryOptions, - ) - await MemberRepository.addToMerge( - [{ members: [returnedMember3.id, returnedMember2.id], similarity: null }], - mockIRepositoryOptions, - ) - - await memberService.addToNoMerge(returnedMember1.id, returnedMember2.id) - - returnedMember1 = await MemberRepository.findById(returnedMember1.id, mockIRepositoryOptions) - - expect(returnedMember1.toMerge).toStrictEqual([returnedMember3.id]) - expect(returnedMember1.noMerge).toStrictEqual([returnedMember2.id]) - - returnedMember2 = await MemberRepository.findById(returnedMember2.id, mockIRepositoryOptions) - - expect(returnedMember2.toMerge).toStrictEqual([returnedMember3.id]) - expect(returnedMember2.noMerge).toStrictEqual([returnedMember1.id]) - - // call addToNoMerge once more, between member1 and member3 - await memberService.addToNoMerge(returnedMember1.id, returnedMember3.id) - - returnedMember1 = await MemberRepository.findById(returnedMember1.id, mockIRepositoryOptions) - - expect(returnedMember1.toMerge).toStrictEqual([]) - expect(returnedMember1.noMerge).toStrictEqual([returnedMember2.id, returnedMember3.id]) - - returnedMember3 = await MemberRepository.findById(returnedMember3.id, mockIRepositoryOptions) - - expect(returnedMember3.toMerge).toStrictEqual([returnedMember2.id]) - expect(returnedMember3.noMerge).toStrictEqual([returnedMember1.id]) - - // only toMerge relation (2,3) left. Testing addToNoMerge(2,3) - await memberService.addToNoMerge(returnedMember3.id, returnedMember2.id) - - returnedMember2 = await MemberRepository.findById(returnedMember2.id, mockIRepositoryOptions) - - expect(returnedMember2.toMerge).toStrictEqual([]) - expect(returnedMember2.noMerge).toStrictEqual([returnedMember1.id, returnedMember3.id]) - - returnedMember3 = await MemberRepository.findById(returnedMember3.id, mockIRepositoryOptions) - - expect(returnedMember3.toMerge).toStrictEqual([]) - expect(returnedMember3.noMerge).toStrictEqual([returnedMember1.id, returnedMember2.id]) - }) - - it('Should throw 404 not found when trying to add non existent members to noMerge', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mas = new MemberAttributeSettingsService(mockIRepositoryOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const memberService = new MemberService(mockIRepositoryOptions) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - }, - displayName: 'Anil', - joinedAt: '2021-05-27T15:14:30Z', - attributes: {}, - } - - const returnedMember1 = await MemberRepository.create(member1, mockIRepositoryOptions) - - const { randomUUID } = require('crypto') - - await expect(() => - memberService.addToNoMerge(returnedMember1.id, randomUUID()), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('memberExists method', () => { - it('Should find existing member with string username and default platform', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mas = new MemberAttributeSettingsService(mockIRepositoryOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const memberService = new MemberService(mockIRepositoryOptions) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - }, - displayName: 'Anil', - joinedAt: '2021-05-27T15:14:30Z', - attributes: {}, - } - - const cloned = lodash.cloneDeep(member1) - const returnedMember1 = await MemberRepository.create(cloned, mockIRepositoryOptions) - delete returnedMember1.toMerge - delete returnedMember1.noMerge - delete returnedMember1.tags - delete returnedMember1.activities - delete returnedMember1.tasks - delete returnedMember1.notes - delete returnedMember1.activityCount - delete returnedMember1.averageSentiment - delete returnedMember1.lastActive - delete returnedMember1.lastActivity - delete returnedMember1.activeOn - delete returnedMember1.identities - delete returnedMember1.activityTypes - delete returnedMember1.activeDaysCount - delete returnedMember1.numberOfOpenSourceContributions - delete returnedMember1.affiliations - delete returnedMember1.manuallyCreated - - returnedMember1.segments = returnedMember1.segments.map((s) => s.id) - - const existing = await memberService.memberExists( - member1.username[PlatformType.GITHUB], - PlatformType.GITHUB, - ) - - expect(existing).toStrictEqual(returnedMember1) - }) - - it('Should return null if member is not found - string type', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mas = new MemberAttributeSettingsService(mockIRepositoryOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const memberService = new MemberService(mockIRepositoryOptions) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - }, - displayName: 'Anil', - joinedAt: '2021-05-27T15:14:30Z', - attributes: {}, - } - - await MemberRepository.create(member1, mockIRepositoryOptions) - - const existing = await memberService.memberExists('some-random-username', PlatformType.GITHUB) - - expect(existing).toBeNull() - }) - - it('Should return null if member is not found - object type', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mas = new MemberAttributeSettingsService(mockIRepositoryOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const memberService = new MemberService(mockIRepositoryOptions) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - }, - displayName: 'Anil', - joinedAt: '2021-05-27T15:14:30Z', - attributes: {}, - } - - await MemberRepository.create(member1, mockIRepositoryOptions) - - const existing = await memberService.memberExists( - { - ...member1.username, - [PlatformType.SLACK]: 'some-slack-username', - }, - PlatformType.SLACK, - ) - - expect(existing).toBeNull() - }) - - it('Should find existing member with object username and given platform', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mas = new MemberAttributeSettingsService(mockIRepositoryOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const memberService = new MemberService(mockIRepositoryOptions) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - [PlatformType.DISCORD]: 'some-other-username', - }, - displayName: 'Anil', - joinedAt: '2021-05-27T15:14:30Z', - attributes: {}, - } - - const returnedMember1 = await MemberRepository.create(member1, mockIRepositoryOptions) - delete returnedMember1.toMerge - delete returnedMember1.noMerge - delete returnedMember1.tags - delete returnedMember1.activities - delete returnedMember1.tasks - delete returnedMember1.notes - delete returnedMember1.activityCount - delete returnedMember1.averageSentiment - delete returnedMember1.lastActive - delete returnedMember1.lastActivity - delete returnedMember1.activeOn - delete returnedMember1.identities - delete returnedMember1.activityTypes - delete returnedMember1.activeDaysCount - delete returnedMember1.numberOfOpenSourceContributions - delete returnedMember1.affiliations - delete returnedMember1.manuallyCreated - - returnedMember1.segments = returnedMember1.segments.map((s) => s.id) - - const existing = await memberService.memberExists( - { [PlatformType.DISCORD]: 'some-other-username' }, - PlatformType.DISCORD, - ) - - expect(returnedMember1).toStrictEqual(existing) - }) - - it('Should throw 400 error when username is type of object and username[platform] is not present ', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mas = new MemberAttributeSettingsService(mockIRepositoryOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const memberService = new MemberService(mockIRepositoryOptions) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - [PlatformType.DISCORD]: 'some-other-username', - }, - displayName: 'Anil', - joinedAt: '2021-05-27T15:14:30Z', - attributes: {}, - } - - await MemberRepository.create(member1, mockIRepositoryOptions) - - await expect(() => - memberService.memberExists( - { [PlatformType.DISCORD]: 'some-other-username' }, - PlatformType.SLACK, - ), - ).rejects.toThrowError(new Error400()) - }) - }) - - describe('Update Reach method', () => { - it('Should keep as total: -1 for an empty new reach and a default old reach', async () => { - const oldReach = { total: -1 } - const updatedReach = MemberService.calculateReach({}, oldReach) - expect(updatedReach).toStrictEqual({ - total: -1, - }) - }) - it('Should keep as total: -1 for a default new reach and a default old reach', async () => { - const oldReach = { total: -1 } - const updatedReach = MemberService.calculateReach({ total: -1 }, oldReach) - expect(updatedReach).toStrictEqual({ - total: -1, - }) - }) - it('Should update for a new reach and a default old reach', async () => { - const oldReach = { total: -1 } - const newReach = { [PlatformType.TWITTER]: 10 } - const updatedReach = MemberService.calculateReach(oldReach, newReach) - expect(updatedReach).toStrictEqual({ - total: 10, - [PlatformType.TWITTER]: 10, - }) - }) - it('Should update for a new reach and old reach in the same platform', async () => { - const oldReach = { [PlatformType.TWITTER]: 5, total: 5 } - const newReach = { [PlatformType.TWITTER]: 10 } - const updatedReach = MemberService.calculateReach(oldReach, newReach) - expect(updatedReach).toStrictEqual({ - total: 10, - [PlatformType.TWITTER]: 10, - }) - }) - it('Should update for a complex reach with different platforms', async () => { - const oldReach = { - [PlatformType.TWITTER]: 10, - [PlatformType.GITHUB]: 20, - [PlatformType.DISCORD]: 50, - total: 10 + 20 + 50, - } - const newReach = { - [PlatformType.TWITTER]: 20, - [PlatformType.GITHUB]: 2, - linkedin: 10, - total: 20 + 2 + 10, - } - const updatedReach = MemberService.calculateReach(oldReach, newReach) - expect(updatedReach).toStrictEqual({ - total: 10 + 20 + 2 + 50, - [PlatformType.TWITTER]: 20, - [PlatformType.GITHUB]: 2, - linkedin: 10, - [PlatformType.DISCORD]: 50, - }) - }) - it('Should work with reach 0', async () => { - const oldReach = { total: -1 } - const newReach = { [PlatformType.TWITTER]: 0 } - const updatedReach = MemberService.calculateReach(oldReach, newReach) - expect(updatedReach).toStrictEqual({ - total: 0, - [PlatformType.TWITTER]: 0, - }) - }) - }) - - describe('getHighestPriorityPlatformForAttributes method', () => { - it('Should return the highest priority platform from a priority array, handling the exceptions', async () => { - const priorityArray = [ - PlatformType.TWITTER, - PlatformType.CROWD, - PlatformType.SLACK, - PlatformType.DEVTO, - PlatformType.DISCORD, - PlatformType.GITHUB, - ] - - let inputPlatforms = [PlatformType.GITHUB, PlatformType.DEVTO] - let highestPriorityPlatform = MemberService.getHighestPriorityPlatformForAttributes( - inputPlatforms, - priorityArray, - ) - - expect(highestPriorityPlatform).toBe(PlatformType.DEVTO) - - inputPlatforms = [PlatformType.GITHUB, 'someOtherPlatform'] as any - highestPriorityPlatform = MemberService.getHighestPriorityPlatformForAttributes( - inputPlatforms, - priorityArray, - ) - - expect(highestPriorityPlatform).toBe(PlatformType.GITHUB) - - inputPlatforms = ['somePlatform1', 'somePlatform2'] as any - - // if no match in the priority array, it should return the first platform it finds - highestPriorityPlatform = MemberService.getHighestPriorityPlatformForAttributes( - inputPlatforms, - priorityArray, - ) - - expect(highestPriorityPlatform).toBe('somePlatform1') - - inputPlatforms = [] - - // if no platforms are sent to choose from, it should return undefined - highestPriorityPlatform = MemberService.getHighestPriorityPlatformForAttributes( - inputPlatforms, - priorityArray, - ) - expect(highestPriorityPlatform).not.toBeDefined() - }) - }) - - describe('validateAttributes method', () => { - it('Should validate attributes object succesfully', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const memberService = new MemberService(mockIServiceOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService(mockIServiceOptions) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(DEVTO_MEMBER_ATTRIBUTES) - - const attributes = { - [MemberAttributeName.NAME]: { - [PlatformType.DEVTO]: 'Dweet Srute', - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://some-github-url', - [PlatformType.TWITTER]: 'https://some-twitter-url', - [PlatformType.DEVTO]: 'https://some-devto-url', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Berlin', - [PlatformType.DEVTO]: 'Istanbul', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Assistant to the Regional Manager', - [PlatformType.DEVTO]: 'Assistant Regional Manager', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://some-image-url', - }, - } - - const validateAttributes = await memberService.validateAttributes(attributes) - - expect(validateAttributes).toEqual(attributes) - }) - - it(`Should accept custom attributes without 'custom' platform key`, async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const memberService = new MemberService(mockIServiceOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService(mockIServiceOptions) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const attributes = { - [MemberAttributeName.BIO]: 'Assistant to the Regional Manager', - } - - const validateAttributes = await memberService.validateAttributes(attributes) - - const expectedValidatedAttributes = { - [MemberAttributeName.BIO]: { - custom: 'Assistant to the Regional Manager', - }, - } - - expect(validateAttributes).toEqual(expectedValidatedAttributes) - }) - - it(`Should accept custom attributes both without and with 'custom' platform key`, async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const memberService = new MemberService(mockIServiceOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService(mockIServiceOptions) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(DEVTO_MEMBER_ATTRIBUTES) - - const attributes = { - [MemberAttributeName.NAME]: 'Dwight Schrute', - [MemberAttributeName.URL]: 'https://some-url', - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Berlin', - [PlatformType.DEVTO]: 'Istanbul', - custom: 'a custom location', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Assistant to the Regional Manager', - [PlatformType.DEVTO]: 'Assistant Regional Manager', - custom: 'a custom bio', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://some-image-url', - custom: 'a custom image url', - }, - } - - const validateAttributes = await memberService.validateAttributes(attributes) - - const expectedValidatedAttributes = { - [MemberAttributeName.NAME]: { - custom: 'Dwight Schrute', - }, - [MemberAttributeName.URL]: { - custom: 'https://some-url', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Berlin', - [PlatformType.DEVTO]: 'Istanbul', - custom: 'a custom location', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Assistant to the Regional Manager', - [PlatformType.DEVTO]: 'Assistant Regional Manager', - custom: 'a custom bio', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://some-image-url', - custom: 'a custom image url', - }, - } - - expect(validateAttributes).toEqual(expectedValidatedAttributes) - }) - - it('Should throw a 400 Error when an attribute does not exist in member attribute settings', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const memberService = new MemberService(mockIServiceOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService(mockIServiceOptions) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - // in settings name has a string type, inserting an integer should throw an error - const attributes = { - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://some-github-url', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://some-image-url', - }, - 'non-existing-attribute': { - [PlatformType.TWITTER]: 'some value', - }, - } - const validateAttributes = await memberService.validateAttributes(attributes) - - // member attribute that is non existing in settings, should be omitted after validate - const expectedValidatedAttributes = { - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://some-github-url', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://some-image-url', - }, - } - expect(validateAttributes).toEqual(expectedValidatedAttributes) - }) - - it('Should throw a 400 Error when the type of an attribute does not match the type in member attribute settings', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const memberService = new MemberService(mockIServiceOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService(mockIServiceOptions) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - // in settings website_url has a url type, inserting an integer should throw an error - const attributes = { - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 55, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://some-github-url', - [PlatformType.TWITTER]: 'https://some-twitter-url', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://some-image-url', - }, - } - - await expect(() => memberService.validateAttributes(attributes)).rejects.toThrowError( - new Error400('en', 'settings.memberAttributes.wrongType'), - ) - }) - }) - describe('setAttributesDefaultValues method', () => { - it('Should return the structured attributes object with default values succesfully', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const memberService = new MemberService(mockIServiceOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService(mockIServiceOptions) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(DEVTO_MEMBER_ATTRIBUTES) - - const attributes = { - [MemberAttributeName.NAME]: { - [PlatformType.DEVTO]: 'Dweet Srute', - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://some-github-url', - [PlatformType.TWITTER]: 'https://some-twitter-url', - [PlatformType.DEVTO]: 'https://some-devto-url', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Berlin', - [PlatformType.DEVTO]: 'Istanbul', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Assistant to the Regional Manager', - [PlatformType.DEVTO]: 'Assistant Regional Manager', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://some-image-url', - }, - } - - const attributesWithDefaultValues = await memberService.setAttributesDefaultValues(attributes) - - // Default platform priority is: custom, twitter, github, devto, slack, discord, crowd - const expectedAttributesWithDefaultValues = { - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.URL][PlatformType.GITHUB], - [PlatformType.TWITTER]: attributes[MemberAttributeName.URL][PlatformType.TWITTER], - [PlatformType.DEVTO]: attributes[MemberAttributeName.URL][PlatformType.DEVTO], - default: attributes[MemberAttributeName.URL][PlatformType.TWITTER], - }, - [MemberAttributeName.NAME]: { - [PlatformType.DEVTO]: attributes[MemberAttributeName.NAME][PlatformType.DEVTO], - default: attributes[MemberAttributeName.NAME][PlatformType.DEVTO], - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: attributes[MemberAttributeName.AVATAR_URL][PlatformType.TWITTER], - default: attributes[MemberAttributeName.AVATAR_URL][PlatformType.TWITTER], - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.BIO][PlatformType.GITHUB], - [PlatformType.DEVTO]: attributes[MemberAttributeName.BIO][PlatformType.DEVTO], - default: attributes[MemberAttributeName.BIO][PlatformType.GITHUB], - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.LOCATION][PlatformType.GITHUB], - [PlatformType.DEVTO]: attributes[MemberAttributeName.LOCATION][PlatformType.DEVTO], - default: attributes[MemberAttributeName.LOCATION][PlatformType.GITHUB], - }, - } - - expect(attributesWithDefaultValues).toEqual(expectedAttributesWithDefaultValues) - }) - - it('Should throw a 400 Error when priority array does not exist', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const memberService = new MemberService(mockIServiceOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService(mockIServiceOptions) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(DEVTO_MEMBER_ATTRIBUTES) - - // Empty default priority array - const settings = await SettingsRepository.findOrCreateDefault({}, mockIServiceOptions) - - await SettingsRepository.save( - { ...settings, attributeSettings: { priorities: [] } }, - mockIServiceOptions, - ) - const attributes = { - [MemberAttributeName.NAME]: { - [PlatformType.DEVTO]: 'Dweet Srute', - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://some-github-url', - [PlatformType.TWITTER]: 'https://some-twitter-url', - [PlatformType.DEVTO]: 'https://some-devto-url', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Berlin', - [PlatformType.DEVTO]: 'Istanbul', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Assistant to the Regional Manager', - [PlatformType.DEVTO]: 'Assistant Regional Manager', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://some-image-url', - }, - } - - await expect(() => memberService.setAttributesDefaultValues(attributes)).rejects.toThrowError( - new Error400('en', 'settings.memberAttributes.priorityArrayNotFound'), - ) - }) - }) - - describe('findAndCountAll method', () => { - it('Should filter and sort by dynamic attributes using advanced filters successfully', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const ms = new MemberService(mockIServiceOptions) - - const mas = new MemberAttributeSettingsService(mockIServiceOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await mas.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - await mas.createPredefined(DISCORD_MEMBER_ATTRIBUTES) - - const attribute1 = { - name: 'aNumberAttribute', - label: 'A number Attribute', - type: MemberAttributeType.NUMBER, - canDelete: true, - show: true, - } - - const attribute2 = { - name: 'aDateAttribute', - label: 'A date Attribute', - type: MemberAttributeType.DATE, - canDelete: true, - show: true, - } - - const attribute3 = { - name: 'aMultiSelectAttribute', - label: 'A multi select Attribute', - options: ['a', 'b', 'c'], - type: MemberAttributeType.MULTI_SELECT, - canDelete: true, - show: true, - } - - await mas.create(attribute1) - await mas.create(attribute2) - await mas.create(attribute3) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - [PlatformType.DISCORD]: 'anil', - [PlatformType.TWITTER]: 'anil', - }, - platform: PlatformType.GITHUB, - emails: ['lala@l.com'], - score: 10, - attributes: { - aDateAttribute: { - custom: '2022-08-01T00:00:00', - }, - aMultiSelectAttribute: { - custom: ['a', 'b'], - github: ['a'], - }, - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: false, - [PlatformType.DISCORD]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/anil', - [PlatformType.TWITTER]: 'https://twitter.com/anil', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.TWITTER]: '#twitterId2', - [PlatformType.DISCORD]: '#discordId1', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://twitter.com/anil/image', - }, - aNumberAttribute: { - [PlatformType.GITHUB]: 1, - [PlatformType.TWITTER]: 2, - [PlatformType.DISCORD]: 300000, - }, - }, - contributions: [ - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - { - id: 112529474, - url: 'https://github.com/bighead/startup-ideas', - topics: ['Ideas', 'Startups'], - summary: 'Startup Ideas: 20 commits in 1 week', - numberCommits: 20, - lastCommitDate: '03/01/2023', - firstCommitDate: '02/22/2023', - }, - ], - joinedAt: '2022-05-28T15:13:30', - } - - const member2 = { - username: { - [PlatformType.GITHUB]: 'michaelScott', - [PlatformType.DISCORD]: 'michaelScott', - [PlatformType.TWITTER]: 'michaelScott', - }, - platform: PlatformType.GITHUB, - emails: ['michael@mifflin.com'], - score: 10, - attributes: { - aDateAttribute: { - custom: '2022-08-06T00:00:00', - }, - aMultiSelectAttribute: { - custom: ['b', 'c'], - github: ['b'], - }, - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - [PlatformType.DISCORD]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/michael-scott', - [PlatformType.TWITTER]: 'https://twitter.com/michael', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://website/michael', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Dunder & Mifflin Regional Manager', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Berlin', - }, - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.TWITTER]: '#twitterId2', - [PlatformType.DISCORD]: '#discordId2', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://twitter.com/michael/image', - }, - aNumberAttribute: { - [PlatformType.GITHUB]: 1500, - [PlatformType.TWITTER]: 2500, - [PlatformType.DISCORD]: 2, - }, - }, - contributions: [ - { - id: 112529472, - url: 'https://github.com/bachman/pied-piper', - topics: ['compression', 'data', 'middle-out', 'Java'], - summary: 'Pied Piper: 10 commits in 1 day', - numberCommits: 10, - lastCommitDate: '2023-03-10', - firstCommitDate: '2023-03-01', - }, - { - id: 112529473, - url: 'https://github.com/bachman/aviato', - topics: ['Python', 'Django'], - summary: 'Aviato: 5 commits in 1 day', - numberCommits: 5, - lastCommitDate: '2023-02-25', - firstCommitDate: '2023-02-20', - }, - { - id: 112529476, - url: 'https://github.com/bachman/erlichbot', - topics: ['Python', 'Slack API'], - summary: 'ErlichBot: 2 commits in 1 day', - numberCommits: 2, - lastCommitDate: '2023-01-25', - firstCommitDate: '2023-01-24', - }, - ], - joinedAt: '2022-09-15T15:13:30', - } - - const member3 = { - username: { - [PlatformType.GITHUB]: 'jimHalpert', - [PlatformType.DISCORD]: 'jimHalpert', - [PlatformType.TWITTER]: 'jimHalpert', - }, - platform: PlatformType.GITHUB, - emails: ['jim@mifflin.com'], - score: 10, - attributes: { - aDateAttribute: { - custom: '2022-08-15T00:00:00', - }, - aMultiSelectAttribute: { - custom: ['a', 'c'], - github: ['c'], - }, - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: false, - [PlatformType.DISCORD]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/jim-halpert', - [PlatformType.TWITTER]: 'https://twitter.com/jim', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://website/jim', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Sales guy', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Scranton', - }, - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.TWITTER]: '#twitterId3', - [PlatformType.DISCORD]: '#discordId3', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://twitter.com/jim/image', - }, - aNumberAttribute: { - [PlatformType.GITHUB]: 15500, - [PlatformType.TWITTER]: 25500, - [PlatformType.DISCORD]: 200000, - }, - }, - joinedAt: '2022-09-16T15:13:30Z', - } - - const member1Created = await ms.upsert(member1) - const member2Created = await ms.upsert(member2) - const member3Created = await ms.upsert(member3) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - // filter and sort by aNumberAttribute default values - let members = await ms.findAndCountAll({ - advancedFilter: { - aNumberAttribute: { - gte: 1000, - }, - }, - orderBy: 'aNumberAttribute_DESC', - }) - - expect(members.count).toBe(2) - expect(members.rows.map((i) => i.id)).toStrictEqual([member3Created.id, member2Created.id]) - - // filter and sort by aNumberAttribute platform specific values - members = await ms.findAndCountAll({ - advancedFilter: { - 'attributes.aNumberAttribute.discord': { - gte: 100000, - }, - }, - orderBy: 'attributes.aNumberAttribute.discord_DESC', - }) - - expect(members.count).toBe(2) - expect(members.rows.map((i) => i.id)).toStrictEqual([member1Created.id, member3Created.id]) - - // filter by isHireable default values - members = await ms.findAndCountAll({ - advancedFilter: { - isHireable: true, - }, - }) - - expect(members.count).toBe(1) - expect(members.rows.map((i) => i.id)).toStrictEqual([member2Created.id]) - - // filter by isHireable platform specific values - members = await ms.findAndCountAll({ - advancedFilter: { - 'attributes.isHireable.discord': true, - }, - }) - - expect(members.count).toBe(3) - expect(members.rows.map((i) => i.id)).toStrictEqual([ - member3Created.id, - member2Created.id, - member1Created.id, - ]) - - // filter and sort by url default values - members = await ms.findAndCountAll({ - advancedFilter: { - url: { - textContains: 'jim', - }, - }, - orderBy: 'url_DESC', - }) - - expect(members.count).toBe(1) - expect(members.rows.map((i) => i.id)).toStrictEqual([member3Created.id]) - - // filter and sort by url platform specific values - members = await ms.findAndCountAll({ - advancedFilter: { - 'attributes.url.github': { - textContains: 'github', - }, - }, - orderBy: 'attributes.url.github_ASC', - }) - - expect(members.count).toBe(3) - - // results will be sorted by github.url anil -> jim -> michael - expect(members.rows.map((i) => i.id)).toStrictEqual([ - member1Created.id, - member3Created.id, - member2Created.id, - ]) - - // filter and sort by custom aDateAttribute - members = await ms.findAndCountAll({ - advancedFilter: { - aDateAttribute: { - lte: '2022-08-06T00:00:00', - }, - }, - orderBy: 'aDateAttribute_DESC', - }) - - expect(members.count).toBe(2) - expect(members.rows.map((i) => i.id)).toStrictEqual([member2Created.id, member1Created.id]) - - // filter by custom aMultiSelectAttribute - members = await ms.findAndCountAll({ - advancedFilter: { - aMultiSelectAttribute: { - overlap: ['a'], - }, - }, - orderBy: 'createdAt_DESC', - }) - expect(members.count).toBe(2) - expect(members.rows.map((i) => i.id)).toStrictEqual([member3Created.id, member1Created.id]) - - // filter by numberOfOpenSourceContributions - members = await ms.findAndCountAll({ - filter: { - numberOfOpenSourceContributionsRange: [2, 6], - }, - }) - expect(members.count).toBe(2) - expect(members.rows.map((i) => i.id)).toEqual([member2Created.id, member1Created.id]) - - // filter by numberOfOpenSourceContributions only start - members = await ms.findAndCountAll({ - filter: { - numberOfOpenSourceContributionsRange: [3], - }, - }) - expect(members.count).toBe(1) - expect(members.rows.map((i) => i.id)).toStrictEqual([member2Created.id]) - - // filter and sort by numberOfOpenSourceContributions - members = await ms.findAndCountAll({ - filter: { - numberOfOpenSourceContributionsRange: [2, 6], - }, - orderBy: 'numberOfOpenSourceContributions_ASC', - }) - expect(members.count).toBe(2) - expect(members.rows.map((i) => i.id)).toStrictEqual([member1Created.id, member2Created.id]) - - // sort by numberOfOpenSourceContributions - members = await ms.findAndCountAll({ - orderBy: 'numberOfOpenSourceContributions_ASC', - }) - expect(members.count).toBe(3) - expect(members.rows.map((i) => i.id)).toStrictEqual([ - member3Created.id, - member1Created.id, - member2Created.id, - ]) - }) - }) -}) diff --git a/backend/src/services/__tests__/microserviceService.test.ts b/backend/src/services/__tests__/microserviceService.test.ts deleted file mode 100644 index 0f8cbdeecc..0000000000 --- a/backend/src/services/__tests__/microserviceService.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import MicroserviceService from '../microserviceService' -import SequelizeTestUtils from '../../database/utils/sequelizeTestUtils' - -const db = null - -describe('MicroService Service tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('CreateIfNotExists method', () => { - it('Should create a microservice succesfully with default values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservice2Add = { type: 'members_score' } - - const microserviceCreated = await new MicroserviceService( - mockIRepositoryOptions, - ).createIfNotExists(microservice2Add) - - microserviceCreated.createdAt = microserviceCreated.createdAt.toISOString().split('T')[0] - microserviceCreated.updatedAt = microserviceCreated.updatedAt.toISOString().split('T')[0] - - const microserviceExpected = { - id: microserviceCreated.id, - init: false, - running: false, - type: microservice2Add.type, - variant: 'default', - settings: {}, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(microserviceCreated).toStrictEqual(microserviceExpected) - }) - it('Should return the existing if it does not exist', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservice2Add = { type: 'members_score' } - - const microserviceCreated = await new MicroserviceService(mockIRepositoryOptions).create( - microservice2Add, - ) - - const secondCreated = await new MicroserviceService(mockIRepositoryOptions).createIfNotExists( - microservice2Add, - ) - - secondCreated.createdAt = secondCreated.createdAt.toISOString().split('T')[0] - secondCreated.updatedAt = secondCreated.updatedAt.toISOString().split('T')[0] - - const microserviceExpected = { - id: microserviceCreated.id, - init: false, - running: false, - type: microservice2Add.type, - variant: 'default', - settings: {}, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(secondCreated).toStrictEqual(microserviceExpected) - const count = (await new MicroserviceService(mockIRepositoryOptions).findAndCountAll({})) - .count - expect(count).toBe(1) - }) - }) -}) diff --git a/backend/src/services/__tests__/organizationService.test.ts b/backend/src/services/__tests__/organizationService.test.ts deleted file mode 100644 index ba8cf1bbdc..0000000000 --- a/backend/src/services/__tests__/organizationService.test.ts +++ /dev/null @@ -1,260 +0,0 @@ -import organizationCacheRepository from '../../database/repositories/organizationCacheRepository' -import SequelizeTestUtils from '../../database/utils/sequelizeTestUtils' -import Plans from '../../security/plans' -import OrganizationService from '../organizationService' - -const db = null - -const expectedEnriched = { - identities: [ - { - name: 'crowd.dev', - platform: 'crowd', - }, - ], - description: - 'Understand, grow, and engage your developer community with zero hassle. With crowd.dev, you can build developer communities that drive your business forward.', - emails: ['hello@crowd.dev', 'jonathan@crowd.dev', 'careers@crowd.dev'], - phoneNumbers: ['+42 424242'], - logo: 'https://logo.clearbit.com/crowd.dev', - tags: [], - twitter: { - handle: 'CrowdDotDev', - id: '1362101830923259908', - bio: 'Community-led Growth for Developer-first Companies.\nJoin our private beta. 👇', - followers: 107, - following: 0, - location: '🌍 remote', - site: 'https://t.co/GRLDhqFWk4', - avatar: 'https://pbs.twimg.com/profile_images/1419741008716251141/6exZe94-_normal.jpg', - }, - linkedin: { - handle: 'company/crowddevhq', - }, - crunchbase: { - handle: null, - }, - employees: 5, - revenueRange: { - min: 0, - max: 1, - }, -} - -describe('OrganizationService tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('Create method', () => { - it('Should add without enriching when enrichP is false', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions( - db, - Plans.values.growth, - ) - const service = new OrganizationService(mockIServiceOptions) - - const toAdd = { - identities: [ - { - name: 'crowd.dev', - platform: 'crowd', - }, - ], - } - - const added = await service.createOrUpdate(toAdd, false) - expect(added.identities[0].url).toEqual(null) - }) - - it('Should add without enriching when tenant is not growth', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - const service = new OrganizationService(mockIServiceOptions) - - const toAdd = { - identities: [ - { - name: 'crowd.dev', - platform: 'crowd', - }, - ], - } - - const added = await service.createOrUpdate(toAdd, true) - expect(added.identities[0].url).toEqual(null) - }) - - it('Should enrich and add an organization by identity name', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions( - db, - Plans.values.growth, - ) - const service = new OrganizationService(mockIServiceOptions) - - const toAdd = { - identities: [ - { - name: 'crowd.dev', - platform: 'crowd', - url: 'https://crowd.dev', - }, - ], - } - - const added = await service.createOrUpdate(toAdd) - expect(added.identities[0].url).toEqual(toAdd.identities[0].url) - expect(added.identities[0].name).toEqual(toAdd.identities[0].name) - expect(added.description).toEqual(expectedEnriched.description) - expect(added.emails).toEqual(expectedEnriched.emails) - expect(added.phoneNumbers).toEqual(expectedEnriched.phoneNumbers) - expect(added.logo).toEqual(expectedEnriched.logo) - expect(added.tags).toStrictEqual(expectedEnriched.tags) - expect(added.twitter).toStrictEqual(expectedEnriched.twitter) - expect(added.linkedin).toStrictEqual(expectedEnriched.linkedin) - expect(added.crunchbase).toStrictEqual(expectedEnriched.crunchbase) - expect(added.employees).toEqual(expectedEnriched.employees) - expect(added.revenueRange).toStrictEqual(expectedEnriched.revenueRange) - - // Check cache table was created - const foundCache = await organizationCacheRepository.findByName( - 'crowd.dev', - mockIServiceOptions, - ) - - expect(foundCache.url).toEqual('crowd.dev') - expect(foundCache.name).toEqual(toAdd.identities[0].name) - expect(foundCache.description).toEqual(expectedEnriched.description) - expect(foundCache.emails).toEqual(expectedEnriched.emails) - expect(foundCache.phoneNumbers).toEqual(expectedEnriched.phoneNumbers) - expect(foundCache.logo).toEqual(expectedEnriched.logo) - expect(foundCache.tags).toStrictEqual(expectedEnriched.tags) - expect(foundCache.twitter).toStrictEqual(expectedEnriched.twitter) - expect(foundCache.linkedin).toStrictEqual(expectedEnriched.linkedin) - expect(foundCache.crunchbase).toStrictEqual(expectedEnriched.crunchbase) - expect(foundCache.employees).toEqual(expectedEnriched.employees) - expect(foundCache.revenueRange).toStrictEqual(expectedEnriched.revenueRange) - }) - - it('Should not re-enrich when the record is already in the cache table. By Name', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions( - db, - Plans.values.growth, - ) - const mockIServiceOptions2 = await SequelizeTestUtils.getTestIServiceOptions( - db, - Plans.values.growth, - ) - - const service = new OrganizationService(mockIServiceOptions) - const service2 = new OrganizationService(mockIServiceOptions2) - - const toAdd = { - identities: [ - { - name: 'crowd.dev', - platform: 'crowd', - url: 'https://crowd.dev', - }, - ], - } - - const added = await service.createOrUpdate(toAdd) - expect(added.identities[0].url).toEqual(toAdd.identities[0].url) - expect(added.identities[0].name).toEqual(toAdd.identities[0].name) - expect(added.description).toEqual(expectedEnriched.description) - expect(added.emails).toEqual(expectedEnriched.emails) - expect(added.phoneNumbers).toEqual(expectedEnriched.phoneNumbers) - expect(added.logo).toEqual(expectedEnriched.logo) - expect(added.tags).toStrictEqual(expectedEnriched.tags) - expect(added.twitter).toStrictEqual(expectedEnriched.twitter) - expect(added.linkedin).toStrictEqual(expectedEnriched.linkedin) - expect(added.crunchbase).toStrictEqual(expectedEnriched.crunchbase) - expect(added.employees).toEqual(expectedEnriched.employees) - expect(added.revenueRange).toStrictEqual(expectedEnriched.revenueRange) - - // Check cache table was created - const foundCache = await organizationCacheRepository.findByName( - 'crowd.dev', - mockIServiceOptions, - ) - - expect(foundCache.name).toEqual('crowd.dev') - expect(foundCache.description).toEqual(expectedEnriched.description) - expect(foundCache.emails).toEqual(expectedEnriched.emails) - expect(foundCache.phoneNumbers).toEqual(expectedEnriched.phoneNumbers) - expect(foundCache.logo).toEqual(expectedEnriched.logo) - expect(foundCache.tags).toStrictEqual(expectedEnriched.tags) - expect(foundCache.twitter).toStrictEqual(expectedEnriched.twitter) - expect(foundCache.linkedin).toStrictEqual(expectedEnriched.linkedin) - expect(foundCache.crunchbase).toStrictEqual(expectedEnriched.crunchbase) - expect(foundCache.employees).toEqual(expectedEnriched.employees) - expect(foundCache.revenueRange).toStrictEqual(expectedEnriched.revenueRange) - - const added2 = await service2.createOrUpdate(toAdd) - expect(added2.identities[0].url).toEqual(toAdd.identities[0].url) - expect(added2.description).toEqual(expectedEnriched.description) - expect(added2.emails).toEqual(expectedEnriched.emails) - expect(added2.phoneNumbers).toEqual(expectedEnriched.phoneNumbers) - expect(added2.logo).toEqual(expectedEnriched.logo) - expect(added2.tags).toStrictEqual(expectedEnriched.tags) - expect(added2.twitter).toStrictEqual(expectedEnriched.twitter) - expect(added2.linkedin).toStrictEqual(expectedEnriched.linkedin) - expect(added2.crunchbase).toStrictEqual(expectedEnriched.crunchbase) - expect(added2.employees).toEqual(expectedEnriched.employees) - expect(added2.revenueRange).toStrictEqual(expectedEnriched.revenueRange) - // Check they are indeed in different tenants - expect(added2.tenantId).not.toBe(added.tenantId) - - const foundCache2 = await organizationCacheRepository.findByName( - 'crowd.dev', - mockIServiceOptions, - ) - expect(foundCache2.id).toEqual(foundCache.id) - }) - - it('Should throw an error when name is not sent', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions( - db, - Plans.values.growth, - ) - const service = new OrganizationService(mockIServiceOptions) - - const toAdd = {} - - await expect(service.createOrUpdate(toAdd as any)).rejects.toThrowError( - 'Missing organization identity while creating/updating organization!', - ) - }) - - it('Should not re-create when existing: not enrich and name', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - const service = new OrganizationService(mockIServiceOptions) - - const toAdd = { - identities: [ - { - name: 'crowd.dev', - platform: 'crowd', - }, - ], - } - - await service.createOrUpdate(toAdd) - - const added = await service.createOrUpdate(toAdd) - expect(added.identities[0].name).toEqual(toAdd.identities[0].name) - expect(added.identities[0].url).toBeNull() - - const foundAll = await service.findAndCountAll({ - filter: {}, - includeOrganizationsWithoutMembers: true, - }) - expect(foundAll.count).toBe(1) - }) - }) -}) diff --git a/backend/src/services/__tests__/taskService.test.ts b/backend/src/services/__tests__/taskService.test.ts deleted file mode 100644 index 24595833ca..0000000000 --- a/backend/src/services/__tests__/taskService.test.ts +++ /dev/null @@ -1,301 +0,0 @@ -import TaskRepository from '../../database/repositories/taskRepository' -import UserRepository from '../../database/repositories/userRepository' -import SequelizeTestUtils from '../../database/utils/sequelizeTestUtils' -import Roles from '../../security/roles' -import TaskService from '../taskService' - -const db = null - -describe('TaskService tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('Assign to user by ID', () => { - it('Should work when sending null or undefined', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const options2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const service = new TaskService(mockIRepositoryOptions) - - const task = await service.create({ - name: 'Task 1', - assignedTo: mockIRepositoryOptions.currentUser.id, - }) - - const task2 = await service.create({ - name: 'Task 2', - assignedTo: options2.currentUser.id, - }) - - const updated = await service.assignTo(task.id, null) - expect(updated.assignees).toStrictEqual([]) - - const updated2 = await service.assignTo(task2.id, undefined) - expect(updated2.assignees).toStrictEqual([]) - }) - - it('Should work when sending changing assignee user with another user', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const user2 = await UserRepository.create( - await SequelizeTestUtils.getRandomUser(), - mockIRepositoryOptions, - ) - - // add user to tenant - await mockIRepositoryOptions.database.tenantUser.create({ - roles: [Roles.values.admin], - status: 'active', - tenantId: mockIRepositoryOptions.currentTenant.id, - userId: user2.id, - }) - - const service = new TaskService(mockIRepositoryOptions) - - const task = await service.create({ - name: 'Task 1', - assignedTo: mockIRepositoryOptions.currentUser.id, - }) - - const updated = await service.assignTo(task.id, [user2.id]) - expect(updated.assignees.map((i) => i.id)).toStrictEqual([user2.id]) - }) - }) - - describe('Assign to user by email', () => { - it('Should work when sending null or undefined', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const user2 = await UserRepository.create( - await SequelizeTestUtils.getRandomUser(), - mockIRepositoryOptions, - ) - - // add user to tenant - await mockIRepositoryOptions.database.tenantUser.create({ - roles: [Roles.values.admin], - status: 'active', - tenantId: mockIRepositoryOptions.currentTenant.id, - userId: user2.id, - }) - - const service = new TaskService(mockIRepositoryOptions) - - const task = await service.create({ - name: 'Task 1', - assignees: [mockIRepositoryOptions.currentUser.id], - }) - - const task2 = await service.create({ - name: 'Task 2', - assignees: [user2.id], - }) - - const updated = await service.assignToByEmail(task.id, null) - expect(updated.assignees).toStrictEqual([]) - - const updated2 = await service.assignToByEmail(task2.id, undefined) - expect(updated2.assignees).toStrictEqual([]) - }) - - it('Should work when sending changing a user for another', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const user2 = await UserRepository.create( - await SequelizeTestUtils.getRandomUser(), - mockIRepositoryOptions, - ) - - // add user to tenant - await mockIRepositoryOptions.database.tenantUser.create({ - roles: [Roles.values.admin], - status: 'active', - tenantId: mockIRepositoryOptions.currentTenant.id, - userId: user2.id, - }) - - const service = new TaskService(mockIRepositoryOptions) - - const task = await service.create({ - name: 'Task 1', - assignedTo: mockIRepositoryOptions.currentUser.id, - }) - - const updated = await service.assignToByEmail(task.id, user2.email) - expect(updated.assignees.map((i) => i.id)).toStrictEqual([user2.id]) - }) - }) - - describe('Change status', () => { - it('Should work when sending an empty array', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const service = new TaskService(mockIRepositoryOptions) - - const task = await service.create({ - name: 'Task 1', - status: 'in-progress', - }) - - const task2 = await service.create({ - name: 'Task 2', - status: 'archived', - }) - - const updated = await service.updateStatus(task.id, null) - expect(updated.status).toBeNull() - - const updated2 = await service.updateStatus(task2.id, undefined) - expect(updated2.status).toBeNull() - }) - - it('Should work when sending a different status', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const service = new TaskService(mockIRepositoryOptions) - - const task = await service.create({ - name: 'Task 1', - status: 'in-progress', - }) - - const updated = await service.updateStatus(task.id, 'done') - expect(updated.status).toBe('done') - }) - }) - - describe('findAndUpdateAll', () => { - it('Should find all tasks with given filter, and update found tasks with given payload', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const service = new TaskService(mockIRepositoryOptions) - - let task1 = await service.create({ - name: 'Task 1', - status: 'in-progress', - }) - - let task2 = await service.create({ - name: 'Task 2', - status: 'in-progress', - }) - - let task3 = await service.create({ - name: 'Task 3', - status: 'archived', - }) - - let task4 = await service.create({ - name: 'Task 4', - status: 'in-progress', - }) - - // change all in-progress to done - let updated = await service.findAndUpdateAll({ - filter: { - status: 'in-progress', - }, - update: { - status: 'done', - }, - }) - - expect(updated.rowsUpdated).toStrictEqual(3) - - task1 = await service.findById(task1.id) - task2 = await service.findById(task2.id) - task3 = await service.findById(task3.id) - task4 = await service.findById(task4.id) - - expect(task1.status).toStrictEqual('done') - expect(task2.status).toStrictEqual('done') - expect(task3.status).toStrictEqual('archived') - expect(task4.status).toStrictEqual('done') - - // change all done to archived - updated = await service.findAndUpdateAll({ - filter: { - status: 'done', - }, - update: { - status: 'archived', - }, - }) - - task1 = await service.findById(task1.id) - task2 = await service.findById(task2.id) - task3 = await service.findById(task3.id) - task4 = await service.findById(task4.id) - - expect(task1.status).toStrictEqual('archived') - expect(task2.status).toStrictEqual('archived') - expect(task3.status).toStrictEqual('archived') - expect(task4.status).toStrictEqual('archived') - - // change all archived to in progress - updated = await service.findAndUpdateAll({ - filter: { - status: 'archived', - }, - update: { - status: 'in-progress', - }, - }) - - task1 = await service.findById(task1.id) - task2 = await service.findById(task2.id) - task3 = await service.findById(task3.id) - task4 = await service.findById(task4.id) - - expect(task1.status).toStrictEqual('in-progress') - expect(task2.status).toStrictEqual('in-progress') - expect(task3.status).toStrictEqual('in-progress') - expect(task4.status).toStrictEqual('in-progress') - }) - }) - describe('findAndUpdateAll', () => { - it('Should find all tasks with given filter, and delete found tasks', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const service = new TaskService(mockIRepositoryOptions) - - await service.create({ - name: 'Task 1', - status: 'in-progress', - }) - - await service.create({ - name: 'Task 2', - status: 'in-progress', - }) - - const task3 = await service.create({ - name: 'Task 3', - status: 'archived', - }) - - await service.create({ - name: 'Task 4', - status: 'in-progress', - }) - - // change all in-progress to done - const deleted = await service.findAndDeleteAll({ - filter: { - status: 'in-progress', - }, - }) - - expect(deleted.rowsDeleted).toStrictEqual(3) - - // get all tasks and check count - const tasks = await TaskRepository.findAndCountAll({ filter: {} }, mockIRepositoryOptions) - - expect(tasks.count).toBe(1) - expect(tasks.rows[0].id).toStrictEqual(task3.id) - }) - }) -}) diff --git a/backend/src/services/__tests__/tenantService.test.ts b/backend/src/services/__tests__/tenantService.test.ts deleted file mode 100644 index 95155e4d4a..0000000000 --- a/backend/src/services/__tests__/tenantService.test.ts +++ /dev/null @@ -1,230 +0,0 @@ -import moment from 'moment' -import SequelizeTestUtils from '../../database/utils/sequelizeTestUtils' -import TenantService from '../tenantService' -import MemberService from '../memberService' -import { IServiceOptions } from '../IServiceOptions' -import MicroserviceService from '../microserviceService' -import { MemberAttributeName, PlatformType } from '@crowd/types' -import MemberAttributeSettingsService from '../memberAttributeSettingsService' -import TaskService from '../taskService' -import Plans from '../../security/plans' -import { generateUUIDv1 } from '@crowd/common' -import { getRedisClient } from '@crowd/redis' -import { REDIS_CONFIG } from '../../conf' - -const db = null - -describe('TenantService tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('findMembersToMerge', () => { - it('Should show the same merge suggestion once, with reverse order', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - const memberService = new MemberService(mockIServiceOptions) - const tenantService = new TenantService(mockIServiceOptions) - - const memberToCreate1 = { - username: { - [PlatformType.SLACK]: { - username: 'member 1', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.SLACK, - email: 'member.1@email.com', - joinedAt: '2020-05-27T15:13:30Z', - } - - const memberToCreate2 = { - username: { - [PlatformType.DISCORD]: { - username: 'member 2', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.DISCORD, - email: 'member.2@email.com', - joinedAt: '2020-05-26T15:13:30Z', - } - - const memberToCreate3 = { - username: { - [PlatformType.GITHUB]: { - username: 'member 3', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.GITHUB, - email: 'member.3@email.com', - joinedAt: '2020-05-25T15:13:30Z', - } - - const memberToCreate4 = { - username: { - [PlatformType.TWITTER]: { - username: 'member 4', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.TWITTER, - email: 'member.4@email.com', - joinedAt: '2020-05-24T15:13:30Z', - } - - const member1 = await memberService.upsert(memberToCreate1) - let member2 = await memberService.upsert(memberToCreate2) - const member3 = await memberService.upsert(memberToCreate3) - let member4 = await memberService.upsert(memberToCreate4) - - await memberService.addToMerge([{ members: [member1.id, member2.id], similarity: null }]) - await memberService.addToMerge([{ members: [member3.id, member4.id], similarity: null }]) - - member2 = await memberService.findById(member2.id) - member4 = await memberService.findById(member4.id) - - const memberToMergeSuggestions = await tenantService.findMembersToMerge({}) - - // In the DB there should be: - // - Member 1 should have member 2 in toMerge - // - Member 3 should have member 4 in toMerge - // - Member 4 should have member 3 in toMerge - // - We should get these 4 combinations - // But this function should not return duplicates, so we should get - // only two pairs: [m2, m1] and [m4, m3] - - expect(memberToMergeSuggestions.count).toEqual(1) - - expect( - memberToMergeSuggestions.rows[0].members - .sort((a, b) => (a.createdAt > b.createdAt ? 1 : -1)) - .map((m) => m.id), - ).toStrictEqual([member1.id, member2.id]) - - expect( - memberToMergeSuggestions.rows[1].members - .sort((a, b) => (a.createdAt > b.createdAt ? 1 : -1)) - .map((m) => m.id), - ).toStrictEqual([member3.id, member4.id]) - }) - }) - - describe('_findAndCountAllForEveryUser method', () => { - it('Should succesfully find all tenants without filtering by currentUser', async () => { - let tenants = await TenantService._findAndCountAllForEveryUser({ filter: {} }) - - expect(tenants.count).toEqual(0) - expect(tenants.rows).toEqual([]) - - // generate 3 tenants - const mockIServiceOptions1 = await SequelizeTestUtils.getTestIServiceOptions(db) - const mockIServiceOptions2 = await SequelizeTestUtils.getTestIServiceOptions(db) - const mockIServiceOptions3 = await SequelizeTestUtils.getTestIServiceOptions(db) - - tenants = await TenantService._findAndCountAllForEveryUser({ filter: {} }) - - expect(tenants.count).toEqual(3) - expect(tenants.rows.map((i) => i.id).sort()).toEqual( - [ - mockIServiceOptions1.currentTenant.id, - mockIServiceOptions2.currentTenant.id, - mockIServiceOptions3.currentTenant.id, - ].sort(), - ) - }) - }) - - describe('create method', () => { - it('Should succesfully create the tenant, related default microservices, settings and suggested tasks', async () => { - const randomUser = await SequelizeTestUtils.getRandomUser() - let db = null - db = await SequelizeTestUtils.getDatabase(db) - - const userModel = await db.user.create(randomUser) - // Get options without currentTenant - const options = { - language: 'en', - currentUser: userModel, - database: db, - redis: await getRedisClient(REDIS_CONFIG, true), - } as IServiceOptions - - const tenantCreated = await new TenantService(options).create({ - name: 'testName', - url: 'testUrl', - integrationsRequired: ['github', 'discord'], - communitySize: '>25000', - }) - - const tenantCreatedPlain = tenantCreated.get({ plain: true }) - - tenantCreatedPlain.createdAt = tenantCreatedPlain.createdAt.toISOString().split('T')[0] - tenantCreatedPlain.updatedAt = tenantCreatedPlain.updatedAt.toISOString().split('T')[0] - - const tenantExpected = { - id: tenantCreatedPlain.id, - name: 'testName', - url: 'testUrl', - plan: Plans.values.essential, - isTrialPlan: false, - trialEndsAt: null, - onboardedAt: null, - integrationsRequired: ['github', 'discord'], - hasSampleData: false, - communitySize: '>25000', - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - createdById: options.currentUser.id, - updatedById: options.currentUser.id, - settings: [], - conversationSettings: [], - planSubscriptionEndsAt: null, - stripeSubscriptionId: null, - reasonForUsingCrowd: null, - } - - expect(tenantCreatedPlain).toStrictEqual(tenantExpected) - - // Check microservices (members_score should be created with tenantService.create) - const ms = new MicroserviceService({ ...options, currentTenant: tenantCreated }) - const microservicesOfTenant = await ms.findAndCountAll({}) - - expect(microservicesOfTenant.count).toEqual(1) - - // findAndCountAll returns sorted by createdAt (desc) by default, so first one should be members_score - expect(microservicesOfTenant.rows[0].type).toEqual('members_score') - - // Check default member attributes - const mas = new MemberAttributeSettingsService({ ...options, currentTenant: tenantCreated }) - const defaultAttributes = await mas.findAndCountAll({ filter: {} }) - - expect(defaultAttributes.rows.map((i) => i.name).sort()).toEqual([ - MemberAttributeName.BIO, - MemberAttributeName.IS_BOT, - MemberAttributeName.IS_ORGANIZATION, - MemberAttributeName.IS_TEAM_MEMBER, - MemberAttributeName.JOB_TITLE, - MemberAttributeName.LOCATION, - MemberAttributeName.URL, - ]) - - const taskService = new TaskService({ ...options, currentTenant: tenantCreated }) - const suggestedTasks = await taskService.findAndCountAll({ filter: {} }) - expect(suggestedTasks.rows.map((i) => i.name).sort()).toStrictEqual([ - 'Check for negative reactions', - 'Engage with relevant content', - 'Reach out to influential contacts', - 'Reach out to poorly engaged contacts', - 'Setup your team', - 'Setup your workpace integrations', - ]) - }) - }) -}) diff --git a/backend/src/services/__tests__/test-sample-data.json b/backend/src/services/__tests__/test-sample-data.json deleted file mode 100644 index e2657978c3..0000000000 --- a/backend/src/services/__tests__/test-sample-data.json +++ /dev/null @@ -1,286 +0,0 @@ -[ - { - "username": { "discord": "considerate snewkes" }, - "type": "member", - "info": {}, - "attributes": { - "url": { "github": "https://github.com/CrowdHQ" }, - "name": { "github": "considerate snewkes" }, - "isHireable": { "github": false }, - "websiteUrl": { "github": "https://crowd.dev" }, - "sourceId": { "discord": "#sample-discord-id" } - }, - "email": "team@crowd.dev", - "score": 10, - "bio": "Member of the @vuejs core team, hobby frontend hacker. Not a dev by profession.", - "organisation": "crowd.dev", - "location": "Mannheim, Germany", - "signals": null, - "joinedAt": "2021-08-18T17:47:52.000Z", - "importHash": null, - "tagsArray": [null], - "activities": [ - { - "type": "joined_guild", - "timestamp": "2022-03-16T21:00:01.000Z", - "platform": "discord", - "info": {}, - "attributes": {}, - "isContribution": false, - "score": 2, - "sourceId": "#sourceId1" - }, - { - "type": "message", - "timestamp": "2022-03-11T21:00:00.000Z", - "platform": "discord", - "info": {}, - "attributes": { "thread": false, "reactions": [], "attachments": [] }, - "isContribution": true, - "score": 1, - "sourceId": "#sourceId2", - "url": "", - "body": "Laboris cillum aliquip cupidatat dolor nisi culpa. Occaecat fugiat sunt anim.", - "channel": "introductions" - } - ], - "reach": { "total": -1 }, - "tags": [], - "noMerge": [], - "toMerge": [] - }, - { - "username": { "github": "amazing worshipper", "discord": "amazing worshipper" }, - "type": "member", - "info": {}, - "attributes": { - "url": { "github": "https://github.com/CrowdHQ" }, - "name": { "github": "amazing worshipper" }, - "isHireable": { "github": true }, - "websiteUrl": { "github": "https://crowd.dev" }, - "sourceId": { "discord": "#sample-discord-id" } - }, - "email": "team@crowd.dev", - "score": 10, - "bio": "Javascript developer. I love Vue!", - "organisation": "crowd.dev", - "location": "Brazil", - "signals": null, - "joinedAt": "2022-01-05T14:40:47.000Z", - "importHash": null, - "tagsArray": [null], - "activities": [ - { - "type": "pull_request-comment", - "timestamp": "2022-01-14T22:30:03.000Z", - "platform": "github", - "info": {}, - "attributes": { "parent_url": "https://github.com/vuejs/core/pull/5228" }, - "isContribution": true, - "score": 3, - "sourceId": "#sourceId3", - "importHash": null, - "parentId": null, - "url": "https://github.com/vuejs/core/pull/5228#issuecomment-1013516865", - "body": "yes, I have some .native listeners, not sure how many, but I'll definitely search all places and go with your suggestion, thanks again, and sorry for the coffee joke, it wasn't my intention to \"buy\" a position in the queue, but to say how important this fix is for me :)", - "title": "fix(compat): ensure fallthrough *Native events are not dropped during props update (fix #5222)", - "channel": "https://github.com/vuejs/core" - }, - { - "type": "issue-comment", - "timestamp": "2022-01-07T21:59:37.000Z", - "platform": "github", - "info": {}, - "attributes": { "parent_url": "https://github.com/vuejs/core/issues/5222" }, - "isContribution": true, - "score": 3, - "sourceId": "#sourceId4", - "importHash": null, - "parentId": null, - "url": "https://github.com/vuejs/core/issues/5222#issuecomment-1007770056", - "body": "here you go: https://github.com/oswaldofreitas/vue-compat-issue", - "title": "@click.native works only first time", - "channel": "https://github.com/vuejs/core" - }, - { - "type": "issue-comment", - "timestamp": "2022-01-07T21:53:15.000Z", - "platform": "github", - "info": {}, - "attributes": { "parent_url": "https://github.com/vuejs/core/issues/5222" }, - "isContribution": true, - "score": 3, - "sourceId": "#sourceId5", - "importHash": null, - "parentId": null, - "url": "https://github.com/vuejs/core/issues/5222#issuecomment-1007766386", - "body": "sure, I can do it. I used that service since it's one of the suggested in the Vue 3 official docs and it was really hard to find one that I could setup the vue compat plugin. I'll create a repo for it now and share as soon as I get it.\nBefore sharing that link here I tested in incognito and it worked for me (without registering), you can click in the play button to run and in the \"Show files\" to see the code", - "title": "@click.native works only first time", - "channel": "https://github.com/vuejs/core" - }, - { - "type": "issues-closed", - "timestamp": "2022-01-07T20:33:21.000Z", - "platform": "github", - "info": {}, - "attributes": { "state": "closed" }, - "isContribution": true, - "score": 4, - "sourceId": "#sourceId6", - "importHash": null, - "parentId": null, - "url": "https://github.com/vuejs/core/issues/5222", - "body": "Version\n3.2.26\nReproduction link\nreplit.com\nSteps to reproduce\n\nclick in the dropdown\nselect an item\nclick in the dropdown again\n\nWhat is expected?\nthe dropdown should open again in the 2nd time\nWhat is actually happening?\nthe dropdown doesn't open\n\nWhen the input event is emitted from the form-dropdown component the event listener for the click event doesn't work anymore. I suppose it's a bug with the vue compat handling the .native modifier", - "title": "@click.native works only first time", - "channel": "https://github.com/vuejs/core" - }, - { - "type": "issue-comment", - "timestamp": "2022-01-05T15:01:11.000Z", - "platform": "github", - "info": {}, - "attributes": { "parent_url": "https://github.com/vuejs/core/issues/5210" }, - "isContribution": true, - "score": 3, - "sourceId": "#sourceId7", - "importHash": null, - "parentId": null, - "url": "https://github.com/vuejs/core/issues/5210#issuecomment-1005761356", - "body": "oh I see, you're right! Thank you", - "title": "Wrong detecting missing v-if in named slot template", - "channel": "https://github.com/vuejs/core" - }, - { - "type": "issues-closed", - "timestamp": "2022-01-05T14:40:47.100Z", - "platform": "github", - "info": {}, - "attributes": { "state": "closed" }, - "isContribution": true, - "score": 4, - "sourceId": "#sourceId8", - "importHash": null, - "parentId": null, - "url": "https://github.com/vuejs/core/issues/5210", - "body": "Version\n3.2.26\nReproduction link\nsfc.vuejs.org/\nSteps to reproduce\nIn the reproduction link there is this code:\n

H1

\n \n\nwhich is working, but if you add a v-else to the template tag it breaks\nWhat is expected?\n - - diff --git a/frontend/src/config/integrations/devto/components/devto-connect.vue b/frontend/src/config/integrations/devto/components/devto-connect.vue new file mode 100644 index 0000000000..e906f63908 --- /dev/null +++ b/frontend/src/config/integrations/devto/components/devto-connect.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/frontend/src/config/integrations/devto/components/devto-params.vue b/frontend/src/config/integrations/devto/components/devto-params.vue new file mode 100644 index 0000000000..a90762d0aa --- /dev/null +++ b/frontend/src/config/integrations/devto/components/devto-params.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/frontend/src/config/integrations/devto/config.ts b/frontend/src/config/integrations/devto/config.ts new file mode 100644 index 0000000000..da525b7d63 --- /dev/null +++ b/frontend/src/config/integrations/devto/config.ts @@ -0,0 +1,24 @@ +import { IntegrationConfig } from '@/config/integrations'; +import DevtoConnect from './components/devto-connect.vue'; +import DevtoParams from './components/devto-params.vue'; + +const image = new URL('@/assets/images/integrations/devto.png', import.meta.url).href; + +const devto: IntegrationConfig = { + key: 'devto', + name: 'DEV', + image, + description: 'Sync profile information and comments on articles.', + link: 'https://docs.linuxfoundation.org/lfx/community-management/integrations', + connectComponent: DevtoConnect, + connectedParamsComponent: DevtoParams, + showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + ], +}; + +export default devto; diff --git a/frontend/src/config/integrations/discord/components/discord-connect.vue b/frontend/src/config/integrations/discord/components/discord-connect.vue new file mode 100644 index 0000000000..17f321d683 --- /dev/null +++ b/frontend/src/config/integrations/discord/components/discord-connect.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/frontend/src/config/integrations/discord/components/discord-params.vue b/frontend/src/config/integrations/discord/components/discord-params.vue new file mode 100644 index 0000000000..4f1fa101a6 --- /dev/null +++ b/frontend/src/config/integrations/discord/components/discord-params.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/frontend/src/config/integrations/discord/config.ts b/frontend/src/config/integrations/discord/config.ts new file mode 100644 index 0000000000..d616cac728 --- /dev/null +++ b/frontend/src/config/integrations/discord/config.ts @@ -0,0 +1,24 @@ +import { IntegrationConfig } from '@/config/integrations'; +import DiscordConnect from './components/discord-connect.vue'; +import DiscordParams from './components/discord-params.vue'; + +const image = new URL('@/assets/images/integrations/discord.png', import.meta.url).href; + +const discord: IntegrationConfig = { + key: 'discord', + name: 'Discord', + image, + description: 'Sync messages, threads, forum channels, and new joiners.', + link: 'https://docs.linuxfoundation.org/lfx/community-management/integrations/discord-integration', + connectComponent: DiscordConnect, + connectedParamsComponent: DiscordParams, + showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + ], +}; + +export default discord; diff --git a/frontend/src/config/integrations/discourse/components/discourse-connect.vue b/frontend/src/config/integrations/discourse/components/discourse-connect.vue new file mode 100644 index 0000000000..50bc32751c --- /dev/null +++ b/frontend/src/config/integrations/discourse/components/discourse-connect.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/frontend/src/config/integrations/discourse/components/discourse-dropdown.vue b/frontend/src/config/integrations/discourse/components/discourse-dropdown.vue new file mode 100644 index 0000000000..ee51b43e4c --- /dev/null +++ b/frontend/src/config/integrations/discourse/components/discourse-dropdown.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/frontend/src/config/integrations/discourse/components/discourse-params.vue b/frontend/src/config/integrations/discourse/components/discourse-params.vue new file mode 100644 index 0000000000..1e10297953 --- /dev/null +++ b/frontend/src/config/integrations/discourse/components/discourse-params.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/frontend/src/config/integrations/discourse/components/discourse-settings-drawer.vue b/frontend/src/config/integrations/discourse/components/discourse-settings-drawer.vue new file mode 100644 index 0000000000..990f25dcbb --- /dev/null +++ b/frontend/src/config/integrations/discourse/components/discourse-settings-drawer.vue @@ -0,0 +1,470 @@ + + + + + diff --git a/frontend/src/config/integrations/discourse/config.ts b/frontend/src/config/integrations/discourse/config.ts new file mode 100644 index 0000000000..ac4f06295c --- /dev/null +++ b/frontend/src/config/integrations/discourse/config.ts @@ -0,0 +1,28 @@ +import { IntegrationConfig } from '@/config/integrations'; +import LfDiscourseSettingsDrawer from '@/config/integrations/discourse/components/discourse-settings-drawer.vue'; +import DiscourseConnect from './components/discourse-connect.vue'; +import DiscourseParams from './components/discourse-params.vue'; +import DiscourseDropdown from './components/discourse-dropdown.vue'; + +const image = new URL('@/assets/images/integrations/discourse.png', import.meta.url).href; + +const discourse: IntegrationConfig = { + key: 'discourse', + name: 'Discourse', + image, + description: 'Sync topics, posts, and replies from your account forums.', + link: 'https://docs.linuxfoundation.org/lfx/community-management/integrations', + connectComponent: DiscourseConnect, + connectedParamsComponent: DiscourseParams, + dropdownComponent: DiscourseDropdown, + settingComponent: LfDiscourseSettingsDrawer, + showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + ], +}; + +export default discourse; diff --git a/frontend/src/config/integrations/gerrit/components/gerrit-connect.vue b/frontend/src/config/integrations/gerrit/components/gerrit-connect.vue new file mode 100644 index 0000000000..5dbc7c3c51 --- /dev/null +++ b/frontend/src/config/integrations/gerrit/components/gerrit-connect.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/frontend/src/config/integrations/gerrit/components/gerrit-dropdown.vue b/frontend/src/config/integrations/gerrit/components/gerrit-dropdown.vue new file mode 100644 index 0000000000..af03e51d70 --- /dev/null +++ b/frontend/src/config/integrations/gerrit/components/gerrit-dropdown.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/frontend/src/config/integrations/gerrit/components/gerrit-params.vue b/frontend/src/config/integrations/gerrit/components/gerrit-params.vue new file mode 100644 index 0000000000..23319252d0 --- /dev/null +++ b/frontend/src/config/integrations/gerrit/components/gerrit-params.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/frontend/src/config/integrations/gerrit/components/gerrit-settings-drawer.vue b/frontend/src/config/integrations/gerrit/components/gerrit-settings-drawer.vue new file mode 100644 index 0000000000..35b367bdb2 --- /dev/null +++ b/frontend/src/config/integrations/gerrit/components/gerrit-settings-drawer.vue @@ -0,0 +1,298 @@ + + + + + diff --git a/frontend/src/config/integrations/gerrit/components/gerrit-settings-empty.vue b/frontend/src/config/integrations/gerrit/components/gerrit-settings-empty.vue new file mode 100644 index 0000000000..379aa42e5f --- /dev/null +++ b/frontend/src/config/integrations/gerrit/components/gerrit-settings-empty.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/frontend/src/config/integrations/gerrit/config.ts b/frontend/src/config/integrations/gerrit/config.ts new file mode 100644 index 0000000000..5f313e2cd0 --- /dev/null +++ b/frontend/src/config/integrations/gerrit/config.ts @@ -0,0 +1,28 @@ +import { IntegrationConfig } from '@/config/integrations'; +import LfGerritSettingsDrawer from '@/config/integrations/gerrit/components/gerrit-settings-drawer.vue'; +import GerritConnect from './components/gerrit-connect.vue'; +import GerritParams from './components/gerrit-params.vue'; +import GerritDropdown from './components/gerrit-dropdown.vue'; + +const image = new URL('@/assets/images/integrations/gerrit.png', import.meta.url).href; + +const gerrit: IntegrationConfig = { + key: 'gerrit', + name: 'Gerrit', + image, + description: 'Sync documentation activities from Gerrit repositories.', + link: 'https://docs.linuxfoundation.org/lfx/community-management/integrations/gerrit', + connectComponent: GerritConnect, + connectedParamsComponent: GerritParams, + dropdownComponent: GerritDropdown, + settingComponent: LfGerritSettingsDrawer, + showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + ], +}; + +export default gerrit; diff --git a/frontend/src/config/integrations/git/components/git-connect.vue b/frontend/src/config/integrations/git/components/git-connect.vue new file mode 100644 index 0000000000..fa1993afd1 --- /dev/null +++ b/frontend/src/config/integrations/git/components/git-connect.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/frontend/src/config/integrations/git/components/git-dropdown.vue b/frontend/src/config/integrations/git/components/git-dropdown.vue new file mode 100644 index 0000000000..2e3ac2bf63 --- /dev/null +++ b/frontend/src/config/integrations/git/components/git-dropdown.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/frontend/src/config/integrations/git/components/git-params.vue b/frontend/src/config/integrations/git/components/git-params.vue new file mode 100644 index 0000000000..f47da4f894 --- /dev/null +++ b/frontend/src/config/integrations/git/components/git-params.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/frontend/src/config/integrations/git/components/git-settings-drawer.vue b/frontend/src/config/integrations/git/components/git-settings-drawer.vue new file mode 100644 index 0000000000..d065b0cf7f --- /dev/null +++ b/frontend/src/config/integrations/git/components/git-settings-drawer.vue @@ -0,0 +1,327 @@ + + + + + diff --git a/frontend/src/config/integrations/git/components/git-settings-empty.vue b/frontend/src/config/integrations/git/components/git-settings-empty.vue new file mode 100644 index 0000000000..9747f44ad6 --- /dev/null +++ b/frontend/src/config/integrations/git/components/git-settings-empty.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/frontend/src/config/integrations/git/config.ts b/frontend/src/config/integrations/git/config.ts new file mode 100644 index 0000000000..6ac7f82283 --- /dev/null +++ b/frontend/src/config/integrations/git/config.ts @@ -0,0 +1,28 @@ +import { IntegrationConfig } from '@/config/integrations'; +import GitConnect from './components/git-connect.vue'; +import GitDropdown from './components/git-dropdown.vue'; +import GitParams from './components/git-params.vue'; +import LfGitSettingsDrawer from './components/git-settings-drawer.vue'; + +const image = new URL('@/assets/images/integrations/git.png', import.meta.url).href; + +const git: IntegrationConfig = { + key: 'git', + name: 'Git', + image, + description: 'Sync commit activities from Git repositories.', + link: 'https://docs.linuxfoundation.org/lfx/community-management/integrations/git-integration', + connectComponent: GitConnect, + dropdownComponent: GitDropdown, + connectedParamsComponent: GitParams, + settingComponent: LfGitSettingsDrawer, + showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + ], +}; + +export default git; diff --git a/frontend/src/config/integrations/github-nango/components/github-details-modal.vue b/frontend/src/config/integrations/github-nango/components/github-details-modal.vue new file mode 100644 index 0000000000..a44dfd1b1e --- /dev/null +++ b/frontend/src/config/integrations/github-nango/components/github-details-modal.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/frontend/src/config/integrations/github-nango/components/github-dropdown.vue b/frontend/src/config/integrations/github-nango/components/github-dropdown.vue new file mode 100644 index 0000000000..475eb4df3a --- /dev/null +++ b/frontend/src/config/integrations/github-nango/components/github-dropdown.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/frontend/src/config/integrations/github-nango/components/github-params.vue b/frontend/src/config/integrations/github-nango/components/github-params.vue new file mode 100644 index 0000000000..e0281b05dd --- /dev/null +++ b/frontend/src/config/integrations/github-nango/components/github-params.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/frontend/src/config/integrations/github-nango/components/settings/github-settings-add-repository-modal.vue b/frontend/src/config/integrations/github-nango/components/settings/github-settings-add-repository-modal.vue new file mode 100644 index 0000000000..94c062830d --- /dev/null +++ b/frontend/src/config/integrations/github-nango/components/settings/github-settings-add-repository-modal.vue @@ -0,0 +1,424 @@ + + + + + diff --git a/frontend/src/config/integrations/github-nango/components/settings/github-settings-drawer.vue b/frontend/src/config/integrations/github-nango/components/settings/github-settings-drawer.vue new file mode 100644 index 0000000000..a0b9e07fda --- /dev/null +++ b/frontend/src/config/integrations/github-nango/components/settings/github-settings-drawer.vue @@ -0,0 +1,354 @@ + + + + + diff --git a/frontend/src/config/integrations/github-nango/components/settings/github-settings-empty.vue b/frontend/src/config/integrations/github-nango/components/settings/github-settings-empty.vue new file mode 100644 index 0000000000..34b80e4745 --- /dev/null +++ b/frontend/src/config/integrations/github-nango/components/settings/github-settings-empty.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/frontend/src/config/integrations/github-nango/components/settings/github-settings-mapping.vue b/frontend/src/config/integrations/github-nango/components/settings/github-settings-mapping.vue new file mode 100644 index 0000000000..eca6bc86f6 --- /dev/null +++ b/frontend/src/config/integrations/github-nango/components/settings/github-settings-mapping.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/frontend/src/config/integrations/github-nango/components/settings/github-settings-org-item.vue b/frontend/src/config/integrations/github-nango/components/settings/github-settings-org-item.vue new file mode 100644 index 0000000000..d118e1cb51 --- /dev/null +++ b/frontend/src/config/integrations/github-nango/components/settings/github-settings-org-item.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/frontend/src/config/integrations/github-nango/components/settings/github-settings-repo-item.vue b/frontend/src/config/integrations/github-nango/components/settings/github-settings-repo-item.vue new file mode 100644 index 0000000000..9fa2aea6ae --- /dev/null +++ b/frontend/src/config/integrations/github-nango/components/settings/github-settings-repo-item.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/frontend/src/config/integrations/github-nango/components/settings/github-settings-repositories-bulk-select.vue b/frontend/src/config/integrations/github-nango/components/settings/github-settings-repositories-bulk-select.vue new file mode 100644 index 0000000000..a75de65a59 --- /dev/null +++ b/frontend/src/config/integrations/github-nango/components/settings/github-settings-repositories-bulk-select.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/frontend/src/config/integrations/github-nango/config.ts b/frontend/src/config/integrations/github-nango/config.ts new file mode 100644 index 0000000000..1d12030655 --- /dev/null +++ b/frontend/src/config/integrations/github-nango/config.ts @@ -0,0 +1,36 @@ +import { IntegrationConfig } from '@/config/integrations'; +import LfGithubSettingsDrawer from '@/config/integrations/github-nango/components/settings/github-settings-drawer.vue'; +// For now we will be referencing the connect component from the github (old) integration +import GithubConnect from '@/config/integrations/github/components/github-connect.vue'; +import GithubMappedRepos from '@/config/integrations/github/components/github-mapped-repos.vue'; +import GithubParams from './components/github-params.vue'; +import GithubDropdown from './components/github-dropdown.vue'; + +const image = new URL('@/assets/images/integrations/github.png', import.meta.url).href; + +const github: IntegrationConfig = { + key: 'github', + name: 'GitHub (v2)', + image, + description: 'Sync profile information, stars, forks, pull requests, issues, and discussions.', + link: 'https://docs.linuxfoundation.org/lfx/community-management/integrations/github-integration', + connectComponent: GithubConnect, + dropdownComponent: GithubDropdown, + statusComponent: GithubParams, + connectedParamsComponent: GithubParams, + mappedReposComponent: GithubMappedRepos, + settingComponent: LfGithubSettingsDrawer, + showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + { + key: 'mapping', + text: 'Select repositories to track and map them to projects.', + }, + ], +}; + +export default github; diff --git a/frontend/src/config/integrations/github-nango/services/github.api.service.ts b/frontend/src/config/integrations/github-nango/services/github.api.service.ts new file mode 100644 index 0000000000..bd003f63b0 --- /dev/null +++ b/frontend/src/config/integrations/github-nango/services/github.api.service.ts @@ -0,0 +1,50 @@ +import authAxios from '@/shared/axios/auth-axios'; +import { + GitHubOrganization, + GitHubRepository, +} from '@/config/integrations/github-nango/types/GithubSettings'; +import { Pagination } from '@/shared/types/Pagination'; + +export class GithubApiService { + static async searchRepositories( + query: string, + offset: number = 0, + limit: number = 20, + ): Promise> { + const response = await authAxios.get('/integration/github/search/repos', { + params: { + query, + offset, + limit, + }, + }); + + return response.data; + } + + static async searchOrganizations( + query: string, + offset: number = 0, + limit: number = 20, + ): Promise> { + const response = await authAxios.get('/integration/github/search/orgs', { + params: { + query, + offset, + limit, + }, + }); + + return response.data; + } + + static async getOrganizationRepositories( + name: string, + ): Promise { + const response = await authAxios.get( + `/integration/github/orgs/${name}/repos`, + ); + + return response.data; + } +} diff --git a/frontend/src/config/integrations/github-nango/types/GithubSettings.ts b/frontend/src/config/integrations/github-nango/types/GithubSettings.ts new file mode 100644 index 0000000000..5f4b648b7b --- /dev/null +++ b/frontend/src/config/integrations/github-nango/types/GithubSettings.ts @@ -0,0 +1,26 @@ +export interface GitHubOrganization { + logo: string; + name: string; + url: string; + updatedAt?: string; +} + +export interface GitHubRepository { + name: string; + url: string; + forkedFrom?: string | null; + org?: GitHubOrganization; +} + +export interface GitHubSettingsRepository extends GitHubRepository { + updatedAt?: string; +} + +export interface GitHubSettingsOrganization extends GitHubOrganization { + fullSync: boolean; + repos: GitHubSettingsRepository[]; +} +export interface GitHubSettings { + orgs: GitHubSettingsOrganization[]; + updateMemberAttributes: boolean; +} diff --git a/frontend/src/config/integrations/github/components/connect/github-connect-finishing-modal.vue b/frontend/src/config/integrations/github/components/connect/github-connect-finishing-modal.vue new file mode 100644 index 0000000000..17a33517fa --- /dev/null +++ b/frontend/src/config/integrations/github/components/connect/github-connect-finishing-modal.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/frontend/src/config/integrations/github/components/connect/github-connect-modal.vue b/frontend/src/config/integrations/github/components/connect/github-connect-modal.vue new file mode 100644 index 0000000000..43466c613f --- /dev/null +++ b/frontend/src/config/integrations/github/components/connect/github-connect-modal.vue @@ -0,0 +1,221 @@ + + + + + diff --git a/frontend/src/config/integrations/github/components/github-action.vue b/frontend/src/config/integrations/github/components/github-action.vue new file mode 100644 index 0000000000..70c17d1a1f --- /dev/null +++ b/frontend/src/config/integrations/github/components/github-action.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/frontend/src/config/integrations/github/components/github-connect.vue b/frontend/src/config/integrations/github/components/github-connect.vue new file mode 100644 index 0000000000..13c5f6dcf1 --- /dev/null +++ b/frontend/src/config/integrations/github/components/github-connect.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/frontend/src/config/integrations/github/components/github-details-modal.vue b/frontend/src/config/integrations/github/components/github-details-modal.vue new file mode 100644 index 0000000000..a44dfd1b1e --- /dev/null +++ b/frontend/src/config/integrations/github/components/github-details-modal.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/frontend/src/config/integrations/github/components/github-mapped-repos.vue b/frontend/src/config/integrations/github/components/github-mapped-repos.vue new file mode 100644 index 0000000000..60ee52d806 --- /dev/null +++ b/frontend/src/config/integrations/github/components/github-mapped-repos.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/frontend/src/config/integrations/github/components/github-mappings-display.vue b/frontend/src/config/integrations/github/components/github-mappings-display.vue new file mode 100644 index 0000000000..431d182ad7 --- /dev/null +++ b/frontend/src/config/integrations/github/components/github-mappings-display.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/frontend/src/config/integrations/github/components/github-params.vue b/frontend/src/config/integrations/github/components/github-params.vue new file mode 100644 index 0000000000..942c81718f --- /dev/null +++ b/frontend/src/config/integrations/github/components/github-params.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/frontend/src/config/integrations/github/components/github-status.vue b/frontend/src/config/integrations/github/components/github-status.vue new file mode 100644 index 0000000000..9559fe4f32 --- /dev/null +++ b/frontend/src/config/integrations/github/components/github-status.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/frontend/src/config/integrations/github/components/github-version-tag.vue b/frontend/src/config/integrations/github/components/github-version-tag.vue new file mode 100644 index 0000000000..164f2e09b8 --- /dev/null +++ b/frontend/src/config/integrations/github/components/github-version-tag.vue @@ -0,0 +1,28 @@ + + + diff --git a/frontend/src/config/integrations/github/components/settings/github-settings-bulk-select.vue b/frontend/src/config/integrations/github/components/settings/github-settings-bulk-select.vue new file mode 100644 index 0000000000..ff878744e1 --- /dev/null +++ b/frontend/src/config/integrations/github/components/settings/github-settings-bulk-select.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/frontend/src/config/integrations/github/components/settings/github-settings-drawer.vue b/frontend/src/config/integrations/github/components/settings/github-settings-drawer.vue new file mode 100644 index 0000000000..19ca04d0e9 --- /dev/null +++ b/frontend/src/config/integrations/github/components/settings/github-settings-drawer.vue @@ -0,0 +1,448 @@ + + + + + diff --git a/frontend/src/config/integrations/github/config.ts b/frontend/src/config/integrations/github/config.ts new file mode 100644 index 0000000000..4a33ae9771 --- /dev/null +++ b/frontend/src/config/integrations/github/config.ts @@ -0,0 +1,38 @@ +import { IntegrationConfig } from '@/config/integrations'; +import GithubConnect from './components/github-connect.vue'; +import GithubStatus from './components/github-status.vue'; +import GithubAction from './components/github-action.vue'; +import GithubParams from './components/github-params.vue'; +import GithubMappedRepos from './components/github-mapped-repos.vue'; + +const image = new URL('@/assets/images/integrations/github.png', import.meta.url).href; + +const github: IntegrationConfig = { + key: 'github', + name: 'GitHub', + image, + description: 'Sync profile information, stars, forks, pull requests, issues, and discussions.', + link: 'https://docs.linuxfoundation.org/lfx/community-management/integrations/github-integration', + connectComponent: GithubConnect, + statusComponent: GithubStatus, + actionComponent: GithubAction, + connectedParamsComponent: GithubParams, + mappedReposComponent: GithubMappedRepos, + showProgress: true, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + { + key: 'mapping', + text: 'Select repositories to track and map them to projects.', + }, + { + key: 'waiting-approval', + text: 'Waiting for organization admin to approve the installation.', + }, + ], +}; + +export default github; diff --git a/frontend/src/config/integrations/gitlab/components/gitlab-action.vue b/frontend/src/config/integrations/gitlab/components/gitlab-action.vue new file mode 100644 index 0000000000..e555388903 --- /dev/null +++ b/frontend/src/config/integrations/gitlab/components/gitlab-action.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/frontend/src/config/integrations/gitlab/components/gitlab-connect.vue b/frontend/src/config/integrations/gitlab/components/gitlab-connect.vue new file mode 100644 index 0000000000..cf40fbc0ce --- /dev/null +++ b/frontend/src/config/integrations/gitlab/components/gitlab-connect.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/frontend/src/config/integrations/gitlab/components/gitlab-params.vue b/frontend/src/config/integrations/gitlab/components/gitlab-params.vue new file mode 100644 index 0000000000..0e1b459878 --- /dev/null +++ b/frontend/src/config/integrations/gitlab/components/gitlab-params.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/frontend/src/config/integrations/gitlab/components/gitlab-settings-bulk-select.vue b/frontend/src/config/integrations/gitlab/components/gitlab-settings-bulk-select.vue new file mode 100644 index 0000000000..014334e03d --- /dev/null +++ b/frontend/src/config/integrations/gitlab/components/gitlab-settings-bulk-select.vue @@ -0,0 +1,166 @@ + + + + + diff --git a/frontend/src/config/integrations/gitlab/components/gitlab-settings-drawer.vue b/frontend/src/config/integrations/gitlab/components/gitlab-settings-drawer.vue new file mode 100644 index 0000000000..297a2e5249 --- /dev/null +++ b/frontend/src/config/integrations/gitlab/components/gitlab-settings-drawer.vue @@ -0,0 +1,486 @@ + + + + + diff --git a/frontend/src/config/integrations/gitlab/components/gitlab-status.vue b/frontend/src/config/integrations/gitlab/components/gitlab-status.vue new file mode 100644 index 0000000000..02f3011713 --- /dev/null +++ b/frontend/src/config/integrations/gitlab/components/gitlab-status.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/frontend/src/config/integrations/gitlab/config.ts b/frontend/src/config/integrations/gitlab/config.ts new file mode 100644 index 0000000000..148482f87f --- /dev/null +++ b/frontend/src/config/integrations/gitlab/config.ts @@ -0,0 +1,32 @@ +import { IntegrationConfig } from '@/config/integrations'; +import GitlabConnect from './components/gitlab-connect.vue'; +import GitlabParams from './components/gitlab-params.vue'; +import GitlabAction from './components/gitlab-action.vue'; +import GitlabStatus from './components/gitlab-status.vue'; + +const image = new URL('@/assets/images/integrations/gitlab.png', import.meta.url).href; + +const gitlab: IntegrationConfig = { + key: 'gitlab', + name: 'GitLab', + image, + description: 'Sync profile information, merge requests, issues, and more.', + link: 'https://docs.linuxfoundation.org/lfx/community-management/integrations', + connectComponent: GitlabConnect, + connectedParamsComponent: GitlabParams, + actionComponent: GitlabAction, + statusComponent: GitlabStatus, + showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + { + key: 'mapping', + text: 'Select repositories to track and map them to projects.', + }, + ], +}; + +export default gitlab; diff --git a/frontend/src/config/integrations/groupsio/components/groupsio-connect.vue b/frontend/src/config/integrations/groupsio/components/groupsio-connect.vue new file mode 100644 index 0000000000..7e24261838 --- /dev/null +++ b/frontend/src/config/integrations/groupsio/components/groupsio-connect.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/frontend/src/config/integrations/groupsio/components/groupsio-dropdown.vue b/frontend/src/config/integrations/groupsio/components/groupsio-dropdown.vue new file mode 100644 index 0000000000..6d5632e253 --- /dev/null +++ b/frontend/src/config/integrations/groupsio/components/groupsio-dropdown.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/frontend/src/config/integrations/groupsio/components/groupsio-params.vue b/frontend/src/config/integrations/groupsio/components/groupsio-params.vue new file mode 100644 index 0000000000..dbe2ee6d51 --- /dev/null +++ b/frontend/src/config/integrations/groupsio/components/groupsio-params.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/frontend/src/config/integrations/groupsio/components/groupsio-settings-drawer.vue b/frontend/src/config/integrations/groupsio/components/groupsio-settings-drawer.vue new file mode 100644 index 0000000000..625ff57f87 --- /dev/null +++ b/frontend/src/config/integrations/groupsio/components/groupsio-settings-drawer.vue @@ -0,0 +1,664 @@ + + + + + + + diff --git a/frontend/src/config/integrations/groupsio/config.ts b/frontend/src/config/integrations/groupsio/config.ts new file mode 100644 index 0000000000..eaba86f72c --- /dev/null +++ b/frontend/src/config/integrations/groupsio/config.ts @@ -0,0 +1,28 @@ +import { IntegrationConfig } from '@/config/integrations'; +import GroupsioConnect from './components/groupsio-connect.vue'; +import GroupsioParams from './components/groupsio-params.vue'; +import GroupsioDropdown from './components/groupsio-dropdown.vue'; +import LfGroupsioSettingsDrawer from './components/groupsio-settings-drawer.vue'; + +const image = new URL('@/assets/images/integrations/groupsio.svg', import.meta.url).href; + +const groupsio: IntegrationConfig = { + key: 'groupsio', + name: 'Groups.io', + image, + description: 'Sync groups and topics activity.', + link: 'https://docs.linuxfoundation.org/lfx/community-management/integrations/groups.io', + connectComponent: GroupsioConnect, + connectedParamsComponent: GroupsioParams, + dropdownComponent: GroupsioDropdown, + settingComponent: LfGroupsioSettingsDrawer, + showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + ], +}; + +export default groupsio; diff --git a/frontend/src/config/integrations/hackernews/components/hackernews-connect.vue b/frontend/src/config/integrations/hackernews/components/hackernews-connect.vue new file mode 100644 index 0000000000..7f9459361c --- /dev/null +++ b/frontend/src/config/integrations/hackernews/components/hackernews-connect.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/frontend/src/config/integrations/hackernews/components/hackernews-params.vue b/frontend/src/config/integrations/hackernews/components/hackernews-params.vue new file mode 100644 index 0000000000..570c7dc65a --- /dev/null +++ b/frontend/src/config/integrations/hackernews/components/hackernews-params.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/frontend/src/config/integrations/hackernews/components/hackernews-settings-drawer.vue b/frontend/src/config/integrations/hackernews/components/hackernews-settings-drawer.vue new file mode 100644 index 0000000000..78deff031f --- /dev/null +++ b/frontend/src/config/integrations/hackernews/components/hackernews-settings-drawer.vue @@ -0,0 +1,350 @@ + + + + diff --git a/frontend/src/config/integrations/hackernews/config.ts b/frontend/src/config/integrations/hackernews/config.ts new file mode 100644 index 0000000000..453a6421d0 --- /dev/null +++ b/frontend/src/config/integrations/hackernews/config.ts @@ -0,0 +1,24 @@ +import { IntegrationConfig } from '@/config/integrations'; +import HackernewsConnect from './components/hackernews-connect.vue'; +import HackernewsParams from './components/hackernews-params.vue'; + +const image = new URL('@/assets/images/integrations/hackernews.svg', import.meta.url).href; + +const hackernews: IntegrationConfig = { + key: 'hackernews', + name: 'Hacker News', + image, + description: 'Sync posts and comments mentioning your community.', + link: 'https://docs.linuxfoundation.org/lfx/community-management/integrations/hacker-news-integration', + connectComponent: HackernewsConnect, + connectedParamsComponent: HackernewsParams, + showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + ], +}; + +export default hackernews; diff --git a/frontend/src/config/integrations/index.ts b/frontend/src/config/integrations/index.ts new file mode 100644 index 0000000000..5bf0a28edd --- /dev/null +++ b/frontend/src/config/integrations/index.ts @@ -0,0 +1,70 @@ +import config from '@/config'; +import github from './github/config'; +import githubNango from './github-nango/config'; +import git from './git/config'; +import groupsio from './groupsio/config'; +import confluence from './confluence/config'; +import jira from './jira/config'; +import slack from './slack/config'; +import discord from './discord/config'; +import linkedin from './linkedin/config'; +import twitter from './twitter/config'; +import reddit from './reddit/config'; +import hackernews from './hackernews/config'; +import stackoverflow from './stackoverflow/config'; +import gitlab from './gitlab/config'; +import gerrit from './gerrit/config'; +import discourse from './discourse/config'; +import devto from './devto/config'; + +export interface ActionRequiredMessage { + key: string + text: string +} +export interface IntegrationConfig { + key: string // Unique key for the integration + name: string // Display name of the integration + image: string // Image URL for the integration + description: string // Description of the integration + link?: string // Documentation link for the integration + connectComponent?: Vue.Component // Component rendered for user to connect integration + actionComponent?: Vue.Component // Component rendered when integration needs user action + statusComponent?: Vue.Component // Component rendered to show integration status + connectedParamsComponent?: Vue.Component // Component rendered to show connected integration params (repositories, channels) + dropdownComponent?: Vue.Component // Component rendered inside dropdown for extra options + settingComponent?: Vue.Component // Component rendered next to dropdown for extra options + mappedReposComponent?: Vue.Component // Component rendered to show mapped repositories + showProgress: boolean // Show progress bar when connecting + actionRequiredMessage?: ActionRequiredMessage[] +} + +export const getGithubIntegration = () => { + if (config.env === 'staging') { + const useGitHubNango = localStorage.getItem('useGitHubNango') === 'true'; + + return useGitHubNango ? githubNango : github; + } + + return github; +}; + +export const lfIntegrations: (useGitHubNango?: boolean) => Record = ( + useGitHubNango?: boolean, +) => ({ + github: useGitHubNango ? githubNango : getGithubIntegration(), + git, + gitlab, + gerrit, + groupsio, + confluence, + jira, + slack, + discord, + linkedin, + twitter, + reddit, + hackernews, + stackoverflow, + discourse, + devto, +}); diff --git a/frontend/src/config/integrations/integrations.helpers.ts b/frontend/src/config/integrations/integrations.helpers.ts new file mode 100644 index 0000000000..1f6db2a145 --- /dev/null +++ b/frontend/src/config/integrations/integrations.helpers.ts @@ -0,0 +1,20 @@ +import { mapGetters } from '@/shared/vuex/vuex.helpers'; +import { lfIntegrations } from '@/config/integrations/index'; + +const useIntegrationsHelpers = () => { + const getActiveIntegrations = () => { + const { findByPlatform }: any = mapGetters('integrations'); + + return Object.values(lfIntegrations()).map((config) => ({ + ...config, + ...(findByPlatform ? findByPlatform(config.key) : {}), + })) + .filter((integration) => integration.status); + }; + + return { + getActiveIntegrations, + }; +}; + +export default useIntegrationsHelpers; diff --git a/frontend/src/config/integrations/jira/components/jira-connect.vue b/frontend/src/config/integrations/jira/components/jira-connect.vue new file mode 100644 index 0000000000..61ee1b9bcb --- /dev/null +++ b/frontend/src/config/integrations/jira/components/jira-connect.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/frontend/src/config/integrations/jira/components/jira-dropdown.vue b/frontend/src/config/integrations/jira/components/jira-dropdown.vue new file mode 100644 index 0000000000..e3c5e23ede --- /dev/null +++ b/frontend/src/config/integrations/jira/components/jira-dropdown.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/frontend/src/config/integrations/jira/components/jira-params.vue b/frontend/src/config/integrations/jira/components/jira-params.vue new file mode 100644 index 0000000000..7983d65400 --- /dev/null +++ b/frontend/src/config/integrations/jira/components/jira-params.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/frontend/src/config/integrations/jira/components/jira-settings-drawer.vue b/frontend/src/config/integrations/jira/components/jira-settings-drawer.vue new file mode 100644 index 0000000000..ec348a0e90 --- /dev/null +++ b/frontend/src/config/integrations/jira/components/jira-settings-drawer.vue @@ -0,0 +1,297 @@ + + + + + diff --git a/frontend/src/config/integrations/jira/config.ts b/frontend/src/config/integrations/jira/config.ts new file mode 100644 index 0000000000..4c84de31cb --- /dev/null +++ b/frontend/src/config/integrations/jira/config.ts @@ -0,0 +1,28 @@ +import { IntegrationConfig } from '@/config/integrations'; +import LfJiraSettingsDrawer from '@/config/integrations/jira/components/jira-settings-drawer.vue'; +import JiraConnect from './components/jira-connect.vue'; +import JiraParams from './components/jira-params.vue'; +import JiraDropdown from './components/jira-dropdown.vue'; + +const image = new URL('@/assets/images/integrations/jira.png', import.meta.url).href; + +const jira: IntegrationConfig = { + key: 'jira', + name: 'Jira', + image, + description: 'Sync issues activities from your projects.', + link: 'https://docs.linuxfoundation.org/lfx/community-management/integrations', + connectComponent: JiraConnect, + connectedParamsComponent: JiraParams, + dropdownComponent: JiraDropdown, + settingComponent: LfJiraSettingsDrawer, + showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + ], +}; + +export default jira; diff --git a/frontend/src/config/integrations/linkedin/components/linkedin-action.vue b/frontend/src/config/integrations/linkedin/components/linkedin-action.vue new file mode 100644 index 0000000000..7a857a572f --- /dev/null +++ b/frontend/src/config/integrations/linkedin/components/linkedin-action.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/frontend/src/config/integrations/linkedin/components/linkedin-connect.vue b/frontend/src/config/integrations/linkedin/components/linkedin-connect.vue new file mode 100644 index 0000000000..88c9ce1a1a --- /dev/null +++ b/frontend/src/config/integrations/linkedin/components/linkedin-connect.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/frontend/src/config/integrations/linkedin/components/linkedin-dropdown.vue b/frontend/src/config/integrations/linkedin/components/linkedin-dropdown.vue new file mode 100644 index 0000000000..788f380aab --- /dev/null +++ b/frontend/src/config/integrations/linkedin/components/linkedin-dropdown.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/frontend/src/config/integrations/linkedin/components/linkedin-params.vue b/frontend/src/config/integrations/linkedin/components/linkedin-params.vue new file mode 100644 index 0000000000..b7e031cb65 --- /dev/null +++ b/frontend/src/config/integrations/linkedin/components/linkedin-params.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/frontend/src/config/integrations/linkedin/components/linkedin-settings-drawer.vue b/frontend/src/config/integrations/linkedin/components/linkedin-settings-drawer.vue new file mode 100644 index 0000000000..89e0d865c9 --- /dev/null +++ b/frontend/src/config/integrations/linkedin/components/linkedin-settings-drawer.vue @@ -0,0 +1,208 @@ + + + + + diff --git a/frontend/src/config/integrations/linkedin/config.ts b/frontend/src/config/integrations/linkedin/config.ts new file mode 100644 index 0000000000..8b07303a98 --- /dev/null +++ b/frontend/src/config/integrations/linkedin/config.ts @@ -0,0 +1,34 @@ +import { IntegrationConfig } from '@/config/integrations'; +import LfLinkedinSettingsDrawer from '@/config/integrations/linkedin/components/linkedin-settings-drawer.vue'; +import LinkedinConnect from './components/linkedin-connect.vue'; +import LinkedinParams from './components/linkedin-params.vue'; +import LinkedinAction from './components/linkedin-action.vue'; +import LinkedinDropdown from './components/linkedin-dropdown.vue'; + +const image = new URL('@/assets/images/integrations/linkedin.png', import.meta.url).href; + +const linkedin: IntegrationConfig = { + key: 'linkedin', + name: 'LinkedIn', + image, + description: "Sync comments and reactions from your organization's posts.", + link: 'https://docs.linuxfoundation.org/lfx/community-management/integrations/linkedin-integration', + connectComponent: LinkedinConnect, + connectedParamsComponent: LinkedinParams, + actionComponent: LinkedinAction, + dropdownComponent: LinkedinDropdown, + settingComponent: LfLinkedinSettingsDrawer, + showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + { + key: 'pending-action', + text: 'Select the LinkedIn organization to connect.', + }, + ], +}; + +export default linkedin; diff --git a/frontend/src/config/integrations/reddit/components/reddit-connect.vue b/frontend/src/config/integrations/reddit/components/reddit-connect.vue new file mode 100644 index 0000000000..fd1ef05a22 --- /dev/null +++ b/frontend/src/config/integrations/reddit/components/reddit-connect.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/frontend/src/config/integrations/reddit/components/reddit-dropdown.vue b/frontend/src/config/integrations/reddit/components/reddit-dropdown.vue new file mode 100644 index 0000000000..ca242fa9ec --- /dev/null +++ b/frontend/src/config/integrations/reddit/components/reddit-dropdown.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/frontend/src/config/integrations/reddit/components/reddit-params.vue b/frontend/src/config/integrations/reddit/components/reddit-params.vue new file mode 100644 index 0000000000..79c3dc70fb --- /dev/null +++ b/frontend/src/config/integrations/reddit/components/reddit-params.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/frontend/src/config/integrations/reddit/components/reddit-settings-drawer.vue b/frontend/src/config/integrations/reddit/components/reddit-settings-drawer.vue new file mode 100644 index 0000000000..e27bc00556 --- /dev/null +++ b/frontend/src/config/integrations/reddit/components/reddit-settings-drawer.vue @@ -0,0 +1,278 @@ + + + + + + + diff --git a/frontend/src/config/integrations/reddit/config.ts b/frontend/src/config/integrations/reddit/config.ts new file mode 100644 index 0000000000..1cb1cb7b21 --- /dev/null +++ b/frontend/src/config/integrations/reddit/config.ts @@ -0,0 +1,28 @@ +import { IntegrationConfig } from '@/config/integrations'; +import LfRedditSettingsDrawer from '@/config/integrations/reddit/components/reddit-settings-drawer.vue'; +import RedditConnect from './components/reddit-connect.vue'; +import RedditParams from './components/reddit-params.vue'; +import RedditDropdown from './components/reddit-dropdown.vue'; + +const image = new URL('@/assets/images/integrations/reddit.svg', import.meta.url).href; + +const reddit: IntegrationConfig = { + key: 'reddit', + name: 'Reddit', + image, + description: 'Sync posts and comments from selected subreddits.', + link: 'https://docs.linuxfoundation.org/lfx/community-management/integrations/reddit-integration', + connectComponent: RedditConnect, + connectedParamsComponent: RedditParams, + dropdownComponent: RedditDropdown, + settingComponent: LfRedditSettingsDrawer, + showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + ], +}; + +export default reddit; diff --git a/frontend/src/config/integrations/slack/components/slack-connect.vue b/frontend/src/config/integrations/slack/components/slack-connect.vue new file mode 100644 index 0000000000..8e621dc8e1 --- /dev/null +++ b/frontend/src/config/integrations/slack/components/slack-connect.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/frontend/src/config/integrations/slack/components/slack-params.vue b/frontend/src/config/integrations/slack/components/slack-params.vue new file mode 100644 index 0000000000..6d50e643a7 --- /dev/null +++ b/frontend/src/config/integrations/slack/components/slack-params.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/frontend/src/config/integrations/slack/config.ts b/frontend/src/config/integrations/slack/config.ts new file mode 100644 index 0000000000..b481c19744 --- /dev/null +++ b/frontend/src/config/integrations/slack/config.ts @@ -0,0 +1,24 @@ +import { IntegrationConfig } from '@/config/integrations'; +import SlackConnect from './components/slack-connect.vue'; +import SlackParams from './components/slack-params.vue'; + +const image = new URL('@/assets/images/integrations/slack.png', import.meta.url).href; + +const slack: IntegrationConfig = { + key: 'slack', + name: 'Slack', + image, + description: 'Sync messages, threads, and new joiners.', + link: 'https://docs.linuxfoundation.org/lfx/community-management/integrations/slack', + connectComponent: SlackConnect, + connectedParamsComponent: SlackParams, + showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + ], +}; + +export default slack; diff --git a/frontend/src/config/integrations/stackoverflow/components/stackoverflow-connect.vue b/frontend/src/config/integrations/stackoverflow/components/stackoverflow-connect.vue new file mode 100644 index 0000000000..d2a7a39307 --- /dev/null +++ b/frontend/src/config/integrations/stackoverflow/components/stackoverflow-connect.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/frontend/src/config/integrations/stackoverflow/components/stackoverflow-dropdown.vue b/frontend/src/config/integrations/stackoverflow/components/stackoverflow-dropdown.vue new file mode 100644 index 0000000000..1f8dfcab9f --- /dev/null +++ b/frontend/src/config/integrations/stackoverflow/components/stackoverflow-dropdown.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/frontend/src/config/integrations/stackoverflow/components/stackoverflow-params.vue b/frontend/src/config/integrations/stackoverflow/components/stackoverflow-params.vue new file mode 100644 index 0000000000..05af62c945 --- /dev/null +++ b/frontend/src/config/integrations/stackoverflow/components/stackoverflow-params.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/frontend/src/config/integrations/stackoverflow/components/stackoverflow-settings-drawer.vue b/frontend/src/config/integrations/stackoverflow/components/stackoverflow-settings-drawer.vue new file mode 100644 index 0000000000..96bdfe3734 --- /dev/null +++ b/frontend/src/config/integrations/stackoverflow/components/stackoverflow-settings-drawer.vue @@ -0,0 +1,441 @@ + + + + + diff --git a/frontend/src/config/integrations/stackoverflow/config.ts b/frontend/src/config/integrations/stackoverflow/config.ts new file mode 100644 index 0000000000..91a27bba5c --- /dev/null +++ b/frontend/src/config/integrations/stackoverflow/config.ts @@ -0,0 +1,28 @@ +import { IntegrationConfig } from '@/config/integrations'; +import LfStackoverflowSettingsDrawer from '@/config/integrations/stackoverflow/components/stackoverflow-settings-drawer.vue'; +import StackoverflowConnect from './components/stackoverflow-connect.vue'; +import StackoverflowDropdown from './components/stackoverflow-dropdown.vue'; +import StackoverflowParams from './components/stackoverflow-params.vue'; + +const image = new URL('@/assets/images/integrations/stackoverflow.png', import.meta.url).href; + +const stackoverflow: IntegrationConfig = { + key: 'stackoverflow', + name: 'Stack Overflow', + image, + description: 'Sync questions and answers based on selected tags.', + link: 'https://docs.linuxfoundation.org/lfx/community-management/integrations/stack-overflow', + connectComponent: StackoverflowConnect, + dropdownComponent: StackoverflowDropdown, + connectedParamsComponent: StackoverflowParams, + settingComponent: LfStackoverflowSettingsDrawer, + showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + ], +}; + +export default stackoverflow; diff --git a/frontend/src/config/integrations/twitter/components/twitter-connect.vue b/frontend/src/config/integrations/twitter/components/twitter-connect.vue new file mode 100644 index 0000000000..1b0d4fd170 --- /dev/null +++ b/frontend/src/config/integrations/twitter/components/twitter-connect.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/frontend/src/config/integrations/twitter/components/twitter-dropdown.vue b/frontend/src/config/integrations/twitter/components/twitter-dropdown.vue new file mode 100644 index 0000000000..19ef5b507b --- /dev/null +++ b/frontend/src/config/integrations/twitter/components/twitter-dropdown.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/frontend/src/config/integrations/twitter/components/twitter-params.vue b/frontend/src/config/integrations/twitter/components/twitter-params.vue new file mode 100644 index 0000000000..f67a432924 --- /dev/null +++ b/frontend/src/config/integrations/twitter/components/twitter-params.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/frontend/src/config/integrations/twitter/components/twitter-settings-drawer.vue b/frontend/src/config/integrations/twitter/components/twitter-settings-drawer.vue new file mode 100644 index 0000000000..96c7001f8e --- /dev/null +++ b/frontend/src/config/integrations/twitter/components/twitter-settings-drawer.vue @@ -0,0 +1,195 @@ + + + + + + + diff --git a/frontend/src/config/integrations/twitter/config.ts b/frontend/src/config/integrations/twitter/config.ts new file mode 100644 index 0000000000..d8182f9cc6 --- /dev/null +++ b/frontend/src/config/integrations/twitter/config.ts @@ -0,0 +1,28 @@ +import { IntegrationConfig } from '@/config/integrations'; +import LfTwitterSettingsDrawer from '@/config/integrations/twitter/components/twitter-settings-drawer.vue'; +import TwitterConnect from './components/twitter-connect.vue'; +import TwitterParams from './components/twitter-params.vue'; +import TwitterDropdown from './components/twitter-dropdown.vue'; + +const image = new URL('@/assets/images/integrations/twitter-x-black.png', import.meta.url).href; + +const twitter: IntegrationConfig = { + key: 'twitter', + name: 'X/Twitter', + image, + description: 'Sync profile information, followers, and relevant tweets.', + link: 'https://docs.linuxfoundation.org/lfx/community-management/integrations/x-twitter-integration', + connectComponent: TwitterConnect, + connectedParamsComponent: TwitterParams, + dropdownComponent: TwitterDropdown, + settingComponent: LfTwitterSettingsDrawer, + showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + ], +}; + +export default twitter; diff --git a/frontend/src/config/links.ts b/frontend/src/config/links.ts new file mode 100644 index 0000000000..13fe7b30ed --- /dev/null +++ b/frontend/src/config/links.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2025 The Linux Foundation and each contributor. +// SPDX-License-Identifier: MIT + +export const links = { + integrations: 'https://docs.linuxfoundation.org/lfx/community-management/integrations', +}; diff --git a/frontend/src/config/permissions/admin.ts b/frontend/src/config/permissions/admin.ts new file mode 100644 index 0000000000..e77d8ec577 --- /dev/null +++ b/frontend/src/config/permissions/admin.ts @@ -0,0 +1,63 @@ +import { LfPermission } from '@/shared/modules/permissions/types/Permissions'; + +const admin: Record = { + [LfPermission.tenantEdit]: true, + [LfPermission.tenantDestroy]: true, + [LfPermission.planEdit]: true, + [LfPermission.planRead]: true, + [LfPermission.userEdit]: true, + [LfPermission.userDestroy]: true, + [LfPermission.userCreate]: true, + [LfPermission.userImport]: true, + [LfPermission.userRead]: true, + [LfPermission.userAutocomplete]: true, + [LfPermission.auditLogRead]: true, + [LfPermission.settingsRead]: true, + [LfPermission.settingsEdit]: true, + [LfPermission.integrationCreate]: true, + [LfPermission.integrationEdit]: true, + [LfPermission.integrationDestroy]: true, + [LfPermission.integrationRead]: true, + [LfPermission.integrationAutocomplete]: true, + [LfPermission.memberImport]: true, + [LfPermission.memberCreate]: true, + [LfPermission.memberEdit]: true, + [LfPermission.memberDestroy]: true, + [LfPermission.memberRead]: true, + [LfPermission.memberAutocomplete]: true, + [LfPermission.tagRead]: true, + [LfPermission.tagImport]: true, + [LfPermission.tagAutocomplete]: true, + [LfPermission.tagCreate]: true, + [LfPermission.tagEdit]: true, + [LfPermission.tagDestroy]: true, + [LfPermission.organizationImport]: true, + [LfPermission.organizationCreate]: true, + [LfPermission.organizationEdit]: true, + [LfPermission.organizationDestroy]: true, + [LfPermission.organizationRead]: true, + [LfPermission.organizationAutocomplete]: true, + [LfPermission.activityImport]: true, + [LfPermission.activityCreate]: true, + [LfPermission.activityEdit]: true, + [LfPermission.activityDestroy]: true, + [LfPermission.activityRead]: true, + [LfPermission.activityAutocomplete]: true, + [LfPermission.projectGroupCreate]: true, + [LfPermission.projectGroupEdit]: true, + [LfPermission.projectCreate]: true, + [LfPermission.projectEdit]: true, + [LfPermission.subProjectCreate]: true, + [LfPermission.subProjectEdit]: true, + [LfPermission.mergeMembers]: true, + [LfPermission.mergeOrganizations]: true, + [LfPermission.customViewsCreate]: true, + [LfPermission.customViewsTenantManage]: true, + [LfPermission.dataQualityRead]: true, + [LfPermission.dataQualityEdit]: true, + [LfPermission.collectionCreate]: true, + [LfPermission.collectionEdit]: true, + [LfPermission.collectionDelete]: true, +}; + +export default admin; diff --git a/frontend/src/config/permissions/index.ts b/frontend/src/config/permissions/index.ts new file mode 100644 index 0000000000..b0f95db269 --- /dev/null +++ b/frontend/src/config/permissions/index.ts @@ -0,0 +1,11 @@ +import { LfRole } from '@/shared/modules/permissions/types/Roles'; +import { LfPermission } from '@/shared/modules/permissions/types/Permissions'; +import readonly from './readonly'; +import admin from './admin'; +import projectAdmin from './projectAdmin'; + +export const lfPermissions: Record> = { + readonly, + admin, + projectAdmin, +}; diff --git a/frontend/src/config/permissions/projectAdmin.ts b/frontend/src/config/permissions/projectAdmin.ts new file mode 100644 index 0000000000..eea6c6867f --- /dev/null +++ b/frontend/src/config/permissions/projectAdmin.ts @@ -0,0 +1,63 @@ +import { LfPermission } from '@/shared/modules/permissions/types/Permissions'; + +const projectAdmin: Record = { + [LfPermission.tenantEdit]: true, + [LfPermission.tenantDestroy]: true, + [LfPermission.planEdit]: true, + [LfPermission.planRead]: true, + [LfPermission.userEdit]: true, + [LfPermission.userDestroy]: true, + [LfPermission.userCreate]: true, + [LfPermission.userImport]: true, + [LfPermission.userRead]: true, + [LfPermission.userAutocomplete]: true, + [LfPermission.auditLogRead]: true, + [LfPermission.settingsRead]: true, + [LfPermission.settingsEdit]: true, + [LfPermission.integrationCreate]: true, + [LfPermission.integrationEdit]: true, + [LfPermission.integrationDestroy]: true, + [LfPermission.integrationRead]: true, + [LfPermission.integrationAutocomplete]: true, + [LfPermission.memberImport]: true, + [LfPermission.memberCreate]: true, + [LfPermission.memberEdit]: true, + [LfPermission.memberDestroy]: true, + [LfPermission.memberRead]: true, + [LfPermission.memberAutocomplete]: true, + [LfPermission.tagRead]: true, + [LfPermission.tagImport]: true, + [LfPermission.tagAutocomplete]: true, + [LfPermission.tagCreate]: true, + [LfPermission.tagEdit]: true, + [LfPermission.tagDestroy]: true, + [LfPermission.organizationImport]: true, + [LfPermission.organizationCreate]: true, + [LfPermission.organizationEdit]: true, + [LfPermission.organizationDestroy]: true, + [LfPermission.organizationRead]: true, + [LfPermission.organizationAutocomplete]: true, + [LfPermission.activityImport]: true, + [LfPermission.activityCreate]: true, + [LfPermission.activityEdit]: true, + [LfPermission.activityDestroy]: true, + [LfPermission.activityRead]: true, + [LfPermission.activityAutocomplete]: true, + [LfPermission.projectGroupCreate]: false, + [LfPermission.projectGroupEdit]: true, + [LfPermission.projectCreate]: true, + [LfPermission.projectEdit]: true, + [LfPermission.subProjectCreate]: true, + [LfPermission.subProjectEdit]: true, + [LfPermission.mergeMembers]: true, + [LfPermission.mergeOrganizations]: true, + [LfPermission.customViewsCreate]: true, + [LfPermission.customViewsTenantManage]: true, + [LfPermission.dataQualityRead]: false, + [LfPermission.dataQualityEdit]: false, + [LfPermission.collectionCreate]: false, + [LfPermission.collectionEdit]: false, + [LfPermission.collectionDelete]: false, +}; + +export default projectAdmin; diff --git a/frontend/src/config/permissions/readonly.ts b/frontend/src/config/permissions/readonly.ts new file mode 100644 index 0000000000..e4c67181ba --- /dev/null +++ b/frontend/src/config/permissions/readonly.ts @@ -0,0 +1,63 @@ +import { LfPermission } from '@/shared/modules/permissions/types/Permissions'; + +const readonly: Record = { + [LfPermission.tenantEdit]: false, + [LfPermission.tenantDestroy]: false, + [LfPermission.planEdit]: false, + [LfPermission.planRead]: false, + [LfPermission.userEdit]: false, + [LfPermission.userDestroy]: false, + [LfPermission.userCreate]: false, + [LfPermission.userImport]: false, + [LfPermission.userRead]: false, + [LfPermission.userAutocomplete]: false, + [LfPermission.auditLogRead]: false, + [LfPermission.settingsRead]: false, + [LfPermission.settingsEdit]: false, + [LfPermission.integrationCreate]: false, + [LfPermission.integrationEdit]: false, + [LfPermission.integrationDestroy]: false, + [LfPermission.integrationRead]: true, + [LfPermission.integrationAutocomplete]: false, + [LfPermission.memberImport]: false, + [LfPermission.memberCreate]: false, + [LfPermission.memberEdit]: false, + [LfPermission.memberDestroy]: false, + [LfPermission.memberRead]: true, + [LfPermission.memberAutocomplete]: false, + [LfPermission.tagRead]: true, + [LfPermission.tagImport]: false, + [LfPermission.tagAutocomplete]: false, + [LfPermission.tagCreate]: false, + [LfPermission.tagEdit]: false, + [LfPermission.tagDestroy]: false, + [LfPermission.organizationImport]: false, + [LfPermission.organizationCreate]: false, + [LfPermission.organizationEdit]: false, + [LfPermission.organizationDestroy]: false, + [LfPermission.organizationRead]: true, + [LfPermission.organizationAutocomplete]: false, + [LfPermission.activityImport]: false, + [LfPermission.activityCreate]: false, + [LfPermission.activityEdit]: false, + [LfPermission.activityDestroy]: false, + [LfPermission.activityRead]: true, + [LfPermission.activityAutocomplete]: false, + [LfPermission.projectGroupCreate]: false, + [LfPermission.projectGroupEdit]: false, + [LfPermission.projectCreate]: false, + [LfPermission.projectEdit]: false, + [LfPermission.subProjectCreate]: false, + [LfPermission.subProjectEdit]: false, + [LfPermission.mergeMembers]: false, + [LfPermission.mergeOrganizations]: false, + [LfPermission.customViewsCreate]: true, + [LfPermission.customViewsTenantManage]: false, + [LfPermission.dataQualityRead]: false, + [LfPermission.dataQualityEdit]: false, + [LfPermission.collectionCreate]: false, + [LfPermission.collectionEdit]: false, + [LfPermission.collectionDelete]: false, +}; + +export default readonly; diff --git a/frontend/src/i18n/en.js b/frontend/src/i18n/en.js deleted file mode 100644 index db6fbde558..0000000000 --- a/frontend/src/i18n/en.js +++ /dev/null @@ -1,887 +0,0 @@ -const en = { - common: { - or: 'or', - cancel: 'Cancel', - reset: 'Reset', - save: 'Save', - search: 'Search', - edit: 'Edit', - remove: 'Remove', - new: 'New', - export: 'Export to Excel', - noDataToExport: 'No data to export', - import: 'Import', - discard: 'Discard', - yes: 'Yes', - no: 'No', - pause: 'Pause', - areYouSure: 'Are you sure?', - view: 'View', - destroy: 'Delete', - mustSelectARow: 'Must select a row', - confirm: 'Confirm', - connect: 'Connect', - filters: { - active: 'Active Filters', - hide: 'Hide Filters', - show: 'Show Filters', - apply: 'Apply Filters', - }, - }, - - app: { - title: 'crowd.dev', - }, - - api: { - menu: 'API', - }, - - entities: { - project: { - name: 'project', - label: 'Projects', - menu: 'Projects', - exporterFileName: 'project_export', - list: { - menu: 'Projects', - title: 'Projects', - }, - create: { - success: 'Project successfully saved', - }, - update: { - success: 'Project successfully saved', - }, - destroy: { - success: 'Project successfully deleted', - }, - destroyAll: { - success: 'Project(s) successfully deleted', - }, - edit: { - title: 'Edit Project', - }, - fields: { - id: 'Id', - name: 'Name', - repos: 'Repos', - integrations: 'Integrations', - info: 'Custom Attributes', - activities: 'Activities', - members: 'Contacts', - latestMetrics: 'LatestMetrics', - membersToMerge: 'Contacts To Merge', - benchmarkRepos: 'BenchmarkRepos', - createdAt: 'Created at', - updatedAt: 'Updated at', - createdAtRange: 'Created at', - }, - enumerators: {}, - placeholders: {}, - hints: {}, - new: { - title: 'New Project', - }, - view: { - title: 'View Project', - }, - }, - - repo: { - name: 'repo', - label: 'Repos', - menu: 'Repos', - exporterFileName: 'repo_export', - list: { - menu: 'Repos', - title: 'Repos', - }, - create: { - success: 'Repo successfully saved', - }, - update: { - success: 'Repo successfully saved', - }, - destroy: { - success: 'Repo successfully deleted', - }, - destroyAll: { - success: 'Repo(s) successfully deleted', - }, - edit: { - title: 'Edit Repo', - }, - fields: { - id: 'Id', - url: 'URL', - network: 'Network', - info: 'Custom Attributes', - project: 'Project', - createdAt: 'Created at', - updatedAt: 'Updated at', - createdAtRange: 'Created at', - }, - enumerators: {}, - placeholders: {}, - hints: {}, - new: { - title: 'New Repo', - }, - view: { - title: 'View Repo', - }, - }, - - emailDigest: { - fields: { - email: 'Email', - frequency: 'Frequency', - time: 'Time', - }, - }, - member: { - name: 'member', - label: 'Contacts', - menu: 'Contacts', - exporterFileName: 'member_export', - list: { - menu: 'Contacts', - title: 'Contacts', - }, - create: { - success: 'Contact added successfully', - error: 'There was an error creating the contact', - message: 'View contact', - }, - update: { - success: 'Contact edited successfully', - error: 'There was an error updating the contact', - }, - destroy: { - success: 'Contact successfully deleted', - }, - destroyAll: { - success: 'Contact(s) successfully deleted', - }, - edit: { - title: 'Edit Contact', - }, - merge: { - title: 'Merge Contact', - success: 'Contacts merged successfully', - }, - attributes: { - error: 'Custom Attributes could not be created', - success: 'Custom Attributes successfuly updated', - }, - fields: { - id: 'Id', - fullName: 'Full Name', - jobTitle: 'Job title', - company: 'Company', - member: 'Contact', - score: 'Score', - estimatedReach: 'Estimated Reach', - numberActivities: '# of Activities', - contact: 'Contact', - tag: 'Tags', - username: 'Username', - displayName: 'Display Name', - activities: 'Activities', - activityCount: '# of activities', - numberOfOpenSourceContributions: '# of open source contributions', - activityTypes: 'Activity type', - location: 'Location', - organization: 'Organization', - organizations: 'Organizations', - signal: 'Signal', - bio: 'Bio', - projects: 'Projects', - info: 'Custom Attributes', - followers: 'Followers', - following: 'Following', - tags: 'Tags', - email: 'Email', - noMerge: 'NoMerge', - crowdInfo: 'CrowdInfo', - reach: 'Reach', - joinedAt: 'Contact since', - createdAt: 'Created at', - updatedAt: 'Updated at', - createdAtRange: 'Created at', - identities: 'Identities', - activeOn: 'Active On', - }, - enumerators: {}, - placeholders: {}, - hints: {}, - new: { - title: 'New Contact', - }, - view: { - title: 'View Contact', - }, - }, - - note: { - fields: { - id: 'ID', - body: 'Note', - }, - }, - - organization: { - name: 'organization', - label: 'Organizations', - menu: 'Organizations', - create: { - success: 'Organization successfully saved', - error: - 'There was an error creating the organization', - }, - update: { - success: 'Organization successfully saved', - error: - 'There was an error updating the organization', - }, - destroy: { - success: 'Organization successfully deleted', - }, - destroyAll: { - success: 'Organization(s) successfully deleted', - }, - edit: { - title: 'Edit Organization', - }, - fields: { - name: 'Name', - description: 'Description', - website: 'Website', - location: 'Location', - employees: 'Number of employees', - revenueRange: 'Annual revenue', - activeSince: 'Active since', - github: 'GitHub', - twitter: 'Twitter', - linkedin: 'LinkedIn', - crunchbase: 'Crunchbase', - }, - }, - - activity: { - name: 'activity', - label: 'Activities', - menu: 'Activities', - exporterFileName: 'activity_export', - list: { - menu: 'Activities', - title: 'Activities', - }, - create: { - success: 'Activity successfully saved', - }, - update: { - success: 'Activity successfully saved', - }, - destroy: { - success: 'Activity successfully deleted', - }, - destroyAll: { - success: 'Activity(s) successfully deleted', - }, - edit: { - title: 'Edit Activity', - }, - fields: { - id: 'Id', - type: 'Activity type', - timestampRange: 'Timestamp', - timestamp: 'Timestamp', - platform: 'Platform', - project: 'Project', - info: 'Custom Attributes', - member: 'Contact', - isContribution: 'Key Action', - crowdInfo: 'CrowdInfo', - createdAt: 'Created at', - updatedAt: 'Updated at', - date: 'Date', - createdAtRange: 'Created at', - }, - enumerators: {}, - placeholders: {}, - hints: {}, - new: { - title: 'New Activity', - }, - view: { - title: 'View Activity', - }, - }, - - report: { - name: 'Reports', - label: 'Reports', - menu: 'Reports', - edit: { - title: 'Edit Report', - }, - new: { - title: 'New Report', - }, - view: { - title: 'View Report', - }, - exporterFileName: 'report_export', - list: { - menu: 'Reports', - title: 'Reports', - }, - create: { - success: 'Report successfully saved', - }, - update: { - success: 'Report successfully saved', - }, - destroy: { - success: 'Report successfully deleted', - }, - destroyAll: { - success: 'Report(s) successfully deleted', - }, - fields: { - name: 'Name', - public: 'Public', - }, - }, - - eagleEye: { - name: 'Eagle Eye', - label: 'Eagle Eye', - menu: 'Eagle Eye', - }, - - automation: { - name: 'Automations', - label: 'Automations', - create: { - success: 'Automation successfully saved', - }, - update: { - success: 'Automation successfully saved', - }, - destroy: { - success: 'Automation successfully deleted', - }, - destroyAll: { - success: 'Automation(s) successfully deleted', - }, - fields: { - type: 'Type', - trigger: 'Choose Trigger', - status: 'Status', - }, - triggers: { - new_activity: - 'New activity happened in your community', - new_member: 'New contact joined your community', - member_attributes_match: 'Contact attributes match condition(s)', - organization_attributes_match: 'Organization attributes match condition(s)', - }, - }, - - conversation: { - name: 'Conversations', - label: 'Conversations', - edit: { - title: 'Edit Conversation', - }, - new: { - title: 'New Conversation', - }, - view: { - title: 'View Conversation', - }, - exporterFileName: 'conversation_export', - list: { - menu: 'Conversations', - title: 'Conversations', - }, - create: { - success: 'Conversation successfully saved', - }, - update: { - success: 'Conversation successfully saved', - }, - destroy: { - success: 'Conversation successfully deleted', - }, - destroyAll: { - success: 'Conversation(s) successfully deleted', - }, - fields: { - title: 'Title', - platform: 'Platform', - channel: 'Channel', - published: 'Published', - activityCount: '# of activities', - createdAt: 'Date started', - lastActive: 'Last activity', - }, - }, - }, - - widget: { - cubejs: { - tooltip: { - Activities: 'Activity', - Contacts: 'Contact', - Members: 'Contact', - Conversations: 'Conversation', - Organizations: 'Organization', - }, - cubes: { - Activities: 'Activities', - Members: 'Contacts', - Conversations: 'Conversations', - Organizations: 'Organizations', - }, - Activities: { - count: '[Activities] Count', - cumulativeCount: '[Activities] Cumulative Count', - type: '[Activities] Type', - platform: '[Activities] Platform', - date: '[Activities] Date', - channel: '[Activities] Channel', - }, - Members: { - count: '[Contacts] Count', - cumulativeCount: '[Contacts] Cumulative Count', - score: '[Contacts] Engagement Level', - location: '[Contacts] Location', - organization: '[Contacts] Organization', - joinedAt: '[Contacts] Joined Date', - }, - MemberTags: { - count: '[Contacts] # of Tags', - }, - Conversations: { - count: '[Conversations] Count', - createdat: '[Conversations] Date', - lastactive: '[Conversations] Last Active', - platform: '[Conversations] Platform', - category: '[Conversations] Category', - published: '[Conversations] Published', - }, - Tags: { - name: '[Tags] Name', - count: '[Tags] Count', - }, - Identities: { - count: '[Identities] Count', - }, - Organizations: { - count: '[Organizations] Count', - createdat: '[Organizations] Date', - }, - Segments: { - count: '[Segments] Count', - name: '[Segments] Name', - }, - Sentiment: { - averageSentiment: '[Sentiment] Average', - date: '[Sentiment] Date', - platform: '[Sentiment] Platform', - }, - }, - }, - - auth: { - tenants: 'Workspaces', - profile: { - title: 'Profile settings', - success: 'Profile successfully updated', - }, - createAnAccount: 'Create an account', - rememberMe: 'Remember me', - forgotPassword: 'Forgot password', - signin: 'Sign in', - signup: 'Sign up', - signout: 'Sign out', - alreadyHaveAnAccount: - 'Already have an account? Sign in.', - social: { - errors: { - 'auth-invalid-provider': - 'This email is already registered to another provider.', - 'auth-no-email': 'The email associated with this account is private or inexistent.', - }, - }, - signinWithAnotherAccount: - 'Sign in with another account', - emailUnverified: { - message: 'Please confirm your email at {0} to continue.', - submit: 'Resend email verification', - }, - emptyPermissions: { - message: 'You have no permissions yet. Wait for the admin to grant you privileges.', - }, - passwordResetEmail: { - message: 'Send password reset e-mail', - error: 'Email not recognized', - }, - passwordReset: { - message: 'Reset password', - }, - passwordChange: { - title: 'Change Password', - success: 'Password successfully changed', - mustMatch: 'Passwords do not match', - }, - emailAddressVerificationEmail: { - error: 'Email not recognized', - }, - verificationEmailSuccess: 'Verification email successfully sent', - passwordResetEmailSuccess: 'Password reset email successfully sent', - passwordResetSuccess: 'Password successfully changed', - verifyEmail: { - success: 'Email successfully verified.', - message: - 'Just a moment, your email is being verified...', - }, - }, - - roles: { - admin: { - label: 'Admin', - description: 'Full access to all resources', - }, - readonly: { - label: 'Read-only', - description: - 'Read access to Community Contacts, Activities, Conversations, and Reports', - }, - }, - - user: { - fields: { - id: 'Id', - avatars: 'Avatar', - email: 'E-mail', - emails: 'Email(s)', - fullName: 'Name', - firstName: 'First name', - lastName: 'Last name', - acceptedTermsAndPrivacy: 'Terms and privacy', - status: 'Status', - phoneNumber: 'Phone Number', - role: 'Role', - createdAt: 'Created at', - updatedAt: 'Updated at', - roleUser: 'Role/User', - roles: 'Roles', - createdAtRange: 'Created at', - password: 'Password', - passwordConfirmation: 'Confirm password', - oldPassword: 'Old password', - newPassword: 'New password', - newPasswordConfirmation: 'Confirm new password', - rememberMe: 'Remember me', - }, - status: { - active: 'Active', - invited: 'Invite sent', - 'empty-permissions': 'Waiting for Permissions', - }, - invite: 'Invite', - validations: { - email: 'Email {value} is invalid', - }, - title: 'Users', - menu: 'Users', - doAddSuccess: 'User successfully invited', - doUpdateSuccess: 'User successfully updated', - exporterFileName: 'users_export', - doDestroySuccess: 'User successfully deleted', - doDestroyAllSuccess: 'Users successfully deleted', - edit: { - title: 'Edit User', - }, - new: { - title: 'Invite User(s)', - titleModal: 'Invite User', - emailsHint: - 'Separate multiple email addresses using the comma character.', - }, - view: { - title: 'View User', - activity: 'Activity', - }, - errors: { - userAlreadyExists: - 'User with this email already exists', - userNotFound: 'User not found', - revokingOwnPermission: 'You can\'t revoke your own admin permission', - }, - }, - - tenant: { - name: 'tenant', - label: 'Workspaces', - menu: 'Manage workspaces', - list: { - menu: 'Workspaces', - title: 'Workspaces', - }, - create: { - button: 'Create Workspace', - success: 'Community has been created', - }, - update: { - success: 'Community has been updated', - }, - destroy: { - success: 'Community successfully deleted', - }, - destroyAll: { - success: 'Workspace(s) successfully deleted', - }, - edit: { - title: 'Edit Workspace', - }, - fields: { - id: 'Id', - name: 'Name', - url: 'URL', - tenantUrl: 'Community URL', - tenantName: 'Community name', - tenantPlatforms: 'Community platforms', - tenantSize: 'Community size', - tenantId: 'Community', - plan: 'Plan', - }, - enumerators: {}, - new: { - title: 'New Workspace', - }, - invitation: { - view: 'View Invitations', - invited: 'Invited', - accept: 'Accept Invitation', - decline: 'Decline Invitation', - declined: 'Invitation successfully declined', - acceptWrongEmail: 'Accept Invitation With This Email', - }, - select: 'Select Workspace', - validation: { - url: 'Your workspace URL can only contain lowercase letters, numbers and dashes (and must start with a letter or number).', - }, - }, - - plan: { - menu: 'Plans', - title: 'Plans', - - free: { - label: 'Free', - price: '$0', - }, - premium: { - label: 'Premium', - price: '$10', - }, - enterprise: { - label: 'Enterprise', - price: '$50', - }, - - pricingPeriod: '/month', - current: 'Current Plan', - subscribe: 'Subscribe', - manage: 'Manage Subscription', - cancelAtPeriodEnd: - 'This plan will be canceled at the end of the period.', - somethingWrong: - 'There is something wrong with your subscription. Please go to manage subscription for more details.', - notPlanUser: 'You are not the manager of this subscription.', - }, - - auditLog: { - menu: 'Audit Logs', - title: 'Audit Logs', - exporterFileName: 'audit_log_export', - entityNamesHint: - 'Separate multiple entities using the comma character.', - fields: { - id: 'Id', - timestampRange: 'Period', - entityName: 'Entity', - entityNames: 'Entities', - entityId: 'Entity ID', - action: 'Action', - values: 'Values', - timestamp: 'Date', - createdByEmail: 'User Email', - }, - }, - settings: { - title: 'Settings', - menu: 'Settings', - save: { - success: - 'Settings successfully saved. The page will reload in {0} seconds for changes to take effect.', - }, - fields: { - theme: 'Theme', - logos: 'Logo', - backgroundImages: 'Background Image', - }, - colors: { - default: 'Default', - cyan: 'Cyan', - 'geek-blue': 'Geek Blue', - gold: 'Gold', - lime: 'Lime', - magenta: 'Magenta', - orange: 'Orange', - 'polar-green': 'Polar Green', - purple: 'Purple', - red: 'Red', - volcano: 'Volcano', - yellow: 'Yellow', - }, - }, - feedback: { - menu: 'Feedback', - }, - integrations: { - menu: 'Integrations', - }, - dashboard: { - menu: 'Home', - message: 'This page uses fake data for demonstration purposes only. You can edit it at ' - + 'frontend/src/modules/dashboard/components/dashboard-page.vue.', - charts: { - day: 'Day', - red: 'Red', - green: 'Green', - yellow: 'Yellow', - grey: 'Grey', - blue: 'Blue', - orange: 'Orange', - months: { - 1: 'January', - 2: 'February', - 3: 'March', - 4: 'April', - 5: 'May', - 6: 'June', - 7: 'July', - }, - eating: 'Eating', - drinking: 'Drinking', - sleeping: 'Sleeping', - designing: 'Designing', - coding: 'Coding', - cycling: 'Cycling', - running: 'Running', - customer: 'Customer', - }, - }, - errors: { - backToHome: 'Back to home', - 403: 'Sorry, you don\'t have access to this page', - 404: 'Sorry, the page you visited does not exist', - 500: 'Sorry, the server is reporting an error', - 429: 'Too many requests. Please try again later.', - forbidden: { - message: 'Forbidden', - }, - validation: { - message: 'An error occurred', - }, - defaultErrorMessage: 'Ops, an error occurred', - }, - - preview: { - error: - 'Sorry, this operation is not allowed in preview mode.', - }, - - // See https://github.com/jquense/yup#using-a-custom-locale-dictionary - /* eslint-disable */ - validation: { - mixed: { - default: 'path} is invalid', - required: 'This field is required', - oneOf: - '{path} must be one of the following values: ${values}', - notOneOf: - '{path} must not be one of the following values: ${values}', - notType: ({ path, type, value, originalValue }) => { - return `${path} must be a ${type}` - } - }, - string: { - length: - '{path} must be exactly ${length} characters', - min: '{path} must be at least ${min} characters', - max: '{path} must be at most ${max} characters', - matches: - '{path} must match the following: "${regex}"', - email: '{path} must be a valid email', - url: '{path} must be a valid URL', - trim: '{path} must be a trimmed string', - lowercase: '{path} must be a lowercase string', - uppercase: '{path} must be a upper case string', - selected: '{path} must be selected' - }, - number: { - min: '{path} must be greater than or equal to ${min}', - max: '{path} must be less than or equal to ${max}', - lessThan: '{path} must be less than ${less}', - moreThan: '{path} must be greater than ${more}', - notEqual: '{path} must be not equal to ${notEqual}', - positive: '{path} must be a positive number', - negative: '{path} must be a negative number', - integer: '{path} must be an integer', - invalid: '{path} must be a number' - }, - date: { - min: '{path} field must be later than ${min}', - max: '{path} field must be at earlier than ${max}' - }, - boolean: {}, - object: { - noUnknown: - '{path} field cannot have keys not specified in the object shape' - }, - array: { - min: '{path} field must have at least ${min} items', - max: '{path} field must have less than or equal to ${max} items' - } - }, - /* eslint-disable */ - fileUploader: { - upload: 'Upload', - image: 'You must upload an image', - size: 'File is too big. Max allowed size is {0}', - formats: `Invalid format. Must be one of: {0}.` - }, - - autocomplete: { - loading: 'Loading...' - }, - - imagesViewer: { - noImage: 'No image' - }, - - external: { - docs: 'Documentation', - community: 'Community' - } -} - -export default en diff --git a/frontend/src/i18n/es.js b/frontend/src/i18n/es.js deleted file mode 100644 index cdb5e22880..0000000000 --- a/frontend/src/i18n/es.js +++ /dev/null @@ -1,663 +0,0 @@ -const es = { - common: { - or: 'o', - cancel: 'Cancelar', - reset: 'Reiniciar', - save: 'Guardar', - search: 'Buscar', - edit: 'Editar', - remove: 'Eliminar', - new: 'Nuevo', - export: 'Exportar a Excel', - noDataToExport: 'No hay datos para exportar', - import: 'Importar', - discard: 'Descartar', - yes: 'Si', - no: 'No', - pause: 'Pausa', - areYouSure: '¿Estás seguro?', - view: 'Ver', - destroy: 'Eliminar', - mustSelectARow: 'Debe seleccionar una fila', - confirm: 'Confirmar', - start: 'Comienzo', - end: 'Final', - filters: { - // TODO: Translate these - active: 'Active Filters', - hide: 'Hide Filters', - show: 'Show Filters', - apply: 'Apply Filters', - }, - }, - app: { - title: 'crowd.dev', - }, - api: { - menu: 'API', - }, - entities: { - project: { - name: 'project', - label: 'Projects', - menu: 'Projects', - exporterFileName: 'exportacion_project', - list: { - menu: 'Projects', - title: 'Projects', - }, - create: { - success: 'Project guardado con éxito', - }, - update: { - success: 'Project guardado con éxito', - }, - destroy: { - success: 'Project eliminado con éxito', - }, - destroyAll: { - success: 'Project(s) eliminado con éxito', - }, - edit: { - title: 'Editar Project', - }, - fields: { - id: 'Id', - name: 'Name', - repos: 'Repos', - integrations: 'Integrations', - info: 'Info', - activities: 'Activities', - communityMembers: 'Members', - latestMetrics: 'LatestMetrics', - membersToMerge: 'Members To Merge', - benchmarkRepos: 'BenchmarkRepos', - createdAt: 'Creado el', - updatedAt: 'Actualizado el', - createdAtRange: 'Creado el', - }, - enumerators: {}, - placeholders: {}, - hints: {}, - new: { - title: 'Nuevo Project', - }, - view: { - title: 'Ver Project', - }, - }, - - repo: { - name: 'repo', - label: 'Repos', - menu: 'Repos', - exporterFileName: 'exportacion_repo', - list: { - menu: 'Repos', - title: 'Repos', - }, - create: { - success: 'Repo guardado con éxito', - }, - update: { - success: 'Repo guardado con éxito', - }, - destroy: { - success: 'Repo eliminado con éxito', - }, - destroyAll: { - success: 'Repo(s) eliminado con éxito', - }, - edit: { - title: 'Editar Repo', - }, - fields: { - id: 'Id', - url: 'URL', - network: 'Network', - info: 'Info', - project: 'Project', - createdAt: 'Creado el', - updatedAt: 'Actualizado el', - createdAtRange: 'Creado el', - }, - enumerators: {}, - placeholders: {}, - hints: {}, - new: { - title: 'Nuevo Repo', - }, - view: { - title: 'Ver Repo', - }, - }, - - communityMember: { - name: 'communityMember', - label: 'Members', - menu: 'Members', - exporterFileName: 'exportacion_communityMember', - list: { - menu: 'Members', - title: 'Members', - }, - create: { - success: 'Member guardado con éxito', - }, - update: { - success: 'Member guardado con éxito', - }, - destroy: { - success: 'Member eliminado con éxito', - }, - destroyAll: { - success: 'Member(s) eliminado con éxito', - }, - edit: { - title: 'Editar Member', - }, - merge: { - title: 'Merge Member', - success: 'Members merged successfully', - }, - fields: { - // TODO: Translate these - id: 'Id', - member: 'Member', - score: 'Score', - estimatedReach: 'Estimated Reach', - numberActivities: '# of Activities', - numberOfOpenSourceContributions: '# of open source contributions', - contact: 'Contact', - tag: 'Tags', - username: 'Username', - activities: 'Activities', - projects: 'Projects', - info: 'Info', - followers: 'Followers', - following: 'Following', - tags: 'Tags', - email: 'Email', - noMerge: 'NoMerge', - crowdInfo: 'CrowdInfo', - createdAt: 'Created at', - updatedAt: 'Updated at', - createdAtRange: 'Created at', - }, - enumerators: {}, - placeholders: {}, - hints: {}, - new: { - title: 'Nuevo Member', - }, - view: { - title: 'Ver Member', - }, - }, - - activity: { - name: 'activity', - label: 'Activities', - menu: 'Activities', - exporterFileName: 'exportacion_activity', - list: { - menu: 'Activities', - title: 'Activities', - }, - create: { - success: 'Activity guardado con éxito', - }, - update: { - success: 'Activity guardado con éxito', - }, - destroy: { - success: 'Activity eliminado con éxito', - }, - destroyAll: { - success: 'Activity(s) eliminado con éxito', - }, - edit: { - title: 'Editar Activity', - }, - fields: { - id: 'Id', - type: 'Type', - timestampRange: 'Timestamp', - timestamp: 'Timestamp', - platform: 'Platform', - project: 'Project', - info: 'Info', - communityMember: 'Member', - isContribution: 'Contribution', - crowdInfo: 'CrowdInfo', - createdAt: 'Creado el', - updatedAt: 'Actualizado el', - createdAtRange: 'Creado el', - }, - enumerators: {}, - placeholders: {}, - hints: {}, - new: { - title: 'Nuevo Activity', - }, - view: { - title: 'Ver Activity', - }, - github: { - // TODO: Translate these - fork: 'forked a repository', - star: 'stared a repository', - unstar: 'unstared a repository', - 'pull_request-open': 'opened a new pull request', - 'pull_request-close': 'closed a pull request', - 'issues-open': 'opened a new issue', - 'issues-close': 'closed an issue', - 'issue-comment': 'commented an issue', - 'commit-comment': 'commented a commit', - 'pull_request-comment': 'commented a pull request', - }, - }, - - report: { - menu: 'Analytics', - }, - }, - auth: { - tenants: 'Espacios de trabajo', - profile: { - title: 'Perfil', - success: 'Perfil actualizado con éxito', - }, - createAnAccount: 'Crea una cuenta', - rememberMe: 'Recuérdame', - forgotPassword: 'Se te olvidó tu contraseña', - signin: 'Iniciar Sesión', - signup: 'Registrarse', - signout: 'Desconectar', - alreadyHaveAnAccount: - '¿Ya tienes una cuenta? Registrarse.', - social: { - errors: { - 'auth-invalid-provider': - 'This email is already registered to another provider.', - 'auth-no-email': 'The email associated with this account is private or inexistent.', - }, - }, - signinWithAnotherAccount: - 'Inicia sesión con otra cuenta', - passwordChange: { - title: 'Cambia la contraseña', - success: 'Contraseña cambiada correctamente', - mustMatch: 'Las contraseñas deben coincidir', - }, - emailUnverified: { - message: - 'Confirme su correo electrónico en {0} para continuar.', - submit: 'Reenviar verificación de correo electrónico', - }, - emptyPermissions: { - message: - 'Aún no tienes permisos. Espera a que el administrador te otorgue privilegios.', - }, - passwordResetEmail: { - message: - 'Enviar contraseña restablecer correo electrónico', - error: 'Correo electrónico no reconocido', - }, - passwordReset: { - message: 'Restablecer la contraseña', - }, - emailAddressVerificationEmail: { - error: 'Correo electrónico no reconocido', - }, - verificationEmailSuccess: - 'Correo electrónico de verificación enviado con éxito', - passwordResetEmailSuccess: - 'Correo electrónico de restablecimiento de contraseña enviado correctamente', - passwordResetSuccess: - 'Contraseña cambiada correctamente', - verifyEmail: { - success: 'Correo electrónico verificado con éxito.', - message: - 'Solo un momento, su correo electrónico está siendo verificado ...', - }, - }, - tenant: { - name: 'inquilino', - label: 'Espacios de trabajo', - menu: 'Espacios de trabajo', - list: { - menu: 'Espacios de trabajo', - title: 'Espacios de trabajo', - }, - create: { - button: 'Crear espacio de trabajo', - success: 'Espacio de trabajo guardado correctamente', - }, - update: { - success: 'Espacio de trabajo guardado correctamente', - }, - destroy: { - success: 'Espacio de trabajo eliminado correctamente', - }, - destroyAll: { - success: - 'Espacio(s) de trabajo eliminado(s) correctamente', - }, - edit: { - title: 'Editar espacio de trabajo', - }, - fields: { - id: 'Id', - name: 'Nombre', - url: 'URL', - tenantName: 'Nombre del espacio de trabajo', - tenantId: 'Espacio de trabajo', - tenantUrl: 'URL del espacio de trabajo', - plan: 'Plan', - }, - enumerators: {}, - new: { - title: 'Nuevo espacio de trabajo', - }, - invitation: { - view: 'Ver invitaciones', - invited: 'Invitado', - accept: 'Aceptar la invitacion', - decline: 'Rechazar invitación', - declined: 'Invitación rechazada con éxito', - acceptWrongEmail: - 'Aceptar invitación con este correo electrónico', - }, - select: 'Seleccionar espacio de trabajo', - validation: { - url: 'La URL de su espacio de trabajo solo puede contener letras minúsculas, números y guiones (y debe comenzar con una letra o número).', - }, - }, - roles: { - admin: { - label: 'Administración', - description: 'Acceso total a todos los recursos.', - }, - custom: { - label: 'Rol personalizado', - description: 'Acceso personalizado a recursos', - }, - }, - user: { - invite: 'Invitación', - title: 'Usuarios', - menu: 'Usuarios', - fields: { - id: 'Id', - avatars: 'Avatar', - email: 'Email', - emails: 'Email(s)', - fullName: 'Nombre completo', - firstName: 'Nombre', - lastName: 'Apellido', - status: 'Estado', - disabled: 'Discapacitado', - phoneNumber: 'Número de teléfono', - role: 'Rol', - createdAt: 'Creado el', - updatedAt: 'Actualizado el', - roleUser: 'Rol/Usuario', - roles: 'Roles', - createdAtRange: 'Creado el', - password: 'Contraseña', - rememberMe: 'Recuérdame', - oldPassword: 'Contraseña anterior', - newPassword: 'Nueva contraseña', - newPasswordConfirmation: - 'Nueva confirmación de contraseña', - }, - enabled: 'Habilitado', - disabled: 'Discapacitado', - validations: { - email: 'El correo electrónico {value} no es válido', - }, - disable: 'Inhabilitar', - enable: 'Habilitar', - doEnableSuccess: 'Usuario habilitado con éxito', - doDisableSuccess: 'Usuario deshabilitado con éxito', - doDisableAllSuccess: - 'Usuario(s) deshabilitado con éxito', - doEnableAllSuccess: - 'Usuario(s) habilitados correctamente', - doAddSuccess: 'Usuario(s) guardado correctamente', - doUpdateSuccess: 'Usuario guardado con éxito', - status: { - active: 'Activo', - invited: 'Invitado', - 'empty-permissions': 'Esperando permisos', - }, - exporterFileName: 'usuarios_exportacion', - doDestroySuccess: 'Usuario eliminado con éxito', - doDestroyAllSelectedSuccess: - 'Usuario(s) eliminado correctamente', - edit: { - title: 'Editar Usuario', - }, - new: { - title: 'Invitar Usuario(s)', - titleModal: 'Nuevo Usuario', - emailsHint: - 'Separe varias direcciones de correo electrónico utilizando el carácter de coma.', - }, - view: { - title: 'Ver Usuario', - activity: 'Actividad', - }, - errors: { - userAlreadyExists: - 'El usuario con este correo electrónico ya existe', - userNotFound: 'Usuario no encontrado', - disablingHimself: 'No puedes inhabilitarte', - revokingOwnPermission: - 'No puede revocar su propio permiso de administrador', - }, - }, - plan: { - menu: 'Planes', - title: 'Planes', - free: { - label: 'Gratis', - price: '$0', - }, - premium: { - label: 'Crecimiento', - price: '$10', - }, - enterprise: { - label: 'Empresa', - price: '$50', - }, - pricingPeriod: '/mes', - current: 'Plan Actual', - subscribe: 'Suscribir', - manage: 'Administrar Suscripción', - cancelAtPeriodEnd: - 'Este plan se cancelará al final del período.', - somethingWrong: - 'Hay algo mal con su suscripción. Vaya a administrar la suscripción para obtener más detalles.', - notPlanUser: - 'No eres el administrador de esta suscripción.', - demoHintHtml: - 'Sugerencia: Use esas tarjetas de prueba para la demostración.', - }, - auditLog: { - menu: 'Registros de auditoría', - title: 'Registros de auditoría', - exporterFileName: 'audit_log_export', - entityNamesHint: - 'Separe varias entidades con el carácter de coma.', - fields: { - id: 'Id', - timestampRange: 'Período', - entityName: 'Entidad', - entityNames: 'Entidades', - entityId: 'ID de entidad', - action: 'Acción', - values: 'Valores', - timestamp: 'Fecha', - createdByEmail: 'Email del usuario', - }, - }, - settings: { - title: 'Configuraciones', - menu: 'Configuraciones', - save: { - success: - 'Configuración guardada con éxito. La página se volverá a cargar en {0} segundos para que los cambios surtan efecto.', - }, - fields: { - theme: 'Tema', - primaryColor: 'Color primario', - secondaryColor: 'Color secundario', - logos: 'Logo', - backgroundImages: 'Imagen de fondo', - }, - colors: { - default: 'Defecto', - cyan: 'Cian', - 'geek-blue': 'Geek Blue', - gold: 'Oro', - lime: 'Lima', - magenta: 'Magenta', - orange: 'Naranja', - 'polar-green': 'Verde Polar', - purple: 'Púrpura', - red: 'Rojo', - volcano: 'Volcán', - yellow: 'Amarillo', - }, - }, - dashboard: { - menu: 'Tablero', - message: - 'Esta página utiliza datos falsos solo con fines de demostración. Puede editarlo en ' - + 'frontend/src/modules/dashboard/components/dashboard-page.vue.', - charts: { - day: 'Día', - red: 'Rojo', - green: 'Verde', - yellow: 'Amarillo', - grey: 'Gris', - blue: 'Azul', - orange: 'Naranja', - months: { - 1: 'Enero', - 2: 'Febrero', - 3: 'Marzo', - 4: 'Abril', - 5: 'Mayo', - 6: 'Junio', - 7: 'Julio', - }, - eating: 'Comiendo', - drinking: 'Bebiendo', - sleeping: 'Dormiendo', - designing: 'Diseñando', - coding: 'Codificando', - cycling: 'Pedalando', - running: 'Corriendo', - customer: 'Cliente', - }, - }, - errors: { - 403: 'Lo sentimos, no tienes acceso a esta página', - 404: 'Lo sentimos, la página que visitaste no existe', - 500: 'Lo sentimos, el servidor informa un error', - 429: 'Demasiadas solicitudes. Por favor, inténtelo de nuevo más tarde.', - backToHome: 'Volver a Inicio', - forbidden: { - message: 'Prohibido', - }, - validation: { - message: 'Ocurrió un error', - }, - defaultErrorMessage: 'Ops, ocurrió un error', - }, - - preview: { - error: - 'Lo sentimos, esta operación no está permitida en el modo de vista previa.', - }, - - /* eslint-disable */ - validation: { - mixed: { - default: '{path} no es válido', - required: '{path} es obligatorio', - oneOf: - '{path} debe ser uno de los siguientes valores: ${values}', - notOneOf: - '{path} no debe ser uno de los siguientes valores: ${values}', - notType: ({ path, type, value, originalValue }) => { - return `${path} debe ser un ${type}`; - }, - }, - string: { - length: - '{path} debe tener exactamente ${length} caracteres', - min: '{path} debe tener al menos ${min} caracteres', - max: - '{path} debe tener como máximo ${max} caracteres', - matches: - '{path} debe coincidir con lo siguiente: "${regex}"', - email: - '{path} debe ser un correo electrónico válido', - url: '{path} debe ser una URL válida', - trim: '{path} debe ser una cadena recortada', - lowercase: - '{path} debe ser una cadena en minúsculas', - uppercase: '{path} debe ser una cadena en mayúscula', - selected: '{path} debe estar seleccionado', - }, - number: { - min: '{path} debe ser mayor o igual que ${min}', - max: '{path} debe ser menor o igual que ${max}', - lessThan: '{path} debe ser menor que ${less}', - moreThan: '{path} debe ser mayor que ${more}', - notEqual: '{path} no debe ser igual a ${notEqual}', - positive: '{path} debe ser un número positivo', - negative: '{path} debe ser un número negativo', - integer: '{path} debe ser un número entero', - invalid: '{path} debe ser un número', - }, - date: { - min: 'El campo {path} debe ser posterior a {min}', - max: 'El campo {path} debe ser anterior a {max}', - }, - boolean: {}, - object: { - noUnknown: - 'El campo ${path} no puede tener claves no especificadas en la forma del objeto', - }, - array: { - min: - 'El campo {path} debe tener al menos {min} elementos', - max: - 'El campo {path} debe tener elementos menores o iguales a ${max}', - }, - }, - - /* eslint-disable */ - fileUploader: { - upload: 'Subir', - image: 'Debes subir una imagen', - size: - 'El archivo es muy grande. El tamaño máximo permitido es {0}', - formats: 'Formato inválido. Debe ser uno de: {0}.', - }, - - autocomplete: { - loading: 'Cargando...', - }, - imagesViewer: { - noImage: 'Sin imágen', - }, -}; - -export default es; diff --git a/frontend/src/i18n/index.js b/frontend/src/i18n/index.js deleted file mode 100644 index b26d7e742f..0000000000 --- a/frontend/src/i18n/index.js +++ /dev/null @@ -1,150 +0,0 @@ -import _get from 'lodash/get'; -import { setLocale as setYupLocale } from 'yup'; - -import i18nEs from '@/i18n/es'; -import i18nEn from '@/i18n/en'; -import i18nPt from '@/i18n/pt-BR'; - -let currentLanguageCode = ''; - -const languages = { - en: { - id: 'en', - label: 'English', - flag: '/images/flags/United-States.png', - elementUI: null, - dictionary: null, - }, - es: { - id: 'es', - label: 'Español', - flag: '/images/flags/Spain.png', - elementUI: null, - dictionary: null, - }, - 'pt-BR': { - id: 'pt-BR', - label: 'Português', - flag: '/images/flags/Brazil.png', - elementUI: null, - dictionary: null, - }, -}; - -function initEs() { - const language = languages.es; - - language.dictionary = i18nEs; - - if (language.dictionary.validation) { - setYupLocale(language.dictionary.validation); - } - - return language; -} - -function initPt() { - const language = languages.pt; - - language.dictionary = i18nPt; - - if (language.dictionary.validation) { - setYupLocale(language.dictionary.validation); - } - - return language; -} - -export function initEn() { - const language = languages.en; - - language.dictionary = i18nEn; - - if (language.dictionary.validation) { - setYupLocale(language.dictionary.validation); - } - - return language; -} - -export function getLanguageCode() { - return currentLanguageCode; -} - -function getLanguage() { - return languages[getLanguageCode()]; -} - -function format(message, args) { - if (!message) { - return null; - } - - try { - return message.replace( - /{(\d+)}/g, - (match, number) => (typeof args[number] !== 'undefined' - ? args[number] - : match), - ); - } catch (error) { - console.error(message, error); - throw error; - } -} - -export function getLanguages() { - return Object.keys(languages).map((language) => languages[language]); -} - -export function getElementUILanguage() { - return getLanguage().elementUI; -} - -export function setLanguageCode(arg) { - if (!languages[arg]) { - throw new Error(`Invalid language ${arg}.`); - } - - localStorage.setItem('language', arg); -} - -export function i18nExists(key) { - if (!getLanguage()) { - return false; - } - - const message = _get(getLanguage().dictionary, key); - return Boolean(message); -} - -export function i18n(key, ...args) { - if (!getLanguage()) { - return key; - } - - const message = _get(getLanguage().dictionary, key); - - if (!message) { - return key; - } - - return format(message, args); -} - -export function init() { - currentLanguageCode = localStorage.getItem('language') || 'en'; - setLanguageCode(currentLanguageCode); - - if (currentLanguageCode === 'en') { - initEn(); - } - - if (currentLanguageCode === 'pt-BR') { - initPt(); - } - - if (currentLanguageCode === 'es') { - initEs(); - } -} diff --git a/frontend/src/i18n/pt-BR.js b/frontend/src/i18n/pt-BR.js deleted file mode 100644 index 0643479ca2..0000000000 --- a/frontend/src/i18n/pt-BR.js +++ /dev/null @@ -1,637 +0,0 @@ -const ptBR = { - common: { - or: 'ou', - cancel: 'Cancelar', - reset: 'Limpar', - save: 'Salvar', - search: 'Buscar', - edit: 'Editar', - remove: 'Remover', - new: 'Novo', - export: 'Exportar para Excel', - noDataToExport: 'Não há dados para exportar', - import: 'Importar', - discard: 'Descartar', - yes: 'Sim', - no: 'Não', - pause: 'Pausar', - areYouSure: 'Tem certeza?', - view: 'Visualizar', - destroy: 'Deletar', - mustSelectARow: 'Selecine uma linha', - confirm: 'Confirmar', - filters: { - // TODO: Translate these - active: 'Active Filters', - hide: 'Hide Filters', - show: 'Show Filters', - apply: 'Apply Filters', - }, - }, - - app: { - title: 'crowd.dev', - }, - api: { - menu: 'API', - }, - - entities: { - project: { - name: 'Project', - label: 'Projects', - menu: 'Projects', - exporterFileName: 'Project_exportados', - list: { - menu: 'Projects', - title: 'Projects', - }, - create: { - success: 'Project salvo com sucesso', - }, - update: { - success: 'Project salvo com sucesso', - }, - destroy: { - success: 'Project deletado com sucesso', - }, - destroyAll: { - success: 'Project(s) deletado com sucesso', - }, - edit: { - title: 'Editar Project', - }, - fields: { - id: 'Id', - name: 'Name', - repos: 'Repos', - integrations: 'Integrations', - info: 'Info', - activities: 'Activities', - communityMembers: 'Members', - latestMetrics: 'LatestMetrics', - membersToMerge: 'Members To Merge', - benchmarkRepos: 'BenchmarkRepos', - createdAt: 'Criado em', - updatedAt: 'Atualizado em', - createdAtRange: 'Criado em', - }, - enumerators: {}, - placeholders: {}, - hints: {}, - new: { - title: 'Novo Project', - }, - view: { - title: 'Visualizar Project', - }, - }, - - repo: { - name: 'Repo', - label: 'Repos', - menu: 'Repos', - exporterFileName: 'Repo_exportados', - list: { - menu: 'Repos', - title: 'Repos', - }, - create: { - success: 'Repo salvo com sucesso', - }, - update: { - success: 'Repo salvo com sucesso', - }, - destroy: { - success: 'Repo deletado com sucesso', - }, - destroyAll: { - success: 'Repo(s) deletado com sucesso', - }, - edit: { - title: 'Editar Repo', - }, - fields: { - id: 'Id', - url: 'URL', - network: 'Network', - info: 'Info', - project: 'Project', - createdAt: 'Criado em', - updatedAt: 'Atualizado em', - createdAtRange: 'Criado em', - }, - enumerators: {}, - placeholders: {}, - hints: {}, - new: { - title: 'Novo Repo', - }, - view: { - title: 'Visualizar Repo', - }, - }, - - communityMember: { - name: 'Member', - label: 'Members', - menu: 'Members', - exporterFileName: 'Member_exportados', - list: { - menu: 'Members', - title: 'Members', - }, - create: { - success: 'Member salvo com sucesso', - }, - update: { - success: 'Member salvo com sucesso', - }, - destroy: { - success: 'Member deletado com sucesso', - }, - destroyAll: { - success: 'Member(s) deletado com sucesso', - }, - edit: { - title: 'Editar Member', - }, - merge: { - title: 'Merge Member', - success: 'Members merged successfully', - }, - fields: { - // TODO: Translate these - id: 'Id', - member: 'Member', - score: 'Score', - estimatedReach: 'Estimated Reach', - numberActivities: '# of Activities', - numberOfOpenSourceContributions: '# of open source contributions', - contact: 'Contact', - tag: 'Tags', - username: 'Username', - activities: 'Activities', - projects: 'Projects', - info: 'Info', - followers: 'Followers', - following: 'Following', - tags: 'Tags', - email: 'Email', - noMerge: 'NoMerge', - crowdInfo: 'CrowdInfo', - createdAt: 'Created at', - updatedAt: 'Updated at', - createdAtRange: 'Created at', - }, - enumerators: {}, - placeholders: {}, - hints: {}, - new: { - title: 'Novo Member', - }, - view: { - title: 'Visualizar Member', - }, - }, - - activity: { - name: 'Activity', - label: 'Activities', - menu: 'Activities', - exporterFileName: 'Activity_exportados', - list: { - menu: 'Activities', - title: 'Activities', - }, - create: { - success: 'Activity salvo com sucesso', - }, - update: { - success: 'Activity salvo com sucesso', - }, - destroy: { - success: 'Activity deletado com sucesso', - }, - destroyAll: { - success: 'Activity(s) deletado com sucesso', - }, - edit: { - title: 'Editar Activity', - }, - fields: { - id: 'Id', - type: 'Type', - timestampRange: 'Timestamp', - timestamp: 'Timestamp', - platform: 'Platform', - project: 'Project', - info: 'Info', - communityMember: 'Member', - isContribution: 'Contribution', - crowdInfo: 'CrowdInfo', - createdAt: 'Criado em', - updatedAt: 'Atualizado em', - createdAtRange: 'Criado em', - }, - enumerators: {}, - placeholders: {}, - hints: {}, - new: { - title: 'Novo Activity', - }, - view: { - title: 'Visualizar Activity', - }, - github: { - // TODO: Translate these - fork: 'forked a repository', - star: 'stared a repository', - unstar: 'unstared a repository', - 'pull_request-open': 'opened a new pull request', - 'pull_request-close': 'closed a pull request', - 'issues-open': 'opened a new issue', - 'issues-close': 'closed an issue', - 'issue-comment': 'commented an issue', - 'commit-comment': 'commented a commit', - 'pull_request-comment': 'commented a pull request', - }, - }, - - report: { - menu: 'Analytics', - }, - }, - - auth: { - tenants: 'Áreas de Trabalho', - profile: { - title: 'Perfil', - success: 'Perfil atualizado com sucesso', - }, - createAnAccount: 'Criar uma conta', - rememberMe: 'Lembrar-me', - forgotPassword: 'Esqueci minha senha', - signin: 'Entrar', - signup: 'Registrar', - signout: 'Sair', - alreadyHaveAnAccount: 'Já possui uma conta? Entre.', - social: { - errors: { - 'auth-invalid-provider': - 'Este email está registrado para outro provedor.', - 'auth-no-email': 'O email associado a esta conta é privado ou não existe.', - }, - }, - signinWithAnotherAccount: 'Entrar com outra conta', - emailUnverified: { - message: 'Por favor, confirme seu email em {0} para continuar.', - submit: 'Reenviar confirmação por email', - }, - passwordResetEmail: { - message: 'Enviar email de redefinição de senha', - error: 'Email não encontrado', - }, - emptyPermissions: { - message: 'Você ainda não possui permissões. Aguarde o administrador conceder seus privilégios.', - }, - passwordReset: { - message: 'Alterar senha', - }, - passwordChange: { - title: 'Mudar a Senha', - success: 'Senha alterada com sucesso', - mustMatch: 'Senhas devem ser iguais', - }, - emailAddressVerificationEmail: { - error: 'Email não encontrado', - }, - verificationEmailSuccess: 'Verificação de email enviada com sucesso', - passwordResetEmailSuccess: 'Email de redefinição de senha enviado com sucesso', - passwordResetSuccess: 'Senha alterada com sucesso', - verifyEmail: { - success: 'Email verificado com sucesso.', - message: - 'Aguarde um momento, seu email está sendo verificado...', - }, - }, - - roles: { - admin: { - label: 'Administrador', - description: 'Acesso completo a todos os recursos', - }, - custom: { - label: 'Perfil Customizado', - description: 'Acesso customizado', - }, - }, - - user: { - fields: { - id: 'Id', - avatars: 'Avatar', - email: 'Email', - emails: 'Email(s)', - fullName: 'Nome', - firstName: 'Nome', - lastName: 'Sobrenome', - status: 'Estado', - phoneNumber: 'Telefone', - role: 'Perfil', - createdAt: 'Criado em', - updatedAt: 'Atualizado em', - roleUser: 'Perfil/Usuário', - roles: 'Perfis', - createdAtRange: 'Criado em', - password: 'Senha', - oldPassword: 'Senha Antiga', - newPassword: 'Nova Senha', - newPasswordConfirmation: 'Confirmação da Nova Senha', - rememberMe: 'Lembrar-me', - }, - status: { - active: 'Ativo', - invited: 'Convidado', - 'empty-permissions': 'Aguardando Permissões', - }, - invite: 'Convidar', - validations: { - email: 'Email {value} é inválido', - }, - title: 'Usuários', - menu: 'Usuários', - doAddSuccess: 'Usuário(s) salvos com sucesso', - doUpdateSuccess: 'Usuário salvo com sucesso', - exporterFileName: 'usuarios_exportados', - doDestroySuccess: 'Usuário deletado com sucesso', - doDestroyAllSelectedSuccess: - 'Usuários deletado com sucesso', - edit: { - title: 'Editar usuário', - }, - new: { - title: 'Novo(s) Usuário(s)', - titleModal: 'Novo Usuário', - emailsHint: - 'Separe múltiplos endereços de e-mail usando a vírgula.', - }, - view: { - title: 'Visualizar Usuário', - activity: 'Atividades', - }, - errors: { - userAlreadyExists: 'Usuário com este email já existe', - userNotFound: 'Usuário não encontrado', - revokingOwnPermission: 'Você não pode revogar sua própria permissão de proprietário', - }, - }, - - tenant: { - name: 'tenant', - label: 'Área de Trabalho', - menu: 'Áreas de Trabalho', - list: { - menu: 'Áreas de Trabalho', - title: 'Áreas de Trabalho', - }, - create: { - button: 'Criar Área de Trabalho', - success: 'Área de Trabalho salva com sucesso', - }, - update: { - success: 'Área de Trabalho salva com sucesso', - }, - destroy: { - success: 'Área de Trabalho deletada com sucesso', - }, - destroyAll: { - success: 'Área(s) de Trabalho deletadas com sucesso', - }, - edit: { - title: 'Editar Área de Trabalho', - }, - fields: { - id: 'Id', - name: 'Nome', - tenantName: 'Nome da Área de Trabalho', - tenantId: 'Área de Trabalho', - tenantUrl: 'URL da Área de Trabalho', - plan: 'Plano', - }, - enumerators: {}, - new: { - title: 'Nova Área de Trabalho', - }, - invitation: { - view: 'Ver Convites', - invited: 'Convidado', - accept: 'Aceitar Convite', - decline: 'Recusar Convite', - declined: 'Convite recusado com sucesso', - acceptWrongEmail: 'Aceitar Convite Com Este Email', - }, - select: 'Selecionar Área de Trabalho', - url: { - exists: 'Esta URL de área de trabalho já está em uso.', - }, - }, - - plan: { - menu: 'Planos', - title: 'Planos', - - free: { - label: 'Gratuito', - price: '$0', - }, - premium: { - label: 'Premium', - price: '$10', - }, - enterprise: { - label: 'Enterprise', - price: '$50', - }, - - pricingPeriod: '/mês', - current: 'Plano Atual', - subscribe: 'Assinar', - manage: 'Gerenciar Assinatura', - somethingWrong: - 'Há algo errado com sua assinatura. Por favor clique em Gerenciar Assinatura para mais informações.', - cancelAtPeriodEnd: - 'O plano será cancelado no fim do período.', - notPlanUser: 'Esta assinatura não é controlada por você.', - }, - - auditLog: { - menu: 'Registros de Auditoria', - title: 'Registros de Auditoria', - exporterFileName: 'registros_autoria_exportados', - entityNamesHint: - 'Separe múltiplas entidades por vírgula', - fields: { - id: 'Id', - timestampRange: 'Período', - entityName: 'Entidade', - entityNames: 'Entidades', - entityId: 'ID da Entidade', - action: 'Ação', - values: 'Valores', - timestamp: 'Data', - createdByEmail: 'Email do Usuário', - }, - }, - settings: { - title: 'Configurações', - menu: 'Configurações', - save: { - success: - 'Configurações salvas com sucesso. A página irá recarregar em {0} para que as alterações tenham efeito.', - }, - fields: { - theme: 'Tema', - logos: 'Logo', - backgroundImages: 'Papel de Parede', - }, - colors: { - default: 'Padrão', - cyan: 'Ciano', - 'geek-blue': 'Azul escuro', - gold: 'Ouro', - lime: 'Limão', - magenta: 'Magenta', - orange: 'Laranja', - 'polar-green': 'Verde polar', - purple: 'Roxo', - red: 'Vermelho', - volcano: 'Vúlcão', - yellow: 'Amarelo', - }, - }, - dashboard: { - menu: 'Dashboard', - message: 'Esta página usa dados falsos apenas para fins de demonstração. Você pode editá-la em ' - + 'frontend/src/modules/dashboard/components/dashboard-page.vue.', - charts: { - day: 'Dia', - red: 'Vermelho', - green: 'Verde', - yellow: 'Amarelho', - grey: 'Cinza', - blue: 'Azul', - orange: 'Laranja', - months: { - 1: 'Janeiro', - 2: 'Fevereiro', - 3: 'Março', - 4: 'Abril', - 5: 'Maio', - 6: 'Junho', - 7: 'Julho', - }, - eating: 'Comendo', - drinking: 'Bebendo', - sleeping: 'Dormindo', - designing: 'Projetando', - coding: 'Codificando', - cycling: 'Pedalando', - running: 'Correndo', - customer: 'Cliente', - }, - }, - errors: { - backToHome: 'Voltar a página inicial', - 403: 'Desculpe, você não tem acesso a esta página', - 404: 'Desculpe, a página que você visitou não existe', - 500: 'Desculpe, o servidor está relatando um erro', - 429: 'Muitas requisições. Por favor, tente novamente mais tarde.', - forbidden: { - message: 'Acesso negado', - }, - validation: { - message: 'Ocorreu um erro', - }, - defaultErrorMessage: 'Ops, ocorreu um erro', - }, - - preview: { - error: - 'Desculpe, esta operação não é permitida em modo de demonstração.', - }, - - // See https://github.com/jquense/yup#using-a-custom-locale-dictionary - /* eslint-disable */ - validation: { - mixed: { - default: '{path} é inválido', - required: '{path} é obrigatório', - oneOf: - '{path} deve ser um dos seguintes valores: {values}', - notOneOf: - '{path} não deve ser um dos seguintes valores: {values}', - notType: ({ path, type, value, originalValue }) => { - return `${path} deve ser um ${type}`; - }, - }, - string: { - length: '{path} deve possuir {length} caracteres', - min: - '{path} deve possuir ao menos {min} caracteres', - max: - '{path} deve possui no máximo {max} caracteres', - matches: - '{path} deve respeitar o padrão: "{regex}"', - email: '{path} deve ser um email válido', - url: '{path} deve ser uma URL válida', - trim: - '{path} deve ser uma palavra sem espaços em branco', - lowercase: '{path} deve ser minúsculo', - uppercase: '{path} deve ser maiúsculo', - selected: '{path} deve ser selecionado', - }, - number: { - min: '{path} deve ser maior ou igual a {min}', - max: '{path} deve ser menor ou igual a {max}', - lessThan: '{path} deve ser menor que {less}', - moreThan: '{path} deve ser maior que {more}', - notEqual: '{path} não deve ser igual a {notEqual}', - positive: '{path} deve ser um número positivo', - negative: '{path} deve ser um número negativo', - integer: '{path} deve ser um inteiro', - invalid: '{path} deve ser um número', - }, - date: { - min: '{path} deve ser posterior a {min}', - max: '{path} deve ser mais cedo do que {max}', - }, - boolean: {}, - object: { - noUnknown: - '{path} não pode ter atributos não especificados no formato do objeto', - }, - array: { - min: '{path} deve possuir ao menos {min} itens', - max: '{path} deve possuir no máximo {max} itens', - }, - }, - /* eslint-disable */ - fileUploader: { - upload: 'Upload', - image: 'Você deve fazer upload de uma imagem', - size: - 'O arquivo é muito grande. O tamanho máximo permitido é {0}', - formats: `Formato inválido. Deve ser um destes: {0}.`, - }, - - autocomplete: { - loading: 'Carregando...', - }, - - imagesViewer: { - noImage: 'Sem imagem', - }, -}; - -export default ptBR; diff --git a/frontend/src/integrations/crunchbase/config.js b/frontend/src/integrations/crunchbase/config.js deleted file mode 100644 index d2a47245a2..0000000000 --- a/frontend/src/integrations/crunchbase/config.js +++ /dev/null @@ -1,8 +0,0 @@ -export default { - image: '/images/integrations/crunchbase.png', - name: 'Crunchbase', - hideAsIntegration: true, - organization: { - handle: (identity) => (identity.url ? identity.url.split('/').at(-1) : identity.name), - }, -}; diff --git a/frontend/src/integrations/crunchbase/index.js b/frontend/src/integrations/crunchbase/index.js deleted file mode 100644 index e81a18f3ed..0000000000 --- a/frontend/src/integrations/crunchbase/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from './config'; - -export default config; diff --git a/frontend/src/integrations/devto/components/devto-connect.vue b/frontend/src/integrations/devto/components/devto-connect.vue deleted file mode 100644 index 0d28ec0833..0000000000 --- a/frontend/src/integrations/devto/components/devto-connect.vue +++ /dev/null @@ -1,31 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/devto/config.js b/frontend/src/integrations/devto/config.js deleted file mode 100644 index 58875d7cd4..0000000000 --- a/frontend/src/integrations/devto/config.js +++ /dev/null @@ -1,32 +0,0 @@ -import DevtoConnect from './components/devto-connect.vue'; - -export default { - enabled: true, - name: 'DEV', - backgroundColor: '#E5E7EB', - borderColor: '#E5E7EB', - description: - 'Connect DEV to sync profile information and comments on articles.', - onboard: { - description: 'Sync profile information and comments on articles.', - }, - image: - 'https://cdn-icons-png.flaticon.com/512/5969/5969051.png', - connectComponent: DevtoConnect, - url: ({ username }) => (username ? `https://dev.to/${username}` : null), - chartColor: '#9CA3AF', - showProfileLink: true, - activityDisplay: { - showLinkToUrl: true, - }, - conversationDisplay: { - replyContent: (conversation) => ({ - icon: 'ri-reply-line', - copy: 'reply', - number: conversation.activityCount - 1, - }), - }, - organization: { - handle: (identity) => (identity.url ? identity.url.split('/').at(-1) : identity.name), - }, -}; diff --git a/frontend/src/integrations/devto/index.js b/frontend/src/integrations/devto/index.js deleted file mode 100644 index e81a18f3ed..0000000000 --- a/frontend/src/integrations/devto/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from './config'; - -export default config; diff --git a/frontend/src/integrations/discord/components/discord-connect.vue b/frontend/src/integrations/discord/components/discord-connect.vue deleted file mode 100644 index 69a95376bd..0000000000 --- a/frontend/src/integrations/discord/components/discord-connect.vue +++ /dev/null @@ -1,27 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/discord/config.js b/frontend/src/integrations/discord/config.js deleted file mode 100644 index 9c4312aaea..0000000000 --- a/frontend/src/integrations/discord/config.js +++ /dev/null @@ -1,32 +0,0 @@ -import DiscordConnect from './components/discord-connect.vue'; - -export default { - enabled: true, - name: 'Discord', - backgroundColor: '#dee0fc', - borderColor: '#dee0fc', - description: - 'Connect Discord to sync messages, threads, forum channels, and new joiners.', - onboard: { - description: 'Sync messages, threads, forum channels, and new joiners.', - }, - image: - 'https://cdn-icons-png.flaticon.com/512/5968/5968756.png', - connectComponent: DiscordConnect, - url: ({ username }) => (username ? `https://discord.com/${username}` : null), - chartColor: '#6875FF', - showProfileLink: false, - activityDisplay: { - showLinkToUrl: true, - }, - conversationDisplay: { - replyContent: (conversation) => ({ - icon: 'ri-reply-line', - copy: 'reply', - number: conversation.activityCount - 1, - }), - }, - organization: { - handle: (identity) => (identity.url ? identity.url.split('/').at(-1) : identity.name), - }, -}; diff --git a/frontend/src/integrations/discord/index.js b/frontend/src/integrations/discord/index.js deleted file mode 100644 index e81a18f3ed..0000000000 --- a/frontend/src/integrations/discord/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from './config'; - -export default config; diff --git a/frontend/src/integrations/discourse/components/discourse-connect-drawer.vue b/frontend/src/integrations/discourse/components/discourse-connect-drawer.vue deleted file mode 100644 index 9266b8e4af..0000000000 --- a/frontend/src/integrations/discourse/components/discourse-connect-drawer.vue +++ /dev/null @@ -1,421 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/discourse/components/discourse-connect.vue b/frontend/src/integrations/discourse/components/discourse-connect.vue deleted file mode 100644 index 7e47cd36d5..0000000000 --- a/frontend/src/integrations/discourse/components/discourse-connect.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/discourse/config.js b/frontend/src/integrations/discourse/config.js deleted file mode 100644 index a3831f773b..0000000000 --- a/frontend/src/integrations/discourse/config.js +++ /dev/null @@ -1,23 +0,0 @@ -import DiscourseConnect from './components/discourse-connect.vue'; - -export default { - enabled: true, - name: 'Discourse', - backgroundColor: '#FFFFFF', - borderColor: '#FFFFFF', - chartColor: '#FFDE92', - description: - 'Connect Discourse to sync topics, posts, and replies from your account forums.', - onboard: { - description: 'Sync topics, posts, and replies from your account forums.', - }, - image: '/images/integrations/discourse.png', - connectComponent: DiscourseConnect, - activityDisplay: { - showLinkToUrl: true, - }, - url: ({ attributes }) => attributes?.url?.discourse, - organization: { - handle: (identity) => (identity.url ? identity.url.split('/').at(-1) : identity.name), - }, -}; diff --git a/frontend/src/integrations/discourse/index.js b/frontend/src/integrations/discourse/index.js deleted file mode 100644 index e81a18f3ed..0000000000 --- a/frontend/src/integrations/discourse/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from './config'; - -export default config; diff --git a/frontend/src/integrations/facebook/config.js b/frontend/src/integrations/facebook/config.js deleted file mode 100644 index bde9e64484..0000000000 --- a/frontend/src/integrations/facebook/config.js +++ /dev/null @@ -1,7 +0,0 @@ -export default { - image: '/images/integrations/facebook.png', - hideAsIntegration: true, - organization: { - handle: (identity) => (identity.url ? identity.url.split('/').at(-1) : identity.name), - }, -}; diff --git a/frontend/src/integrations/facebook/index.js b/frontend/src/integrations/facebook/index.js deleted file mode 100644 index e81a18f3ed..0000000000 --- a/frontend/src/integrations/facebook/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from './config'; - -export default config; diff --git a/frontend/src/integrations/git/components/git-connect-drawer.vue b/frontend/src/integrations/git/components/git-connect-drawer.vue deleted file mode 100644 index 99916f59c7..0000000000 --- a/frontend/src/integrations/git/components/git-connect-drawer.vue +++ /dev/null @@ -1,147 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/git/components/git-connect.vue b/frontend/src/integrations/git/components/git-connect.vue deleted file mode 100644 index c8aa716601..0000000000 --- a/frontend/src/integrations/git/components/git-connect.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/git/config.js b/frontend/src/integrations/git/config.js deleted file mode 100644 index fa0094ecc5..0000000000 --- a/frontend/src/integrations/git/config.js +++ /dev/null @@ -1,40 +0,0 @@ -import config from '@/config'; -import GitConnect from './components/git-connect.vue'; - -export default { - enabled: config.isGitIntegrationEnabled, - hideAsIntegration: !config.isGitIntegrationEnabled, - name: 'Git', - backgroundColor: '#FFFFFF', - borderColor: '#FFFFFF', - description: - 'Connect Git to sync commit activities from your repos.', - onboard: { - description: 'Sync commit activities from your repos.', - }, - image: - '/images/integrations/git.png', - connectComponent: GitConnect, - url: () => null, - showProfileLink: false, - chartColor: '#E5512C', - activityDisplay: { - showContentDetails: true, - showLinkToUrl: false, - showSourceId: true, - typeIcon: 'commit', - }, - conversationDisplay: { - showConversationAttributes: true, - replyContent: () => null, - attributes: (attributes) => ({ - changes: attributes.lines, - changesCopy: 'line', - insertions: attributes.insertions, - deletions: attributes.deletions, - }), - }, - organization: { - handle: (identity) => (identity.url ? identity.url.split('/').at(-1) : identity.name), - }, -}; diff --git a/frontend/src/integrations/git/index.js b/frontend/src/integrations/git/index.js deleted file mode 100644 index e81a18f3ed..0000000000 --- a/frontend/src/integrations/git/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from './config'; - -export default config; diff --git a/frontend/src/integrations/github/components/github-connect.vue b/frontend/src/integrations/github/components/github-connect.vue deleted file mode 100644 index ce175bbd0a..0000000000 --- a/frontend/src/integrations/github/components/github-connect.vue +++ /dev/null @@ -1,64 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/github/config.js b/frontend/src/integrations/github/config.js deleted file mode 100644 index 010693534e..0000000000 --- a/frontend/src/integrations/github/config.js +++ /dev/null @@ -1,54 +0,0 @@ -import GithubConnect from './components/github-connect.vue'; - -export default { - enabled: true, - name: 'GitHub', - backgroundColor: '#E5E7EB', - borderColor: '#E5E7EB', - description: - 'Connect GitHub to sync profile information, stars, forks, pull requests, issues, and discussions.', - onboard: { - description: `GitHub is one of the richest places for developer activity and information. - Connect GitHub to track all relevant activities with no historical import limitations like repo stars, discussions, comments, and more.`, - image: '/images/integrations/onboard/onboard-github-preview.png', - highlight: true, - }, - image: - 'https://cdn-icons-png.flaticon.com/512/25/25231.png', - connectComponent: GithubConnect, - url: ({ username }) => (username ? `https://github.com/${username}` : null), - chartColor: '#111827', - showProfileLink: true, - activityDisplay: { - showLinkToUrl: true, - }, - conversationDisplay: { - showLabels: true, - showConversationAttributes: true, - separatorContent: 'activity', - replyContent: (conversation) => { - const activities = conversation.lastReplies || conversation.activities; - - return { - icon: 'ri-chat-4-line', - copy: 'comment', - number: activities.reduce((acc, activity) => { - if (activity.type.includes('comment')) { - return acc + 1; - } - - return acc; - }, 0), - }; - }, - attributes: (attributes) => ({ - changes: attributes.changedFiles, - changesCopy: 'file change', - insertions: attributes.additions, - deletions: attributes.deletions, - }), - }, - organization: { - handle: (identity) => (identity.url ? identity.url.split('/').at(-1) : identity.name), - }, -}; diff --git a/frontend/src/integrations/github/index.js b/frontend/src/integrations/github/index.js deleted file mode 100644 index e81a18f3ed..0000000000 --- a/frontend/src/integrations/github/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from './config'; - -export default config; diff --git a/frontend/src/integrations/groupsio/components/groupsio-array-input.vue b/frontend/src/integrations/groupsio/components/groupsio-array-input.vue deleted file mode 100644 index 86f58691fc..0000000000 --- a/frontend/src/integrations/groupsio/components/groupsio-array-input.vue +++ /dev/null @@ -1,83 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/groupsio/components/groupsio-connect-drawer.vue b/frontend/src/integrations/groupsio/components/groupsio-connect-drawer.vue deleted file mode 100644 index 208e74bfff..0000000000 --- a/frontend/src/integrations/groupsio/components/groupsio-connect-drawer.vue +++ /dev/null @@ -1,378 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/groupsio/components/groupsio-connect.vue b/frontend/src/integrations/groupsio/components/groupsio-connect.vue deleted file mode 100644 index 6de5fc7df6..0000000000 --- a/frontend/src/integrations/groupsio/components/groupsio-connect.vue +++ /dev/null @@ -1,32 +0,0 @@ - - - - diff --git a/frontend/src/integrations/groupsio/config.js b/frontend/src/integrations/groupsio/config.js deleted file mode 100644 index d7c57e4d39..0000000000 --- a/frontend/src/integrations/groupsio/config.js +++ /dev/null @@ -1,23 +0,0 @@ -import config from '@/config'; -import GroupsioConnect from './components/groupsio-connect.vue'; - -export default { - enabled: config.isGroupsioIntegrationEnabled, - hideAsIntegration: !config.isGroupsioIntegrationEnabled, - name: 'Groups.io', - backgroundColor: '#FFFFFF', - borderColor: '#FFFFFF', - description: - 'Connect Groups.io to sync groups and topics activity.', - image: - '/images/integrations/groupsio.svg', - connectComponent: GroupsioConnect, - chartColor: '#111827', - showProfileLink: true, - activityDisplay: { - showLinkToUrl: true, - }, - organization: { - handle: (identity) => (identity.url ? identity.url.split('/').at(-1) : identity.name), - }, -}; diff --git a/frontend/src/integrations/groupsio/index.js b/frontend/src/integrations/groupsio/index.js deleted file mode 100644 index e81a18f3ed..0000000000 --- a/frontend/src/integrations/groupsio/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from './config'; - -export default config; diff --git a/frontend/src/integrations/hackernews/components/hackerNews-connect-drawer.vue b/frontend/src/integrations/hackernews/components/hackerNews-connect-drawer.vue deleted file mode 100644 index b380aaace3..0000000000 --- a/frontend/src/integrations/hackernews/components/hackerNews-connect-drawer.vue +++ /dev/null @@ -1,307 +0,0 @@ - - - - diff --git a/frontend/src/integrations/hackernews/components/hackerNews-connect.vue b/frontend/src/integrations/hackernews/components/hackerNews-connect.vue deleted file mode 100644 index f5d8423a88..0000000000 --- a/frontend/src/integrations/hackernews/components/hackerNews-connect.vue +++ /dev/null @@ -1,31 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/hackernews/config.js b/frontend/src/integrations/hackernews/config.js deleted file mode 100644 index d938c66525..0000000000 --- a/frontend/src/integrations/hackernews/config.js +++ /dev/null @@ -1,31 +0,0 @@ -import HackerNewsConnect from './components/hackerNews-connect.vue'; - -export default { - enabled: true, - name: 'Hacker News', - backgroundColor: '#ffdecf', - borderColor: '#ffdecf', - description: - 'Connect Hacker News to get posts as well as their comments mentioning your community.', - onboard: { - description: 'Get posts as well as their comments mentioning your community.', - }, - image: '/images/integrations/hackernews.svg', - connectComponent: HackerNewsConnect, - url: ({ username }) => (username ? `https://news.ycombinator.com/user?id=${username}` : null), - chartColor: '#FF712E', - showProfileLink: true, - activityDisplay: { - showLinkToUrl: true, - }, - conversationDisplay: { - replyContent: (conversation) => ({ - icon: 'ri-reply-line', - copy: 'reply', - number: conversation.activityCount - 1, - }), - }, - organization: { - handle: (identity) => (identity.url ? identity.url.split('/').at(-1) : identity.name), - }, -}; diff --git a/frontend/src/integrations/hackernews/index.js b/frontend/src/integrations/hackernews/index.js deleted file mode 100644 index e81a18f3ed..0000000000 --- a/frontend/src/integrations/hackernews/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from './config'; - -export default config; diff --git a/frontend/src/integrations/hubspot/components/hubspot-connect.vue b/frontend/src/integrations/hubspot/components/hubspot-connect.vue deleted file mode 100644 index 6a40cdb330..0000000000 --- a/frontend/src/integrations/hubspot/components/hubspot-connect.vue +++ /dev/null @@ -1,70 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/hubspot/components/hubspot-property-map.vue b/frontend/src/integrations/hubspot/components/hubspot-property-map.vue deleted file mode 100644 index 244ab0abd0..0000000000 --- a/frontend/src/integrations/hubspot/components/hubspot-property-map.vue +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - diff --git a/frontend/src/integrations/hubspot/components/hubspot-readonly-attr-popover.vue b/frontend/src/integrations/hubspot/components/hubspot-readonly-attr-popover.vue deleted file mode 100644 index 2ef1f79a63..0000000000 --- a/frontend/src/integrations/hubspot/components/hubspot-readonly-attr-popover.vue +++ /dev/null @@ -1,89 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/hubspot/components/hubspot-settings-drawer.vue b/frontend/src/integrations/hubspot/components/hubspot-settings-drawer.vue deleted file mode 100644 index 1eb37cff80..0000000000 --- a/frontend/src/integrations/hubspot/components/hubspot-settings-drawer.vue +++ /dev/null @@ -1,481 +0,0 @@ - - - - - - - diff --git a/frontend/src/integrations/hubspot/config.js b/frontend/src/integrations/hubspot/config.js deleted file mode 100644 index c747b4267d..0000000000 --- a/frontend/src/integrations/hubspot/config.js +++ /dev/null @@ -1,29 +0,0 @@ -import HubspotConnect from './components/hubspot-connect.vue'; - -export default { - name: 'HubSpot', - backgroundColor: '#FFFFFF', - borderColor: '#FFFFFF', - description: 'Create a 2-way sync with HubSpot.', - image: - '/images/integrations/hubspot.png', - connectComponent: HubspotConnect, - enabled: true, - url: (username) => null, - scale: true, - chartColor: '#FF712E', - showProfileLink: true, - activityDisplay: { - showLinkToUrl: true, - }, - conversationDisplay: { - replyContent: (conversation) => ({ - icon: 'ri-reply-line', - copy: 'reply', - number: conversation.activityCount - 1, - }), - }, - organization: { - handle: (identity) => identity.name, - }, -}; diff --git a/frontend/src/integrations/hubspot/hubspot.api.service.ts b/frontend/src/integrations/hubspot/hubspot.api.service.ts deleted file mode 100644 index 784f04de3c..0000000000 --- a/frontend/src/integrations/hubspot/hubspot.api.service.ts +++ /dev/null @@ -1,91 +0,0 @@ -import AuthCurrentTenant from '@/modules/auth/auth-current-tenant'; -import authAxios from '@/shared/axios/auth-axios'; -import { MappableFields } from '@/integrations/hubspot/types/MappableFields'; -import { HubspotOnboard } from '@/integrations/hubspot/types/HubspotOnboard'; -import { HubspotLists } from '@/integrations/hubspot/types/HubspotLists'; - -export class HubspotApiService { - static getMappableFields(): Promise { - const tenantId = AuthCurrentTenant.get(); - - return authAxios.get( - `/tenant/${tenantId}/hubspot-mappable-fields`, - ) - .then((response) => response.data); - } - - static updateAttributes(): Promise { - const tenantId = AuthCurrentTenant.get(); - return authAxios.post( - `/tenant/${tenantId}/hubspot-update-properties`, - ) - .then((response) => response.data); - } - - static finishOnboard(data: HubspotOnboard): Promise { - const tenantId = AuthCurrentTenant.get(); - - return authAxios.post( - `/tenant/${tenantId}/hubspot-onboard`, - data, - ) - .then((response) => response.data); - } - - static syncMember(memberId: string): Promise { - const tenantId = AuthCurrentTenant.get(); - - return authAxios.post( - `/tenant/${tenantId}/hubspot-sync-member`, - { - memberId, - }, - ) - .then((response) => response.data); - } - - static stopSyncMember(memberId: string): Promise { - const tenantId = AuthCurrentTenant.get(); - - return authAxios.post( - `/tenant/${tenantId}/hubspot-stop-sync-member`, - { - memberId, - }, - ) - .then((response) => response.data); - } - - static syncOrganization(organizationId: string): Promise { - const tenantId = AuthCurrentTenant.get(); - - return authAxios.post( - `/tenant/${tenantId}/hubspot-sync-organization`, - { - organizationId, - }, - ) - .then((response) => response.data); - } - - static stopSyncOrganization(organizationId: string): Promise { - const tenantId = AuthCurrentTenant.get(); - - return authAxios.post( - `/tenant/${tenantId}/hubspot-stop-sync-organization`, - { - organizationId, - }, - ) - .then((response) => response.data); - } - - static getLists(): Promise { - const tenantId = AuthCurrentTenant.get(); - - return authAxios.get( - `/tenant/${tenantId}/hubspot-get-lists`, - ) - .then((response) => response.data); - } -} diff --git a/frontend/src/integrations/hubspot/index.js b/frontend/src/integrations/hubspot/index.js deleted file mode 100644 index e81a18f3ed..0000000000 --- a/frontend/src/integrations/hubspot/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from './config'; - -export default config; diff --git a/frontend/src/integrations/hubspot/types/HubspotEntity.ts b/frontend/src/integrations/hubspot/types/HubspotEntity.ts deleted file mode 100644 index b6539c7f38..0000000000 --- a/frontend/src/integrations/hubspot/types/HubspotEntity.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum HubspotEntity { - MEMBERS= 'members', - ORGANIZATIONS = 'organizations' -} diff --git a/frontend/src/integrations/hubspot/types/HubspotLists.ts b/frontend/src/integrations/hubspot/types/HubspotLists.ts deleted file mode 100644 index 18f4221327..0000000000 --- a/frontend/src/integrations/hubspot/types/HubspotLists.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface HubspotList { - id: string; - name: string; -} - -export interface HubspotLists { - members: HubspotList[]; - organizations: HubspotList[]; -} diff --git a/frontend/src/integrations/hubspot/types/HubspotOnboard.ts b/frontend/src/integrations/hubspot/types/HubspotOnboard.ts deleted file mode 100644 index 4d6da16dd2..0000000000 --- a/frontend/src/integrations/hubspot/types/HubspotOnboard.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { HubspotEntity } from '@/integrations/hubspot/types/HubspotEntity'; - -export interface HubspotOnboard{ - enabledFor: HubspotEntity[]; - attributesMapping: { - [HubspotEntity.MEMBERS]?: Record; - [HubspotEntity.ORGANIZATIONS]?: Record; - }; -} diff --git a/frontend/src/integrations/hubspot/types/HubspotProperty.ts b/frontend/src/integrations/hubspot/types/HubspotProperty.ts deleted file mode 100644 index 9c7a55347b..0000000000 --- a/frontend/src/integrations/hubspot/types/HubspotProperty.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface HubspotProperty { - label: string; - name: string; - type: string; -} - -export interface HubspotProperties { - members: HubspotProperty[]; - organizations: HubspotProperty[]; -} diff --git a/frontend/src/integrations/hubspot/types/MappableFields.ts b/frontend/src/integrations/hubspot/types/MappableFields.ts deleted file mode 100644 index 9b25aaa022..0000000000 --- a/frontend/src/integrations/hubspot/types/MappableFields.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface MappableFields { - members: Record, - organizations: Record, -} diff --git a/frontend/src/integrations/integrations-config.js b/frontend/src/integrations/integrations-config.js deleted file mode 100644 index b2279eae93..0000000000 --- a/frontend/src/integrations/integrations-config.js +++ /dev/null @@ -1,93 +0,0 @@ -import github from './github'; -import discord from './discord'; -import slack from './slack'; -import twitter from './twitter'; -import devto from './devto'; -import hackernews from './hackernews'; -import discourse from './discourse'; -import hubspot from './hubspot'; -import stackoverflow from './stackoverflow'; -import reddit from './reddit'; -import linkedin from './linkedin'; -import zapier from './zapier'; -import crunchbase from './crunchbase'; -// import make from './make'; -import git from './git'; -import facebook from './facebook'; -import n8n from './n8n'; -import groupsio from './groupsio'; - -class IntegrationsConfig { - get integrations() { - return { - github, - discord, - slack, - twitter, - devto, - hackernews, - reddit, - linkedin, - stackoverflow, - zapier, - n8n, - git, - crunchbase, - discourse, - groupsio, - hubspot, - // make, - facebook, - }; - } - - getConfig(platform) { - return this.integrations[platform]; - } - - get configs() { - return Object.entries(this.integrations).map( - ([key, config]) => ({ - ...config, - platform: key, - }), - ); - } - - get enabledConfigs() { - return this.configs.filter((config) => config.enabled); - } - - mapper(integration, store) { - return { - ...integration, - ...store.getters['integration/findByPlatform']( - integration.platform, - ), - }; - } - - getMappedConfig(platform, store) { - return this.mapper( - { - ...this.getConfig(platform), - platform, - }, - store, - ); - } - - mappedConfigs(store) { - return this.configs - .map((i) => this.mapper(i, store)) - .filter((i) => !i.hideAsIntegration); - } - - mappedEnabledConfigs(store) { - return this.enabledConfigs - .map((i) => this.mapper(i, store)) - .filter((i) => !i.hideAsIntegration); - } -} - -export const CrowdIntegrations = new IntegrationsConfig(); diff --git a/frontend/src/integrations/linkedin/components/linkedin-connect.vue b/frontend/src/integrations/linkedin/components/linkedin-connect.vue deleted file mode 100644 index 3074c62c72..0000000000 --- a/frontend/src/integrations/linkedin/components/linkedin-connect.vue +++ /dev/null @@ -1,97 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/linkedin/components/linkedin-settings-drawer.vue b/frontend/src/integrations/linkedin/components/linkedin-settings-drawer.vue deleted file mode 100644 index cf034505ae..0000000000 --- a/frontend/src/integrations/linkedin/components/linkedin-settings-drawer.vue +++ /dev/null @@ -1,194 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/linkedin/config.js b/frontend/src/integrations/linkedin/config.js deleted file mode 100644 index c9ae4b962c..0000000000 --- a/frontend/src/integrations/linkedin/config.js +++ /dev/null @@ -1,38 +0,0 @@ -import LinkedInConnect from './components/linkedin-connect.vue'; - -export default { - enabled: true, - name: 'LinkedIn', - backgroundColor: '#D4E1F0', - borderColor: '#D4E1F0', - description: - "Connect LinkedIn to sync comments and reactions from your organization's posts.", - image: '/images/integrations/linkedin.png', - connectComponent: LinkedInConnect, - reactions: { - like: 'Like', - praise: 'Celebrate', - maybe: 'Curious', - empathy: 'Love', - interest: 'Insightful', - appreciation: 'Support', - entertainment: 'Funny', - }, - premium: true, - url: ({ username }) => (!username?.includes('private-') ? `https://linkedin.com/in/${username}` : null), - chartColor: '#2867B2', - showProfileLink: true, - activityDisplay: { - showLinkToUrl: true, - }, - conversationDisplay: { - replyContent: (conversation) => ({ - icon: 'ri-reply-line', - copy: 'reply', - number: conversation.activityCount - 1, - }), - }, - organization: { - handle: (identity) => identity.name, - }, -}; diff --git a/frontend/src/integrations/linkedin/index.js b/frontend/src/integrations/linkedin/index.js deleted file mode 100644 index e81a18f3ed..0000000000 --- a/frontend/src/integrations/linkedin/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from './config'; - -export default config; diff --git a/frontend/src/integrations/make/config.js b/frontend/src/integrations/make/config.js deleted file mode 100644 index 0e9fb7ed5d..0000000000 --- a/frontend/src/integrations/make/config.js +++ /dev/null @@ -1,12 +0,0 @@ -export default { - enabled: false, - name: 'Make', - backgroundColor: '#FFFFFF', - borderColor: '#FFFFFF', - description: - "We're currently working on this integration.", - image: '/images/integrations/make.svg', - organization: { - handle: (identity) => (identity.url ? identity.url.split('/').at(-1) : identity.name), - }, -}; diff --git a/frontend/src/integrations/make/index.js b/frontend/src/integrations/make/index.js deleted file mode 100644 index e81a18f3ed..0000000000 --- a/frontend/src/integrations/make/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from './config'; - -export default config; diff --git a/frontend/src/integrations/n8n/components/n8n-connect-drawer.vue b/frontend/src/integrations/n8n/components/n8n-connect-drawer.vue deleted file mode 100644 index 61b3476241..0000000000 --- a/frontend/src/integrations/n8n/components/n8n-connect-drawer.vue +++ /dev/null @@ -1,117 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/n8n/components/n8n-connect.vue b/frontend/src/integrations/n8n/components/n8n-connect.vue deleted file mode 100644 index cb1de0a175..0000000000 --- a/frontend/src/integrations/n8n/components/n8n-connect.vue +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/n8n/config.js b/frontend/src/integrations/n8n/config.js deleted file mode 100644 index 2150c2f417..0000000000 --- a/frontend/src/integrations/n8n/config.js +++ /dev/null @@ -1,18 +0,0 @@ -import N8nConnect from './components/n8n-connect.vue'; - -export default { - enabled: true, - name: 'n8n', - backgroundColor: '#FFFFFF', - borderColor: '#FFFFFF', - description: 'Use n8n to connect crowd.dev with 250+ apps and services.', - onboard: { - description: 'Connect crowd.dev with 250+ apps and services.', - }, - image: - 'https://asset.brandfetch.io/idO6_6uqJ9/id9y5Acqtx.svg', - connectComponent: N8nConnect, - organization: { - handle: (identity) => (identity.url ? identity.url.split('/').at(-1) : identity.name), - }, -}; diff --git a/frontend/src/integrations/n8n/index.js b/frontend/src/integrations/n8n/index.js deleted file mode 100644 index e81a18f3ed..0000000000 --- a/frontend/src/integrations/n8n/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from './config'; - -export default config; diff --git a/frontend/src/integrations/reddit/components/reddit-connect-drawer.vue b/frontend/src/integrations/reddit/components/reddit-connect-drawer.vue deleted file mode 100644 index e9b5ebf19d..0000000000 --- a/frontend/src/integrations/reddit/components/reddit-connect-drawer.vue +++ /dev/null @@ -1,268 +0,0 @@ - - - - - - - diff --git a/frontend/src/integrations/reddit/components/reddit-connect.vue b/frontend/src/integrations/reddit/components/reddit-connect.vue deleted file mode 100644 index 4ce0cc7799..0000000000 --- a/frontend/src/integrations/reddit/components/reddit-connect.vue +++ /dev/null @@ -1,42 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/reddit/config.js b/frontend/src/integrations/reddit/config.js deleted file mode 100644 index fb2035b17f..0000000000 --- a/frontend/src/integrations/reddit/config.js +++ /dev/null @@ -1,31 +0,0 @@ -import RedditConnect from './components/reddit-connect.vue'; - -export default { - enabled: true, - name: 'Reddit', - backgroundColor: '#ffd8ca', - borderColor: '#ffd8ca', - description: - 'Connect Reddit to sync posts and comments from selected subreddits.', - onboard: { - description: 'Sync posts and comments from selected subreddits.', - }, - image: '/images/integrations/reddit.svg', - connectComponent: RedditConnect, - url: ({ username }) => (username ? `https://reddit.com/user/${username}` : null), - chartColor: '#FF4500', - showProfileLink: true, - activityDisplay: { - showLinkToUrl: true, - }, - conversationDisplay: { - replyContent: (conversation) => ({ - icon: 'ri-reply-line', - copy: 'reply', - number: conversation.activityCount - 1, - }), - }, - organization: { - handle: (identity) => (identity.url ? identity.url.split('/').at(-1) : identity.name), - }, -}; diff --git a/frontend/src/integrations/reddit/index.js b/frontend/src/integrations/reddit/index.js deleted file mode 100644 index e81a18f3ed..0000000000 --- a/frontend/src/integrations/reddit/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from './config'; - -export default config; diff --git a/frontend/src/integrations/slack/components/slack-connect.vue b/frontend/src/integrations/slack/components/slack-connect.vue deleted file mode 100644 index 103573b1ee..0000000000 --- a/frontend/src/integrations/slack/components/slack-connect.vue +++ /dev/null @@ -1,37 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/slack/config.js b/frontend/src/integrations/slack/config.js deleted file mode 100644 index f9bdc0c35a..0000000000 --- a/frontend/src/integrations/slack/config.js +++ /dev/null @@ -1,32 +0,0 @@ -import SlackConnect from './components/slack-connect.vue'; - -export default { - enabled: true, - name: 'Slack', - backgroundColor: '#FFFFFF', - borderColor: '#E5E7EB', - description: - 'Connect Slack to sync messages, threads, and new joiners.', - onboard: { - description: 'Sync messages, threads, and new joiners.', - }, - image: - 'https://cdn-icons-png.flaticon.com/512/3800/3800024.png', - connectComponent: SlackConnect, - url: ({ username }) => (username ? `https://slack.com/${username}` : null), - chartColor: '#E41756', - showProfileLink: false, - activityDisplay: { - showLinkToUrl: true, - }, - conversationDisplay: { - replyContent: (conversation) => ({ - icon: 'ri-reply-line', - copy: 'reply', - number: conversation.activityCount - 1, - }), - }, - organization: { - handle: (identity) => (identity.url ? identity.url.split('/').at(-1) : identity.name), - }, -}; diff --git a/frontend/src/integrations/slack/index.js b/frontend/src/integrations/slack/index.js deleted file mode 100644 index e81a18f3ed..0000000000 --- a/frontend/src/integrations/slack/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from './config'; - -export default config; diff --git a/frontend/src/integrations/stackoverflow/components/stackoverflow-connect-drawer.vue b/frontend/src/integrations/stackoverflow/components/stackoverflow-connect-drawer.vue deleted file mode 100644 index 3be9166333..0000000000 --- a/frontend/src/integrations/stackoverflow/components/stackoverflow-connect-drawer.vue +++ /dev/null @@ -1,418 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/stackoverflow/components/stackoverflow-connect.vue b/frontend/src/integrations/stackoverflow/components/stackoverflow-connect.vue deleted file mode 100644 index 9bc13bfb11..0000000000 --- a/frontend/src/integrations/stackoverflow/components/stackoverflow-connect.vue +++ /dev/null @@ -1,42 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/stackoverflow/config.js b/frontend/src/integrations/stackoverflow/config.js deleted file mode 100644 index 3cb8cf7159..0000000000 --- a/frontend/src/integrations/stackoverflow/config.js +++ /dev/null @@ -1,32 +0,0 @@ -import StackOverflowConnect from './components/stackoverflow-connect.vue'; - -export default { - enabled: true, - name: 'Stack Overflow', - backgroundColor: '#FFFFFF', - borderColor: '#FFFFFF', - description: - 'Connect Stack Overflow to sync questions and answers based on selected tags.', - onboard: { - description: 'Sync questions and answers based on selected tags.', - }, - image: - 'https://cdn-icons-png.flaticon.com/512/2111/2111628.png', - connectComponent: StackOverflowConnect, - url: ({ attributes }) => attributes?.url?.stackoverflow, - chartColor: '#FF9845', - showProfileLink: true, - activityDisplay: { - showLinkToUrl: true, - }, - conversationDisplay: { - replyContent: (conversation) => ({ - icon: 'ri-reply-line', - copy: 'reply', - number: conversation.activityCount - 1, - }), - }, - organization: { - handle: (identity) => (identity.url ? identity.url.split('/').at(-1) : identity.name), - }, -}; diff --git a/frontend/src/integrations/stackoverflow/index.js b/frontend/src/integrations/stackoverflow/index.js deleted file mode 100644 index e81a18f3ed..0000000000 --- a/frontend/src/integrations/stackoverflow/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from './config'; - -export default config; diff --git a/frontend/src/integrations/twitter/components/twitter-connect-2.vue b/frontend/src/integrations/twitter/components/twitter-connect-2.vue deleted file mode 100644 index d521970b35..0000000000 --- a/frontend/src/integrations/twitter/components/twitter-connect-2.vue +++ /dev/null @@ -1,83 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/twitter/components/twitter-connect-drawer.vue b/frontend/src/integrations/twitter/components/twitter-connect-drawer.vue deleted file mode 100644 index 4e30d93cb5..0000000000 --- a/frontend/src/integrations/twitter/components/twitter-connect-drawer.vue +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - - diff --git a/frontend/src/integrations/twitter/components/twitter-connect-modal.vue b/frontend/src/integrations/twitter/components/twitter-connect-modal.vue deleted file mode 100644 index 3ef0c1d719..0000000000 --- a/frontend/src/integrations/twitter/components/twitter-connect-modal.vue +++ /dev/null @@ -1,65 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/twitter/components/twitter-connect.vue b/frontend/src/integrations/twitter/components/twitter-connect.vue deleted file mode 100644 index 4b2619ea5c..0000000000 --- a/frontend/src/integrations/twitter/components/twitter-connect.vue +++ /dev/null @@ -1,27 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/twitter/config.js b/frontend/src/integrations/twitter/config.js deleted file mode 100644 index 86e0355ea8..0000000000 --- a/frontend/src/integrations/twitter/config.js +++ /dev/null @@ -1,31 +0,0 @@ -import config from '@/config'; -import TwitterConnect2 from './components/twitter-connect-2.vue'; -import TwitterConnect from './components/twitter-connect.vue'; - -export default { - enabled: true, - name: 'Twitter', - backgroundColor: '#d2ebfc', - borderColor: '#d2ebfc', - description: - 'Connect Twitter to sync profile information, followers, and relevant tweets.', - image: - 'https://cdn-icons-png.flaticon.com/512/733/733579.png', - connectComponent: config.isTwitterIntegrationEnabled ? TwitterConnect2 : TwitterConnect, - url: ({ username }) => (username ? `https://twitter.com/${username}` : null), - chartColor: '#1D9BF0', - showProfileLink: true, - activityDisplay: { - showLinkToUrl: true, - }, - conversationDisplay: { - replyContent: (conversation) => ({ - icon: 'ri-reply-line', - copy: 'reply', - number: conversation.activityCount - 1, - }), - }, - organization: { - handle: (identity) => (identity.url ? identity.url.split('/').at(-1) : identity.name), - }, -}; diff --git a/frontend/src/integrations/twitter/index.js b/frontend/src/integrations/twitter/index.js deleted file mode 100644 index e81a18f3ed..0000000000 --- a/frontend/src/integrations/twitter/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from './config'; - -export default config; diff --git a/frontend/src/integrations/zapier/components/zapier-connect-drawer.vue b/frontend/src/integrations/zapier/components/zapier-connect-drawer.vue deleted file mode 100644 index 11c9b40566..0000000000 --- a/frontend/src/integrations/zapier/components/zapier-connect-drawer.vue +++ /dev/null @@ -1,119 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/zapier/components/zapier-connect.vue b/frontend/src/integrations/zapier/components/zapier-connect.vue deleted file mode 100644 index 168c585506..0000000000 --- a/frontend/src/integrations/zapier/components/zapier-connect.vue +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/frontend/src/integrations/zapier/config.js b/frontend/src/integrations/zapier/config.js deleted file mode 100644 index a7803acaa7..0000000000 --- a/frontend/src/integrations/zapier/config.js +++ /dev/null @@ -1,31 +0,0 @@ -import ZapierConnect from './components/zapier-connect.vue'; - -export default { - enabled: true, - name: 'Zapier', - backgroundColor: '#FFFFFF', - borderColor: '#FFFFFF', - description: 'Use Zapier to connect crowd.dev with 5,000+ apps.', - onboard: { - description: 'Connect crowd.dev with 5,000+ apps.', - }, - image: - 'https://www.seekpng.com/png/full/67-672759_zapiers-new-cli-tool-for-creating-apps-zapier.png', - connectComponent: ZapierConnect, - url: () => null, - chartColor: '#FF9676', - showProfileLink: true, - activityDisplay: { - showLinkToUrl: true, - }, - conversationDisplay: { - replyContent: (conversation) => ({ - icon: 'ri-reply-line', - copy: 'reply', - number: conversation.activityCount - 1, - }), - }, - organization: { - handle: (identity) => (identity.url ? identity.url.split('/').at(-1) : identity.name), - }, -}; diff --git a/frontend/src/integrations/zapier/index.js b/frontend/src/integrations/zapier/index.js deleted file mode 100644 index e81a18f3ed..0000000000 --- a/frontend/src/integrations/zapier/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from './config'; - -export default config; diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 473b3c772b..53206fb1f8 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -5,51 +5,51 @@ import VueGridLayout from 'vue-grid-layout'; // @ts-ignore import Vue3Sanitize from 'vue-3-sanitize'; import LogRocketClient from 'logrocket'; -import VNetworkGraph from 'v-network-graph'; import VueLazyLoad from 'vue3-lazyload'; import { createPinia } from 'pinia'; import { createRouter } from '@/router'; import { createStore } from '@/store'; -import plugins from '@/plugins'; import modules from '@/modules'; import config from '@/config'; -import formbricks from '@/plugins/formbricks'; - -import { init as i18nInit } from '@/i18n'; - -import { AuthService } from '@/modules/auth/auth-service'; -import { AuthToken } from '@/modules/auth/auth-token'; -import { TenantService } from '@/modules/tenant/tenant-service'; -import 'v-network-graph/lib/style.css'; - import App from '@/app.vue'; -import { vueSanitizeOptions } from '@/plugins/sanitize'; -import marked from '@/plugins/marked'; +import { vueSanitizeOptions } from '@/shared/plugins/sanitize'; +import marked from '@/shared/plugins/marked'; import { useLogRocket } from '@/utils/logRocket'; +import { initRUM } from '@/utils/datadog/rum'; +import { VueQueryPlugin, QueryClient } from '@tanstack/vue-query'; + +declare module 'vue' { + interface ComponentCustomProperties { + $sanitize: (key: string) => string; + } +} -i18nInit(); /** * We're using Immediately Invoked Function Expressions (IIFE) here because of the async/awaits * (We should probably revisit/refactor this later to be less confusing) */ /* eslint-disable no-param-reassign, no-underscore-dangle, func-names */ (async function () { + const pinia = createPinia(); + const app = createApp(App); + app.use(pinia); + + // Create a client + const queryClient = new QueryClient({ + defaultOptions: { queries: { staleTime: 1000 * 60 * 5 } }, + }); + + // Install the VueQuery plugin + app.use(VueQueryPlugin, { + queryClient, + }); + const { captureException } = useLogRocket(); - const app = createApp(App); - const pinia = createPinia(); const router = await createRouter(); const store = await createStore(LogRocketClient); - const isSocialOnboardRequested = AuthService.isSocialOnboardRequested(); - - AuthToken.applyFromLocationUrlIfExists(); - await TenantService.fetchAndApply(); - if (isSocialOnboardRequested) { - await AuthService.socialOnboard(); - } - app.use(VueGridLayout); app.use(Vue3Sanitize, vueSanitizeOptions); app.use(VueClickAway); @@ -59,9 +59,8 @@ i18nInit(); (app.config as any).productionTip = process.env.NODE_ENV === 'production'; app.config.errorHandler = (err: any) => { - if (config.env === 'local') { - console.error(err); - } else { + console.error(err); + if (config.env !== 'local') { captureException(err); } }; @@ -76,16 +75,6 @@ i18nInit(); }); }); - router.afterEach(() => { - if (typeof formbricks !== 'undefined') { - formbricks.registerRouteChange(); - } - }); - - Object.values(plugins).map((plugin) => app.use(plugin)); - app.use(VNetworkGraph); - - app.use(pinia); app.use(store).use(router).mount('#app'); if ((window as any).Cypress) { @@ -124,4 +113,8 @@ i18nInit(); '.js?sv=', )); } + + if (config.env === 'production' && config.datadog.rum) { + initRUM(); + } }()); diff --git a/frontend/src/middleware/auth/auth-guard.js b/frontend/src/middleware/auth/auth-guard.js index a08bfb6bd8..0c0511088f 100644 --- a/frontend/src/middleware/auth/auth-guard.js +++ b/frontend/src/middleware/auth/auth-guard.js @@ -1,108 +1,29 @@ -import { PermissionChecker } from '@/modules/user/permission-checker'; -import config from '@/config'; -import { tenantSubdomain } from '@/modules/tenant/tenant-subdomain'; - -function isGoingToIntegrationsPage(to) { - return to.name === 'integration'; -} +import { useAuthStore } from '@/modules/auth/store/auth.store'; +import { storeToRefs } from 'pinia'; /** * Auth Guard * * This middleware runs before rendering any route that has meta.auth = true * - * It uses the PermissionChecker to validate if: - * - User is authenticated, and both currentTenant & currentUser exist within our store (if not, redirects to /auth/signup) - * - Email of that user is verified (if not, redirects to /auth/email-unverified) - * - User is onboarded (if not, redirects to /onboard) - * - User has permissions (if not, redirects to /auth/empty-permissions) + * User is authenticated, and both tenant & ser exist within our store (if not, redirects to /auth/signup) * * @param to - * @param store * @param router * @returns {Promise<*>} */ + export default async function ({ - to, from, store, router, + to, router, }) { if (!to.meta || !to.meta.auth) { return; } - await store.dispatch('auth/doWaitUntilInit'); - - const currentUser = store.getters['auth/currentUser']; - - const permissionChecker = new PermissionChecker( - store.getters['auth/currentTenant'], - currentUser, - ); - - if (!permissionChecker.isAuthenticated) { - router.push({ path: '/auth/signup' }); - return; - } - - if ( - to.path !== '/auth/email-unverified' - && !permissionChecker.isEmailVerified - ) { - router.push({ path: '/auth/email-unverified' }); - return; - } - - // Temporary fix - if ( - to.meta.permission - && (!permissionChecker.match(to.meta.permission) - || permissionChecker.lockedForSampleData( - to.meta.permission, - )) - ) { - router.push('/403'); - return; - } - - if (!currentUser.acceptedTermsAndPrivacy) { - router.push({ path: '/auth/terms-and-privacy' }); - return; - } - - if ( - ['multi', 'multi-with-subdomain'].includes( - config.tenantMode, - ) - && !tenantSubdomain.isSubdomain - ) { - // Protect onboard routes if user is already onboarded - if ((to.path === '/onboard' || (from.path !== '/onboard' && to.path === '/onboard/demo')) - && (!permissionChecker.isEmptyTenant && store.getters['auth/currentTenant'].onboardedAt)) { - router.push('/'); - } - - if (to.path === '/onboard/demo' && (permissionChecker.isEmptyTenant || !store.getters['auth/currentTenant'].onboardedAt)) { - router.push('/onboard'); - } - - if ( - to.path !== '/onboard' - && permissionChecker.isEmailVerified - && (permissionChecker.isEmptyTenant - || !store.getters['auth/currentTenant'].onboardedAt) - ) { - router.push({ - path: '/onboard', - query: isGoingToIntegrationsPage(to) - ? to.query - : undefined, - }); - } - } else if ( - to.path !== '/auth/empty-permissions' - && permissionChecker.isEmailVerified - && permissionChecker.isEmptyPermissions - ) { - router.push({ - path: '/auth/empty-permissions', - }); + const authStore = useAuthStore(); + const { ensureLoaded } = authStore; + const { user } = storeToRefs(authStore); + await ensureLoaded(); + if (!user.value || !user.value.id) { + router.push('/auth/signin'); } } diff --git a/frontend/src/middleware/auth/email-already-verified-guard.js b/frontend/src/middleware/auth/email-already-verified-guard.js deleted file mode 100644 index 68fee14269..0000000000 --- a/frontend/src/middleware/auth/email-already-verified-guard.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Email Already Verified Guard - * - * This middleware runs before rendering any route that has meta.emailAlreadyVerified = true - * - * It checks if the emailVerified attribute is set within our currentUser store object (if not, redirects to /) - * - * @param to - * @param store - * @param router - * @returns {Promise<*>} - */ -export default async function ({ to, store, router }) { - if (!to.meta || !to.meta.emailAlreadyVerified) { - return; - } - - await store.dispatch('auth/doWaitUntilInit'); - - if ( - store.getters['auth/signedIn'] - && store.getters['auth/currentUser'].emailVerified - ) { - router.push('/'); - } -} diff --git a/frontend/src/middleware/auth/index.js b/frontend/src/middleware/auth/index.js index 7b24a148ff..8a9a715101 100644 --- a/frontend/src/middleware/auth/index.js +++ b/frontend/src/middleware/auth/index.js @@ -1,16 +1,9 @@ import AuthGuard from '@/middleware/auth/auth-guard'; import UnauthGuard from '@/middleware/auth/unauth-guard'; -// import EmailAlreadyVerifiedGuard from '@/middleware/auth/email-already-verified-guard'; -// import PermissionGuard from '@/middleware/auth/permission-guard'; -// import NotEmptyTenant from '@/middleware/auth/not-empty-tenant-guard'; -// import NotEmptyPermissionsGuard from '@/middleware/auth/not-empty-permissions-guard'; +import SegmentGuard from '@/middleware/auth/segment-guard'; -/* Temporarly disabling guards, only AuthGuard has been working before and other caused too much issues when enabled */ export default [ AuthGuard, UnauthGuard, - // EmailAlreadyVerifiedGuard, - // PermissionGuard, - // NotEmptyTenant, - // NotEmptyPermissionsGuard, + SegmentGuard, ]; diff --git a/frontend/src/middleware/auth/not-empty-permissions-guard.js b/frontend/src/middleware/auth/not-empty-permissions-guard.js deleted file mode 100644 index 188f50c0e2..0000000000 --- a/frontend/src/middleware/auth/not-empty-permissions-guard.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Not Empty Permissions Guard - * - * This middleware runs before rendering any route that has meta.notEmptyPermissions = true - * - * It checks if currentUser has roles set (if not, redirects to /) - * - * @param to - * @param store - * @param router - * @returns {Promise<*>} - */ -export default async function ({ to, store, router }) { - if (!to.meta || !to.meta.notEmptyPermissions) { - return; - } - - await store.dispatch('auth/doWaitUntilInit'); - - if ( - store.getters['auth/signedIn'] - && store.getters['auth/roles'].length - ) { - router.push('/'); - } -} diff --git a/frontend/src/middleware/auth/not-empty-tenant-guard.js b/frontend/src/middleware/auth/not-empty-tenant-guard.js deleted file mode 100644 index 808ba44f1c..0000000000 --- a/frontend/src/middleware/auth/not-empty-tenant-guard.js +++ /dev/null @@ -1,36 +0,0 @@ -import { PermissionChecker } from '@/modules/user/permission-checker'; - -/** - * Not Empty Tenant Guard - * - * This middleware runs before rendering any route that has meta.notEmptyTenant = true - * - * It checks if: - * - currentUser is authenticated (if not, redirects to /auth/signin) - * - currentTenant is set (if not, redirects to /) - * - * @param to - * @param store - * @param router - * @returns {Promise<*>} - */ -export default async function ({ to, store, router }) { - if (!to.meta || !to.meta.notEmptyTenant) { - return; - } - - await store.dispatch('auth/doWaitUntilInit'); - - const permissionChecker = new PermissionChecker( - store.getters['auth/currentTenant'], - store.getters['auth/currentUser'], - ); - - if (!permissionChecker.isAuthenticated) { - router.push('/auth/signin'); - } - - if (!permissionChecker.isEmptyTenant) { - router.push('/'); - } -} diff --git a/frontend/src/middleware/auth/permission-guard.js b/frontend/src/middleware/auth/permission-guard.js deleted file mode 100644 index 7fb6599a98..0000000000 --- a/frontend/src/middleware/auth/permission-guard.js +++ /dev/null @@ -1,35 +0,0 @@ -import { PermissionChecker } from '@/modules/user/permission-checker'; - -/** - * Permission Guard - * - * This middleware runs before rendering any route that has meta.permission = true - * - * It checks if currentUser has to.meta.permission within its permissions (if not, redirects to /403) - * - * @param to - * @param store - * @param router - * @returns {Promise<*>} - */ -export default async function ({ to, store, router }) { - if (!to.meta || !to.meta.permission) { - return; - } - - await store.dispatch('auth/doWaitUntilInit'); - - const permissionChecker = new PermissionChecker( - store.getters['auth/currentTenant'], - store.getters['auth/currentUser'], - ); - - if ( - !permissionChecker.match(to.meta.permission) - || permissionChecker.lockedForSampleData( - to.meta.permission, - ) - ) { - router.push('/403'); - } -} diff --git a/frontend/src/middleware/auth/reset-store.js b/frontend/src/middleware/auth/reset-store.js deleted file mode 100644 index e27558e649..0000000000 --- a/frontend/src/middleware/auth/reset-store.js +++ /dev/null @@ -1,20 +0,0 @@ -import { buildInitialState } from '@/store'; - -/** - * Reset Store - * - * This middleware runs before rendering /auth/signin route - * - * It runs buildInitialState to reset our Vuex store to prevent data leaking from different (and consecutives) signins - * - * @param to - * @param store - * @returns {Promise<*>} - */ -export default async function ({ to, store }) { - if (to.path === '/auth/signin') { - const initialState = buildInitialState(); - - store.replaceState(initialState); - } -} diff --git a/frontend/src/middleware/auth/segment-guard.js b/frontend/src/middleware/auth/segment-guard.js new file mode 100644 index 0000000000..d86c1abf53 --- /dev/null +++ b/frontend/src/middleware/auth/segment-guard.js @@ -0,0 +1,44 @@ +import { useLfSegmentsStore } from '@/modules/lf/segments/store'; +import usePermissions from '@/shared/modules/permissions/helpers/usePermissions'; +import { useAuthStore } from '@/modules/auth/store/auth.store'; +import { ToastStore } from '@/shared/message/notification'; + +/** + * Segment Guard + * + * This middleware runs before rendering any route that has meta.paramSegmentAccess = (route param that holds segmentId) + * + * It checks if user has access to segment (if not, redirects to /403) + * + * @param to + * @param store + * @param router + * @returns {Promise<*>} + */ +export default async function ({ to, store, router }) { + if (!to.meta || !to.meta.paramSegmentAccess) { + return; + } + + const { ensureLoaded } = useAuthStore(); + await ensureLoaded(); + + const lsSegmentsStore = useLfSegmentsStore(); + + const { hasAccessToProjectGroup, hasAccessToSegmentId } = usePermissions(); + const isCheckingProjectGroup = to.meta.paramSegmentAccess.name === 'grandparent'; + let hasPermission; + + if (isCheckingProjectGroup) { + await lsSegmentsStore.listAdminProjectGroups(); + + hasPermission = hasAccessToProjectGroup(to.params[to.meta.paramSegmentAccess.parameter]); + } else { + hasPermission = hasAccessToSegmentId(to.params[to.meta.paramSegmentAccess.parameter]); + } + + if (!hasPermission) { + ToastStore.error('You don\'t have access to this page'); + router.push('/'); + } +} diff --git a/frontend/src/middleware/auth/unauth-guard.js b/frontend/src/middleware/auth/unauth-guard.js index 7fca625080..ccfa9ed580 100644 --- a/frontend/src/middleware/auth/unauth-guard.js +++ b/frontend/src/middleware/auth/unauth-guard.js @@ -1,27 +1,25 @@ -import { AuthToken } from '@/modules/auth/auth-token'; -import AuthCurrentTenant from '@/modules/auth/auth-current-tenant'; +import { AuthService } from '@/modules/auth/services/auth.service'; /** * Unauth Guard * * This middleware runs before rendering any route that has meta.unauth = true * - * It checks if currentUser is undefined (if not, redirects to /) + * It checks if user is undefined (if not, redirects to /) * * @param to * @param store * @param router * @returns {Promise<*>} */ -export default function ({ to, router }) { +export default async function ({ to, router }) { if (!to.meta || !to.meta.unauth) { return; } - const token = AuthToken.get(); - const tenantId = AuthCurrentTenant.get(); + const token = AuthService.getToken(); - if (token && tenantId) { + if (token) { // `window.history.replaceState` to replace the current URL with the root URL window.history.replaceState(null, '', '/'); diff --git a/frontend/src/middleware/navigation/navigation-guard.ts b/frontend/src/middleware/navigation/navigation-guard.ts new file mode 100644 index 0000000000..296e3db7bf --- /dev/null +++ b/frontend/src/middleware/navigation/navigation-guard.ts @@ -0,0 +1,53 @@ +/** + * Nativation Guard + * + * TBD + * + * @param to + * @param router + * @returns {Promise<*>} + */ + +import { + EventType, + PageEventKey, +} from '@/shared/modules/monitoring/types/event'; +import useProductTracking from '@/shared/modules/monitoring/useProductTracking'; +import { RouteLocationNormalized } from 'vue-router'; +import { useAuthStore } from '@/modules/auth/store/auth.store'; + +export default async function ({ to }: { to: RouteLocationNormalized }) { + const { trackEvent } = useProductTracking(); + + const authStore = useAuthStore(); + const { ensureLoaded, ensureTrackingSession } = authStore; + + await ensureLoaded(); + await ensureTrackingSession(); + + if (to.meta.eventKey && !to.redirectedFrom) { + let eventKey = to.meta.eventKey as PageEventKey; + + if (eventKey === PageEventKey.ADMIN_PANEL) { + if (to.hash === '#project-groups') { + eventKey = PageEventKey.ADMIN_PANEL_PROJECT_GROUPS; + } else if (to.hash === '#api-keys') { + eventKey = PageEventKey.ADMIN_PANEL_API_KEYS; + } else if (to.hash === '#audit-logs') { + eventKey = PageEventKey.ADMIN_PANEL_AUDIT_LOGS; + } else { + return; + } + } + + trackEvent({ + type: EventType.PAGE, + key: eventKey, + properties: { + path: to.path, + name: to.meta.title, + url: window.location.origin + to.fullPath, + }, + }); + } +} diff --git a/frontend/src/modules/activity/activity-model.js b/frontend/src/modules/activity/activity-model.js deleted file mode 100644 index 3407dcbf32..0000000000 --- a/frontend/src/modules/activity/activity-model.js +++ /dev/null @@ -1,61 +0,0 @@ -import { i18n, init as i18nInit } from '@/i18n'; -import { GenericModel } from '@/shared/model/generic-model'; -import { MemberField } from '@/modules/member/member-field'; -import SearchField from '@/shared/fields/search-field'; -import SentimentField from '@/shared/fields/sentiment-field'; -import ActivityDateField from '@/shared/fields/activity-date-field'; -import ActivityChannelsField from '@/shared/fields/activity-channels-field'; -import ActivityPlatformField from './activity-platform-field'; -import ActivityTypeField from './activity-type-field'; - -function label(name) { - return i18n(`entities.activity.fields.${name}`); -} - -i18nInit(); - -const fields = { - search: new SearchField('search', label('search'), { - fields: ['title', 'body'], - }), - member: MemberField.relationToOne( - 'memberId', - label('member'), - { - required: true, - filterable: true, - }, - ), - date: new ActivityDateField('timestamp', label('date'), { - filterable: true, - }), - platform: new ActivityPlatformField( - 'platform', - label('platform'), - { - required: true, - min: 2, - filterable: true, - }, - ), - type: new ActivityTypeField('type', label('type'), { - required: true, - filterable: true, - }), - sentiment: new SentimentField('sentiment', 'Sentiment', { - filterable: true, - }), - activityChannels: new ActivityChannelsField( - 'channel', - 'Channel', - { - filterable: true, - }, - ), -}; - -export class ActivityModel extends GenericModel { - static get fields() { - return fields; - } -} diff --git a/frontend/src/modules/activity/activity-permissions.js b/frontend/src/modules/activity/activity-permissions.js deleted file mode 100644 index 25560e8011..0000000000 --- a/frontend/src/modules/activity/activity-permissions.js +++ /dev/null @@ -1,42 +0,0 @@ -import Permissions from '@/security/permissions'; -import { PermissionChecker } from '@/modules/user/permission-checker'; - -export class ActivityPermissions { - constructor(currentTenant, currentUser) { - const permissionChecker = new PermissionChecker( - currentTenant, - currentUser, - ); - - this.read = permissionChecker.match( - Permissions.values.activityRead, - ); - this.import = permissionChecker.match( - Permissions.values.activityImport, - ); - this.activityAutocomplete = permissionChecker.match( - Permissions.values.activityAutocomplete, - ); - this.create = permissionChecker.match( - Permissions.values.activityCreate, - ); - this.edit = permissionChecker.match( - Permissions.values.activityEdit, - ); - this.destroy = permissionChecker.match( - Permissions.values.activityDestroy, - ); - this.lockedForCurrentPlan = permissionChecker.lockedForCurrentPlan( - Permissions.values.activityRead, - ); - this.createLockedForSampleData = permissionChecker.lockedForSampleData( - Permissions.values.activityCreate, - ); - this.editLockedForSampleData = permissionChecker.lockedForSampleData( - Permissions.values.activityEdit, - ); - this.destroyLockedForSampleData = permissionChecker.lockedForSampleData( - Permissions.values.activityDestroy, - ); - } -} diff --git a/frontend/src/modules/activity/activity-platform-field.js b/frontend/src/modules/activity/activity-platform-field.js index 1d9e82fd77..5e5051d098 100644 --- a/frontend/src/modules/activity/activity-platform-field.js +++ b/frontend/src/modules/activity/activity-platform-field.js @@ -29,7 +29,7 @@ export default class ActivityPlatformField extends StringField { }, { value: 'twitter', - label: 'Twitter', + label: 'X/Twitter', }, { value: 'devto', diff --git a/frontend/src/modules/activity/activity-routes.js b/frontend/src/modules/activity/activity-routes.js index 03bc04396d..91d551e3ab 100644 --- a/frontend/src/modules/activity/activity-routes.js +++ b/frontend/src/modules/activity/activity-routes.js @@ -1,5 +1,7 @@ import Layout from '@/modules/layout/components/layout.vue'; -import Permissions from '@/security/permissions'; +import { PageEventKey } from '@/shared/modules/monitoring/types/event'; +import { PermissionGuard } from '@/shared/modules/permissions/router/PermissionGuard'; +import { LfPermission } from '@/shared/modules/permissions/types/Permissions'; const ActivityListPage = () => import('@/modules/activity/pages/activity-list-page.vue'); @@ -8,7 +10,14 @@ export default [ name: '', path: '', component: Layout, - meta: { auth: true, title: 'Activities' }, + meta: { + auth: true, + title: 'Activities', + eventKey: PageEventKey.ACTIVITIES, + segments: { + requireSelectedProjectGroup: true, + }, + }, children: [ { name: 'activity', @@ -16,8 +25,10 @@ export default [ component: ActivityListPage, meta: { auth: true, - permission: Permissions.values.activityRead, }, + beforeEnter: [ + PermissionGuard(LfPermission.activityRead), + ], }, ], }, diff --git a/frontend/src/modules/activity/activity-service.js b/frontend/src/modules/activity/activity-service.js index faf60d6c78..da581bdc50 100644 --- a/frontend/src/modules/activity/activity-service.js +++ b/frontend/src/modules/activity/activity-service.js @@ -1,58 +1,60 @@ import authAxios from '@/shared/axios/auth-axios'; -import AuthCurrentTenant from '@/modules/auth/auth-current-tenant'; +import { AuthService } from '@/modules/auth/services/auth.service'; +import { useLfSegmentsStore } from '@/modules/lf/segments/store'; +import { getSegmentsFromProjectGroup } from '@/utils/segments'; +import { storeToRefs } from 'pinia'; -export class ActivityService { - static async update(id, data) { - const tenantId = AuthCurrentTenant.get(); - - const response = await authAxios.put( - `/tenant/${tenantId}/activity/${id}`, - data, - ); - - return response.data; - } +const getSelectedProjectGroup = () => { + const lsSegmentsStore = useLfSegmentsStore(); + const { selectedProjectGroup } = storeToRefs(lsSegmentsStore); - static async destroyAll(ids) { - const params = { - ids, - }; + return selectedProjectGroup.value; +}; - const tenantId = AuthCurrentTenant.get(); - - const response = await authAxios.delete( - `/tenant/${tenantId}/activity`, +export class ActivityService { + static async query(body, countOnly = false) { + // const segments = [ + // ...body?.segments ?? getSegmentsFromProjectGroup(getSelectedProjectGroup()), + // getSelectedProjectGroup().id, + // ]; + const response = await authAxios.post( + '/activity/query', { - params, + ...body, + countOnly, + segments: body.segments, + }, + { + headers: { + 'x-crowd-api-version': '1', + }, }, ); return response.data; } - static async create(data) { - const tenantId = AuthCurrentTenant.get(); - - const response = await authAxios.post( - `/tenant/${tenantId}/activity`, - data.data, + static async listActivityTypes(segment) { + const segments = segment || getSelectedProjectGroup()?.id; + const response = await authAxios.get( + '/activity/type', + { + params: { + segments: segments ? [segments] : [], + }, + }, ); return response.data; } - static async query( - body, - ) { - const sampleTenant = AuthCurrentTenant.getSampleTenantData(); - const tenantId = sampleTenant?.id || AuthCurrentTenant.get(); - - const response = await authAxios.post( - `/tenant/${tenantId}/activity/query`, - body, + static async listActivityChannels(segment) { + const segments = segment || getSelectedProjectGroup()?.id; + const response = await authAxios.get( + '/activity/channel', { - headers: { - Authorization: sampleTenant?.token, + params: { + segments: segments ? [segments] : [], }, }, ); diff --git a/frontend/src/modules/activity/activity-type-field.js b/frontend/src/modules/activity/activity-type-field.js index 45f0bdd4e1..d0a543f64d 100644 --- a/frontend/src/modules/activity/activity-type-field.js +++ b/frontend/src/modules/activity/activity-type-field.js @@ -1,9 +1,9 @@ import JSONField from '@/shared/fields/json-field'; import { toSentenceCase } from '@/utils/string'; -import { CrowdIntegrations } from '@/integrations/integrations-config'; import { storeToRefs } from 'pinia'; import { useActivityTypeStore } from '@/modules/activity/store/type'; import appConfig from '@/config'; +import { lfIdentities } from '@/config/identities'; export default class ActivityTypeField extends JSONField { constructor(name, label, config = {}) { @@ -24,7 +24,7 @@ export default class ActivityTypeField extends JSONField { // (temporary fix for default activity types stored in custom ones) .filter(([k, v]) => (!!Object.keys(v || {}).length) && (!appConfig.isGitIntegrationEnabled ? k !== 'git' : true)) .map(([key, value]) => { - let platformName = CrowdIntegrations.getConfig(key)?.name; + let platformName = lfIdentities[key]?.name; if (!platformName) { platformName = key === 'other' ? 'Custom' : key; diff --git a/frontend/src/modules/activity/components/activity-content-footer.vue b/frontend/src/modules/activity/components/activity-content-footer.vue index 44b49287b7..d765589feb 100644 --- a/frontend/src/modules/activity/components/activity-content-footer.vue +++ b/frontend/src/modules/activity/components/activity-content-footer.vue @@ -9,9 +9,7 @@
- +

@@ -27,7 +25,7 @@

- +
SHA: {{ sourceId }}
@@ -37,6 +35,7 @@ diff --git a/frontend/src/modules/activity/components/activity-content.vue b/frontend/src/modules/activity/components/activity-content.vue index c15af57484..e3ce164987 100644 --- a/frontend/src/modules/activity/components/activity-content.vue +++ b/frontend/src/modules/activity/components/activity-content.vue @@ -41,22 +41,7 @@ class="mt-3" />
- -
+
@@ -88,7 +73,7 @@
Show {{ more ? 'less' : 'more' }} @@ -118,7 +103,6 @@ diff --git a/frontend/src/modules/activity/components/activity-form-drawer.vue b/frontend/src/modules/activity/components/activity-form-drawer.vue deleted file mode 100644 index 33f36253b9..0000000000 --- a/frontend/src/modules/activity/components/activity-form-drawer.vue +++ /dev/null @@ -1,387 +0,0 @@ - - - - - diff --git a/frontend/src/modules/activity/components/activity-header.vue b/frontend/src/modules/activity/components/activity-header.vue index 0f0d629a59..909b589d74 100644 --- a/frontend/src/modules/activity/components/activity-header.vue +++ b/frontend/src/modules/activity/components/activity-header.vue @@ -3,10 +3,11 @@
-
+
·
- {{ activity.organization.displayName }} + {{ activity.organization.displayName }} +
@@ -47,14 +59,35 @@ import { computed } from 'vue'; import AppActivityMessage from '@/modules/activity/components/activity-message.vue'; import AppActivitySentiment from '@/modules/activity/components/activity-sentiment.vue'; import { formatDateToTimeAgo } from '@/utils/date'; +import { storeToRefs } from 'pinia'; +import { useLfSegmentsStore } from '@/modules/lf/segments/store'; +import LfOrganizationLfMemberTag from '@/modules/organization/components/lf-member/organization-lf-member-tag.vue'; +import { useActivityStore } from '../store/pinia'; const props = defineProps({ activity: { type: Object, default: () => {}, }, + showAffiliations: { + type: Boolean, + default: true, + }, }); +const lsSegmentsStore = useLfSegmentsStore(); +const { selectedProjectGroup } = storeToRefs(lsSegmentsStore); + +const activityStore = useActivityStore(); +const { filters } = storeToRefs(activityStore); + +const segmentId = computed(() => { + if (!filters.value.projects) { + return selectedProjectGroup.value?.id; + } + + return filters.value.projects.value[0]; +}); const timeAgo = computed(() => formatDateToTimeAgo(props.activity.timestamp)); const sentiment = computed(() => props.activity?.sentiment?.sentiment || 0); diff --git a/frontend/src/modules/activity/components/activity-icon.vue b/frontend/src/modules/activity/components/activity-icon.vue index f7958414b0..ac885997ff 100644 --- a/frontend/src/modules/activity/components/activity-icon.vue +++ b/frontend/src/modules/activity/components/activity-icon.vue @@ -1,12 +1,19 @@ @@ -27,59 +34,78 @@ defineProps({ const icons = ref({ github: { 'issue-comment': { - iconClass: 'ri-chat-4-line', + iconClass: 'fa-message fa-light', color: 'text-black', bgColor: 'bg-gray-200', }, 'pull_request-opened': { - iconClass: 'ri-git-pull-request-line', + iconClass: 'fa-code-pull-request fa-light', color: 'text-white', bgColor: 'bg-green-600', }, 'pull_request-closed': { - iconClass: 'ri-git-close-pull-request-line', + iconClass: 'fa-code-pull-request-closed fa-light', color: 'text-white', bgColor: 'bg-red-600', }, 'pull_request-comment': { - iconClass: 'ri-chat-4-line', + iconClass: 'fa-message fa-light', color: 'text-black', bgColor: 'bg-gray-200', }, 'discussion-comment': { - iconClass: 'ri-chat-4-line', + iconClass: 'fa-message fa-light', color: 'text-black', bgColor: 'bg-gray-200', }, 'pull_request-review-requested': { - iconClass: 'ri-eye-line', + iconClass: 'fa-eye fa-light', color: 'text-black', bgColor: 'bg-gray-200', }, 'pull_request-reviewed': { - iconClass: 'ri-eye-line', + iconClass: 'fa-eye fa-light', color: 'text-black', bgColor: 'bg-gray-200', }, 'pull_request-assigned': { - iconClass: 'ri-user-shared-line', + iconClass: 'fa-paper-plane-top fa-light', color: 'text-black', bgColor: 'bg-gray-200', }, 'pull_request-merged': { - iconClass: 'ri-git-merge-line', + iconClass: 'fa-code-merge fa-light', color: 'text-white', bgColor: 'bg-purple-600', }, 'pull_request-review-thread-comment': { - iconClass: 'ri-chat-4-line', + iconClass: 'fa-message fa-light', color: 'text-black', bgColor: 'bg-gray-200', }, }, git: { commit: { - imgSrc: '/images/integrations/git.png', + imgSrc: new URL('@/assets/images/integrations/git.png', import.meta.url) + .href, + color: 'text-black', + bgColor: 'bg-white', + }, + }, + confluence: { + page: { + imgSrc: new URL('@/assets/images/integrations/conf.jpg', import.meta.url) + .href, + color: 'text-black', + bgColor: 'bg-white', + }, + }, + gerrit: { + page: { + imgSrc: new URL( + '@/assets/images/integrations/gerrit.jpg', + import.meta.url, + ).href, color: 'text-black', bgColor: 'bg-white', }, diff --git a/frontend/src/modules/activity/components/activity-item.vue b/frontend/src/modules/activity/components/activity-item.vue index d7d1c25d82..a890fef36f 100644 --- a/frontend/src/modules/activity/components/activity-item.vue +++ b/frontend/src/modules/activity/components/activity-item.vue @@ -3,16 +3,23 @@
+ +
+ +
@@ -121,58 +106,44 @@
- + + diff --git a/frontend/src/modules/activity/components/activity-link.vue b/frontend/src/modules/activity/components/activity-link.vue index f5faddb4fe..ffe76c7597 100644 --- a/frontend/src/modules/activity/components/activity-link.vue +++ b/frontend/src/modules/activity/components/activity-link.vue @@ -1,12 +1,13 @@ - + + diff --git a/frontend/src/modules/layout/pages/paywall-page.vue b/frontend/src/modules/layout/pages/paywall-page.vue deleted file mode 100644 index a9a90aa8af..0000000000 --- a/frontend/src/modules/layout/pages/paywall-page.vue +++ /dev/null @@ -1,132 +0,0 @@ - - - - - diff --git a/frontend/src/modules/layout/pages/resize-page.vue b/frontend/src/modules/layout/pages/resize-page.vue index 9242827031..1b8c0c8621 100644 --- a/frontend/src/modules/layout/pages/resize-page.vue +++ b/frontend/src/modules/layout/pages/resize-page.vue @@ -1,9 +1,7 @@ + diff --git a/frontend/src/modules/layout/pages/temporary-paywall-page.vue b/frontend/src/modules/layout/pages/temporary-paywall-page.vue index e51fe8d737..5e9633e8de 100644 --- a/frontend/src/modules/layout/pages/temporary-paywall-page.vue +++ b/frontend/src/modules/layout/pages/temporary-paywall-page.vue @@ -1,17 +1,13 @@ + + diff --git a/frontend/src/modules/lf/config/audit-logs/filters/action/config.ts b/frontend/src/modules/lf/config/audit-logs/filters/action/config.ts new file mode 100644 index 0000000000..ca42079f35 --- /dev/null +++ b/frontend/src/modules/lf/config/audit-logs/filters/action/config.ts @@ -0,0 +1,30 @@ +import { FilterConfigType } from '@/shared/modules/filters/types/FilterConfig'; +import { itemLabelRendererByType } from '@/shared/modules/filters/config/itemLabelRendererByType'; +import { + MultiSelectFilterConfig, MultiSelectFilterOptions, + MultiSelectFilterValue, +} from '@/shared/modules/filters/types/filterTypes/MultiSelectFilterConfig'; +import options from './options'; + +const action: MultiSelectFilterConfig = { + id: 'action', + label: 'Action', + iconClass: 'arrow-pointer', + type: FilterConfigType.MULTISELECT, + options: { + options, + }, + itemLabelRenderer(value: MultiSelectFilterValue, options: MultiSelectFilterOptions, data: any): string { + return itemLabelRendererByType[FilterConfigType.MULTISELECT]('Action', value, options, data); + }, + apiFilterRenderer({ value, include }: MultiSelectFilterValue): any[] { + const filter = { + actionType: { in: value }, + }; + return [ + (include ? filter : { not: filter }), + ]; + }, +}; + +export default action; diff --git a/frontend/src/modules/lf/config/audit-logs/filters/action/options.ts b/frontend/src/modules/lf/config/audit-logs/filters/action/options.ts new file mode 100644 index 0000000000..85a318a848 --- /dev/null +++ b/frontend/src/modules/lf/config/audit-logs/filters/action/options.ts @@ -0,0 +1,77 @@ +import { SelectFilterOptionGroup } from '@/shared/modules/filters/types/filterTypes/SelectFilterConfig'; + +const options: SelectFilterOptionGroup[] = [ + { + label: 'Person', + options: [ + { + label: 'Profiles merged', + value: 'members-merge', + }, + { + label: 'Profiles unmerged', + value: 'members-unmerge', + }, + { + label: 'Profile identities updated', + value: 'members-edit-identities', + }, + { + label: 'Profile work experience updated', + value: 'members-edit-organizations', + }, + { + label: 'Profile affiliation updated', + value: 'members-edit-manual-affiliation', + }, + { + label: 'Profile updated', + value: 'members-edit-profile', + }, + { + label: 'Profile created', + value: 'members-create', + }, + ], + }, + { + label: 'Organization', + options: [ + { + label: 'Organizations merged', + value: 'organizations-merged', + }, + { + label: 'Organizations unmerged', + value: 'organizations-unmerged', + }, + { + label: 'Organization identities updated', + value: 'organizations-edit-identities', + }, + { + label: 'Organization profile updated', + value: 'organizations-edit-profile', + }, + { + label: 'Organization created', + value: 'organizations-create', + }, + ], + }, + { + label: 'Integration', + options: [ + { + label: 'Integration connected', + value: 'integrations-connect', + }, + { + label: 'Integration re-connected', + value: 'integrations-reconnect', + }, + ], + }, +]; + +export default options; diff --git a/frontend/src/modules/lf/config/audit-logs/filters/actor/config.ts b/frontend/src/modules/lf/config/audit-logs/filters/actor/config.ts new file mode 100644 index 0000000000..698bdc2ddb --- /dev/null +++ b/frontend/src/modules/lf/config/audit-logs/filters/actor/config.ts @@ -0,0 +1,38 @@ +import { FilterConfigType } from '@/shared/modules/filters/types/FilterConfig'; +import { itemLabelRendererByType } from '@/shared/modules/filters/config/itemLabelRendererByType'; +import { LfService } from '@/modules/lf/segments/lf-segments-service'; +import { + SelectAsyncFilterConfig, SelectAsyncFilterValue, + SelectAsyncFilterOptions, +} from '@/shared/modules/filters/types/filterTypes/SelectAsyncFilterConfig'; + +const actor: SelectAsyncFilterConfig = { + id: 'actor', + label: 'Actor', + iconClass: 'circle-user', + type: FilterConfigType.SELECT_ASYNC, + options: { + hideIncludeSwitch: true, + remoteMethod: (query) => LfService.fetchUsers(query, 10) + .then((rows: any) => rows.map((actor: any) => ({ + label: actor.label, + description: `${actor.email}`, + value: actor.id, + }))), + remotePopulateItems: (id: string) => LfService.getUser(id) + .then((actor: any) => ({ + label: actor.fullName, + value: actor.id, + })), + }, + itemLabelRenderer(value: SelectAsyncFilterValue, options: SelectAsyncFilterOptions, data: any): string { + return itemLabelRendererByType[FilterConfigType.SELECT_ASYNC]('Actor', value, options, data); + }, + apiFilterRenderer({ value }: SelectAsyncFilterValue): any[] { + return [{ + actorId: value, + }]; + }, +}; + +export default actor; diff --git a/frontend/src/modules/lf/config/audit-logs/filters/entityId/config.ts b/frontend/src/modules/lf/config/audit-logs/filters/entityId/config.ts new file mode 100644 index 0000000000..6d17720d72 --- /dev/null +++ b/frontend/src/modules/lf/config/audit-logs/filters/entityId/config.ts @@ -0,0 +1,28 @@ +import { FilterConfigType } from '@/shared/modules/filters/types/FilterConfig'; +import { + StringFilterConfig, + StringFilterOptions, + StringFilterValue, +} from '@/shared/modules/filters/types/filterTypes/StringFilterConfig'; +import { itemLabelRendererByType } from '@/shared/modules/filters/config/itemLabelRendererByType'; +import { FilterStringOperator } from '@/shared/modules/filters/config/constants/string.constants'; + +const entityId: StringFilterConfig = { + id: 'entityId', + label: 'Entity Id', + iconClass: 'shapes', + type: FilterConfigType.STRING, + options: { + fixedOperator: FilterStringOperator.EQ, + }, + itemLabelRenderer(value: StringFilterValue, options: StringFilterOptions): string { + return itemLabelRendererByType[FilterConfigType.STRING]('Entity Id', value, options); + }, + apiFilterRenderer(value: StringFilterValue): any[] { + return [{ + entityId: value.value, + }]; + }, +}; + +export default entityId; diff --git a/frontend/src/modules/lf/config/audit-logs/filters/main.ts b/frontend/src/modules/lf/config/audit-logs/filters/main.ts new file mode 100644 index 0000000000..9ba923d204 --- /dev/null +++ b/frontend/src/modules/lf/config/audit-logs/filters/main.ts @@ -0,0 +1,10 @@ +import { FilterConfig } from '@/shared/modules/filters/types/FilterConfig'; +import actor from './actor/config'; +import entityId from './entityId/config'; +import action from './action/config'; + +export const auditLogsFilters: Record = { + entityId, + actor, + action, +}; diff --git a/frontend/src/modules/lf/config/audit-logs/log-rendering/index.ts b/frontend/src/modules/lf/config/audit-logs/log-rendering/index.ts new file mode 100644 index 0000000000..ac563e1d42 --- /dev/null +++ b/frontend/src/modules/lf/config/audit-logs/log-rendering/index.ts @@ -0,0 +1,52 @@ +import { ActionType, AuditLog } from '@/modules/lf/segments/types/AuditLog'; +import integrationsConnect from './integrations-connect'; +import integrationsReconnect from './integrations-reconnect'; +import membersCreate from './members-create'; +import membersEditIdentities from './members-edit-identities'; +import membersEditManualAffiliation from './members-edit-manual-affiliation'; +import membersEditOrganizations from './members-edit-organizations'; +import membersEditProfile from './members-edit-profile'; +import membersMerge from './members-merge'; +import membersUnmerge from './members-unmerge'; +import organizationsCreate from './organizations-create'; +import organizationsEditIdentities from './organizations-edit-identities'; +import organizationsEditProfile from './organizations-edit-profile'; +import organizationsMerge from './organizations-merge'; +import organizationsUnmerge from './organizations-unmerge'; + +export interface LogRenderingConfig { + label: string; + description: (log: AuditLog) => string; + properties?: (log: AuditLog) => {label: string, value: string;}[]; + changes?: (log: AuditLog) => Promise<{ + removals: string[] + additions: string[] + changes: string[] + } | null> | { + removals: string[] + additions: string[] + changes: string[] + } | null +} + +export const logRenderingConfig: Record = { + // Integrations + [ActionType.INTEGRATIONS_CONNECT]: integrationsConnect, + [ActionType.INTEGRATIONS_RECONNECT]: integrationsReconnect, + + // Members + [ActionType.MEMBERS_CREATE]: membersCreate, + [ActionType.MEMBERS_EDIT_IDENTITIES]: membersEditIdentities, + [ActionType.MEMBERS_EDIT_MANUAL_AFFILIATION]: membersEditManualAffiliation, + [ActionType.MEMBERS_EDIT_ORGANIZATIONS]: membersEditOrganizations, + [ActionType.MEMBERS_EDIT_PROFILE]: membersEditProfile, + [ActionType.MEMBERS_MERGE]: membersMerge, + [ActionType.MEMBERS_UNMERGE]: membersUnmerge, + + // Organizations + [ActionType.ORGANIZATIONS_CREATE]: organizationsCreate, + [ActionType.ORGANIZATIONS_EDIT_IDENTITIES]: organizationsEditIdentities, + [ActionType.ORGANIZATIONS_EDIT_PROFILE]: organizationsEditProfile, + [ActionType.ORGANIZATIONS_MERGE]: organizationsMerge, + [ActionType.ORGANIZATIONS_UNMERGE]: organizationsUnmerge, +}; diff --git a/frontend/src/modules/lf/config/audit-logs/log-rendering/integrations-connect.ts b/frontend/src/modules/lf/config/audit-logs/log-rendering/integrations-connect.ts new file mode 100644 index 0000000000..c318db1cf4 --- /dev/null +++ b/frontend/src/modules/lf/config/audit-logs/log-rendering/integrations-connect.ts @@ -0,0 +1,26 @@ +import { LogRenderingConfig } from '@/modules/lf/config/audit-logs/log-rendering/index'; +import { lfIdentities } from '@/config/identities'; + +const integrationsConnect: LogRenderingConfig = { + label: 'Integration connected', + changes: () => null, + description: (log) => { + const integration = lfIdentities[log.newState?.platform || log.oldState?.platform]; + if (integration) { + return `Integration: ${integration.name}`; + } + return ''; + }, + properties: (log) => { + const integration = lfIdentities[log.newState?.platform || log.oldState?.platform]; + if (integration) { + return [{ + label: 'Integration', + value: integration.name, + }]; + } + return []; + }, +}; + +export default integrationsConnect; diff --git a/frontend/src/modules/lf/config/audit-logs/log-rendering/integrations-reconnect.ts b/frontend/src/modules/lf/config/audit-logs/log-rendering/integrations-reconnect.ts new file mode 100644 index 0000000000..15c15ec57b --- /dev/null +++ b/frontend/src/modules/lf/config/audit-logs/log-rendering/integrations-reconnect.ts @@ -0,0 +1,26 @@ +import { LogRenderingConfig } from '@/modules/lf/config/audit-logs/log-rendering/index'; +import { lfIdentities } from '@/config/identities'; + +const integrationsReconnect: LogRenderingConfig = { + label: 'Integration re-connected', + changes: () => null, + description: (log) => { + const integration = lfIdentities[log.newState?.platform || log.oldState?.platform]; + if (integration) { + return `Integration: ${integration.name}`; + } + return ''; + }, + properties: (log) => { + const integration = lfIdentities[log.newState?.platform || log.oldState?.platform]; + if (integration) { + return [{ + label: 'Integration', + value: integration.name, + }]; + } + return []; + }, +}; + +export default integrationsReconnect; diff --git a/frontend/src/modules/lf/config/audit-logs/log-rendering/members-create.ts b/frontend/src/modules/lf/config/audit-logs/log-rendering/members-create.ts new file mode 100644 index 0000000000..34edeb8824 --- /dev/null +++ b/frontend/src/modules/lf/config/audit-logs/log-rendering/members-create.ts @@ -0,0 +1,29 @@ +import { LogRenderingConfig } from '@/modules/lf/config/audit-logs/log-rendering/index'; + +const membersCreate: LogRenderingConfig = { + label: 'Profile created', + changes: () => null, + description: (log) => { + const member = log.newState?.displayName; + + if (member) { + return `${member}
ID: ${log.entityId}`; + } + + return ''; + }, + properties: (log) => { + const member = log.newState?.displayName; + + if (member) { + return [{ + label: 'Profile', + value: `${member}
ID: ${log.entityId}`, + }]; + } + + return []; + }, +}; + +export default membersCreate; diff --git a/frontend/src/modules/lf/config/audit-logs/log-rendering/members-edit-identities.ts b/frontend/src/modules/lf/config/audit-logs/log-rendering/members-edit-identities.ts new file mode 100644 index 0000000000..887410c466 --- /dev/null +++ b/frontend/src/modules/lf/config/audit-logs/log-rendering/members-edit-identities.ts @@ -0,0 +1,37 @@ +import { LogRenderingConfig } from '@/modules/lf/config/audit-logs/log-rendering/index'; +import { lfIdentities } from '@/config/identities'; + +const membersEditIdentities: LogRenderingConfig = { + label: 'Profile identities updated', + changes: (log) => { + const removals = []; + const additions = []; + const changes = []; + + Object.keys(log.oldState).forEach((platform) => { + log.oldState[platform].forEach((identity) => { + if (!log.newState[platform] || log.newState[platform].length === 0 || !log.newState[platform].includes(identity)) { + removals.push(`${lfIdentities[platform]?.name || platform} username: ${identity}`); + } + }); + }); + + // Check for additions in newState + Object.keys(log.newState).forEach((platform) => { + log.newState[platform].forEach((identity) => { + if (!log.oldState[platform] || !log.oldState[platform].includes(identity)) { + additions.push(`${lfIdentities[platform]?.name || platform} username: ${identity}`); + } + }); + }); + + return { removals, additions, changes }; + }, + description: (log) => `ID: ${log.entityId}`, + properties: (log) => [{ + label: 'Profile', + value: `ID: ${log.entityId}`, + }], +}; + +export default membersEditIdentities; diff --git a/frontend/src/modules/lf/config/audit-logs/log-rendering/members-edit-manual-affiliation.ts b/frontend/src/modules/lf/config/audit-logs/log-rendering/members-edit-manual-affiliation.ts new file mode 100644 index 0000000000..b558942ddc --- /dev/null +++ b/frontend/src/modules/lf/config/audit-logs/log-rendering/members-edit-manual-affiliation.ts @@ -0,0 +1,101 @@ +import { LogRenderingConfig } from '@/modules/lf/config/audit-logs/log-rendering/index'; +import { OrganizationService } from '@/modules/organization/organization-service'; +import { LfService } from '@/modules/lf/segments/lf-segments-service'; +import { dateHelper } from '@/shared/date-helper/date-helper'; + +const formatDateRange = (dateStart, dateEnd) => { + // eslint-disable-next-line no-nested-ternary + const dateStartFormat = dateStart + ? dateHelper(dateStart).utc().format('MMMM YYYY') + : 'Unknown'; + // eslint-disable-next-line no-nested-ternary + const dateEndFormat = dateEnd + ? dateHelper(dateEnd).utc().format('MMMM YYYY') + : (dateStart ? 'Present' : 'Unknown'); + return `${dateStartFormat} -> ${dateEndFormat}`; +}; + +const membersEditManualAffiliation: LogRenderingConfig = { + label: 'Profile affiliation updated', + changes: async (log) => { + const changes = { + removals: [], + additions: [], + changes: [], + }; + + const oldStateMap = new Map(log.oldState.map((org) => [org.organizationId, org])); + const newStateMap = new Map(log.newState.map((org) => [org.organizationId, org])); + + const orgIds = [ + ...new Set([ + ...log.oldState.map((org) => org.organizationId), + ...log.newState.map((org) => org.organizationId), + ]), + ]; + + const segmentIds = [ + ...new Set([ + ...log.oldState.map((org) => org.segmentId), + ...log.newState.map((org) => org.segmentId), + ]), + ]; + + const orgs = await OrganizationService.listByIds(orgIds); + const segments = await LfService.listSegmentsByIds(segmentIds); + + const orgById = orgs.reduce((obj, org) => ({ + ...obj, + [org.id]: org.displayName, + }), {}); + + const segmentById = segments.reduce((obj, org) => ({ + ...obj, + [org.id]: org.name, + }), {}); + + // Check for removals and modifications + log.oldState.forEach((org) => { + if (!newStateMap.has(org.organizationId)) { + const { + organizationId, dateStart, dateEnd, segmentId, + } = org; + changes.removals.push( + `${organizationId ? (orgById[organizationId]) : 'Individual'}: ${segmentId ? segmentById[segmentId] : 'None'} +
(${formatDateRange(dateStart, dateEnd)})`, + ); + } else { + const newOrg = newStateMap.get(org.organizationId); + if (org.dateStart !== newOrg.dateStart || org.dateEnd !== newOrg.dateEnd || org.segmentId !== newOrg.segmentId) { + changes.changes.push( + `${org.organizationId ? (orgById[org.organizationId]) : 'Individual'} : +
${org.segmentId ? segmentById[org.segmentId] : 'None'} (${formatDateRange(org.dateStart, org.dateEnd)}) +
${newOrg.segmentId ? segmentById[newOrg.segmentId] : 'None'} (${formatDateRange(newOrg.dateStart, newOrg.dateEnd)})`, + ); + } + } + }); + + // Check for additions + log.newState.forEach((org) => { + if (!oldStateMap.has(org.organizationId)) { + const { + organizationId, dateStart, dateEnd, segmentId, + } = org; + changes.additions.push( + `${organizationId ? (orgById[organizationId]) : 'Individual'}: ${segmentId ? segmentById[segmentId] : 'None'} +
(${formatDateRange(dateStart, dateEnd)})`, + ); + } + }); + + return changes; + }, + description: (log) => `ID: ${log.entityId}`, + properties: (log) => [{ + label: 'Profile', + value: `ID: ${log.entityId}`, + }], +}; + +export default membersEditManualAffiliation; diff --git a/frontend/src/modules/lf/config/audit-logs/log-rendering/members-edit-organizations.ts b/frontend/src/modules/lf/config/audit-logs/log-rendering/members-edit-organizations.ts new file mode 100644 index 0000000000..f80a6817c2 --- /dev/null +++ b/frontend/src/modules/lf/config/audit-logs/log-rendering/members-edit-organizations.ts @@ -0,0 +1,83 @@ +import { LogRenderingConfig } from '@/modules/lf/config/audit-logs/log-rendering/index'; +import { OrganizationService } from '@/modules/organization/organization-service'; +import { dateHelper } from '@/shared/date-helper/date-helper'; + +const formatDateRange = (dateStart, dateEnd) => { + // eslint-disable-next-line no-nested-ternary + const dateStartFormat = dateStart + ? dateHelper(dateStart) + .utc() + .format('MMMM YYYY') + : 'Unknown'; + // eslint-disable-next-line no-nested-ternary + const dateEndFormat = dateEnd + ? dateHelper(dateEnd) + .utc() + .format('MMMM YYYY') + : (dateStart ? 'Present' : 'Unknown'); + return `${dateStartFormat} -> ${dateEndFormat}`; +}; + +const membersEditOrganizations: LogRenderingConfig = { + label: 'Profile work experience updated', + changes: async (log) => { + const changes = { + removals: [], + additions: [], + changes: [], + }; + + const oldStateMap = new Map(log.oldState.map((org) => [org.organizationId, org])); + const newStateMap = new Map( + log.newState + .filter((org) => !!org.dateStart && !!org.dateEnd) + .map((org) => [org.organizationId, org]), + ); + + const orgIds = [ + ...new Set([ + ...log.oldState.map((org) => org.organizationId), + ...log.newState.map((org) => org.organizationId), + ]), + ]; + + const orgs = await OrganizationService.listByIds(orgIds); + const orgById = orgs.reduce((obj, org) => ({ + ...obj, + [org.id]: org.displayName, + }), {}); + + // Check for removals and modifications + log.oldState.forEach((org) => { + if (!newStateMap.has(org.organizationId)) { + changes.removals.push(`Organization: ${org.organizationId ? (orgById[org.organizationId]) : 'Individual'}`); + } else { + const newOrg = newStateMap.get(org.organizationId); + if ( + formatDateRange(org.dateStart, org.dateEnd) !== formatDateRange(newOrg.dateStart, newOrg.dateEnd) + || (org.title || '') !== (newOrg.title || '')) { + changes.changes.push(`Organization: ${org.organizationId ? (orgById[org.organizationId]) : 'Individual'} +
${org.title ? `${org.title}: ` : ''}${formatDateRange(org.dateStart, org.dateEnd)} +
${newOrg.title ? `${org.title}: ` : ''}${formatDateRange(newOrg.dateStart, newOrg.dateEnd)} + `); + } + } + }); + + // Check for additions + log.newState.forEach((org) => { + if (!oldStateMap.has(org.organizationId)) { + changes.additions.push(`Organization: ${org.organizationId ? (orgById[org.organizationId]) : 'Individual'}`); + } + }); + + return changes; + }, + description: (log) => `ID: ${log.entityId}`, + properties: (log) => [{ + label: 'Profile', + value: `ID: ${log.entityId}`, + }], +}; + +export default membersEditOrganizations; diff --git a/frontend/src/modules/lf/config/audit-logs/log-rendering/members-edit-profile.ts b/frontend/src/modules/lf/config/audit-logs/log-rendering/members-edit-profile.ts new file mode 100644 index 0000000000..20d7e212d0 --- /dev/null +++ b/frontend/src/modules/lf/config/audit-logs/log-rendering/members-edit-profile.ts @@ -0,0 +1,67 @@ +import { LogRenderingConfig } from '@/modules/lf/config/audit-logs/log-rendering/index'; + +function camelCaseToName(camelCase) { + return camelCase.replace(/([A-Z])/g, ' $1') + .replace(/^./, (str) => str.toUpperCase()); +} + +function flattenObject(obj) { + if (!obj) { + return {}; + } + const flattenedObj = {}; + Object.keys(obj).forEach((key) => { + Object.keys(obj[key]).forEach((nestedKey) => { + const capitalizedNestedKey = nestedKey.charAt(0).toUpperCase() + nestedKey.slice(1); + flattenedObj[`${key}${capitalizedNestedKey}`] = obj[key][nestedKey]; + }); + }); + return flattenedObj; +} + +const membersEditProfile: LogRenderingConfig = { + label: 'Profile updated', + changes: (log) => { + const additions: any[] = []; + const removals: any[] = []; + const changes: any[] = []; + + const oldState = { ...log.oldState, ...flattenObject(log.oldState.attributes) }; + const newState = { ...log.newState, ...flattenObject(log.newState.attributes) }; + const diff = { + ...log.diff, + ...{ ...flattenObject(log.oldState.attributes), ...flattenObject(log.newState.attributes) }, + }; + delete oldState.attributes; + delete newState.attributes; + + Object.keys(diff) + .forEach((key) => { + const keyName = camelCaseToName(key); + if (!!oldState[key] && !newState[key] && newState[key] !== undefined) { + const display = typeof oldState[key] === 'object' ? JSON.stringify(oldState[key]) : `${oldState[key]}`; + removals.push(`${keyName}: ${display}`); + } else if (!oldState[key] && !!newState[key]) { + const display = typeof newState[key] === 'object' ? JSON.stringify(newState[key]) : `${newState[key]}`; + additions.push(`${keyName}: ${display}`); + } else if (oldState[key] !== newState[key] && newState[key] !== undefined) { + const displayOld = typeof oldState[key] === 'object' ? JSON.stringify(oldState[key]) : `${oldState[key]}`; + const displayNew = typeof newState[key] === 'object' ? JSON.stringify(newState[key]) : `${newState[key]}`; + changes.push(`${keyName}: ${displayOld} ${displayNew}`); + } + }); + + return { + additions, + removals, + changes, + }; + }, + description: (log) => `ID: ${log.entityId}`, + properties: (log) => [{ + label: 'Profile', + value: `ID: ${log.entityId}`, + }], +}; + +export default membersEditProfile; diff --git a/frontend/src/modules/lf/config/audit-logs/log-rendering/members-merge.ts b/frontend/src/modules/lf/config/audit-logs/log-rendering/members-merge.ts new file mode 100644 index 0000000000..48b7f7020d --- /dev/null +++ b/frontend/src/modules/lf/config/audit-logs/log-rendering/members-merge.ts @@ -0,0 +1,42 @@ +import { LogRenderingConfig } from '@/modules/lf/config/audit-logs/log-rendering/index'; + +const membersMerge: LogRenderingConfig = { + label: 'Profiles merged', + changes: (log) => { + const primary = log.oldState?.primary; + const secondary = log.oldState?.secondary; + const merged = log.newState?.primary; + return { + removals: merged ? [ + `${primary?.displayName} ・ ID: ${primary?.id}`, + `${secondary?.displayName} ・ ID: ${secondary?.id}`, + ] : [], + additions: merged ? [ + `${merged?.displayName || primary?.displayName} ・ ID: ${merged?.id || primary?.id}`, + ] : [], + changes: [], + }; + }, + description: (log) => { + const member = log.newState?.primary?.displayName || log.oldState?.primary?.displayName; + + if (member) { + return `${member}
ID: ${log.entityId}`; + } + + return ''; + }, + properties: (log) => { + const member = log.newState?.primary?.displayName || log.oldState?.primary?.displayName; + + if (member) { + return [{ + label: 'Profile', + value: `${member}
ID: ${log.entityId}`, + }]; + } + return []; + }, +}; + +export default membersMerge; diff --git a/frontend/src/modules/lf/config/audit-logs/log-rendering/members-unmerge.ts b/frontend/src/modules/lf/config/audit-logs/log-rendering/members-unmerge.ts new file mode 100644 index 0000000000..621b269a97 --- /dev/null +++ b/frontend/src/modules/lf/config/audit-logs/log-rendering/members-unmerge.ts @@ -0,0 +1,42 @@ +import { LogRenderingConfig } from '@/modules/lf/config/audit-logs/log-rendering/index'; + +const membersMerge: LogRenderingConfig = { + label: 'Profiles unmerged', + changes: (log) => { + const primary = log.oldState?.primary; + const secondary = log.newState?.secondary; + const merged = log.newState?.primary; + return { + removals: merged ? [ + `${primary?.displayName} ・ ID: ${primary?.id}`, + `${secondary?.displayName} ・ ID: ${secondary?.id}`, + ] : [], + additions: merged ? [ + `${merged?.displayName || primary?.displayName} ・ ID: ${merged?.id || primary?.id}`, + ] : [], + changes: [], + }; + }, + description: (log) => { + const member = log.newState?.primary?.displayName || log.oldState?.primary?.displayName; + + if (member) { + return `${member}
ID: ${log.entityId}`; + } + + return ''; + }, + properties: (log) => { + const member = log.newState?.primary?.displayName || log.oldState?.primary?.displayName; + + if (member) { + return [{ + label: 'Profile', + value: `${member}
ID: ${log.entityId}`, + }]; + } + return []; + }, +}; + +export default membersMerge; diff --git a/frontend/src/modules/lf/config/audit-logs/log-rendering/organizations-create.ts b/frontend/src/modules/lf/config/audit-logs/log-rendering/organizations-create.ts new file mode 100644 index 0000000000..c665851969 --- /dev/null +++ b/frontend/src/modules/lf/config/audit-logs/log-rendering/organizations-create.ts @@ -0,0 +1,25 @@ +import { LogRenderingConfig } from '@/modules/lf/config/audit-logs/log-rendering/index'; + +const organizationsCreate: LogRenderingConfig = { + label: 'Organization created', + changes: () => null, + description: (log) => { + const organization = log.newState?.displayName; + if (organization) { + return `${organization}
ID: ${log.entityId}`; + } + return ''; + }, + properties: (log) => { + const organization = log.newState?.displayName; + if (organization) { + return [{ + label: 'Organization', + value: `${organization}
ID: ${log.entityId}`, + }]; + } + return []; + }, +}; + +export default organizationsCreate; diff --git a/frontend/src/modules/lf/config/audit-logs/log-rendering/organizations-edit-identities.ts b/frontend/src/modules/lf/config/audit-logs/log-rendering/organizations-edit-identities.ts new file mode 100644 index 0000000000..79446358e7 --- /dev/null +++ b/frontend/src/modules/lf/config/audit-logs/log-rendering/organizations-edit-identities.ts @@ -0,0 +1,42 @@ +import { LogRenderingConfig } from '@/modules/lf/config/audit-logs/log-rendering/index'; +import { lfIdentities } from '@/config/identities'; + +const organizationsEditIdentities: LogRenderingConfig = { + label: 'Organization identities updated', + changes: ({ oldState, newState }) => { + const additions: any[] = []; + const removals: any[] = []; + + // Helper function to process states + const keys = new Set([...Object.keys(oldState), ...Object.keys(newState)]); + keys.forEach((key) => { + const oldValues = new Set(oldState[key] || []); + const newValues = new Set(newState[key] || []); + + newValues.forEach((value) => { + if (!oldValues.has(value)) { + additions.push({ platform: key, name: value }); + } + }); + + oldValues.forEach((value) => { + if (!newValues.has(value)) { + removals.push({ platform: key, name: value }); + } + }); + }); + + return { + additions: (additions || []).map((p) => `${lfIdentities[p.platform]?.name || p.platform}: ${p.name}`), + removals: (removals || []).map((p) => `${lfIdentities[p.platform]?.name || p.platform}: ${p.name}`), + changes: [], + }; + }, + description: (log) => `ID: ${log.entityId}`, + properties: (log) => [{ + label: 'Organization', + value: `ID: ${log.entityId}`, + }], +}; + +export default organizationsEditIdentities; diff --git a/frontend/src/modules/lf/config/audit-logs/log-rendering/organizations-edit-profile.ts b/frontend/src/modules/lf/config/audit-logs/log-rendering/organizations-edit-profile.ts new file mode 100644 index 0000000000..ec942a47a0 --- /dev/null +++ b/frontend/src/modules/lf/config/audit-logs/log-rendering/organizations-edit-profile.ts @@ -0,0 +1,43 @@ +import { LogRenderingConfig } from '@/modules/lf/config/audit-logs/log-rendering/index'; + +function camelCaseToName(camelCase) { + return camelCase.replace(/([A-Z])/g, ' $1') + .replace(/^./, (str) => str.toUpperCase()); +} + +const organizationsEditProfile: LogRenderingConfig = { + label: 'Organization profile updated', + changes: ({ oldState, newState, diff }) => { + const additions = []; + const removals = []; + const changes = []; + + Object.keys(diff).forEach((key) => { + const keyName = camelCaseToName(key); + if (!!oldState[key] && !newState[key] && newState[key] !== undefined) { + const display = typeof oldState[key] === 'object' ? JSON.stringify(oldState[key]) : `${oldState[key]}`; + removals.push(`${keyName}: ${display}`); + } else if (!oldState[key] && !!newState[key]) { + const display = typeof newState[key] === 'object' ? JSON.stringify(newState[key]) : `${newState[key]}`; + additions.push(`${keyName}: ${display}`); + } else if (oldState[key] !== newState[key] && newState[key] !== undefined) { + const displayOld = typeof oldState[key] === 'object' ? JSON.stringify(oldState[key]) : `${oldState[key]}`; + const displayNew = typeof newState[key] === 'object' ? JSON.stringify(newState[key]) : `${newState[key]}`; + changes.push(`${keyName}: ${displayOld} ${displayNew}`); + } + }); + + return { + additions, + removals, + changes, + }; + }, + description: (log) => `ID: ${log.entityId}`, + properties: (log) => [{ + label: 'Organization', + value: `ID: ${log.entityId}`, + }], +}; + +export default organizationsEditProfile; diff --git a/frontend/src/modules/lf/config/audit-logs/log-rendering/organizations-merge.ts b/frontend/src/modules/lf/config/audit-logs/log-rendering/organizations-merge.ts new file mode 100644 index 0000000000..1e16742924 --- /dev/null +++ b/frontend/src/modules/lf/config/audit-logs/log-rendering/organizations-merge.ts @@ -0,0 +1,39 @@ +import { LogRenderingConfig } from '@/modules/lf/config/audit-logs/log-rendering/index'; + +const organizationsMerge: LogRenderingConfig = { + label: 'Organizations merged', + changes: (log) => { + const primary = log.oldState?.primary; + const secondary = log.oldState?.secondary; + const merged = log.newState?.primary; + return { + removals: merged ? [ + `${primary?.displayName} ・ ID: ${primary?.id}`, + `${secondary?.displayName} ・ ID: ${secondary?.id}`, + ] : [], + additions: merged ? [ + `${merged?.displayName || primary.displayName} ・ ID: ${merged?.id || primary.id}`, + ] : [], + changes: [], + }; + }, + description: (log) => { + const organization = log.newState?.primary?.displayName || log.oldState?.primary?.displayName; + if (organization) { + return `${organization}
ID: ${log.entityId}`; + } + return ''; + }, + properties: (log) => { + const organization = log.newState?.primary?.displayName || log.oldState?.primary?.displayName; + if (organization) { + return [{ + label: 'Organization', + value: `${organization}
ID: ${log.entityId}`, + }]; + } + return []; + }, +}; + +export default organizationsMerge; diff --git a/frontend/src/modules/lf/config/audit-logs/log-rendering/organizations-unmerge.ts b/frontend/src/modules/lf/config/audit-logs/log-rendering/organizations-unmerge.ts new file mode 100644 index 0000000000..e37640a2eb --- /dev/null +++ b/frontend/src/modules/lf/config/audit-logs/log-rendering/organizations-unmerge.ts @@ -0,0 +1,39 @@ +import { LogRenderingConfig } from '@/modules/lf/config/audit-logs/log-rendering/index'; + +const organizationsMerge: LogRenderingConfig = { + label: 'Organizations unmerged', + changes: (log) => { + const primary = log.oldState?.primary; + const secondary = log.newState?.secondary; + const merged = log.newState?.primary; + return { + removals: merged ? [ + `${primary?.displayName} ・ ID: ${primary?.id}`, + `${secondary?.displayName} ・ ID: ${secondary?.id}`, + ] : [], + additions: merged ? [ + `${merged?.displayName || primary.displayName} ・ ID: ${merged?.id || primary.id}`, + ] : [], + changes: [], + }; + }, + description: (log) => { + const organization = log.newState?.primary?.displayName || log.oldState?.primary?.displayName; + if (organization) { + return `${organization}
ID: ${log.entityId}`; + } + return ''; + }, + properties: (log) => { + const organization = log.newState?.primary?.displayName || log.oldState?.primary?.displayName; + if (organization) { + return [{ + label: 'Organization', + value: `${organization}
ID: ${log.entityId}`, + }]; + } + return []; + }, +}; + +export default organizationsMerge; diff --git a/frontend/src/modules/lf/config/status.js b/frontend/src/modules/lf/config/status.js new file mode 100644 index 0000000000..f731deacc6 --- /dev/null +++ b/frontend/src/modules/lf/config/status.js @@ -0,0 +1,26 @@ +export default [ + { + class: 'success', + color: 'bg-green-500', + label: 'Active', + value: 'active', + }, + { + class: 'primary', + color: 'bg-primary-500', + label: 'Formation', + value: 'formation', + }, + { + class: 'warning', + color: 'bg-yellow-500', + label: 'Prospect', + value: 'prospect', + }, + { + class: 'secondary', + color: 'bg-gray-400', + label: 'Archived', + value: 'archived', + }, +]; diff --git a/frontend/src/modules/lf/layout/components/lf-banners.vue b/frontend/src/modules/lf/layout/components/lf-banners.vue new file mode 100644 index 0000000000..aa94ec67b4 --- /dev/null +++ b/frontend/src/modules/lf/layout/components/lf-banners.vue @@ -0,0 +1,300 @@ + + + + + diff --git a/frontend/src/modules/lf/layout/components/lf-menu-project-group-selection.vue b/frontend/src/modules/lf/layout/components/lf-menu-project-group-selection.vue new file mode 100644 index 0000000000..1e0eb023ac --- /dev/null +++ b/frontend/src/modules/lf/layout/components/lf-menu-project-group-selection.vue @@ -0,0 +1,281 @@ + + + + + + + diff --git a/frontend/src/modules/lf/layout/components/lf-page-header.vue b/frontend/src/modules/lf/layout/components/lf-page-header.vue new file mode 100644 index 0000000000..9cf325e003 --- /dev/null +++ b/frontend/src/modules/lf/layout/components/lf-page-header.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/frontend/src/modules/lf/segments/components/filter/lf-project-filter-button.vue b/frontend/src/modules/lf/segments/components/filter/lf-project-filter-button.vue new file mode 100644 index 0000000000..9ed96824b8 --- /dev/null +++ b/frontend/src/modules/lf/segments/components/filter/lf-project-filter-button.vue @@ -0,0 +1,284 @@ + + + + + + + diff --git a/frontend/src/modules/lf/segments/components/filter/lf-project-filter.vue b/frontend/src/modules/lf/segments/components/filter/lf-project-filter.vue new file mode 100644 index 0000000000..c25c920335 --- /dev/null +++ b/frontend/src/modules/lf/segments/components/filter/lf-project-filter.vue @@ -0,0 +1,74 @@ + + + + + + + diff --git a/frontend/src/modules/lf/segments/components/filter/lf-radio-cascader.vue b/frontend/src/modules/lf/segments/components/filter/lf-radio-cascader.vue new file mode 100644 index 0000000000..05f8d9b108 --- /dev/null +++ b/frontend/src/modules/lf/segments/components/filter/lf-radio-cascader.vue @@ -0,0 +1,124 @@ + + + + + + + diff --git a/frontend/src/modules/lf/segments/components/logs/log.drawer.vue b/frontend/src/modules/lf/segments/components/logs/log.drawer.vue new file mode 100644 index 0000000000..1d2d3c22ed --- /dev/null +++ b/frontend/src/modules/lf/segments/components/logs/log.drawer.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/frontend/src/modules/lf/segments/components/logs/sections/log-changes.vue b/frontend/src/modules/lf/segments/components/logs/sections/log-changes.vue new file mode 100644 index 0000000000..c5d8b87f85 --- /dev/null +++ b/frontend/src/modules/lf/segments/components/logs/sections/log-changes.vue @@ -0,0 +1,83 @@ + + + + + + + diff --git a/frontend/src/modules/lf/segments/components/logs/sections/log-json.vue b/frontend/src/modules/lf/segments/components/logs/sections/log-json.vue new file mode 100644 index 0000000000..56f431199a --- /dev/null +++ b/frontend/src/modules/lf/segments/components/logs/sections/log-json.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/frontend/src/modules/lf/segments/components/logs/sections/log-properties.vue b/frontend/src/modules/lf/segments/components/logs/sections/log-properties.vue new file mode 100644 index 0000000000..d01c381e00 --- /dev/null +++ b/frontend/src/modules/lf/segments/components/logs/sections/log-properties.vue @@ -0,0 +1,130 @@ + + + + + + + diff --git a/frontend/src/modules/lf/segments/lf-segments-service.js b/frontend/src/modules/lf/segments/lf-segments-service.js new file mode 100644 index 0000000000..d72b29ae81 --- /dev/null +++ b/frontend/src/modules/lf/segments/lf-segments-service.js @@ -0,0 +1,139 @@ +import authAxios from '@/shared/axios/auth-axios'; +import { AuthService } from '@/modules/auth/services/auth.service'; + +export class LfService { + // Segments + + static async findSegment(id) { + const response = await authAxios.get( + `/segment/${id}`, + { + params: { + segments: [id], + }, + }, + ); + + return response.data; + } + + static async listSegmentsByIds(ids) { + const response = await authAxios.post( + '/segment/id', + { + ids, + }, + ); + + return response.data; + } + + static async updateSegment(id, data) { + const response = await authAxios.put( + `/segment/${id}`, + { + ...data, + segments: [id], + }, + ); + + return response.data; + } + + // Project Groups + + static async queryProjectGroups(body) { + const response = await authAxios.post( + '/segment/projectGroup/query', + { + ...body, + excludeSegments: true, + }, + ); + + return response.data; + } + + static async createProjectGroup(body) { + const response = await authAxios.post( + '/segment/projectGroup', + { + ...body, + excludeSegments: true, + }, + ); + + return response.data; + } + + // Projects + + static async queryProjects(body) { + const response = await authAxios.post( + '/segment/project/query', + { + ...body, + ...(body.segments ? { segments: body.segments } : { excludeSegments: true }), + }, + ); + + return response.data; + } + + static async createProject(body) { + const response = await authAxios.post( + '/segment/project', + body, + ); + + return response.data; + } + + // Sub-projects + + static async createSubProject(body) { + const response = await authAxios.post( + '/segment/subproject', + body, + ); + + return response.data; + } + + // Users + + static async fetchUsers(query, limit) { + const params = { + query, + limit, + }; + + const response = await authAxios.get( + '/user/autocomplete', + { + params, + }, + ); + + return response.data; + } + + static async getUser(id) { + const response = await authAxios.get( + `/user/${id}`, + ); + + return response.data; + } + + // AuditLogs + + static async fetchAuditLogs(data) { + const response = await authAxios.post( + '/audit-logs/query', + data, + ); + + return response.data; + } +} diff --git a/frontend/src/modules/lf/segments/pages/lf-audit-logs-page.vue b/frontend/src/modules/lf/segments/pages/lf-audit-logs-page.vue new file mode 100644 index 0000000000..55fd963202 --- /dev/null +++ b/frontend/src/modules/lf/segments/pages/lf-audit-logs-page.vue @@ -0,0 +1,249 @@ + + + + + diff --git a/frontend/src/modules/lf/segments/segments.service.ts b/frontend/src/modules/lf/segments/segments.service.ts new file mode 100644 index 0000000000..b3e1ffae2c --- /dev/null +++ b/frontend/src/modules/lf/segments/segments.service.ts @@ -0,0 +1,64 @@ +import authAxios from '@/shared/axios/auth-axios'; +import { Pagination } from '@/shared/types/Pagination'; +import { QueryFunction } from '@tanstack/vue-query'; +import { Project, ProjectGroup, ProjectRequest } from './types/Segments'; + +class SegmentsService { + queryProjectGroups( + query: () => Record, + ): QueryFunction> { + return ({ pageParam = 0 }) => authAxios + .post>('/segment/projectGroup/query', { + ...query(), + offset: pageParam, + excludeSegments: true, + }) + .then((res) => res.data); + } + + getSegmentById(id: string) { + return authAxios + .get(`/segment/${id}`, { + params: { + segments: [id], + }, + }) + .then((res) => res.data); + } + + updateSegment(id: string, data: ProjectRequest | ProjectGroup) { + return authAxios + .put(`/segment/${id}`, { + ...data, + segments: [id], + }) + .then((res) => res.data); + } + + createProjectGroup(projectGroup: ProjectGroup) { + return authAxios + .post('/segment/projectGroup', { + ...projectGroup, + excludeSegments: true, + }) + .then((res) => res.data); + } + + createProject(req: { + project: ProjectRequest; + segments: string[]; + + }) { + return authAxios + .post( + '/segment/project', + { + ...req.project, + segments: req.segments, + }, + ) + .then((res) => res.data); + } +} + +export const segmentService = new SegmentsService(); diff --git a/frontend/src/modules/lf/segments/store/actions.js b/frontend/src/modules/lf/segments/store/actions.js new file mode 100644 index 0000000000..1683085967 --- /dev/null +++ b/frontend/src/modules/lf/segments/store/actions.js @@ -0,0 +1,298 @@ +import { LfService } from '@/modules/lf/segments/lf-segments-service'; + +import { ToastStore } from '@/shared/message/notification'; +import { router } from '@/router'; +import { useAuthStore } from '@/modules/auth/store/auth.store'; +import { storeToRefs } from 'pinia'; +import { LfRole } from '@/shared/modules/permissions/types/Roles'; +import { getAxiosErrorMessage } from '@/shared/helpers/error-message.helper'; + +const isAdminOnly = () => { + const authStore = useAuthStore(); + const { roles } = storeToRefs(authStore); + + return roles.value.includes(LfRole.projectAdmin); +}; + +export default { + // Project Groups + listProjectGroups({ + search = null, offset, limit, reset = false, adminOnly, + } = {}) { + if (reset) { + this.projectGroups.pagination = { + pageSize: 20, + currentPage: 1, + total: 0, + count: 0, + }; + } + const offsetLoad = offset !== undefined ? offset : this.projectGroupOffset; + const limitLoad = limit !== undefined ? limit : this.projectGroups.pagination.pageSize; + this.projectGroups.loading = offsetLoad === 0; + this.projectGroups.paginating = offsetLoad > 0; + + return LfService.queryProjectGroups({ + limit: limitLoad, + offset: offsetLoad, + filter: { + name: search, + adminOnly, + }, + }) + .then((response) => { + const count = Number(response.count); + + if (offsetLoad === 0 || reset) { + this.projectGroups.list = response.rows; + } else { + this.projectGroups.list = [...this.projectGroups.list, ...response.rows]; + } + + if (!search) { + this.projectGroups.pagination.total = count; + } + + this.projectGroups.pagination.count = count; + return Promise.resolve(); + }) + .catch(() => { + ToastStore.error('Something went wrong while fetching project groups'); + return Promise.reject(); + }) + .finally(() => { + this.projectGroups.paginating = false; + this.projectGroups.loading = false; + }); + }, + listAdminProjectGroups() { + return LfService.queryProjectGroups({ + limit: null, + offset: 0, + filter: { + adminOnly: true, + }, + }) + .then((response) => { + this.adminProjectGroups.list = response.rows; + return Promise.resolve(); + }) + .catch(() => Promise.reject()); + }, + findProjectGroup(id) { + return LfService.findSegment(id) + .then((projectGroup) => Promise.resolve(projectGroup)) + .catch(() => { + ToastStore.error('Something went wrong while getting the project group'); + Promise.resolve(); + }); + }, + createProjectGroup(data) { + return LfService.createProjectGroup(data) + .then(() => { + ToastStore.success('Project Group created successfully'); + + this.listProjectGroups({ + adminOnly: isAdminOnly(), + }); + }) + .catch(() => { + ToastStore.error('Something went wrong while creating the project group'); + }) + .finally(() => Promise.resolve()); + }, + updateProjectGroup(id, data) { + return LfService.updateSegment(id, data) + .then(() => { + ToastStore.success('Project Group updated successfully'); + + this.updateProjectGroupList(id, data); + }) + .catch(() => { + ToastStore.error('Something went wrong while updating the project group'); + }) + .finally(() => Promise.resolve()); + }, + updateProjectGroupList(id, data) { + this.projectGroups.list = this.projectGroups.list.map((p) => { + if (p.id === id) { + return { ...p.value, ...data }; + } + return p; + }); + this.projectGroups.reload = true; + }, + searchProjectGroup(search, limit, adminOnly, page) { + this.projectGroups.pagination.currentPage = page !== undefined ? page : 1; + this.listProjectGroups({ + search, + limit, + offset: page !== undefined ? undefined : 0, + adminOnly: adminOnly !== undefined ? adminOnly : isAdminOnly(), + }); + }, + // Projects + listProjects({ + parentSlug = null, search = null, offset, limit, reset = false, + } = {}) { + this.projects.loading = true; + + if (parentSlug) { + this.projects.parentSlug = parentSlug; + } + + if (reset) { + this.projects.pagination = { + pageSize: 20, + currentPage: 1, + total: 0, + count: 0, + }; + } + + const offsetLoad = offset !== undefined ? offset : this.projectOffset; + const limitLoad = limit !== undefined ? limit : this.projects.pagination.pageSize; + this.projects.loading = offsetLoad === 0; + this.projects.paginating = offsetLoad > 0; + + LfService.queryProjects({ + limit: limitLoad, + offset: offsetLoad, + filter: { + name: search, + parentSlug: this.projects.parentSlug, + }, + }) + .then((response) => { + const count = Number(response.count); + + if (offsetLoad === 0 || reset) { + this.projects.list = response.rows; + } else { + this.projects.list = [...this.projects.list, ...response.rows]; + } + + if (!search) { + this.projects.pagination.total = count; + } + + this.projects.pagination.count = count; + }) + .catch(() => { + ToastStore.error('Something went wrong while fetching projects'); + }) + .finally(() => { + this.projects.loading = false; + this.projects.paginating = false; + }); + }, + findProject(id) { + return LfService.findSegment(id) + .then((project) => Promise.resolve(project)) + .catch(() => { + ToastStore.error('Something went wrong while getting the project'); + Promise.resolve(); + }); + }, + createProject(data) { + return LfService.createProject(data) + .then(() => { + ToastStore.success('Project created successfully'); + this.listProjects(); + }) + .catch((err) => { + ToastStore.error(getAxiosErrorMessage(err, 'Something went wrong while creating the project')); + }) + .finally(() => Promise.resolve()); + }, + updateProject(id, data) { + return LfService.updateSegment(id, data) + .then(() => { + ToastStore.success('Project updated successfully'); + this.updateProjectList(id, data); + }) + .catch((err) => { + ToastStore.error(getAxiosErrorMessage(err, 'Something went wrong while updating the project')); + }) + .finally(() => Promise.resolve()); + }, + updateProjectList(id, data) { + this.projects.list = this.projects.list.map((p) => { + if (p.id === id) { + return { ...p, ...data }; + } + return p; + }); + }, + searchProject(search, page = null) { + this.projects.pagination.currentPage = page !== null ? page : 1; + this.listProjects({ search, offset: page !== null ? undefined : 0 }); + }, + findSubProject(id) { + return LfService.findSegment(id) + .then((project) => Promise.resolve(project)) + .catch(() => { + ToastStore.error('Something went wrong while getting the sub-project'); + Promise.resolve(); + }); + }, + // Sub-projects + createSubProject(data) { + return LfService.createSubProject(data) + .then(() => { + ToastStore.success('Sub-project created successfully'); + this.listProjects(); + }) + .catch((err) => { + ToastStore.error(getAxiosErrorMessage(err, 'Something went wrong while creating the sub-project')); + }) + .finally(() => Promise.resolve()); + }, + updateSubProject(id, data) { + return LfService.updateSegment(id, data) + .then(() => { + ToastStore.success('Sub-project updated successfully'); + this.listProjects(); + }) + .catch((err) => { + ToastStore.error(getAxiosErrorMessage(err, 'Something went wrong while updating the sub-project')); + }) + .finally(() => Promise.resolve()); + }, + // Pagination + updateProjectGroupsPageSize(pageSize) { + this.projectGroups.pagination.pageSize = pageSize; + + this.listProjectGroups(); + }, + updateProjectsPageSize(pageSize) { + this.projects.pagination.pageSize = pageSize; + + this.listProjects(); + }, + updateSelectedProjectGroup(projectGroupId, sendToDashboard = true) { + if (projectGroupId) { + const projectGroup = this.projectGroups.list.find((p) => p.id === projectGroupId); + + this.selectedProjectGroup = projectGroup; + + if (sendToDashboard) { + router.push({ + name: 'member', + }); + } + } else { + this.selectedProjectGroup = null; + } + }, + doChangeProjectGroupCurrentPage(currentPage) { + this.projectGroups.pagination.currentPage = currentPage; + + this.listProjectGroups(); + }, + doChangeProjectCurrentPage(currentPage) { + this.projects.pagination.currentPage = currentPage; + + this.listProjects(); + }, +}; diff --git a/frontend/src/modules/lf/segments/store/getters.ts b/frontend/src/modules/lf/segments/store/getters.ts new file mode 100644 index 0000000000..37fb0582c0 --- /dev/null +++ b/frontend/src/modules/lf/segments/store/getters.ts @@ -0,0 +1,14 @@ +import { SegmentsState } from './state'; + +export default { + projectGroupOffset: (state: SegmentsState) => { + const currentPage = state.projectGroups.pagination.currentPage || 1; + + return (currentPage - 1) * state.projectGroups.pagination.pageSize; + }, + projectOffset: (state: SegmentsState) => { + const currentPage = state.projects.pagination.currentPage || 1; + + return (currentPage - 1) * state.projects.pagination.pageSize; + }, +}; diff --git a/frontend/src/modules/lf/segments/store/index.ts b/frontend/src/modules/lf/segments/store/index.ts new file mode 100644 index 0000000000..20f8d83d3c --- /dev/null +++ b/frontend/src/modules/lf/segments/store/index.ts @@ -0,0 +1,13 @@ +import { defineStore } from 'pinia'; +import state from './state'; +import actions from './actions'; +import getters from './getters'; + +export const useLfSegmentsStore = defineStore( + 'lf-segments', + { + state, + actions, + getters, + }, +); diff --git a/frontend/src/modules/lf/segments/store/state.ts b/frontend/src/modules/lf/segments/store/state.ts new file mode 100644 index 0000000000..d4ee342d15 --- /dev/null +++ b/frontend/src/modules/lf/segments/store/state.ts @@ -0,0 +1,61 @@ +import { ProjectGroup, Project } from '@/modules/lf/segments/types/Segments'; + +export interface SegmentsState { + selectedProjectGroup: ProjectGroup | null + adminProjectGroups: { + list: ProjectGroup[] + } + projectGroups: { + list: ProjectGroup[] + loading: boolean + paginating: boolean + pagination: { + pageSize: number + currentPage: number + total: number + count: number + } + } + projects: { + list: Project[] + parentSlug: string + loading: boolean + paginating: boolean + pagination: { + pageSize: number + currentPage: number + total: number + count: number + } + } +} + +const state: SegmentsState = { + selectedProjectGroup: null, + adminProjectGroups: { + list: [], + }, + projectGroups: { + list: [], + loading: true, + pagination: { + pageSize: 20, + currentPage: 1, + total: 0, + count: 0, + }, + }, + projects: { + list: [], + parentSlug: '', + loading: true, + pagination: { + pageSize: 20, + currentPage: 1, + total: 0, + count: 0, + }, + }, +}; + +export default () => state; diff --git a/frontend/src/modules/lf/segments/types/AuditLog.ts b/frontend/src/modules/lf/segments/types/AuditLog.ts new file mode 100644 index 0000000000..d93237923e --- /dev/null +++ b/frontend/src/modules/lf/segments/types/AuditLog.ts @@ -0,0 +1,38 @@ +export enum ActionType { + MEMBERS_MERGE = 'members-merge', + MEMBERS_UNMERGE = 'members-unmerge', + MEMBERS_EDIT_IDENTITIES = 'members-edit-identities', + MEMBERS_EDIT_ORGANIZATIONS = 'members-edit-organizations', + MEMBERS_EDIT_MANUAL_AFFILIATION = 'members-edit-manual-affiliation', + MEMBERS_EDIT_PROFILE = 'members-edit-profile', + MEMBERS_CREATE = 'members-create', + ORGANIZATIONS_MERGE = 'organizations-merge', + ORGANIZATIONS_UNMERGE = 'organizations-unmerge', + ORGANIZATIONS_EDIT_IDENTITIES = 'organizations-edit-identities', + ORGANIZATIONS_EDIT_PROFILE = 'organizations-edit-profile', + ORGANIZATIONS_CREATE = 'organizations-create', + INTEGRATIONS_CONNECT = 'integrations-connect', + INTEGRATIONS_RECONNECT = 'integrations-reconnect', +} + +export interface Actor { + id: string; + type: 'user' | 'service'; + fullName: string; + email: string | null; +} + +export interface AuditLog{ + id: string; + timestamp: string; + actor: Actor; + ipAddress?: string; + userAgent?: string; + requestId: string; + actionType: ActionType; + success: boolean; + entityId: string; + oldState: any; + newState: any; + diff: any; +} diff --git a/frontend/src/modules/lf/segments/types/Filters.ts b/frontend/src/modules/lf/segments/types/Filters.ts new file mode 100644 index 0000000000..e2cab6694a --- /dev/null +++ b/frontend/src/modules/lf/segments/types/Filters.ts @@ -0,0 +1,31 @@ +import { CustomFilterConfig } from '@/shared/modules/filters/types/filterTypes/CustomFilterConfig'; +import { Project } from '@/modules/lf/segments/types/Segments'; + +export interface ProjectsFilterValue { + value: string[]; + parentValues: string[] +} + +export interface SubProjectsOption { + id: string; + label: string; + selected: boolean; +} + +export interface ProjectsOption { + id: string; + label: string; + selected: boolean; + children: SubProjectsOption[]; +} + +export interface ProjectsCustomFilterOptions { + remoteMethod: (value: { + query: string; + parentSlug: string | undefined; + }) => Promise; +} + +export interface ProjectsCustomFilterConfig extends CustomFilterConfig { + options: ProjectsCustomFilterOptions; +} diff --git a/frontend/src/modules/lf/segments/types/Segments.ts b/frontend/src/modules/lf/segments/types/Segments.ts new file mode 100644 index 0000000000..8364ac35c4 --- /dev/null +++ b/frontend/src/modules/lf/segments/types/Segments.ts @@ -0,0 +1,64 @@ +export interface SubProject { + id: string; + name: string; + status: string; + integrations?: { + id: string + platform: string; + status: string; + type?: string; + [key: string]: string | undefined; + }[]; +} + +export interface Project { + id: string; + url: string; + name: string; + parentName: string; + grandparentName: string; + slug: string; + parentSlug: string; + grandparentSlug: string; + status: string; + description: string; + sourceId: string; + sourceParentId: string; + customActivityTypes: string; + activityChannels: object; + createdAt: string; + updatedAt: string; + subproject_count: number; + subprojects: SubProject[]; + isLF: boolean; +} + +export interface ProjectGroup { + id: string; + url: string; + name: string; + parentName: string; + grandparentName: string; + slug: string; + parentSlug: string; + grandparentSlug: string; + status: string; + description: string; + sourceId: string; + sourceParentId: string; + customActivityTypes: object; + activityChannels: object; + createdAt: string; + updatedAt: string; + projects: Project[]; + isLF: boolean; +} + +export interface ProjectRequest { + name: string; + slug: string; + sourceId: string; + status: string; + isLF: boolean; + parentSlug: string; +} diff --git a/frontend/src/modules/lf/utils/filters.ts b/frontend/src/modules/lf/utils/filters.ts new file mode 100644 index 0000000000..d0fe1440b7 --- /dev/null +++ b/frontend/src/modules/lf/utils/filters.ts @@ -0,0 +1,29 @@ +import { Project } from '@/modules/lf/segments/types/Segments'; + +export const filterLabel = (value: string[], parentValues: string[], options: Project[]) => { + let text: string[] = []; + + if (!options?.length) { + text = ['All']; + } else { + options.forEach((project) => { + const selectedProject = value.includes(project.id); + + if (selectedProject) { + text.push(`${project.name} (all sub-projects)`); + } else { + const selectedSubprojects = project.subprojects.filter( + (sp) => value.includes(sp.id), + ).map((sp) => sp.name); + + if (project.subprojects.length === selectedSubprojects.length && parentValues?.includes(project.id)) { + text.push(`${project.name} (all sub-projects)`); + } else if (selectedSubprojects.length) { + text.push(`${selectedSubprojects.join(', ')} (${project.name})`); + } + } + }); + } + + return text.join(', '); +}; diff --git a/frontend/src/modules/member/components/bulk/bulk-edit-attribute-dropdown.vue b/frontend/src/modules/member/components/bulk/bulk-edit-attribute-dropdown.vue index dc1f508f39..9d2918315d 100644 --- a/frontend/src/modules/member/components/bulk/bulk-edit-attribute-dropdown.vue +++ b/frontend/src/modules/member/components/bulk/bulk-edit-attribute-dropdown.vue @@ -20,7 +20,7 @@ data-qa="filter-list-search" >
@@ -41,23 +41,6 @@ - -
- - + + + + - - - - - - - -
- - - Values will be added to each selected contact and the existing ones won’t be overwritten. - -
- -
- - Changes will overwrite the current attribute value of the selected contacts. -
+ +
+ + + +
+ + + Values will be added to each selected profile and the existing + ones won’t be overwritten. + +
+ +
+ + Changes will overwrite the current attribute value of the + selected profile.
- -
- -
- - Cancel - - - Submit - -
- - +
+ +
+ +
+ + Cancel + + + Submit + +
+ diff --git a/frontend/src/modules/member/components/form/member-form-global-attributes.vue b/frontend/src/modules/member/components/form/member-form-global-attributes.vue index 0b08a0e39b..15fb79845b 100644 --- a/frontend/src/modules/member/components/form/member-form-global-attributes.vue +++ b/frontend/src/modules/member/components/form/member-form-global-attributes.vue @@ -12,7 +12,7 @@
- Name * + Name *
@@ -56,23 +56,26 @@
- - - + + - + Add attribute - + @@ -124,12 +130,16 @@ import isEqual from 'lodash/isEqual'; import cloneDeep from 'lodash/cloneDeep'; import attributeTypes from '@/jsons/member-custom-attributes.json'; import ConfirmDialog from '@/shared/dialog/confirm-dialog'; -import Message from '@/shared/message/message'; -import { i18n } from '@/i18n'; + +import { ToastStore } from '@/shared/message/notification'; import parseCustomAttributes from '@/shared/fields/parse-custom-attributes'; import { onSelectMouseLeave } from '@/utils/select'; import { useMemberStore } from '@/modules/member/store/pinia'; import { storeToRefs } from 'pinia'; +import useProductTracking from '@/shared/modules/monitoring/useProductTracking'; +import { EventType, FeatureEventKey } from '@/shared/modules/monitoring/types/event'; +import LfIcon from '@/ui-kit/icon/Icon.vue'; +import LfButton from '@/ui-kit/button/Button.vue'; const emit = defineEmits(['update:modelValue']); const props = defineProps({ @@ -139,6 +149,8 @@ const props = defineProps({ }, }); +const { trackEvent } = useProductTracking(); + const store = useStore(); const memberStore = useMemberStore(); const { customAttributes } = storeToRefs(memberStore); @@ -185,6 +197,14 @@ async function onSubmit() { confirmButtonText: 'Confirm update', }); + trackEvent({ + key: FeatureEventKey.DELETE_GLOBAL_ATTRIBUTE, + type: EventType.FEATURE, + properties: { + data: deletedFields, + }, + }); + const ids = deletedFields.map( (deletedField) => deletedField.id, ); @@ -201,6 +221,14 @@ async function onSubmit() { // Handle added fields if (addedFields.length) { + trackEvent({ + key: FeatureEventKey.ADD_GLOBAL_ATTRIBUTE, + type: EventType.FEATURE, + properties: { + data: addedFields, + }, + }); + addedFields.forEach(async ({ type, label }) => { try { await store.dispatch( @@ -218,6 +246,14 @@ async function onSubmit() { // Handle edited fields if (editedFields.length) { + trackEvent({ + key: FeatureEventKey.EDIT_GLOBAL_ATTRIBUTE, + type: EventType.FEATURE, + properties: { + data: editedFields, + }, + }); + editedFields.forEach(async ({ id, label }) => { try { await store.dispatch( @@ -236,10 +272,10 @@ async function onSubmit() { } if (hasErrorOccurred) { - Message.error(i18n('errors.defaultErrorMessage')); + ToastStore.error('Ops, an error occurred'); } else { - Message.success( - i18n('entities.member.attributes.success'), + ToastStore.success( + 'Custom Attributes successfully updated', ); isDrawerOpen.value = false; } diff --git a/frontend/src/modules/member/components/form/member-form-identities.vue b/frontend/src/modules/member/components/form/member-form-identities.vue deleted file mode 100644 index c3a80ac1da..0000000000 --- a/frontend/src/modules/member/components/form/member-form-identities.vue +++ /dev/null @@ -1,311 +0,0 @@ - - - diff --git a/frontend/src/modules/member/components/form/member-form-organizations-create.vue b/frontend/src/modules/member/components/form/member-form-organizations-create.vue deleted file mode 100644 index 7bd28fb93f..0000000000 --- a/frontend/src/modules/member/components/form/member-form-organizations-create.vue +++ /dev/null @@ -1,259 +0,0 @@ - - - - - - - diff --git a/frontend/src/modules/member/components/form/member-form-organizations-drawer.vue b/frontend/src/modules/member/components/form/member-form-organizations-drawer.vue deleted file mode 100644 index 30524ac81e..0000000000 --- a/frontend/src/modules/member/components/form/member-form-organizations-drawer.vue +++ /dev/null @@ -1,158 +0,0 @@ - - - - - diff --git a/frontend/src/modules/member/components/form/member-form-organizations.vue b/frontend/src/modules/member/components/form/member-form-organizations.vue deleted file mode 100644 index dd3206b8f3..0000000000 --- a/frontend/src/modules/member/components/form/member-form-organizations.vue +++ /dev/null @@ -1,161 +0,0 @@ - - - - - diff --git a/frontend/src/modules/member/components/list/columns/member-list-emails.vue b/frontend/src/modules/member/components/list/columns/member-list-emails.vue new file mode 100644 index 0000000000..ed13f77928 --- /dev/null +++ b/frontend/src/modules/member/components/list/columns/member-list-emails.vue @@ -0,0 +1,174 @@ + + + + + diff --git a/frontend/src/modules/member/components/list/member-list-table.vue b/frontend/src/modules/member/components/list/member-list-table.vue index 538f322b9d..d1f6bb0d32 100644 --- a/frontend/src/modules/member/components/list/member-list-table.vue +++ b/frontend/src/modules/member/components/list/member-list-table.vue @@ -12,27 +12,25 @@ @@ -45,14 +43,22 @@ :current-page="pagination.page" :has-page-counter="false" :export="doExport" - module="contact" + module="member" position="top" @change-sorter="doChangePaginationPageSize" - /> + > + + -
+
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +
+ + + + + +
+
+
+
+
+
+
@@ -443,59 +367,79 @@
+
+ + -
diff --git a/frontend/src/modules/member/components/list/member-list-toolbar.vue b/frontend/src/modules/member/components/list/member-list-toolbar.vue index 9fb281fdd1..e2b7744017 100644 --- a/frontend/src/modules/member/components/list/member-list-toolbar.vue +++ b/frontend/src/modules/member/components/list/member-list-toolbar.vue @@ -1,252 +1,239 @@ + - - - -
+ diff --git a/frontend/src/modules/member/components/member-badge.vue b/frontend/src/modules/member/components/member-badge.vue index e78aa20d78..f1b557956e 100644 --- a/frontend/src/modules/member/components/member-badge.vue +++ b/frontend/src/modules/member/components/member-badge.vue @@ -1,23 +1,14 @@ - - - - diff --git a/frontend/src/modules/member/components/member-merge-dialog.vue b/frontend/src/modules/member/components/member-merge-dialog.vue index 122542c2cc..848eee7681 100644 --- a/frontend/src/modules/member/components/member-merge-dialog.vue +++ b/frontend/src/modules/member/components/member-merge-dialog.vue @@ -1,75 +1,95 @@ + + diff --git a/frontend/src/modules/member/components/member-merge-suggestions.vue b/frontend/src/modules/member/components/member-merge-suggestions.vue new file mode 100644 index 0000000000..c337504fd4 --- /dev/null +++ b/frontend/src/modules/member/components/member-merge-suggestions.vue @@ -0,0 +1,338 @@ + + + + + diff --git a/frontend/src/modules/member/components/member-organizations-vertical.vue b/frontend/src/modules/member/components/member-organizations-vertical.vue new file mode 100644 index 0000000000..6a028caeab --- /dev/null +++ b/frontend/src/modules/member/components/member-organizations-vertical.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/frontend/src/modules/member/components/member-organizations.vue b/frontend/src/modules/member/components/member-organizations.vue index 38fc1cfb03..c2878d7478 100644 --- a/frontend/src/modules/member/components/member-organizations.vue +++ b/frontend/src/modules/member/components/member-organizations.vue @@ -5,38 +5,48 @@ :to="{ name: 'organizationView', params: { id: activeOrganization.id }, + query: { + projectGroup: selectedProjectGroup?.id, + segmentId: member.segmentId, + }, }" class="flex items-start hover:cursor-pointer" @click.stop >
-
+
Logo
-
+

{{ activeOrganization.displayName || activeOrganization.name || '-' }}

+
{{ - props.member.attributes.jobTitle?.default + props.member.attributes?.jobTitle?.default + || activeOrganization?.memberOrganizations?.title || '-' }}

- @@ -46,7 +56,9 @@ class="text-gray-500 text-2xs" > {{ - props.member.attributes.jobTitle?.default || '-' + props.member.attributes.jobTitle?.default + || activeOrganization?.memberOrganizations?.title + || '-' }}

@@ -62,9 +74,9 @@ class="flex items-start grow mt-2" > {{ member.attributes.jobTitle.default }} + >{{ member.attributes?.jobTitle?.default || activeOrganization?.memberOrganizations?.title }} {{ activeOrganization ? 'at' : '' }}
{{ activeOrganization.displayName || activeOrganization.name || '-' }} +
@@ -97,6 +117,9 @@ diff --git a/frontend/src/modules/member/components/member-reach.vue b/frontend/src/modules/member/components/member-reach.vue index 6264ebe53c..9687427136 100644 --- a/frontend/src/modules/member/components/member-reach.vue +++ b/frontend/src/modules/member/components/member-reach.vue @@ -3,62 +3,67 @@
- {{ - formatNumberToCompact(member.reach) - }} - {{ - formatNumberToCompact(member.reach.total) - }} - + + {{ + formatNumberToCompact(reach.total) + }} + + -
- diff --git a/frontend/src/modules/member/components/view/_aside/_aside-enriched.vue b/frontend/src/modules/member/components/view/_aside/_aside-enriched.vue deleted file mode 100644 index 5ca4a6f7ca..0000000000 --- a/frontend/src/modules/member/components/view/_aside/_aside-enriched.vue +++ /dev/null @@ -1,88 +0,0 @@ - - - diff --git a/frontend/src/modules/member/components/view/_aside/_aside-identities.vue b/frontend/src/modules/member/components/view/_aside/_aside-identities.vue deleted file mode 100644 index 15b8e77c69..0000000000 --- a/frontend/src/modules/member/components/view/_aside/_aside-identities.vue +++ /dev/null @@ -1,127 +0,0 @@ - - - diff --git a/frontend/src/modules/member/components/view/_aside/_aside-organizations.vue b/frontend/src/modules/member/components/view/_aside/_aside-organizations.vue deleted file mode 100644 index 20d7a773a5..0000000000 --- a/frontend/src/modules/member/components/view/_aside/_aside-organizations.vue +++ /dev/null @@ -1,118 +0,0 @@ - - - diff --git a/frontend/src/modules/member/components/view/member-view-aside.vue b/frontend/src/modules/member/components/view/member-view-aside.vue deleted file mode 100644 index 290f9b9aa0..0000000000 --- a/frontend/src/modules/member/components/view/member-view-aside.vue +++ /dev/null @@ -1,46 +0,0 @@ - - - - - diff --git a/frontend/src/modules/member/components/view/member-view-contributions.vue b/frontend/src/modules/member/components/view/member-view-contributions.vue deleted file mode 100644 index b1b35227ab..0000000000 --- a/frontend/src/modules/member/components/view/member-view-contributions.vue +++ /dev/null @@ -1,587 +0,0 @@ - - - - - diff --git a/frontend/src/modules/member/components/view/member-view-header.vue b/frontend/src/modules/member/components/view/member-view-header.vue deleted file mode 100644 index b499afd676..0000000000 --- a/frontend/src/modules/member/components/view/member-view-header.vue +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - - diff --git a/frontend/src/modules/member/components/view/member-view-notes.vue b/frontend/src/modules/member/components/view/member-view-notes.vue deleted file mode 100644 index 0dfdbfa4d5..0000000000 --- a/frontend/src/modules/member/components/view/member-view-notes.vue +++ /dev/null @@ -1,90 +0,0 @@ - - - - - diff --git a/frontend/src/modules/member/components/view/member-view-tasks.vue b/frontend/src/modules/member/components/view/member-view-tasks.vue deleted file mode 100644 index 665fef0771..0000000000 --- a/frontend/src/modules/member/components/view/member-view-tasks.vue +++ /dev/null @@ -1,241 +0,0 @@ - - - - - diff --git a/frontend/src/modules/member/config/filters/activeOn/config.ts b/frontend/src/modules/member/config/filters/activeOn/config.ts index 040c92158a..de7065c4bb 100644 --- a/frontend/src/modules/member/config/filters/activeOn/config.ts +++ b/frontend/src/modules/member/config/filters/activeOn/config.ts @@ -4,22 +4,22 @@ import { MultiSelectFilterOptions, MultiSelectFilterValue, } from '@/shared/modules/filters/types/filterTypes/MultiSelectFilterConfig'; -import { CrowdIntegrations } from '@/integrations/integrations-config'; import { apiFilterRendererByType } from '@/shared/modules/filters/config/apiFilterRendererByType'; import { itemLabelRendererByType } from '@/shared/modules/filters/config/itemLabelRendererByType'; +import { lfIdentities } from '@/config/identities'; const activeOn: MultiSelectFilterConfig = { id: 'activeOn', label: 'Active on', - iconClass: 'ri-apps-2-line', + iconClass: 'grid-round-2', type: FilterConfigType.MULTISELECT, options: { options: [ { options: [ - ...(CrowdIntegrations.enabledConfigs.map((platform) => ({ - label: (platform as any).name, - value: platform.platform, + ...(Object.values(lfIdentities).map((identity) => ({ + label: identity.name, + value: identity.key, }))), ], }, diff --git a/frontend/src/modules/member/config/filters/activityType/ActivityTypeFilter.vue b/frontend/src/modules/member/config/filters/activityType/ActivityTypeFilter.vue index 1aea5f99e3..e2e6f2a10a 100644 --- a/frontend/src/modules/member/config/filters/activityType/ActivityTypeFilter.vue +++ b/frontend/src/modules/member/config/filters/activityType/ActivityTypeFilter.vue @@ -1,36 +1,40 @@ diff --git a/frontend/src/modules/member/config/filters/activityType/config.ts b/frontend/src/modules/member/config/filters/activityType/config.ts index d094bda4bd..1a17181518 100644 --- a/frontend/src/modules/member/config/filters/activityType/config.ts +++ b/frontend/src/modules/member/config/filters/activityType/config.ts @@ -11,7 +11,7 @@ import { itemLabelRendererByType } from '@/shared/modules/filters/config/itemLab const activityType: CustomFilterConfig = { id: 'activityType', label: 'Activity type', - iconClass: 'ri-radar-line', + iconClass: 'list-timeline', type: FilterConfigType.CUSTOM, component: ActivityTypeFilter, options: { diff --git a/frontend/src/modules/member/config/filters/avgSentiment/config.ts b/frontend/src/modules/member/config/filters/avgSentiment/config.ts index 2bc85ec525..be52d54ae5 100644 --- a/frontend/src/modules/member/config/filters/avgSentiment/config.ts +++ b/frontend/src/modules/member/config/filters/avgSentiment/config.ts @@ -10,7 +10,7 @@ import options from './options'; const avgSentiment: MultiSelectFilterConfig = { id: 'avgSentiment', label: 'Avg. sentiment', - iconClass: 'ri-speed-up-line', + iconClass: 'gauge-high', type: FilterConfigType.MULTISELECT, options: { options, diff --git a/frontend/src/modules/member/config/filters/engagementLevel/config.ts b/frontend/src/modules/member/config/filters/engagementLevel/config.ts index d86980d919..73f5934600 100644 --- a/frontend/src/modules/member/config/filters/engagementLevel/config.ts +++ b/frontend/src/modules/member/config/filters/engagementLevel/config.ts @@ -10,7 +10,7 @@ import options from './options'; const engagementLevel: MultiSelectFilterConfig = { id: 'engagementLevel', label: 'Engagement level', - iconClass: 'ri-user-voice-line', + iconClass: 'signal-bars', type: FilterConfigType.MULTISELECT, options: { options, diff --git a/frontend/src/modules/member/config/filters/enrichedMember/config.ts b/frontend/src/modules/member/config/filters/enrichedMember/config.ts deleted file mode 100644 index e7308d2432..0000000000 --- a/frontend/src/modules/member/config/filters/enrichedMember/config.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { FilterConfigType } from '@/shared/modules/filters/types/FilterConfig'; -import { - BooleanFilterConfig, BooleanFilterOptions, - BooleanFilterValue, -} from '@/shared/modules/filters/types/filterTypes/BooleanFilterConfig'; -import { itemLabelRendererByType } from '@/shared/modules/filters/config/itemLabelRendererByType'; - -const enrichedMember: BooleanFilterConfig = { - id: 'enrichedMember', - label: 'Enriched contact', - iconClass: 'ri-sparkling-line', - type: FilterConfigType.BOOLEAN, - options: {}, - itemLabelRenderer(value: BooleanFilterValue, options: BooleanFilterOptions): string { - return itemLabelRendererByType[FilterConfigType.BOOLEAN]('Enriched contact', value, options); - }, - apiFilterRenderer({ value, include }: BooleanFilterValue): any[] { - const filter = { - lastEnriched: { - [value ? 'ne' : 'eq']: null, - }, - }; - return [ - (include ? filter : { not: filter }), - ]; - }, -}; - -export default enrichedMember; diff --git a/frontend/src/modules/member/config/filters/identities/config.ts b/frontend/src/modules/member/config/filters/identities/config.ts index c0cb53ec2f..7a9d99554d 100644 --- a/frontend/src/modules/member/config/filters/identities/config.ts +++ b/frontend/src/modules/member/config/filters/identities/config.ts @@ -4,23 +4,23 @@ import { MultiSelectFilterOptions, MultiSelectFilterValue, } from '@/shared/modules/filters/types/filterTypes/MultiSelectFilterConfig'; -import { CrowdIntegrations } from '@/integrations/integrations-config'; import { itemLabelRendererByType } from '@/shared/modules/filters/config/itemLabelRendererByType'; import { apiFilterRendererByType } from '@/shared/modules/filters/config/apiFilterRendererByType'; +import { lfIdentities } from '@/config/identities'; const identities: MultiSelectFilterConfig = { id: 'identities', label: 'Identities', - iconClass: 'ri-fingerprint-line', + iconClass: 'fingerprint', type: FilterConfigType.MULTISELECT, options: { options: [ { options: [ - ...(CrowdIntegrations.enabledConfigs.map((platform) => ({ - label: (platform as any).name, - value: platform.platform, - }))), + ...Object.values(lfIdentities).map((identity) => ({ + label: identity.name, + value: identity.key, + })), ], }, ], @@ -29,7 +29,7 @@ const identities: MultiSelectFilterConfig = { return itemLabelRendererByType[FilterConfigType.MULTISELECT]('Identities', value, options); }, apiFilterRenderer(value: MultiSelectFilterValue): any[] { - return apiFilterRendererByType[FilterConfigType.MULTISELECT]('identities', value); + return apiFilterRendererByType[FilterConfigType.MULTISELECT]('identityPlatforms', value); }, }; diff --git a/frontend/src/modules/member/config/filters/jobTitle/config.ts b/frontend/src/modules/member/config/filters/jobTitle/config.ts index b3f825733e..a5841fcd18 100644 --- a/frontend/src/modules/member/config/filters/jobTitle/config.ts +++ b/frontend/src/modules/member/config/filters/jobTitle/config.ts @@ -10,7 +10,7 @@ import { apiFilterRendererByType } from '@/shared/modules/filters/config/apiFilt const jobTitle: StringFilterConfig = { id: 'jobTitle', label: 'Job title', - iconClass: 'ri-suitcase-line', + iconClass: 'suitcase', type: FilterConfigType.STRING, options: {}, itemLabelRenderer(value: StringFilterValue, options: StringFilterOptions): string { diff --git a/frontend/src/modules/member/config/filters/joinedDate/config.ts b/frontend/src/modules/member/config/filters/joinedDate/config.ts index e21bb3f481..e8d5a52654 100644 --- a/frontend/src/modules/member/config/filters/joinedDate/config.ts +++ b/frontend/src/modules/member/config/filters/joinedDate/config.ts @@ -10,7 +10,7 @@ import { apiFilterRendererByType } from '@/shared/modules/filters/config/apiFilt const joinedDate: DateFilterConfig = { id: 'joinedDate', label: 'Joined date', - iconClass: 'ri-calendar-event-line', + iconClass: 'calendar', type: FilterConfigType.DATE, options: {}, itemLabelRenderer(value: DateFilterValue, options: DateFilterOptions): string { diff --git a/frontend/src/modules/member/config/filters/lastActivityDate/config.ts b/frontend/src/modules/member/config/filters/lastActivityDate/config.ts index 7de4734bea..52b9a6dd4c 100644 --- a/frontend/src/modules/member/config/filters/lastActivityDate/config.ts +++ b/frontend/src/modules/member/config/filters/lastActivityDate/config.ts @@ -10,7 +10,7 @@ import { apiFilterRendererByType } from '@/shared/modules/filters/config/apiFilt const lastActivityDate: DateFilterConfig = { id: 'lastActivityDate', label: 'Last activity date', - iconClass: 'ri-calendar-event-line', + iconClass: 'calendar', type: FilterConfigType.DATE, options: {}, itemLabelRenderer(value: DateFilterValue, options: DateFilterOptions): string { diff --git a/frontend/src/modules/member/config/filters/main.ts b/frontend/src/modules/member/config/filters/main.ts index aa669811fb..78b7e4cbb3 100644 --- a/frontend/src/modules/member/config/filters/main.ts +++ b/frontend/src/modules/member/config/filters/main.ts @@ -1,51 +1,33 @@ +import unaffiliated from '@/modules/member/config/filters/unaffiliated/config'; import { FilterConfig } from '@/shared/modules/filters/types/FilterConfig'; import { SearchFilterConfig } from '@/shared/modules/filters/types/filterTypes/SearchFilterConfig'; -import { trimAndReduceSpaces } from '@/utils/string'; -import noOfActivities from './noOfActivities/config'; -import noOfOSSContributions from './noOfOSSContributions/config'; -import activeOn from './activeOn/config'; -import activityType from './activityType/config'; -import avgSentiment from './avgSentiment/config'; -import engagementLevel from './engagementLevel/config'; -import enrichedMember from './enrichedMember/config'; import identities from './identities/config'; -import joinedDate from './joinedDate/config'; -import lastActivityDate from './lastActivityDate/config'; -import reach from './reach/config'; -import tags from './tags/config'; import memberName from './memberName/config'; -import jobTitle from './jobTitle/config'; +import noOfActivities from './noOfActivities/config'; import organizations from './organizations/config'; +import projects from './projects/config'; export const memberFilters: Record = { memberName, organizations, noOfActivities, - noOfOSSContributions, - jobTitle, - activeOn, - activityType, - avgSentiment, - engagementLevel, - enrichedMember, + // noOfOSSContributions, + // jobTitle, + // activeOn, + // activityType, + // avgSentiment, + // engagementLevel, identities, - joinedDate, - lastActivityDate, - reach, - tags, + // joinedDate, + // lastActivityDate, + // reach, + projects, + unaffiliated, }; export const memberSearchFilter: SearchFilterConfig = { - placeholder: 'Search contacts', - apiFilterRenderer(value: string): any[] { - const trimmedValue = trimAndReduceSpaces(value); - return [ - { - or: [ - { displayName: { textContains: trimmedValue } }, - { emails: { textContains: trimmedValue } }, - ], - }, - ]; + placeholder: 'Search people', + apiFilterRenderer(): any[] { + return []; }, }; diff --git a/frontend/src/modules/member/config/filters/memberName/config.ts b/frontend/src/modules/member/config/filters/memberName/config.ts index 0bfcc862ac..f1336f8458 100644 --- a/frontend/src/modules/member/config/filters/memberName/config.ts +++ b/frontend/src/modules/member/config/filters/memberName/config.ts @@ -9,8 +9,8 @@ import { apiFilterRendererByType } from '@/shared/modules/filters/config/apiFilt const memberName: StringFilterConfig = { id: 'memberName', - label: 'Contact name', - iconClass: 'ri-account-circle-line', + label: 'Person name', + iconClass: 'circle-user', type: FilterConfigType.STRING, options: {}, itemLabelRenderer( @@ -18,7 +18,7 @@ const memberName: StringFilterConfig = { options: StringFilterOptions, ): string { return itemLabelRendererByType[FilterConfigType.STRING]( - 'Contact name', + 'Person name', value, options, ); diff --git a/frontend/src/modules/member/config/filters/noOfActivities/config.ts b/frontend/src/modules/member/config/filters/noOfActivities/config.ts index 7ab53a370e..74da122365 100644 --- a/frontend/src/modules/member/config/filters/noOfActivities/config.ts +++ b/frontend/src/modules/member/config/filters/noOfActivities/config.ts @@ -10,7 +10,7 @@ import { apiFilterRendererByType } from '@/shared/modules/filters/config/apiFilt const noOfActivities: NumberFilterConfig = { id: 'noOfActivities', label: '# of activities', - iconClass: 'ri-radar-line', + iconClass: 'list-timeline', type: FilterConfigType.NUMBER, options: {}, itemLabelRenderer(value: NumberFilterValue, options: NumberFilterOptions): string { diff --git a/frontend/src/modules/member/config/filters/noOfOSSContributions/config.ts b/frontend/src/modules/member/config/filters/noOfOSSContributions/config.ts index 7b8f5bfd22..05ff84c28e 100644 --- a/frontend/src/modules/member/config/filters/noOfOSSContributions/config.ts +++ b/frontend/src/modules/member/config/filters/noOfOSSContributions/config.ts @@ -10,7 +10,7 @@ import { apiFilterRendererByType } from '@/shared/modules/filters/config/apiFilt const noOfOSSContributions: NumberFilterConfig = { id: 'noOfOSSContributions', label: '# of open source contributions', - iconClass: 'ri-code-line', + iconClass: 'laptop-code', type: FilterConfigType.NUMBER, options: {}, itemLabelRenderer(value: NumberFilterValue, options: NumberFilterOptions): string { diff --git a/frontend/src/modules/member/config/filters/organizations/OrganizationsFilter.vue b/frontend/src/modules/member/config/filters/organizations/OrganizationsFilter.vue new file mode 100644 index 0000000000..49427252ac --- /dev/null +++ b/frontend/src/modules/member/config/filters/organizations/OrganizationsFilter.vue @@ -0,0 +1,214 @@ + + + + + + + diff --git a/frontend/src/modules/member/config/filters/organizations/config.ts b/frontend/src/modules/member/config/filters/organizations/config.ts index f1379061f1..dbde7a1a0a 100644 --- a/frontend/src/modules/member/config/filters/organizations/config.ts +++ b/frontend/src/modules/member/config/filters/organizations/config.ts @@ -1,25 +1,44 @@ import { FilterConfigType } from '@/shared/modules/filters/types/FilterConfig'; import { itemLabelRendererByType } from '@/shared/modules/filters/config/itemLabelRendererByType'; import { - MultiSelectAsyncFilterConfig, MultiSelectAsyncFilterOptions, MultiSelectAsyncFilterValue, } from '@/shared/modules/filters/types/filterTypes/MultiSelectAsyncFilterConfig'; -import { OrganizationService } from '@/modules/organization/organization-service'; +import { CustomFilterConfig } from '@/shared/modules/filters/types/filterTypes/CustomFilterConfig'; import { DEFAULT_ORGANIZATION_FILTERS } from '@/modules/organization/store/constants'; +import { OrganizationService } from '@/modules/organization/organization-service'; +import { trimAndReduceSpaces } from '@/utils/string'; +import { queryUrlParserByType } from '@/shared/modules/filters/config/queryUrlParserByType'; +import { Organization } from '@/modules/organization/types/Organization'; +import OrganizationsFilter from './OrganizationsFilter.vue'; -const organizations: MultiSelectAsyncFilterConfig = { +const organizations: CustomFilterConfig = { id: 'organizations', label: 'Organization', - iconClass: 'ri-community-line', - type: FilterConfigType.MULTISELECT_ASYNC, + iconClass: 'building', + type: FilterConfigType.CUSTOM, + component: OrganizationsFilter, options: { - remoteMethod: (query) => OrganizationService.listAutocomplete(query, 10) - .then((data: any[]) => data.map((organization) => ({ - label: organization.label, + remoteMethod: (query: string) => OrganizationService.query({ + filter: { + or: [ + { displayName: { textContains: trimAndReduceSpaces(query) } }, + ], + }, + limit: 10, + segments: [], + }) + .then(({ rows }: { rows: Organization[] }) => rows.map((organization) => ({ + label: organization.displayName || organization.name, + lfxMembership: organization.lfxMembership, value: organization.id, - logo: organization.logo, + prefix: ` + + ${organization.logo + ? `${organization.displayName}` + : ''} + `, }))), - remotePopulateItems: (ids: string[]) => OrganizationService.listAutocomplete({ + remotePopulateItems: (ids: string[]) => OrganizationService.query({ filter: { and: [ ...DEFAULT_ORGANIZATION_FILTERS, @@ -28,22 +47,23 @@ const organizations: MultiSelectAsyncFilterConfig = { }, ], }, - orderBy: null, - limit: ids.length, - offset: 0, + limit: ids?.length, + segments: [], }) .then(({ rows }: any) => rows.map((organization: any) => ({ label: organization.displayName, + lfxMembership: organization.lfxMembership, value: organization.id, logo: organization.logo, }))), }, + queryUrlParser: queryUrlParserByType[FilterConfigType.MULTISELECT_ASYNC], itemLabelRenderer(value: MultiSelectAsyncFilterValue, options: MultiSelectAsyncFilterOptions, data: any): string { return itemLabelRendererByType[FilterConfigType.MULTISELECT_ASYNC]('Organization', value, options, data); }, apiFilterRenderer({ value, include }: MultiSelectAsyncFilterValue): any[] { const filter = { - or: value.map((id) => ({ organizations: { eq: id } })), + or: value.map((id) => ({ organizations: { contains: [id] } })), }; return [ (include ? filter : { not: filter }), diff --git a/frontend/src/modules/member/config/filters/projects/ProjectsFilter.vue b/frontend/src/modules/member/config/filters/projects/ProjectsFilter.vue new file mode 100644 index 0000000000..57c94a387f --- /dev/null +++ b/frontend/src/modules/member/config/filters/projects/ProjectsFilter.vue @@ -0,0 +1,135 @@ + + + diff --git a/frontend/src/modules/member/config/filters/projects/config.ts b/frontend/src/modules/member/config/filters/projects/config.ts index 88f83586d1..1018a36fd2 100644 --- a/frontend/src/modules/member/config/filters/projects/config.ts +++ b/frontend/src/modules/member/config/filters/projects/config.ts @@ -1,27 +1,44 @@ import { FilterConfigType } from '@/shared/modules/filters/types/FilterConfig'; -import { CustomFilterConfig } from '@/shared/modules/filters/types/filterTypes/CustomFilterConfig'; +import ProjectsFilter from '@/modules/member/config/filters/projects/ProjectsFilter.vue'; +import { LfService } from '@/modules/lf/segments/lf-segments-service'; +import { Project } from '@/modules/lf/segments/types/Segments'; +import { ProjectsFilterValue, ProjectsCustomFilterConfig } from '@/modules/lf/segments/types/Filters'; +import { filterLabel } from '@/modules/lf/utils/filters'; -const projects: CustomFilterConfig = { +const projects: ProjectsCustomFilterConfig = { id: 'projects', label: 'Projects', - iconClass: 'ri-stack-line', - featureFlag: 'projects-filter', + iconClass: 'layer-group', inBody: true, type: FilterConfigType.CUSTOM, - component: null, + component: ProjectsFilter, options: { + remoteMethod: ({ + query, + parentSlug, + }) => LfService.queryProjects({ + limit: 10, + filter: { + name: query, + parentSlug, + }, + }) + .then(({ rows }) => rows), }, - queryUrlParser({ value, include }: any): Record { + queryUrlParser({ value }: {value: string}): Record { return { - include: include === 'true', value: value.split(','), }; }, - itemLabelRenderer(value: any, options: any): string { - console.log(value, options); - return 'Projects...'; + itemLabelRenderer({ value, parentValues }: ProjectsFilterValue, options: any, data: { options: Project[] }): string { + const charLimit = 30; + const text = filterLabel(value, parentValues, data.options); + const trimmedValueText = text.length > charLimit ? `${text.substring(0, charLimit - 3)}...` : text; + const tooltip = trimmedValueText.length < text.length ? `data-tooltip="${text}"` : ''; + + return `Projects:${trimmedValueText || '...'}`; }, - apiFilterRenderer({ value }: any): any[] { + apiFilterRenderer({ value }: ProjectsFilterValue): any[] { return [ { segments: value }, ]; diff --git a/frontend/src/modules/member/config/filters/reach/config.ts b/frontend/src/modules/member/config/filters/reach/config.ts index 73925edea4..429943dd61 100644 --- a/frontend/src/modules/member/config/filters/reach/config.ts +++ b/frontend/src/modules/member/config/filters/reach/config.ts @@ -10,7 +10,7 @@ import { apiFilterRendererByType } from '@/shared/modules/filters/config/apiFilt const reach: NumberFilterConfig = { id: 'reach', label: 'Reach', - iconClass: 'ri-parent-line', + iconClass: 'bullhorn', type: FilterConfigType.NUMBER, options: {}, itemLabelRenderer(value: NumberFilterValue, options: NumberFilterOptions): string { diff --git a/frontend/src/modules/member/config/filters/role/config.ts b/frontend/src/modules/member/config/filters/role/config.ts new file mode 100644 index 0000000000..ea9aa917a6 --- /dev/null +++ b/frontend/src/modules/member/config/filters/role/config.ts @@ -0,0 +1,33 @@ +import { FilterConfigType } from '@/shared/modules/filters/types/FilterConfig'; +import { + MultiSelectFilterConfig, + MultiSelectFilterOptions, + MultiSelectFilterValue, +} from '@/shared/modules/filters/types/filterTypes/MultiSelectFilterConfig'; +import { itemLabelRendererByType } from '@/shared/modules/filters/config/itemLabelRendererByType'; +import options from './options'; + +const role: MultiSelectFilterConfig = { + id: 'role', + label: 'Role', + iconClass: 'id-card', + type: FilterConfigType.MULTISELECT, + options: { + options, + }, + itemLabelRenderer(value: MultiSelectFilterValue, options: MultiSelectFilterOptions): string { + return itemLabelRendererByType[FilterConfigType.MULTISELECT]('Role', value, options); + }, + apiFilterRenderer({ value, include }: MultiSelectFilterValue): any[] { + const filter = { + role: { + in: value, + }, + }; + return [ + (include ? filter : { not: filter }), + ]; + }, +}; + +export default role; diff --git a/frontend/src/modules/member/config/filters/role/options.ts b/frontend/src/modules/member/config/filters/role/options.ts new file mode 100644 index 0000000000..d882098175 --- /dev/null +++ b/frontend/src/modules/member/config/filters/role/options.ts @@ -0,0 +1,18 @@ +import { MultiSelectFilterOptionGroup } from '@/shared/modules/filters/types/filterTypes/MultiSelectFilterConfig'; + +const options: MultiSelectFilterOptionGroup[] = [ + { + options: [ + { + label: 'Maintainer', + value: 'maintainer', + }, + { + label: 'Contributor', + value: 'contributor', + }, + ], + }, +]; + +export default options; diff --git a/frontend/src/modules/member/config/filters/seniorityLevel/config.ts b/frontend/src/modules/member/config/filters/seniorityLevel/config.ts deleted file mode 100644 index 17ccfbcb7a..0000000000 --- a/frontend/src/modules/member/config/filters/seniorityLevel/config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { FilterConfigType } from '@/shared/modules/filters/types/FilterConfig'; -import { itemLabelRendererByType } from '@/shared/modules/filters/config/itemLabelRendererByType'; -import { apiFilterRendererByType } from '@/shared/modules/filters/config/apiFilterRendererByType'; -import { StringFilterConfig, StringFilterOptions, StringFilterValue } from '@/shared/modules/filters/types/filterTypes/StringFilterConfig'; - -const seniorityLevel: StringFilterConfig = { - id: 'seniorityLevel', - label: 'Seniority level', - iconClass: 'ri-menu-2-line', - type: FilterConfigType.STRING, - options: {}, - itemLabelRenderer(value: StringFilterValue, options: StringFilterOptions): string { - return itemLabelRendererByType[FilterConfigType.STRING]('Seniority level', value, options); - }, - apiFilterRenderer(value: StringFilterValue): any[] { - return apiFilterRendererByType[FilterConfigType.STRING]('attributes.seniorityLevel.default', value); - }, -}; -export default seniorityLevel; diff --git a/frontend/src/modules/member/config/filters/tags/config.ts b/frontend/src/modules/member/config/filters/tags/config.ts deleted file mode 100644 index 4bea046f36..0000000000 --- a/frontend/src/modules/member/config/filters/tags/config.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { FilterConfigType } from '@/shared/modules/filters/types/FilterConfig'; -import { itemLabelRendererByType } from '@/shared/modules/filters/config/itemLabelRendererByType'; -import { - MultiSelectAsyncFilterConfig, MultiSelectAsyncFilterOptions, - MultiSelectAsyncFilterValue, -} from '@/shared/modules/filters/types/filterTypes/MultiSelectAsyncFilterConfig'; -import { TagService } from '@/modules/tag/tag-service'; - -const tags: MultiSelectAsyncFilterConfig = { - id: 'tags', - label: 'Tags', - iconClass: 'ri-bookmark-line', - type: FilterConfigType.MULTISELECT_ASYNC, - options: { - remoteMethod: (query) => TagService.listAutocomplete(query, 10) - .then((data: any[]) => data.map((tag) => ({ - label: tag.label, - value: tag.id, - }))), - remotePopulateItems: (ids: string[]) => TagService.list({ - ids, - }, null, ids.length, 0) - .then(({ rows }: any) => rows.map((tag: any) => ({ - label: tag.name, - value: tag.id, - }))), - }, - itemLabelRenderer(value: MultiSelectAsyncFilterValue, options: MultiSelectAsyncFilterOptions, data: any): string { - return itemLabelRendererByType[FilterConfigType.MULTISELECT_ASYNC]('Tags', value, options, data); - }, - apiFilterRenderer({ value, include }: MultiSelectAsyncFilterValue): any[] { - const filter = { tags: value }; - return [ - (include ? filter : { not: filter }), - ]; - }, -}; - -export default tags; diff --git a/frontend/src/modules/member/config/filters/unaffiliated/config.ts b/frontend/src/modules/member/config/filters/unaffiliated/config.ts new file mode 100644 index 0000000000..b6ebea6542 --- /dev/null +++ b/frontend/src/modules/member/config/filters/unaffiliated/config.ts @@ -0,0 +1,29 @@ +import { FilterConfigType } from '@/shared/modules/filters/types/FilterConfig'; +import { + BooleanFilterConfig, BooleanFilterOptions, + BooleanFilterValue, +} from '@/shared/modules/filters/types/filterTypes/BooleanFilterConfig'; +import { itemLabelRendererByType } from '@/shared/modules/filters/config/itemLabelRendererByType'; + +const unaffiliated: BooleanFilterConfig = { + id: 'unaffiliated', + label: 'Unaffiliated profile', + iconClass: 'id-card', + type: FilterConfigType.BOOLEAN, + options: {}, + itemLabelRenderer(value: BooleanFilterValue, options: BooleanFilterOptions): string { + return itemLabelRendererByType[FilterConfigType.BOOLEAN]('Unaffiliated profile', value, options); + }, + apiFilterRenderer({ value }: BooleanFilterValue): any[] { + const filter = { + organizations: { + [value ? 'eq' : 'ne']: null, + }, + }; + return [ + filter, + ]; + }, +}; + +export default unaffiliated; diff --git a/frontend/src/modules/member/config/saved-views/main.ts b/frontend/src/modules/member/config/saved-views/main.ts index 9f9b029317..0b3b763378 100644 --- a/frontend/src/modules/member/config/saved-views/main.ts +++ b/frontend/src/modules/member/config/saved-views/main.ts @@ -1,36 +1,27 @@ +import { memberDefaultFilterRenderer } from '@/shared/modules/filters/config/defaultFilterRenderer/member.defaultFilter.renderer'; import { SavedView, SavedViewsConfig } from '@/shared/modules/saved-views/types/SavedViewsConfig'; -import allContacts from './views/all-contacts'; -import newAndActive from './views/new-and-active'; -import slippingAway from './views/slipping-away'; -import mostEngaged from './views/most-engaged'; -import influential from './views/influential'; -import teamMembers from './views/team-members'; - import bot from './settings/bot/config'; -import teamMember from './settings/teamMember/config'; import organization from './settings/organization/config'; +import allMembers from './views/all-members'; +import toReview from './views/to-review'; +import unaffiliated from './views/unaffiliated'; export const memberSavedViews: SavedViewsConfig = { - defaultView: allContacts, + defaultView: allMembers, settings: { - teamMember, bot, organization, }, + defaultFilters: { + render: memberDefaultFilterRenderer, + }, sorting: { - displayName: 'Contact', + displayName: 'Person', activityCount: '# of activities', - score: 'Engagement level', - lastActive: 'Last activity', - joinedAt: 'Joined date', - numberOfOpenSourceContributions: '# of OSS contributions', }, }; -export const memberViews: SavedView[] = [ - newAndActive, - slippingAway, - mostEngaged, - influential, - teamMembers, +export const memberStaticViews: SavedView[] = [ + unaffiliated, + toReview, ]; diff --git a/frontend/src/modules/member/config/saved-views/settings/bot/MemberBotSetting.vue b/frontend/src/modules/member/config/saved-views/settings/bot/MemberBotSetting.vue index ed65f6eced..f55009e011 100644 --- a/frontend/src/modules/member/config/saved-views/settings/bot/MemberBotSetting.vue +++ b/frontend/src/modules/member/config/saved-views/settings/bot/MemberBotSetting.vue @@ -1,14 +1,14 @@ - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/frontend/src/modules/organization/components/form/organization-form-emails.vue b/frontend/src/modules/organization/components/form/organization-form-emails.vue new file mode 100644 index 0000000000..46927e24b3 --- /dev/null +++ b/frontend/src/modules/organization/components/form/organization-form-emails.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/frontend/src/modules/organization/components/form/organization-form-identities.vue b/frontend/src/modules/organization/components/form/organization-form-identities.vue index f3b562a69d..9b735bb81f 100644 --- a/frontend/src/modules/organization/components/form/organization-form-identities.vue +++ b/frontend/src/modules/organization/components/form/organization-form-identities.vue @@ -1,108 +1,96 @@ @@ -975,35 +745,9 @@ export default { diff --git a/frontend/src/modules/organization/components/organization-merge-dialog.vue b/frontend/src/modules/organization/components/organization-merge-dialog.vue index 63e1732808..34cab1b112 100644 --- a/frontend/src/modules/organization/components/organization-merge-dialog.vue +++ b/frontend/src/modules/organization/components/organization-merge-dialog.vue @@ -1,91 +1,115 @@ + + diff --git a/frontend/src/modules/organization/components/organization-merge-suggestions.vue b/frontend/src/modules/organization/components/organization-merge-suggestions.vue new file mode 100644 index 0000000000..6581b5088e --- /dev/null +++ b/frontend/src/modules/organization/components/organization-merge-suggestions.vue @@ -0,0 +1,341 @@ + + + + + diff --git a/frontend/src/modules/organization/components/organization-name.vue b/frontend/src/modules/organization/components/organization-name.vue index 516759a6a8..a26a992035 100644 --- a/frontend/src/modules/organization/components/organization-name.vue +++ b/frontend/src/modules/organization/components/organization-name.vue @@ -2,7 +2,7 @@
- + +
-
- -
+
+ - {{ organization.displayName || organization.name }} -
- +
+ {{ organization.displayName || organization.name }} +
+ + +
import { ref } from 'vue'; import AppOrganizationBadge from '@/modules/organization/components/organization-badge.vue'; +import AppAvatarNewBadge from '@/shared/avatar/avatar-new-badge.vue'; import AppAvatarImage from '@/shared/avatar-image/avatar-image.vue'; +import LfOrganizationLfMemberTag from '@/modules/organization/components/lf-member/organization-lf-member-tag.vue'; +import LfIcon from '@/ui-kit/icon/Icon.vue'; defineProps({ organization: { diff --git a/frontend/src/modules/organization/components/organization-selection-dropdown.vue b/frontend/src/modules/organization/components/organization-selection-dropdown.vue index 92f2c86627..00bb414135 100644 --- a/frontend/src/modules/organization/components/organization-selection-dropdown.vue +++ b/frontend/src/modules/organization/components/organization-selection-dropdown.vue @@ -1,9 +1,7 @@ @@ -43,11 +47,13 @@ import { computed, ref, defineProps, - defineEmits, + defineEmits, onMounted, } from 'vue'; import AppAutocompleteOneInput from '@/shared/form/autocomplete-one-input.vue'; import AppAvatar from '@/shared/avatar/avatar.vue'; import { OrganizationService } from '@/modules/organization/organization-service'; +import { useRoute } from 'vue-router'; +import LfIcon from '@/ui-kit/icon/Icon.vue'; const emit = defineEmits('update:modelValue'); const props = defineProps({ @@ -55,11 +61,19 @@ const props = defineProps({ type: Object, required: true, }, + primaryOrganization: { + type: Object, + required: true, + }, id: { type: String, required: true, }, }); + +const route = useRoute(); + +const segments = ref([]); const loadingOrganizationToMerge = ref(); const computedOrganizationToMerge = computed({ get() { @@ -68,18 +82,20 @@ const computedOrganizationToMerge = computed({ async set(value) { loadingOrganizationToMerge.value = true; - const response = await OrganizationService.find(value.id); + const response = await OrganizationService.find(value.id, segments.value); emit('update:modelValue', response); loadingOrganizationToMerge.value = false; }, }); -const fetchFn = async (query, limit) => { - const options = await OrganizationService.listAutocomplete( +const fetchFn = async ({ query, limit }) => { + const options = await OrganizationService.listOrganizationsAutocomplete({ query, limit, - ); + excludeLfMember: true, + segments: segments.value, + }); // Remove primary organization from organizations that can be merged with const filteredOptions = options.filter((m) => m.id !== props.id); @@ -88,10 +104,15 @@ const fetchFn = async (query, limit) => { if (options.length !== filteredOptions.length) { filteredOptions.push({}); } - console.log(filteredOptions); return filteredOptions; }; + +onMounted(() => { + segments.value = route.query.segmentId ? [route.query.segmentId] : [route.query.projectGroup]; +}); + +const disableOption = (option) => !!option.lfxMembership && !!props.primaryOrganization?.lfxMembership; + + diff --git a/frontend/src/modules/organization/components/shared/organization-dropdown.vue b/frontend/src/modules/organization/components/shared/organization-dropdown.vue new file mode 100644 index 0000000000..59b62aa7b3 --- /dev/null +++ b/frontend/src/modules/organization/components/shared/organization-dropdown.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/frontend/src/modules/organization/components/shared/organization-last-enrichment.vue b/frontend/src/modules/organization/components/shared/organization-last-enrichment.vue new file mode 100644 index 0000000000..378c3144df --- /dev/null +++ b/frontend/src/modules/organization/components/shared/organization-last-enrichment.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/frontend/src/modules/organization/components/shared/organization-membership.vue b/frontend/src/modules/organization/components/shared/organization-membership.vue new file mode 100644 index 0000000000..3064414887 --- /dev/null +++ b/frontend/src/modules/organization/components/shared/organization-membership.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/frontend/src/modules/organization/components/shared/organization-select.vue b/frontend/src/modules/organization/components/shared/organization-select.vue new file mode 100644 index 0000000000..53566e37ab --- /dev/null +++ b/frontend/src/modules/organization/components/shared/organization-select.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/frontend/src/modules/organization/components/shared/organization-syncing-activities.vue b/frontend/src/modules/organization/components/shared/organization-syncing-activities.vue new file mode 100644 index 0000000000..9b9a9130a1 --- /dev/null +++ b/frontend/src/modules/organization/components/shared/organization-syncing-activities.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/frontend/src/modules/organization/components/suggestions/organization-merge-suggestions-details.vue b/frontend/src/modules/organization/components/suggestions/organization-merge-suggestions-details.vue index 2afb8af7ad..ef27607878 100644 --- a/frontend/src/modules/organization/components/suggestions/organization-merge-suggestions-details.vue +++ b/frontend/src/modules/organization/components/suggestions/organization-merge-suggestions-details.vue @@ -24,55 +24,118 @@ :class="{ 'bg-gray-50': props.isPrimary }" > -
-
- Primary organization + +
+
+ Preview +
+
+ Primary organization +
+ + + + + Make primary + + + +
- - -
-
- -
-
+
+
+ + + + +
+
+ +
+
+ +
+
+
+
+ +
@@ -37,6 +42,18 @@ const props = defineProps({ type: String, default: null, }, + disabled: { + type: Boolean, + default: false, + }, + inputClass: { + type: String, + default: '', + }, + inputLabel: { + type: String, + default: '', + }, }); const rules = { @@ -57,6 +74,6 @@ const $v = useVuelidate(rules, model); diff --git a/frontend/src/shared/form/autocomplete-many-input.vue b/frontend/src/shared/form/autocomplete-many-input.vue index 444c2c0b62..df72ff0e00 100644 --- a/frontend/src/shared/form/autocomplete-many-input.vue +++ b/frontend/src/shared/form/autocomplete-many-input.vue @@ -132,6 +132,10 @@ export default { type: Boolean, default: false, }, + optionsLimit: { + type: Number, + default: null, + }, }, emits: ['remove-tag', 'update:modelValue'], data() { @@ -208,10 +212,10 @@ export default { // Rendered available options should be dependent on the current query availableOptions() { if (this.currentQuery) { - return this.filteredOptions; + return this.optionsLimit ? this.filteredOptions.slice(0, this.optionsLimit) : this.filteredOptions; } - return this.localOptions; + return this.optionsLimit ? this.localOptions.slice(0, this.optionsLimit) : this.localOptions; }, }, @@ -311,10 +315,10 @@ export default { this.loading = true; try { - const response = await this.fetchFn( - value, - this.limit, - ); + const response = await this.fetchFn({ + query: value, + limit: this.limit, + }); this.localOptions = response; diff --git a/frontend/src/shared/form/autocomplete-one-input.vue b/frontend/src/shared/form/autocomplete-one-input.vue index d6e652aaec..1e4e08d516 100644 --- a/frontend/src/shared/form/autocomplete-one-input.vue +++ b/frontend/src/shared/form/autocomplete-one-input.vue @@ -40,19 +40,32 @@ v-for="record in localOptions" :key="record.id" > - - - - {{ record.label }} - - - + + + + + {{ record.label }} + + + + +
{}, + }, }, emits: ['update:modelValue'], data() { @@ -171,15 +188,12 @@ export default { methods: { async onChange(value) { - const { query } = this.$refs.input; - if ( - typeof query === 'string' - && query !== '' + this.currentQuery !== '' && this.createIfNotFound && !value ) { - const newItem = await this.createFn(query); + const newItem = await this.createFn(this.currentQuery); this.localOptions.push(newItem); this.$emit('update:modelValue', newItem); } else { @@ -206,10 +220,11 @@ export default { async fetchAllResults() { this.loading = true; try { - this.localOptions = await this.fetchFn( - this.currentQuery, - AUTOCOMPLETE_SERVER_FETCH_SIZE, - ); + this.localOptions.length = 0; + this.localOptions = await this.fetchFn({ + query: this.currentQuery, + limit: AUTOCOMPLETE_SERVER_FETCH_SIZE, + }); this.loading = false; } catch (error) { console.error(error); @@ -226,14 +241,16 @@ export default { this.loading = true; try { - this.localOptions = await this.fetchFn( - value, - AUTOCOMPLETE_SERVER_FETCH_SIZE, - ); + this.localOptions.length = 0; + this.localOptions = await this.fetchFn({ + query: value, + limit: AUTOCOMPLETE_SERVER_FETCH_SIZE, + }); + this.loading = false; } catch (error) { console.error(error); - this.localOptions = []; + this.localOptions.length = 0; this.loading = false; } }, diff --git a/frontend/src/shared/form/editor.vue b/frontend/src/shared/form/editor.vue deleted file mode 100644 index a473dfe925..0000000000 --- a/frontend/src/shared/form/editor.vue +++ /dev/null @@ -1,173 +0,0 @@ - - - - - - - diff --git a/frontend/src/shared/form/fields/roles-field.js b/frontend/src/shared/form/fields/roles-field.js deleted file mode 100644 index 79e47f908e..0000000000 --- a/frontend/src/shared/form/fields/roles-field.js +++ /dev/null @@ -1,22 +0,0 @@ -import * as yup from 'yup'; -import StringArrayField from '@/shared/fields/string-array-field'; -import Roles from '@/security/roles'; - -export class RolesField extends StringArrayField { - constructor(name, fieldLabel, config = {}) { - super(name, fieldLabel, config); - - this.options = Roles.selectOptions; - } - - forExport() { - return yup - .mixed() - .label(this.label) - .transform((values) => (values - ? values - .map((value) => Roles.labelOf(value)) - .join(' ') - : null)); - } -} diff --git a/frontend/src/shared/form/form-change.js b/frontend/src/shared/form/form-change.js index a5dd686f85..54b7942b01 100644 --- a/frontend/src/shared/form/form-change.js +++ b/frontend/src/shared/form/form-change.js @@ -7,7 +7,7 @@ export default function formChangeDetector(form) { temporaryForm.value = JSON.stringify(form); } - const hasFormChanged = computed(() => temporaryForm.value !== JSON.stringify(form)); + const hasFormChanged = computed(() => temporaryForm.value !== JSON.stringify(form), { deep: true }); return { temporaryForm, diff --git a/frontend/src/shared/form/form-item.vue b/frontend/src/shared/form/form-item.vue index db17073433..abd4ea84ef 100644 --- a/frontend/src/shared/form/form-item.vue +++ b/frontend/src/shared/form/form-item.vue @@ -6,7 +6,7 @@ }" v-bind="$attrs" > -
+
@@ -34,7 +34,7 @@ - - diff --git a/frontend/src/shared/helpers/attribute.helpers.ts b/frontend/src/shared/helpers/attribute.helpers.ts new file mode 100644 index 0000000000..8123e1055a --- /dev/null +++ b/frontend/src/shared/helpers/attribute.helpers.ts @@ -0,0 +1,32 @@ +import { isEqual } from 'lodash'; +import { lfIdentities } from '@/config/identities'; +import useIdentitiesHelpers from '@/config/identities/identities.helpers'; + +export const getAttributeSources = (attribute: Record): string[] => { + const defaultValue: string | undefined = attribute.default; + return Object.keys(attribute).filter((key) => !['default'].includes(key) && isEqual(attribute[key], defaultValue)); +}; + +export const getAttributeSourceName = (attribute: Record): string | null => { + if (!attribute) { + return null; + } + const sources = getAttributeSources(attribute); + if (sources.length === 0) { + return null; + } + + // Sort that integrations are first, then others like enrichment and last is custom + const prioritySortedSources = sources.sort((a, b) => { + const aConfig = !!lfIdentities[a]?.name; + const bConfig = !!lfIdentities[b]?.name; + + if (aConfig && !bConfig) return -1; // a matches the criteria and b doesn't, a should come first + if (!aConfig && bConfig) return 1; // b matches the criteria and a doesn't, b should come first + + return 0; // If both match or both don't match the criteria, keep their order + }); + const { getPlatformsLabel } = useIdentitiesHelpers(); + const selectedSource = prioritySortedSources[0]; + return getPlatformsLabel([selectedSource]); +}; diff --git a/frontend/src/shared/filter/helpers/different-util.js b/frontend/src/shared/helpers/different-util.js similarity index 100% rename from frontend/src/shared/filter/helpers/different-util.js rename to frontend/src/shared/helpers/different-util.js diff --git a/frontend/src/shared/helpers/error-message.helper.ts b/frontend/src/shared/helpers/error-message.helper.ts new file mode 100644 index 0000000000..5c378d1025 --- /dev/null +++ b/frontend/src/shared/helpers/error-message.helper.ts @@ -0,0 +1,70 @@ +import { AxiosError } from 'axios'; +import { h } from 'vue'; +import { ToastStore } from '../message/notification'; + +export const getAxiosErrorMessage = ( + error: AxiosError, + defaultMessage: string = 'Something went wrong', +): string => { + if (error && error.response && error.response.data) { + const errMsg = error.response.data && typeof error.response.data === 'string' + ? error.response.data + : error.message; + return errMsg; + } + return error.message || defaultMessage; +}; + +export const parseDuplicateRepoError = ( + errorMessage: string, + defaultMessage: string, +): { repo: string; eId: string } | null => { + const pattern = new RegExp( + 'Trying to update repo (?[^\\s]+) mapping with integrationId (?[^\\s]+) ' + + 'but it is already mapped to integration (?[^\\s!]+)', + ); + const match = errorMessage.match(pattern); + + if (match?.groups) { + const { repo, eId } = match.groups; + + return { repo, eId }; + } + ToastStore.error(defaultMessage); + return null; +}; + +const getSegmentLink = (segment: any) => `/integrations/${segment.grandparentId}/${segment.id}`; +export const customRepoErrorMessage = ( + segment: any, + githubRepo: string, + integrationName: string, +) => { + ToastStore.error( + h( + 'span', + { + class: 'whitespace-normal', + }, + [ + `The ${integrationName} repo`, + ' ', + h('strong', githubRepo), + ' ', + 'is already connected with project', + ' ', + h( + 'a', + { + href: getSegmentLink(segment), + class: 'text-blue-500 underline hover:text-blue-600', + }, + segment.name || 'Unknown Project', + ), + ], + ), + { + title: 'Conflict Detected', + }, + ); +}; diff --git a/frontend/src/shared/helpers/manualAction.helpers.ts b/frontend/src/shared/helpers/manualAction.helpers.ts new file mode 100644 index 0000000000..7b22ccfcb8 --- /dev/null +++ b/frontend/src/shared/helpers/manualAction.helpers.ts @@ -0,0 +1,33 @@ +import { ToastStore } from '@/shared/message/notification'; + +export const doManualAction = async ({ + loadingMessage, + actionFn, + successMessage, + errorMessage, +}: { + loadingMessage?: string; + successMessage?: string; + errorMessage?: string; + actionFn: Promise; +}) => { + if (loadingMessage) { + ToastStore.info(loadingMessage); + } + + return actionFn + .then(() => { + if (successMessage) { + ToastStore.closeAll(); + ToastStore.success(successMessage); + } + Promise.resolve(); + }) + .catch(() => { + if (errorMessage) { + ToastStore.closeAll(); + ToastStore.error(errorMessage); + } + Promise.reject(); + }); +}; diff --git a/frontend/src/shared/i18n/i18n.vue b/frontend/src/shared/i18n/i18n.vue deleted file mode 100644 index bdf33d63fc..0000000000 --- a/frontend/src/shared/i18n/i18n.vue +++ /dev/null @@ -1,41 +0,0 @@ - - - - - diff --git a/frontend/src/shared/layout/page-wrapper.vue b/frontend/src/shared/layout/page-wrapper.vue index b7f37ac222..6062ad52c2 100644 --- a/frontend/src/shared/layout/page-wrapper.vue +++ b/frontend/src/shared/layout/page-wrapper.vue @@ -1,11 +1,13 @@ @@ -26,11 +28,11 @@ const props = defineProps({ const computedClass = computed(() => { if (props.size === 'default') { - return 'max-w-6xl mx-auto'; + return 'max-w-6xl'; } if (props.size === 'narrow') { - return 'max-w-5xl mx-auto'; + return 'max-w-5xl'; } if (props.size === 'full-width') { - return 'max-w-full mx-auto'; + return 'max-w-full'; } return ''; }); diff --git a/frontend/src/shared/message/message.js b/frontend/src/shared/message/message.js deleted file mode 100644 index 546bef6f9c..0000000000 --- a/frontend/src/shared/message/message.js +++ /dev/null @@ -1,89 +0,0 @@ -import { i18n } from '@/i18n'; -import { ElNotification } from 'element-plus'; -import 'element-plus/es/components/notification/style/css'; - -import { h } from 'vue'; - -const successIcon = h( - 'i', // type - { class: 'ri-checkbox-circle-fill text-green-500' }, // props - [], -); - -const errorIcon = h( - 'i', // type - { class: 'ri-error-warning-fill text-red-500' }, // props - [], -); - -const infoIcon = h( - 'i', // type - { class: 'ri-loader-4-line text-blue-600 animate-spin' }, // props - [], -); - -export default class Message { - static success(message, options = {}) { - ElNotification( - { - - title: options.title ? options.title : message, - showClose: true, - message: options.title ? message : null, - customClass: 'success', - icon: successIcon, - duration: 6000, - dangerouslyUseHTMLString: true, - position: 'bottom-right', - offset: 24, - ...options, - }, - ); - } - - static error(payload, options = {}) { - let message = payload; - - if (!message) { - message = i18n('errors.defaultErrorMessage'); - } - - ElNotification( - { - - title: options.title ? options.title : message, - showClose: true, - message: options.title ? message : null, - customClass: 'error', - icon: errorIcon, - duration: 0, - dangerouslyUseHTMLString: true, - position: 'bottom-right', - offset: 24, - ...options, - }, - ); - } - - static info(message, options = {}) { - ElNotification( - { - - title: options.title ? options.title : message, - showClose: true, - message: options.title ? message : null, - customClass: 'info', - icon: infoIcon, - duration: 0, - dangerouslyUseHTMLString: true, - position: 'bottom-right', - offset: 24, - ...options, - }, - ); - } - - static closeAll() { - ElNotification.closeAll(); - } -} diff --git a/frontend/src/shared/message/notification.ts b/frontend/src/shared/message/notification.ts new file mode 100644 index 0000000000..f397f14484 --- /dev/null +++ b/frontend/src/shared/message/notification.ts @@ -0,0 +1,91 @@ +import { reactive, VNode } from 'vue'; + +export const notificationTypes = ['info', 'success', 'error'] as const; + +export type NotificationTypes = (typeof notificationTypes)[number]; +export interface ToastI { + id: number; + title: string | VNode; // vNode is used for rendering Vue components + message?: string | VNode; // vNode is used for rendering Vue components + type: NotificationTypes; + duration: number; // in milliseconds +} + +let toastIdCounter = 0; + +export const ToastStore = reactive({ + toasts: [] as ToastI[], + maxVisible: 5, + + success( + message: string | VNode, + options: { title?: string; duration?: number; message?: string | VNode } = {}, + ) { + const mess = options.title ? message : options.message; + this.add( + options.title ? options.title : message, + options.message ? options.message : mess, + 'success', + options.duration, + ); + }, + + error( + message: string | VNode, + options: { title?: string; duration?: number; message?: string | VNode } = {}, + ) { + const mess = options.title ? message : options.message; + this.add( + options.title ? options.title : message, + options.message ? options.message : mess, + 'error', + options.duration, + ); + }, + + info( + message: string | VNode, + options: { title?: string; duration?: number; message?: string | VNode } = {}, + ) { + const mess = options.title ? message : options.message; + this.add( + options.title ? options.title : message, + options.message ? options.message : mess, + 'info', + options.duration ? options.duration : 0, + ); + }, + + add( + title: string | VNode, + message: string | VNode | undefined, + type: NotificationTypes = 'success', + duration: number = 6000, + ) { + toastIdCounter += 1; // Increment the counter for unique IDs + const toast = { + id: toastIdCounter, + title, + message, + type, + duration, + }; + this.toasts.push(toast); + + if (this.toasts.length > this.maxVisible) { + this.toasts.shift(); // remove the oldest + } + + setTimeout(() => { + this.remove(toast.id); + }, toast.duration); + }, + + closeAll() { + this.toasts = []; + }, + + remove(id: number) { + this.toasts = this.toasts.filter((t) => t.id !== id); + }, +}); diff --git a/frontend/src/shared/message/toaster-container.vue b/frontend/src/shared/message/toaster-container.vue new file mode 100644 index 0000000000..b2496b8336 --- /dev/null +++ b/frontend/src/shared/message/toaster-container.vue @@ -0,0 +1,105 @@ + + + + + + + + diff --git a/frontend/src/shared/modules/activity/components/activity-display.vue b/frontend/src/shared/modules/activity/components/activity-display.vue new file mode 100644 index 0000000000..d9a4ed441f --- /dev/null +++ b/frontend/src/shared/modules/activity/components/activity-display.vue @@ -0,0 +1,26 @@ + + + diff --git a/frontend/src/shared/modules/activity/components/activity-member-organization.vue b/frontend/src/shared/modules/activity/components/activity-member-organization.vue new file mode 100644 index 0000000000..3544331cce --- /dev/null +++ b/frontend/src/shared/modules/activity/components/activity-member-organization.vue @@ -0,0 +1,67 @@ + + + diff --git a/frontend/src/shared/modules/activity/types/Activity.ts b/frontend/src/shared/modules/activity/types/Activity.ts new file mode 100644 index 0000000000..77e23ba1b1 --- /dev/null +++ b/frontend/src/shared/modules/activity/types/Activity.ts @@ -0,0 +1,48 @@ +import { Member } from '@/modules/member/types/Member'; +import { Organization } from '@/modules/organization/types/Organization'; +import { Platform } from '../../platform/types/Platform'; + +export interface Activity { + id: string; + type: string; + timestamp: string; + platform: Platform; + score: number; + sourceId: string; + sourceParentId: string; + username: string; + attributes: any; + channel: string; + body: string; + title: string; + url: string; + sentiment: { + label: string; + mixed: number; + neutral: number; + negative: number; + positive: number; + sentiment: number; + }; + organizationId: string; + createdAt: string; + updatedAt: string; + deletedAt: string; + memberId: string; + segmentId: string; + objectMemberId: string; + conversationId: string; + parentId: string; + tenantId: string; + createdById: string; + updatedById: string; + member: Member; + parent: Activity; + organization: Organization; + display: { + default: string; + short: string; + author: string; + channel: string; + }; +} diff --git a/frontend/src/shared/modules/activity/types/DisplayConfig.ts b/frontend/src/shared/modules/activity/types/DisplayConfig.ts new file mode 100644 index 0000000000..a3a16addc6 --- /dev/null +++ b/frontend/src/shared/modules/activity/types/DisplayConfig.ts @@ -0,0 +1,13 @@ +import { Platform } from '@/shared/modules/platform/types/Platform'; +import { type Component } from 'vue'; + +export interface ActivityDisplayConfig { + id: string; + platform: Platform; + activityHeaderContent: Component; + activityContent: Component; +} + +export type ActivityDisplayPlatformConfig = { + [key in Platform]?: ActivityDisplayConfig +} diff --git a/frontend/src/shared/modules/back-link/components/back-link.vue b/frontend/src/shared/modules/back-link/components/back-link.vue new file mode 100644 index 0000000000..236fc292f6 --- /dev/null +++ b/frontend/src/shared/modules/back-link/components/back-link.vue @@ -0,0 +1,55 @@ + + + diff --git a/frontend/src/shared/modules/default-filters/components/default-filters.vue b/frontend/src/shared/modules/default-filters/components/default-filters.vue new file mode 100644 index 0000000000..e6716c5ec9 --- /dev/null +++ b/frontend/src/shared/modules/default-filters/components/default-filters.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/shared/modules/entities/Entities.vue b/frontend/src/shared/modules/entities/Entities.vue new file mode 100644 index 0000000000..fe0a16d6e5 --- /dev/null +++ b/frontend/src/shared/modules/entities/Entities.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/frontend/src/shared/modules/filters/components/Filter.vue b/frontend/src/shared/modules/filters/components/Filter.vue index e441fb4556..4437ff9041 100644 --- a/frontend/src/shared/modules/filters/components/Filter.vue +++ b/frontend/src/shared/modules/filters/components/Filter.vue @@ -1,11 +1,24 @@ @@ -70,7 +65,7 @@ export default { @apply mr-2; } - i:not(.ri-delete-bin-line) { + i:not(.fa-trash-can) { @apply text-gray-400; } @@ -86,13 +81,13 @@ export default { &.is-selected, &:focus.is-selected { - @apply relative bg-brand-50; + @apply relative bg-primary-50; i { - @apply mr-3 text-brand-600; + @apply mr-3 text-primary-600; } &:hover{ - @apply bg-brand-50; + @apply bg-primary-50; } } } diff --git a/frontend/src/shared/modules/filters/components/partials/select/FilterSelectOption.vue b/frontend/src/shared/modules/filters/components/partials/select/FilterSelectOption.vue index 822966c747..e97db62810 100644 --- a/frontend/src/shared/modules/filters/components/partials/select/FilterSelectOption.vue +++ b/frontend/src/shared/modules/filters/components/partials/select/FilterSelectOption.vue @@ -6,18 +6,16 @@ @click="selectOption()" > - +
@@ -50,7 +48,7 @@ export default { @apply mr-2; } - i:not(.ri-delete-bin-line) { + i:not(.fa-trash-can) { @apply text-gray-400; } @@ -66,13 +64,13 @@ export default { &.is-selected, &:focus.is-selected { - @apply relative bg-brand-50; + @apply relative bg-primary-50; i { - @apply mr-3 text-brand-600; + @apply mr-3 text-primary-600; } &:hover{ - @apply bg-brand-50; + @apply bg-primary-50; } } } diff --git a/frontend/src/shared/modules/filters/components/partials/string/FilterInput.vue b/frontend/src/shared/modules/filters/components/partials/string/FilterInput.vue index 50d31669dd..8a27c3ded2 100644 --- a/frontend/src/shared/modules/filters/components/partials/string/FilterInput.vue +++ b/frontend/src/shared/modules/filters/components/partials/string/FilterInput.vue @@ -40,7 +40,7 @@ const model = computed({ diff --git a/frontend/src/shared/modules/filters/config/apiFilterRenderer/boolean.filter.renderer.ts b/frontend/src/shared/modules/filters/config/apiFilterRenderer/boolean.filter.renderer.ts index 61303f7ac1..89263e2dd4 100644 --- a/frontend/src/shared/modules/filters/config/apiFilterRenderer/boolean.filter.renderer.ts +++ b/frontend/src/shared/modules/filters/config/apiFilterRenderer/boolean.filter.renderer.ts @@ -2,6 +2,6 @@ import { BooleanFilterValue } from '@/shared/modules/filters/types/filterTypes/B export const booleanApiFilterRenderer = (property: string, { value }: BooleanFilterValue): any[] => [ { - [property]: { eq: value }, + [property]: value ? { eq: true } : { not: true }, }, ]; diff --git a/frontend/src/shared/modules/filters/config/apiFilterRenderer/date.filter.renderer.ts b/frontend/src/shared/modules/filters/config/apiFilterRenderer/date.filter.renderer.ts index c7774d6129..7bfe51fb61 100644 --- a/frontend/src/shared/modules/filters/config/apiFilterRenderer/date.filter.renderer.ts +++ b/frontend/src/shared/modules/filters/config/apiFilterRenderer/date.filter.renderer.ts @@ -3,7 +3,7 @@ import { dateFilterTimePickerOptions, FilterDateOperator, } from '@/shared/modules/filters/config/constants/date.constants'; -import moment from 'moment'; +import { dateHelper } from '@/shared/date-helper/date-helper'; export const dateApiFilterRenderer = (property: string, { value, operator }: DateFilterValue): any[] => { const dateOption = dateFilterTimePickerOptions.find((option) => option.value === value); @@ -25,8 +25,8 @@ export const dateApiFilterRenderer = (property: string, { value, operator }: Dat filter = { [property]: { [filterOperator]: [ - moment.utc(mappedValue[0]).startOf('day').toISOString(), - moment.utc(mappedValue[1]).endOf('day').toISOString(), + dateHelper.utc(mappedValue[0]).startOf('day').toISOString(), + dateHelper.utc(mappedValue[1]).endOf('day').toISOString(), ], }, }; @@ -34,18 +34,18 @@ export const dateApiFilterRenderer = (property: string, { value, operator }: Dat filter = { [property]: { between: [ - moment.utc(mappedValue).startOf('day').toISOString(), - moment.utc(mappedValue).endOf('day').toISOString(), + dateHelper.utc(mappedValue).startOf('day').toISOString(), + dateHelper.utc(mappedValue).endOf('day').toISOString(), ], }, }; } else { - let parsedValue = moment.utc(mappedValue).startOf('day').toISOString(); + let parsedValue = dateHelper.utc(mappedValue).startOf('day').toISOString(); if (['last24h'].includes(value as string)) { parsedValue = mappedValue as string; - } else if ([FilterDateOperator.GT].includes(operator)) { - parsedValue = moment.utc(mappedValue).endOf('day').toISOString(); + } else if ([FilterDateOperator.GT, FilterDateOperator.GTE].includes(operator)) { + parsedValue = dateHelper.utc(mappedValue).endOf('day').toISOString(); } filter = { diff --git a/frontend/src/shared/modules/filters/config/apiFilterRendererByType.ts b/frontend/src/shared/modules/filters/config/apiFilterRendererByType.ts index 0e010e30ab..c45a670c22 100644 --- a/frontend/src/shared/modules/filters/config/apiFilterRendererByType.ts +++ b/frontend/src/shared/modules/filters/config/apiFilterRendererByType.ts @@ -13,6 +13,7 @@ export const apiFilterRendererByType: Record dateHelper().subtract(24, 'hour').toISOString(), + }, + { + id: 'olderLast7days', + value: 'last7days', + label: 'Older than 7 days', + operator: FilterDateOperator.LT, + getDate: () => dateHelper().subtract(7, 'day').format('YYYY-MM-DD'), + }, + { + id: 'olderLast14days', + value: 'last14days', + label: 'Older than 14 days', + operator: FilterDateOperator.LT, + getDate: () => dateHelper().subtract(14, 'day').format('YYYY-MM-DD'), + }, + { + id: 'olderLastMonth', + value: 'lastMonth', + label: 'Older than 30 days', + operator: FilterDateOperator.LT, + getDate: () => dateHelper().subtract(30, 'day').format('YYYY-MM-DD'), + }, + { + id: 'olderLast90days', + value: 'last90days', + label: 'Older than 90 days', + operator: FilterDateOperator.LT, + getDate: () => dateHelper().subtract(90, 'day').format('YYYY-MM-DD'), + }, + { + id: 'last24h', value: 'last24h', label: 'Last 24 hours', - getDate: () => moment().subtract(24, 'hour').toISOString(), + operator: FilterDateOperator.GT, + getDate: () => dateHelper().subtract(24, 'hour').toISOString(), }, { + id: 'last7days', value: 'last7days', label: 'Last 7 days', - getDate: () => moment().subtract(7, 'day').format('YYYY-MM-DD'), + operator: FilterDateOperator.GT, + getDate: () => dateHelper().subtract(7, 'day').format('YYYY-MM-DD'), }, { + id: 'last14days', value: 'last14days', label: 'Last 14 days', - getDate: () => moment().subtract(14, 'day').format('YYYY-MM-DD'), + operator: FilterDateOperator.GT, + getDate: () => dateHelper().subtract(14, 'day').format('YYYY-MM-DD'), }, { + id: 'lastMonth', value: 'lastMonth', label: 'Last 30 days', - getDate: () => moment().subtract(30, 'day').format('YYYY-MM-DD'), + operator: FilterDateOperator.GT, + getDate: () => dateHelper().subtract(30, 'day').format('YYYY-MM-DD'), }, { + id: 'last90days', value: 'last90days', label: 'Last 90 days', - getDate: () => moment().subtract(90, 'day').format('YYYY-MM-DD'), + operator: FilterDateOperator.GT, + getDate: () => dateHelper().subtract(90, 'day').format('YYYY-MM-DD'), }, ]; diff --git a/frontend/src/shared/modules/filters/config/defaultFilterRenderer/member.defaultFilter.renderer.ts b/frontend/src/shared/modules/filters/config/defaultFilterRenderer/member.defaultFilter.renderer.ts new file mode 100644 index 0000000000..40420d9b34 --- /dev/null +++ b/frontend/src/shared/modules/filters/config/defaultFilterRenderer/member.defaultFilter.renderer.ts @@ -0,0 +1,22 @@ +import { IncludeEnum } from '@/modules/member/config/saved-views/settings/common/types/IncludeEnum'; +import { DefaultFiltersSettings } from '@/shared/modules/saved-views/types/SavedViewsConfig'; + +export const memberDefaultFilterRenderer = ({ teamMember, bot }: DefaultFiltersSettings) => { + if (teamMember === IncludeEnum.EXCLUDE && bot === IncludeEnum.EXCLUDE) { + return 'Excl. team members and bots'; + } + + if (teamMember === IncludeEnum.EXCLUDE && bot === IncludeEnum.INCLUDE) { + return 'Incl. bots and excl. team members'; + } + + if (teamMember === IncludeEnum.INCLUDE && bot === IncludeEnum.EXCLUDE) { + return 'Incl. team members and excl. bots'; + } + + if (teamMember === IncludeEnum.FILTER) { + return 'Team members only'; + } + + return 'Incl. team members and bots'; +}; diff --git a/frontend/src/shared/modules/filters/config/defaultFilterRenderer/organization.defaultFilter.renderer.ts b/frontend/src/shared/modules/filters/config/defaultFilterRenderer/organization.defaultFilter.renderer.ts new file mode 100644 index 0000000000..4ef661ef31 --- /dev/null +++ b/frontend/src/shared/modules/filters/config/defaultFilterRenderer/organization.defaultFilter.renderer.ts @@ -0,0 +1,18 @@ +import { IncludeEnum } from '@/modules/organization/config/saved-views/settings/types/IncludeEnum'; +import { DefaultFiltersSettings } from '@/shared/modules/saved-views/types/SavedViewsConfig'; + +export const organizationDefaultFilterRenderer = ({ teamOrganization }: DefaultFiltersSettings) => { + if (teamOrganization === IncludeEnum.EXCLUDE) { + return 'Excl. team organizations'; + } + + if (teamOrganization === IncludeEnum.INCLUDE) { + return 'Incl. team organizations'; + } + + if (teamOrganization === IncludeEnum.FILTER) { + return 'Team organizations only'; + } + + return ''; +}; diff --git a/frontend/src/shared/modules/filters/config/filterComponentByType.ts b/frontend/src/shared/modules/filters/config/filterComponentByType.ts index f736ad06dd..a704231aad 100644 --- a/frontend/src/shared/modules/filters/config/filterComponentByType.ts +++ b/frontend/src/shared/modules/filters/config/filterComponentByType.ts @@ -3,6 +3,7 @@ import { Component } from 'vue'; import BooleanFilter from '@/shared/modules/filters/components/filterTypes/BooleanFilter.vue'; import MultiSelectFilter from '@/shared/modules/filters/components/filterTypes/MultiSelectFilter.vue'; import SelectFilter from '@/shared/modules/filters/components/filterTypes/SelectFilter.vue'; +import SelectAsyncFilter from '@/shared/modules/filters/components/filterTypes/SelectAsyncFilter.vue'; import DateFilter from '@/shared/modules/filters/components/filterTypes/DateFilter.vue'; import NumberFilter from '@/shared/modules/filters/components/filterTypes/NumberFilter.vue'; import StringFilter from '@/shared/modules/filters/components/filterTypes/StringFilter.vue'; @@ -13,6 +14,7 @@ export const filterComponentByType: Record = [FilterConfigType.NUMBER]: NumberFilter, [FilterConfigType.DATE]: DateFilter, [FilterConfigType.SELECT]: SelectFilter, + [FilterConfigType.SELECT_ASYNC]: SelectAsyncFilter, [FilterConfigType.MULTISELECT]: MultiSelectFilter, [FilterConfigType.MULTISELECT_ASYNC]: MultiSelectAsyncFilter, [FilterConfigType.STRING]: StringFilter, diff --git a/frontend/src/shared/modules/filters/config/itemLabelRenderer/date.label.renderer.ts b/frontend/src/shared/modules/filters/config/itemLabelRenderer/date.label.renderer.ts index b8e2b7c621..2a355cecfe 100644 --- a/frontend/src/shared/modules/filters/config/itemLabelRenderer/date.label.renderer.ts +++ b/frontend/src/shared/modules/filters/config/itemLabelRenderer/date.label.renderer.ts @@ -14,7 +14,7 @@ const operatorText: Record = { }; export const dateItemLabelRenderer = (property: string, { value, operator }: DateFilterValue): string => { - const dateOption = dateFilterTimePickerOptions.find((option) => option.value === value); + const dateOption = dateFilterTimePickerOptions.find((option) => option.value === value && option.operator === operator); let valueText = `${value}`; let operatorTextDisplay = operatorText[operator].length > 0 ? `${operatorText[operator]} ` : ''; @@ -25,7 +25,7 @@ export const dateItemLabelRenderer = (property: string, { value, operator }: Dat const isBetween = [FilterDateOperator.BETWEEN, FilterDateOperator.NOT_BETWEEN].includes(operator); if (isBetween) { const [from, to] = value; - valueText = `${from} ${to}`; + valueText = `${from} ${to}`; } } diff --git a/frontend/src/shared/modules/filters/config/itemLabelRenderer/selectasync.label.renderer.ts b/frontend/src/shared/modules/filters/config/itemLabelRenderer/selectasync.label.renderer.ts new file mode 100644 index 0000000000..2e7690cb0e --- /dev/null +++ b/frontend/src/shared/modules/filters/config/itemLabelRenderer/selectasync.label.renderer.ts @@ -0,0 +1,16 @@ +import { + SelectAsyncFilterOptions, + SelectAsyncFilterValue, +} from '@/shared/modules/filters/types/filterTypes/SelectAsyncFilterConfig'; + +export const selectAsyncItemLabelRenderer = ( + property: string, + { value, include }: SelectAsyncFilterValue, + options: SelectAsyncFilterOptions, + data: any, +): string => { + const excludeText = !include ? ' (exclude)' : ''; + + const valueText = data.selected.value === value ? data.selected.label : ''; + return `${property}${excludeText}:${valueText || '...'}`; +}; diff --git a/frontend/src/shared/modules/filters/config/itemLabelRendererByType.ts b/frontend/src/shared/modules/filters/config/itemLabelRendererByType.ts index 5cc49a3d19..73cc97787c 100644 --- a/frontend/src/shared/modules/filters/config/itemLabelRendererByType.ts +++ b/frontend/src/shared/modules/filters/config/itemLabelRendererByType.ts @@ -3,6 +3,9 @@ import { stringItemLabelRenderer } from '@/shared/modules/filters/config/itemLab import { multiSelectAsyncItemLabelRenderer, } from '@/shared/modules/filters/config/itemLabelRenderer/multiselectasync.label.renderer'; +import { + selectAsyncItemLabelRenderer, +} from '@/shared/modules/filters/config/itemLabelRenderer/selectasync.label.renderer'; import { booleanItemLabelRenderer } from './itemLabelRenderer/boolean.label.renderer'; import { numberItemLabelRenderer } from './itemLabelRenderer/number.label.renderer'; import { dateItemLabelRenderer } from './itemLabelRenderer/date.label.renderer'; @@ -22,6 +25,7 @@ export const itemLabelRendererByType: Record any> [FilterConfigType.NUMBER]: numberQueryUrlParser, [FilterConfigType.DATE]: dateQueryUrlParser, [FilterConfigType.SELECT]: selectQueryUrlParser, + [FilterConfigType.SELECT_ASYNC]: selectQueryUrlParser, [FilterConfigType.MULTISELECT]: multiSelectQueryUrlParser, [FilterConfigType.MULTISELECT_ASYNC]: multiSelectQueryUrlParser, [FilterConfigType.STRING]: stringQueryUrlParser, diff --git a/frontend/src/shared/modules/filters/services/custom-attributes.service.ts b/frontend/src/shared/modules/filters/services/custom-attributes.service.ts index 4cf753ba66..a5d8a35284 100644 --- a/frontend/src/shared/modules/filters/services/custom-attributes.service.ts +++ b/frontend/src/shared/modules/filters/services/custom-attributes.service.ts @@ -39,7 +39,7 @@ export const customAttributesService = () => { filters[attribute.name] = { id: attribute.name, label: attribute.label, - iconClass: 'ri-hashtag', + iconClass: 'fa-hashtag fa-light', type: FilterConfigType.NUMBER, options: { hideIncludeSwitch: true, @@ -58,7 +58,7 @@ export const customAttributesService = () => { filters[attribute.name] = { id: attribute.name, label: attribute.label, - iconClass: 'ri-toggle-line', + iconClass: 'fa-toggle-off fa-solid', type: FilterConfigType.BOOLEAN, options: { hideIncludeSwitch: true, @@ -77,7 +77,7 @@ export const customAttributesService = () => { filters[attribute.name] = { id: attribute.name, label: attribute.label, - iconClass: 'ri-menu-2-line', + iconClass: 'fa-bars fa-light', type: FilterConfigType.STRING, options: { hideIncludeSwitch: true, @@ -96,7 +96,7 @@ export const customAttributesService = () => { filters[attribute.name] = { id: attribute.name, label: attribute.label, - iconClass: 'ri-calendar-2-line', + iconClass: 'fa-calendar-range fa-light', type: FilterConfigType.DATE, options: { hideIncludeSwitch: true, @@ -112,20 +112,20 @@ export const customAttributesService = () => { } // Multiselect type if (attribute.type === FilterCustomAttributeType.MULTISELECT - || (attribute.type === FilterCustomAttributeType.SPECIAL && attribute.options.length > 0)) { + || (attribute.type === FilterCustomAttributeType.SPECIAL && attribute.options?.length > 0)) { filters[attribute.name] = { id: attribute.name, label: attribute.label, - iconClass: 'ri-list-unordered', + iconClass: 'fa-list-ul fa-light', type: FilterConfigType.MULTISELECT, options: { hideIncludeSwitch: true, options: [ { - options: attribute.options.map((option) => ({ + options: attribute.options?.map((option) => ({ value: option, label: option, - })), + })) || [], }, ], }, diff --git a/frontend/src/shared/modules/filters/services/filter-api.service.ts b/frontend/src/shared/modules/filters/services/filter-api.service.ts index 3919b6207b..d01181c612 100644 --- a/frontend/src/shared/modules/filters/services/filter-api.service.ts +++ b/frontend/src/shared/modules/filters/services/filter-api.service.ts @@ -7,7 +7,7 @@ export const filterApiService = () => { function buildApiFilter( values: Filter, configuration: Record, - searchConfig: SearchFilterConfig, + searchConfig?: SearchFilterConfig, savedViewsConfig?: SavedViewsConfig, ): FilterQuery { const { @@ -26,13 +26,19 @@ export const filterApiService = () => { if (search && search.length > 0) { baseFilters = [ ...baseFilters, - ...searchConfig.apiFilterRenderer(search), + ...(searchConfig?.apiFilterRenderer ? searchConfig.apiFilterRenderer(search) : []), ]; } // Settings if (savedViewsConfig && settings) { Object.entries(settings).forEach(([setting, value]) => { + const config: FilterConfig = configuration[setting]; + + if (config?.inBody) { + return; + } + const filter = savedViewsConfig.settings[setting]?.apiFilterRenderer(value); if (filter) { baseFilters = [ @@ -89,6 +95,7 @@ export const filterApiService = () => { const orderBy = `${order.prop}_${order.order === 'descending' ? 'DESC' : 'ASC'}`; return { + search, filter, orderBy, body, diff --git a/frontend/src/shared/modules/filters/services/filter-query.service.ts b/frontend/src/shared/modules/filters/services/filter-query.service.ts index d293437af9..49a56e9043 100644 --- a/frontend/src/shared/modules/filters/services/filter-query.service.ts +++ b/frontend/src/shared/modules/filters/services/filter-query.service.ts @@ -24,7 +24,7 @@ export const filterQueryService = () => { Object.keys(object).forEach((key) => { if (key === 'settings' && savedViewsConfig) { Object.keys(object[key]).forEach((setting) => { - object[key][setting] = savedViewsConfig.settings[setting].queryUrlParser(object[key][setting]); + object[key][setting] = savedViewsConfig.settings[setting]?.queryUrlParser(object[key][setting]); }); } else if (key in config) { const { type } = config[key]; diff --git a/frontend/src/shared/modules/filters/types/FilterConfig.ts b/frontend/src/shared/modules/filters/types/FilterConfig.ts index aab26460de..e8cb6602ef 100644 --- a/frontend/src/shared/modules/filters/types/FilterConfig.ts +++ b/frontend/src/shared/modules/filters/types/FilterConfig.ts @@ -6,10 +6,12 @@ import { DateFilterConfig } from '@/shared/modules/filters/types/filterTypes/Dat import { CustomFilterConfig } from '@/shared/modules/filters/types/filterTypes/CustomFilterConfig'; import { StringFilterConfig } from '@/shared/modules/filters/types/filterTypes/StringFilterConfig'; import { MultiSelectAsyncFilterConfig } from '@/shared/modules/filters/types/filterTypes/MultiSelectAsyncFilterConfig'; +import { SelectAsyncFilterConfig } from '@/shared/modules/filters/types/filterTypes/SelectAsyncFilterConfig'; export enum FilterConfigType { NUMBER = 'number', SELECT = 'select', + SELECT_ASYNC = 'select-async', MULTISELECT = 'multiselect', MULTISELECT_ASYNC = 'multiselect-async', BOOLEAN = 'boolean', @@ -23,13 +25,13 @@ export interface BaseFilterConfig { label: string; iconClass: string; inBody?: boolean; - featureFlag?: string; } export type FilterConfig = NumberFilterConfig | MultiSelectFilterConfig | MultiSelectAsyncFilterConfig | SelectFilterConfig + | SelectAsyncFilterConfig | BooleanFilterConfig | DateFilterConfig | StringFilterConfig diff --git a/frontend/src/shared/modules/filters/types/FilterQuery.ts b/frontend/src/shared/modules/filters/types/FilterQuery.ts index fed80c01e0..6fd7abb645 100644 --- a/frontend/src/shared/modules/filters/types/FilterQuery.ts +++ b/frontend/src/shared/modules/filters/types/FilterQuery.ts @@ -1,4 +1,5 @@ export interface FilterQuery { + search: string, filter: any, body: any, orderBy: string, diff --git a/frontend/src/shared/modules/filters/types/FilterTimeOptions.ts b/frontend/src/shared/modules/filters/types/FilterTimeOptions.ts index 843dde30b2..39abf44ecf 100644 --- a/frontend/src/shared/modules/filters/types/FilterTimeOptions.ts +++ b/frontend/src/shared/modules/filters/types/FilterTimeOptions.ts @@ -1,5 +1,11 @@ +import { + FilterDateOperator, +} from '@/shared/modules/filters/config/constants/date.constants'; + export interface FilterTimeOptions { + id: string; value: string; label: string; + operator: FilterDateOperator; getDate: () => string; } diff --git a/frontend/src/shared/modules/filters/types/filterTypes/MultiSelectAsyncFilterConfig.ts b/frontend/src/shared/modules/filters/types/filterTypes/MultiSelectAsyncFilterConfig.ts index f1dc47919d..b0721519a6 100644 --- a/frontend/src/shared/modules/filters/types/filterTypes/MultiSelectAsyncFilterConfig.ts +++ b/frontend/src/shared/modules/filters/types/filterTypes/MultiSelectAsyncFilterConfig.ts @@ -4,7 +4,8 @@ import { BaseFilterConfig, FilterConfigType } from '@/shared/modules/filters/typ export interface MultiSelectAsyncFilterOption { label: string; value: string; - logo?: string; + prefix?: string; + description?: string; } export interface MultiSelectAsyncFilterOptions { hideIncludeSwitch?: boolean; diff --git a/frontend/src/shared/modules/filters/types/filterTypes/SelectAsyncFilterConfig.ts b/frontend/src/shared/modules/filters/types/filterTypes/SelectAsyncFilterConfig.ts new file mode 100644 index 0000000000..6409c07b6f --- /dev/null +++ b/frontend/src/shared/modules/filters/types/filterTypes/SelectAsyncFilterConfig.ts @@ -0,0 +1,26 @@ +/* eslint-disable no-unused-vars */ +import { BaseFilterConfig, FilterConfigType } from '@/shared/modules/filters/types/FilterConfig'; + +export interface SelectAsyncFilterOption { + label: string; + value: string; + prefix?: string; + description?: string; +} +export interface SelectAsyncFilterOptions { + hideIncludeSwitch?: boolean; + remoteMethod: (query: string) => Promise + remotePopulateItems: (values: string) => Promise +} + +export interface SelectAsyncFilterValue { + value: string, + include: boolean, +} + +export interface SelectAsyncFilterConfig extends BaseFilterConfig { + type: FilterConfigType.SELECT_ASYNC; + options: SelectAsyncFilterOptions; + itemLabelRenderer: (value: SelectAsyncFilterValue, options: SelectAsyncFilterOptions, data: any) => string; + apiFilterRenderer: (value: SelectAsyncFilterValue) => any[]; +} diff --git a/frontend/src/shared/modules/filters/types/filterTypes/StringFilterConfig.ts b/frontend/src/shared/modules/filters/types/filterTypes/StringFilterConfig.ts index 41310900cd..e0f905eceb 100644 --- a/frontend/src/shared/modules/filters/types/filterTypes/StringFilterConfig.ts +++ b/frontend/src/shared/modules/filters/types/filterTypes/StringFilterConfig.ts @@ -2,7 +2,9 @@ import { BaseFilterConfig, FilterConfigType } from '@/shared/modules/filters/types/FilterConfig'; import { FilterStringOperator } from '@/shared/modules/filters/config/constants/string.constants'; -export interface StringFilterOptions {} +export interface StringFilterOptions { + fixedOperator?: FilterStringOperator; +} export interface StringFilterValue { operator: FilterStringOperator, diff --git a/frontend/src/shared/modules/identities/components/emails-vertical-list.vue b/frontend/src/shared/modules/identities/components/emails-vertical-list.vue new file mode 100644 index 0000000000..a6c9c25bf9 --- /dev/null +++ b/frontend/src/shared/modules/identities/components/emails-vertical-list.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/frontend/src/shared/modules/identities/components/identities-horizontal-list-members.vue b/frontend/src/shared/modules/identities/components/identities-horizontal-list-members.vue new file mode 100644 index 0000000000..53a4390233 --- /dev/null +++ b/frontend/src/shared/modules/identities/components/identities-horizontal-list-members.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/frontend/src/shared/modules/identities/components/identities-horizontal-list-organizations.vue b/frontend/src/shared/modules/identities/components/identities-horizontal-list-organizations.vue new file mode 100644 index 0000000000..61684cd604 --- /dev/null +++ b/frontend/src/shared/modules/identities/components/identities-horizontal-list-organizations.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/frontend/src/shared/modules/identities/components/identities-horizontal-list.vue b/frontend/src/shared/modules/identities/components/identities-horizontal-list.vue new file mode 100644 index 0000000000..a46b938dde --- /dev/null +++ b/frontend/src/shared/modules/identities/components/identities-horizontal-list.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/frontend/src/shared/modules/identities/components/identities-vertical-list-members.vue b/frontend/src/shared/modules/identities/components/identities-vertical-list-members.vue new file mode 100644 index 0000000000..54cebd2efe --- /dev/null +++ b/frontend/src/shared/modules/identities/components/identities-vertical-list-members.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/frontend/src/shared/modules/identities/components/identities-vertical-list-organizations.vue b/frontend/src/shared/modules/identities/components/identities-vertical-list-organizations.vue new file mode 100644 index 0000000000..ccb765d64b --- /dev/null +++ b/frontend/src/shared/modules/identities/components/identities-vertical-list-organizations.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/frontend/src/shared/modules/identities/components/identities-vertical-list.vue b/frontend/src/shared/modules/identities/components/identities-vertical-list.vue new file mode 100644 index 0000000000..5ca8d649df --- /dev/null +++ b/frontend/src/shared/modules/identities/components/identities-vertical-list.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/frontend/src/shared/modules/identities/components/verified-identity-badge.vue b/frontend/src/shared/modules/identities/components/verified-identity-badge.vue new file mode 100644 index 0000000000..e4fd026e5f --- /dev/null +++ b/frontend/src/shared/modules/identities/components/verified-identity-badge.vue @@ -0,0 +1,27 @@ + + + diff --git a/frontend/src/shared/modules/identities/config/identitiesOrder/member/index.ts b/frontend/src/shared/modules/identities/config/identitiesOrder/member/index.ts new file mode 100644 index 0000000000..f02d97fd77 --- /dev/null +++ b/frontend/src/shared/modules/identities/config/identitiesOrder/member/index.ts @@ -0,0 +1,9 @@ +import list from './list'; +import profile from './profile'; +import suggestions from './suggestions'; + +export default { + list, + profile, + suggestions, +}; diff --git a/frontend/src/shared/modules/identities/config/identitiesOrder/member/list.ts b/frontend/src/shared/modules/identities/config/identitiesOrder/member/list.ts new file mode 100644 index 0000000000..83795b6d91 --- /dev/null +++ b/frontend/src/shared/modules/identities/config/identitiesOrder/member/list.ts @@ -0,0 +1,18 @@ +import { Platform } from '@/shared/modules/platform/types/Platform'; + +export default [ + Platform.GITHUB, + Platform.DISCORD, + Platform.HACKER_NEWS, + Platform.LINKEDIN, + Platform.TWITTER, + Platform.SLACK, + Platform.DEVTO, + Platform.REDDIT, + Platform.STACK_OVERFLOW, + Platform.DISCOURSE, + Platform.HUBSPOT, + Platform.GIT, + Platform.GROUPS_IO, + Platform.CUSTOM, +]; diff --git a/frontend/src/shared/modules/identities/config/identitiesOrder/member/profile.ts b/frontend/src/shared/modules/identities/config/identitiesOrder/member/profile.ts new file mode 100644 index 0000000000..83795b6d91 --- /dev/null +++ b/frontend/src/shared/modules/identities/config/identitiesOrder/member/profile.ts @@ -0,0 +1,18 @@ +import { Platform } from '@/shared/modules/platform/types/Platform'; + +export default [ + Platform.GITHUB, + Platform.DISCORD, + Platform.HACKER_NEWS, + Platform.LINKEDIN, + Platform.TWITTER, + Platform.SLACK, + Platform.DEVTO, + Platform.REDDIT, + Platform.STACK_OVERFLOW, + Platform.DISCOURSE, + Platform.HUBSPOT, + Platform.GIT, + Platform.GROUPS_IO, + Platform.CUSTOM, +]; diff --git a/frontend/src/shared/modules/identities/config/identitiesOrder/member/suggestions.ts b/frontend/src/shared/modules/identities/config/identitiesOrder/member/suggestions.ts new file mode 100644 index 0000000000..83795b6d91 --- /dev/null +++ b/frontend/src/shared/modules/identities/config/identitiesOrder/member/suggestions.ts @@ -0,0 +1,18 @@ +import { Platform } from '@/shared/modules/platform/types/Platform'; + +export default [ + Platform.GITHUB, + Platform.DISCORD, + Platform.HACKER_NEWS, + Platform.LINKEDIN, + Platform.TWITTER, + Platform.SLACK, + Platform.DEVTO, + Platform.REDDIT, + Platform.STACK_OVERFLOW, + Platform.DISCOURSE, + Platform.HUBSPOT, + Platform.GIT, + Platform.GROUPS_IO, + Platform.CUSTOM, +]; diff --git a/frontend/src/shared/modules/identities/config/identitiesOrder/organization/index.ts b/frontend/src/shared/modules/identities/config/identitiesOrder/organization/index.ts new file mode 100644 index 0000000000..f02d97fd77 --- /dev/null +++ b/frontend/src/shared/modules/identities/config/identitiesOrder/organization/index.ts @@ -0,0 +1,9 @@ +import list from './list'; +import profile from './profile'; +import suggestions from './suggestions'; + +export default { + list, + profile, + suggestions, +}; diff --git a/frontend/src/shared/modules/identities/config/identitiesOrder/organization/list.ts b/frontend/src/shared/modules/identities/config/identitiesOrder/organization/list.ts new file mode 100644 index 0000000000..1aceb5547e --- /dev/null +++ b/frontend/src/shared/modules/identities/config/identitiesOrder/organization/list.ts @@ -0,0 +1,10 @@ +import { Platform } from '@/shared/modules/platform/types/Platform'; + +export default [ + Platform.GITHUB, + Platform.LINKEDIN, + Platform.TWITTER, + Platform.CRUNCHBASE, + Platform.HUBSPOT, + Platform.CUSTOM, +]; diff --git a/frontend/src/shared/modules/identities/config/identitiesOrder/organization/profile.ts b/frontend/src/shared/modules/identities/config/identitiesOrder/organization/profile.ts new file mode 100644 index 0000000000..1aceb5547e --- /dev/null +++ b/frontend/src/shared/modules/identities/config/identitiesOrder/organization/profile.ts @@ -0,0 +1,10 @@ +import { Platform } from '@/shared/modules/platform/types/Platform'; + +export default [ + Platform.GITHUB, + Platform.LINKEDIN, + Platform.TWITTER, + Platform.CRUNCHBASE, + Platform.HUBSPOT, + Platform.CUSTOM, +]; diff --git a/frontend/src/shared/modules/identities/config/identitiesOrder/organization/suggestions.ts b/frontend/src/shared/modules/identities/config/identitiesOrder/organization/suggestions.ts new file mode 100644 index 0000000000..1aceb5547e --- /dev/null +++ b/frontend/src/shared/modules/identities/config/identitiesOrder/organization/suggestions.ts @@ -0,0 +1,10 @@ +import { Platform } from '@/shared/modules/platform/types/Platform'; + +export default [ + Platform.GITHUB, + Platform.LINKEDIN, + Platform.TWITTER, + Platform.CRUNCHBASE, + Platform.HUBSPOT, + Platform.CUSTOM, +]; diff --git a/frontend/src/shared/modules/identities/config/useMemberIdentities.ts b/frontend/src/shared/modules/identities/config/useMemberIdentities.ts new file mode 100644 index 0000000000..a1706ed9ca --- /dev/null +++ b/frontend/src/shared/modules/identities/config/useMemberIdentities.ts @@ -0,0 +1,142 @@ +import { Member } from '@/modules/member/types/Member'; +import { Platform } from '@/shared/modules/platform/types/Platform'; +import { lfIdentities } from '@/config/identities'; + +export default ({ + member, + order, +}: { + member: Partial; + order: Platform[]; +}) => { + const { + attributes = {}, identities, + } = member || {}; + + const getIdentityHandles = (platform: string) => { + if (platform === Platform.CUSTOM) { + const mainPlatforms = (Object.values(Platform) as string[]).filter((p) => p !== 'custom'); + return (identities || []) + .filter((i) => !mainPlatforms.includes(i.platform) && i.type !== 'email') + .map((i) => ({ + platform: i.platform, + url: null, + name: i.value, + verified: i.verified, + })); + } + return (identities || []) + .filter((i) => i.platform === platform && i.type !== 'email') + .map((i) => ({ + platform: i.platform, + url: null, + name: i.value, + verified: i.verified, + })); + }; + + const getIdentityLink = (identity: { + platform: Platform; + url: string | null; + name: string; + verified: boolean; + }, platform: string) => { + if (!lfIdentities[platform]?.member?.url) { + return null; + } + + return ( + identity.url + ?? lfIdentities[platform]?.member?.url?.({ + identity: { + platform: identity.platform, + value: identity.name, + verified: identity.verified, + } as any, + attributes, + }) + ?? attributes?.url?.[platform as keyof typeof attributes.url] + ?? null + ); + }; + + const getIdentities = (): { + [key: string]: { + handle: string; + link: string | null; + verified: boolean; + }[]; + } => order.reduce((acc, platform) => { + const handles = getIdentityHandles(platform); + + if (platform === Platform.CUSTOM && handles.length) { + const sortedCustomIdentities = handles.sort((a, b) => { + const platformComparison = a.platform.localeCompare(b.platform); + + if (platformComparison === 0) { + // If platforms are equal, sort by name + return a.name.localeCompare(b.name); + } + + return platformComparison; // Otherwise, sort by platform + }); + + sortedCustomIdentities.forEach((identity) => { + if (acc[identity.platform]?.length) { + acc[identity.platform].push({ + handle: identity.name, + link: getIdentityLink(identity, platform), + verified: identity.verified, + }); + } else { + acc[identity.platform] = [ + { + handle: identity.name, + link: getIdentityLink(identity, platform), + verified: identity.verified, + }, + ]; + } + }); + } else { + const platformHandlesValues = handles.map((identity) => ({ + handle: identity.name, + link: getIdentityLink(identity, platform), + verified: identity.verified, + })); + + if (platformHandlesValues.length) { + acc[platform] = platformHandlesValues; + } + } + + return acc; + }, {} as Record); + + const getEmails = (): { + handle: string; + link: string | null; + verified: boolean; + }[] => (identities || []) + .filter((i) => i.type === 'email') + .map((i) => ({ + link: `mailto:${i.value}`, + handle: i.value, + verified: i.verified, + platform: i.platform, + })) + .sort((a, b) => { + const indexA = order.findIndex((p) => p === a.platform); + const indexB = order.findIndex((p) => p === b.platform); + + const orderA = indexA === -1 ? order.length : indexA; + const orderB = indexB === -1 ? order.length : indexB; + + return orderA - orderB; + }); + + return { + getIdentities, + getEmails, + }; +}; diff --git a/frontend/src/shared/modules/identities/config/useOrganizationIdentities.ts b/frontend/src/shared/modules/identities/config/useOrganizationIdentities.ts new file mode 100644 index 0000000000..c3c5c3a4bc --- /dev/null +++ b/frontend/src/shared/modules/identities/config/useOrganizationIdentities.ts @@ -0,0 +1,193 @@ +import { + Organization, + OrganizationIdentity, + OrganizationIdentityType, +} from '@/modules/organization/types/Organization'; +import { Platform } from '@/shared/modules/platform/types/Platform'; +import { withHttp } from '@/utils/string'; +import { lfIdentities } from '@/config/identities'; + +export default ({ + organization, + order, +}: { + organization: Partial; + order: Platform[]; +}) => { + const { identities = [], phoneNumbers = [] } = organization || {}; + + const getIdentityHandles = (platform: Platform) => { + if ([Platform.CUSTOM, Platform.ENRICHMENT].includes(platform)) { + const mainPlatforms = (Object.values(Platform) as string[]).filter( + (p) => p !== Platform.CUSTOM && p !== Platform.ENRICHMENT, + ); + + return (identities || []) + .filter( + (i) => !mainPlatforms.includes(i.platform) + && [OrganizationIdentityType.USERNAME].includes(i.type), + ) + .map((i) => ({ + ...i, + value: lfIdentities[platform]?.organization?.handle + ? lfIdentities[platform]?.organization?.handle?.(i) + : i.value, + })); + } + + return (identities || []) + .filter( + (i) => i.platform === platform + && [OrganizationIdentityType.USERNAME].includes(i.type), + ) + .map((i) => ({ + ...i, + value: lfIdentities[platform]?.organization?.handle + ? lfIdentities[platform]?.organization?.handle?.(i) + : i.value, + })); + }; + + const getIdentityLink = ( + identity: OrganizationIdentity, + platform: string, + ) => { + if (!lfIdentities[platform]?.organization?.url) { + return null; + } + + return lfIdentities[platform]?.organization?.url?.(identity); + }; + + const getIdentities = (): { + [key: string]: { + handle: string; + link: string | null; + verified: boolean; + }[]; + } => order.reduce((acc, platform) => { + const handles = getIdentityHandles(platform); + + if ([Platform.CUSTOM, Platform.ENRICHMENT].includes(platform) && handles.length) { + const sortedCustomIdentities = handles.sort((a, b) => { + const platformComparison = a.platform.localeCompare(b.platform); + + if (platformComparison === 0) { + // If platforms are equal, sort by name + return a.value.localeCompare(b.value); + } + + return platformComparison; // Otherwise, sort by platform + }); + + sortedCustomIdentities.forEach((identity) => { + if (acc[identity.platform]?.length) { + acc[identity.platform].push({ + handle: identity.value, + link: getIdentityLink(identity, platform), + verified: identity.verified, + }); + } else { + acc[identity.platform] = [ + { + handle: identity.value, + link: getIdentityLink(identity, platform), + verified: identity.verified, + }, + ]; + } + }); + } else { + const platformHandlesValues = handles.map((identity) => ({ + handle: identity.value, + link: getIdentityLink(identity, platform), + verified: identity.verified, + })); + + if (platformHandlesValues.length) { + acc[platform] = platformHandlesValues; + } + } + + return acc; + }, {} as Record); + + const getAffiliatedProfiles = (): { + handle: string; + link: string | null; + verified: boolean; + }[] => { + const parsedIdentities = identities?.length ? identities : []; + + const identitiesDomains = parsedIdentities + .filter((identity) => [ + OrganizationIdentityType.AFFILIATED_PROFILE, + ].includes(identity.type)) + .map((identity) => ({ + link: null, + handle: identity.value, + verified: identity.verified, + })); + + return identitiesDomains; + }; + + const getDomains = (): { + handle: string; + link: string | null; + verified: boolean; + }[] => { + const parsedIdentities = identities?.length ? identities : []; + + const identitiesDomains = parsedIdentities + .filter((identity) => [ + OrganizationIdentityType.PRIMARY_DOMAIN, + OrganizationIdentityType.ALTERNATIVE_DOMAIN, + ].includes(identity.type)) + .map((identity) => ({ + link: withHttp(identity.value) as string | null, + handle: identity.value, + verified: identity.verified, + })); + + return identitiesDomains; + }; + + const getEmails = (): { + handle: string; + link: string | null; + verified: boolean; + }[] => { + const parsedIdentities = identities?.length ? identities : []; + + const identitiesDomains = parsedIdentities + .filter((identity) => [ + OrganizationIdentityType.EMAIL, + ].includes(identity.type)) + .map((identity) => ({ + link: `mailto:${identity.value}`, + handle: identity.value, + verified: identity.verified, + })); + + return identitiesDomains; + }; + + const getPhoneNumbers = (): { + handle: string; + link: string | null; + verified: boolean; + }[] => (phoneNumbers || []).map((p) => ({ + link: `tel:${p}`, + handle: p, + verified: false, + })); + + return { + getIdentities, + getAffiliatedProfiles, + getEmails, + getDomains, + getPhoneNumbers, + }; +}; diff --git a/frontend/src/shared/modules/merge/config/member/apiErrorMessage.ts b/frontend/src/shared/modules/merge/config/member/apiErrorMessage.ts new file mode 100644 index 0000000000..bb8a2d03a0 --- /dev/null +++ b/frontend/src/shared/modules/merge/config/member/apiErrorMessage.ts @@ -0,0 +1,16 @@ +import { ToastStore } from '@/shared/message/notification'; +import { ErrorMessage } from '../../types/MemberMessage'; + +export default ({ error }: ErrorMessage) => { + ToastStore.closeAll(); + if (error.response.status === 404) { + ToastStore.success('Profiles already merged or deleted', { + message: `Sorry, the profiles you are trying to merge might have already been merged or deleted. + Please refresh to see the updated information.`, + }); + return true; + } + + ToastStore.error('There was an error merging profiles'); + return false; +}; diff --git a/frontend/src/shared/modules/merge/config/member/loadingMessage.ts b/frontend/src/shared/modules/merge/config/member/loadingMessage.ts new file mode 100644 index 0000000000..7cdcdec29f --- /dev/null +++ b/frontend/src/shared/modules/merge/config/member/loadingMessage.ts @@ -0,0 +1,5 @@ +import { ToastStore } from '@/shared/message/notification'; + +export default () => { + ToastStore.info('Profiles are being merged'); +}; diff --git a/frontend/src/shared/modules/merge/config/member/successMessage.ts b/frontend/src/shared/modules/merge/config/member/successMessage.ts new file mode 100644 index 0000000000..c94c72fa8c --- /dev/null +++ b/frontend/src/shared/modules/merge/config/member/successMessage.ts @@ -0,0 +1,46 @@ +import { h } from 'vue'; +import { ToastStore } from '@/shared/message/notification'; +import { router } from '@/router'; +import { SuccessMessage } from '../../types/MemberMessage'; + +export default ({ primaryMember, secondaryMember, selectedProjectGroupId }: SuccessMessage) => { + const { id, displayName: primaryDisplayName } = primaryMember; + const { displayName: secondaryDisplayName } = secondaryMember; + + ToastStore.closeAll(); + ToastStore.success( + h( + 'div', + { + class: 'flex flex-col gap-2', + }, + [ + h( + 'span', + { + innerHTML: `${secondaryDisplayName} merged with ${primaryDisplayName}.`, + }, + ), + h( + 'button', + { + class: 'c-btn c-btn--tiny c-btn--secondary-gray !h-6 !w-fit', + onClick: () => { + router.push({ + name: 'memberView', + params: { id }, + query: { projectGroup: selectedProjectGroupId }, + }); + ToastStore.closeAll(); + }, + }, + 'View profile', + ), + ], + ), + { + title: + 'Profiles merged successfully', + }, + ); +}; diff --git a/frontend/src/shared/modules/merge/config/organization/apiErrorMessage.ts b/frontend/src/shared/modules/merge/config/organization/apiErrorMessage.ts new file mode 100644 index 0000000000..d16056acf3 --- /dev/null +++ b/frontend/src/shared/modules/merge/config/organization/apiErrorMessage.ts @@ -0,0 +1,18 @@ +import { ToastStore } from '@/shared/message/notification'; +import { ApiErrorMessage } from '../../types/OrganizationMessage'; + +export default ({ error }: ApiErrorMessage) => { + ToastStore.closeAll(); + + if (error.response.status === 404) { + ToastStore.success('Organizations already merged or deleted', { + message: `Sorry, the organizations you are trying to merge might have already been merged or deleted. + Please refresh to see the updated information.`, + }); + + return true; + } + + ToastStore.error('There was an error merging organizations'); + return false; +}; diff --git a/frontend/src/shared/modules/merge/config/organization/loadingMessage.ts b/frontend/src/shared/modules/merge/config/organization/loadingMessage.ts new file mode 100644 index 0000000000..f774cd260d --- /dev/null +++ b/frontend/src/shared/modules/merge/config/organization/loadingMessage.ts @@ -0,0 +1,18 @@ +import { ToastStore } from '@/shared/message/notification'; +import { useOrganizationStore } from '@/modules/organization/store/pinia'; +import { storeToRefs } from 'pinia'; + +export default () => { + const organizationStore = useOrganizationStore(); + const { + mergedOrganizations, + } = storeToRefs(organizationStore); + + const processesRunning = Object.keys(mergedOrganizations.value).length; + + ToastStore.closeAll(); + ToastStore.info('', { + title: 'Organizations merging in progress', + message: processesRunning > 1 ? `${processesRunning} processes running` : undefined, + }); +}; diff --git a/frontend/src/shared/modules/merge/config/organization/socketErrorMessage.ts b/frontend/src/shared/modules/merge/config/organization/socketErrorMessage.ts new file mode 100644 index 0000000000..7b7f79e98b --- /dev/null +++ b/frontend/src/shared/modules/merge/config/organization/socketErrorMessage.ts @@ -0,0 +1,10 @@ +import { ToastStore } from '@/shared/message/notification'; +import { SocketErrorMessage } from '../../types/OrganizationMessage'; + +export default ({ primaryOrganization, secondaryOrganization }: SocketErrorMessage) => { + const { displayName: primaryDisplayName } = primaryOrganization; + const { displayName: secondaryDisplayName } = secondaryOrganization; + + ToastStore.closeAll(); + ToastStore.error(`There was an error merging ${secondaryDisplayName} with ${primaryDisplayName}`); +}; diff --git a/frontend/src/shared/modules/merge/config/organization/successMessage.ts b/frontend/src/shared/modules/merge/config/organization/successMessage.ts new file mode 100644 index 0000000000..d2302bcbd1 --- /dev/null +++ b/frontend/src/shared/modules/merge/config/organization/successMessage.ts @@ -0,0 +1,51 @@ +import { h } from 'vue'; +import { ToastStore } from '@/shared/message/notification'; +import { router } from '@/router'; +import { SuccessMessage } from '../../types/OrganizationMessage'; + +export default ({ primaryOrganization, secondaryOrganization }: SuccessMessage) => { + const { id, displayName: primaryDisplayName } = primaryOrganization; + const { displayName: secondaryDisplayName } = secondaryOrganization; + + const buttonElement = h( + 'button', + { + class: 'c-btn c-btn--tiny c-btn--secondary-gray !h-6 !w-fit', + onClick: () => { + router.push({ + name: 'organizationView', + params: { id }, + }); + ToastStore.closeAll(); + }, + }, + 'View organization', + ); + + const messageElements = [buttonElement]; + + if (primaryDisplayName && secondaryDisplayName) { + const descriptionElement = h( + 'span', + { + innerHTML: `${secondaryDisplayName} merged with ${primaryDisplayName}.`, + }, + ); + + messageElements.unshift(descriptionElement); + } + + ToastStore.closeAll(); + ToastStore.success( + h( + 'div', + { + class: 'flex flex-col gap-2', + }, + messageElements, + ), + { + title: 'Organizations merged successfully', + }, + ); +}; diff --git a/frontend/src/shared/modules/merge/config/useMemberMergeMessage.ts b/frontend/src/shared/modules/merge/config/useMemberMergeMessage.ts new file mode 100644 index 0000000000..5d86611852 --- /dev/null +++ b/frontend/src/shared/modules/merge/config/useMemberMergeMessage.ts @@ -0,0 +1,10 @@ +import apiErrorMessage from './member/apiErrorMessage'; +import loadingMessage from './member/loadingMessage'; +import successMessage from './member/successMessage'; +import { MemberMessage } from '../types/MemberMessage'; + +export default { + loadingMessage, + successMessage, + apiErrorMessage, +}; diff --git a/frontend/src/shared/modules/merge/config/useOrganizationMergeMessage.ts b/frontend/src/shared/modules/merge/config/useOrganizationMergeMessage.ts new file mode 100644 index 0000000000..fadc589dc9 --- /dev/null +++ b/frontend/src/shared/modules/merge/config/useOrganizationMergeMessage.ts @@ -0,0 +1,12 @@ +import { OrganizationMessage } from '../types/OrganizationMessage'; +import apiErrorMessage from './organization/apiErrorMessage'; +import loadingMessage from './organization/loadingMessage'; +import socketErrorMessage from './organization/socketErrorMessage'; +import successMessage from './organization/successMessage'; + +export default { + loadingMessage, + successMessage, + apiErrorMessage, + socketErrorMessage, +}; diff --git a/frontend/src/shared/modules/merge/services/merge-actions.service.ts b/frontend/src/shared/modules/merge/services/merge-actions.service.ts new file mode 100644 index 0000000000..7cfcfc9581 --- /dev/null +++ b/frontend/src/shared/modules/merge/services/merge-actions.service.ts @@ -0,0 +1,29 @@ +import { useLfSegmentsStore } from '@/modules/lf/segments/store'; +import authAxios from '@/shared/axios/auth-axios'; +import { MergeAction } from '@/shared/modules/merge/types/MemberActions'; +import { storeToRefs } from 'pinia'; + +const getSelectedProjectGroup = () => { + const lsSegmentsStore = useLfSegmentsStore(); + const { selectedProjectGroup } = storeToRefs(lsSegmentsStore); + + return selectedProjectGroup.value; +}; + +export class MergeActionsService { + static async list(entityId: string, type: string = 'member', limit = 1): Promise { + const response = await authAxios.get( + '/mergeActions', + { + params: { + entityId, + type, + limit, + segments: getSelectedProjectGroup()?.id ? [getSelectedProjectGroup()?.id] : [], + }, + }, + ); + + return response.data; + } +} diff --git a/frontend/src/shared/modules/merge/types/MemberActions.ts b/frontend/src/shared/modules/merge/types/MemberActions.ts new file mode 100644 index 0000000000..a1ee6802b9 --- /dev/null +++ b/frontend/src/shared/modules/merge/types/MemberActions.ts @@ -0,0 +1,12 @@ +export enum MergeActionState { + IN_PROGRESS = 'in-progress', + DONE = 'done', + ERROR = 'error', +} + +export interface MergeAction { + operationType: string; + primaryId: string; + secondaryId: string; + state: MergeActionState; +} diff --git a/frontend/src/shared/modules/merge/types/MemberMessage.ts b/frontend/src/shared/modules/merge/types/MemberMessage.ts new file mode 100644 index 0000000000..493dc0b2f6 --- /dev/null +++ b/frontend/src/shared/modules/merge/types/MemberMessage.ts @@ -0,0 +1,18 @@ +import { Member } from '@/modules/member/types/Member'; +import { AxiosError } from 'axios'; + +export interface SuccessMessage { + primaryMember: Member; + secondaryMember: Member; + selectedProjectGroupId: number; +} + +export interface ErrorMessage { + error: AxiosError +} + +export interface MemberMessage { + loadingMessage: () => void; + successMessage: ({ primaryMember, secondaryMember, selectedProjectGroupId }: SuccessMessage) => void; + apiErrorMessage: ({ error }: ErrorMessage) => void; + } diff --git a/frontend/src/shared/modules/merge/types/OrganizationMessage.ts b/frontend/src/shared/modules/merge/types/OrganizationMessage.ts new file mode 100644 index 0000000000..4c7490bcbb --- /dev/null +++ b/frontend/src/shared/modules/merge/types/OrganizationMessage.ts @@ -0,0 +1,38 @@ +import { AxiosError } from 'axios'; + +export interface SuccessMessage { + primaryOrganization: { + id: string; + displayName: string; + }; + secondaryOrganization: { + displayName: string; + }; +} + +export interface ApiErrorMessage { + error: AxiosError; +} + +export interface SocketErrorMessage { + primaryOrganization: { + id: string; + displayName: string; + }; + secondaryOrganization: { + displayName: string; + } +} + +export interface OrganizationMessage { + loadingMessage: () => void; + successMessage: ({ + primaryOrganization, + secondaryOrganization, + }: SuccessMessage) => void; + apiErrorMessage: ({ error }: ApiErrorMessage) => void; + socketErrorMessage: ({ + primaryOrganization, + secondaryOrganization, + }: SocketErrorMessage) => void; +} diff --git a/frontend/src/shared/monitoring/identify.js b/frontend/src/shared/modules/monitoring/identify.js similarity index 100% rename from frontend/src/shared/monitoring/identify.js rename to frontend/src/shared/modules/monitoring/identify.js diff --git a/frontend/src/shared/modules/monitoring/tracking-service.ts b/frontend/src/shared/modules/monitoring/tracking-service.ts new file mode 100644 index 0000000000..eb1f3533fb --- /dev/null +++ b/frontend/src/shared/modules/monitoring/tracking-service.ts @@ -0,0 +1,39 @@ +import authAxios from '@/shared/axios/auth-axios'; +import { Session } from './types/session'; +import { Event } from './types/event'; + +export const createSession: (session: Session) => Promise = async (session: Session) => { + const response = await authAxios.post( + '/product/session', + { + ...session, + excludeSegments: true, + }, + ); + + return response.data; +}; + +export const updateSession = async (id: string, endTime: string) => { + const response = await authAxios.put( + `/product/session/${id}`, + { + endTime, + excludeSegments: true, + }, + ); + + return response.data; +}; + +export const createEvent = async (event: Event) => { + const response = await authAxios.post( + '/product/event', + { + ...event, + excludeSegments: true, + }, + ); + + return response.data; +}; diff --git a/frontend/src/shared/modules/monitoring/types/event.ts b/frontend/src/shared/modules/monitoring/types/event.ts new file mode 100644 index 0000000000..1f8e330d55 --- /dev/null +++ b/frontend/src/shared/modules/monitoring/types/event.ts @@ -0,0 +1,119 @@ +export interface Event { + userId: string + userEmail: string + key: string + type: string + sessionId: string + properties?: Record +} + +export enum EventType { + PAGE = 'Page', + FEATURE = 'Feature', +} + +export enum PageEventKey { + OVERVIEW = 'Overview', + PROJECT_GROUPS = 'Project groups', + MEMBERS = 'Contributors', + ORGANIZATIONS = 'Organizations', + ACTIVITIES = 'Activities', + ADMIN_PANEL = 'Admin panel', + ADMIN_PANEL_PROJECT_GROUPS = 'Admin panel project groups', + ADMIN_PANEL_API_KEYS = 'Admin panel api keys', + ADMIN_PANEL_AUDIT_LOGS = 'Admin panel audit logs', + NEW_MEMBER = 'New contributor', + EDIT_MEMBER = 'Edit contributor', + MEMBER_PROFILE = 'Contributor profile', + NEW_ORGANIZATION = 'New organization', + EDIT_ORGANIZATION = 'Edit organization', + ORGANIZATION_PROFILE = 'Organization profile', + ORGANIZATIONS_MERGE_SUGGESTIONS = 'Organizations merge suggestions', + MEMBERS_MERGE_SUGGESTIONS = 'Contributors merge suggestions', + MANAGE_PROJECTS = 'Manage projects', + INTEGRATIONS = 'Integrations', + DATA_QUALITY_ASSISTANT = 'Data Quality Copilot', +} + +export enum FeatureEventKey { + ADD_MEMBER = 'Add contributor', + EDIT_MEMBER = 'Edit contributor', + FIND_IDENTITY = 'Find identity', + MERGE_MEMBER = 'Merge contributor', + MARK_AS_TEAM_MEMBER = 'Mark as team contributor', + MARK_AS_BOT = 'Mark as bot', + DELETE_MEMBER = 'Delete contributor', + EXPORT_MEMBERS = 'Export contributors', + EDIT_MEMBER_TAGS = 'Edit contributor tags', + EDIT_MEMBER_ATTRIBUTES = 'Edit contributor attributes', + FILTER_MEMBERS = 'Filter contributors', + ADD_CUSTOM_VIEW = 'Add custom view', + DELETE_CUSTOM_VIEW = 'Delete custom view', + DUPLICATE_CUSTOM_VIEW = 'Duplicate custom view', + EDIT_CUSTOM_VIEW = 'Edit custom view', + REORDER_CUSTOM_VIEW = 'Reorder custom view', + SEARCH_MEMBERS = 'Search contributors', + SORT_MEMBERS = 'Sort contributors', + ADD_ORGANIZATION = 'Add organization', + EDIT_ORGANIZATION = 'Edit organization', + MERGE_ORGANIZATION = 'Merge organization', + MARK_AS_TEAM_ORGANIZATION = 'Mark as team organization', + TOGGLE_ORGANIZATION_AFFILIATIONS = 'Toggle organization affiliations', + DELETE_ORGANIZATION = 'Delete organization', + EXPORT_ORGANIZATIONS = 'Export organizations', + FILTER_ORGANIZATIONS = 'Filter organizations', + SEARCH_ORGANIZATIONS = 'Search organizations', + SORT_ORGANIZATIONS = 'Sort organizations', + FILTER_ACTIVITIES = 'Filter activities', + SEARCH_ACTIVITIES = 'Search activities', + EDIT_MEMBER_IDENTITY = 'Edit contributor identity', + EDIT_MEMBER_EMAIL = 'Edit contributor email', + ADD_WORK_EXPERIENCE = 'Add work experience', + EDIT_WORK_EXPERIENCE = 'Edit work experience', + DELETE_WORK_EXPERIENCE = 'Delete work experience', + ADD_GLOBAL_ATTRIBUTE = 'Add global attribute', + EDIT_GLOBAL_ATTRIBUTE = 'Edit global attribute', + DELETE_GLOBAL_ATTRIBUTE = 'Delete global attribute', + UNMERGE_MEMBER_IDENTITY = 'Unmerge contributor identity', + EDIT_ORGANIZATION_IDENTITY = 'Edit organization identity', + EDIT_ORGANIZATION_EMAIL_DOMAIN = 'Edit organization email domain', + EDIT_ORGANIZATION_PHONE_NUMBER = 'Edit organization phone number', + UNMERGE_ORGANIZATION_IDENTITY = 'Unmerge organization identity', + FILTER_MEMBERS_MERGE_SUGGESTIONS = 'Filter contributors merge suggestions', + SEARCH_MEMBERS_MERGE_SUGGESTIONS = 'Search contributors merge suggestions', + SORT_MEMBERS_MERGE_SUGGESTIONS = 'Sort contributors merge suggestions', + FILTER_ORGANIZATIONS_MERGE_SUGGESTIONS = 'Filter organizations merge suggestions', + SEARCH_ORGANIZATIONS_MERGE_SUGGESTIONS = 'Search organizations merge suggestions', + SORT_ORGANIZATIONS_MERGE_SUGGESTIONS = 'Sort organizations merge suggestions', + VIEW_MEMBER_MERGE_SUGGESTION = 'View contributor merge suggestion', + NAVIGATE_MEMBERS_MERGE_SUGGESTIONS = 'Navigate contributors merge suggestions', + IGNORE_MEMBER_MERGE_SUGGESTION = 'Ignore contributor merge suggestion', + IGNORE_MEMBER_BOT_SUGGESTION = 'Ignore contributor bot suggestion', + VIEW_ORGANIZATION_MERGE_SUGGESTION = 'View organization merge suggestion', + NAVIGATE_ORGANIZATIONS_MERGE_SUGGESTIONS = 'Navigate organizations merge suggestions', + IGNORE_ORGANIZATION_MERGE_SUGGESTION = 'Ignore organization merge suggestion', + MERGE_MEMBER_MERGE_SUGGESTION = 'Merge contributor merge suggestion', + MARK_MEMBER_BOT_SUGGESTION = 'Mark contributor as bot suggestion', + MERGE_ORGANIZATION_MERGE_SUGGESTION = 'Merge organization merge suggestion', + ADD_PROJECT_GROUP = 'Add project group', + SEARCH_PROJECT_GROUPS = 'Search project groups', + EDIT_PROJECT_GROUP = 'Edit project group', + ADD_PROJECT = 'Add project', + EDIT_PROJECT = 'Edit project', + ADD_SUB_PROJECT = 'Add sub project', + EDIT_SUB_PROJECT = 'Edit sub project', + CONNECT_INTEGRATION = 'Connect integration', + DISCONNECT_INTEGRATION = 'Disconnect integration', + EDIT_INTEGRATION_SETTINGS = 'Edit integration settings', + SEARCH_PROJECTS = 'Search projects', + COPY_AUTH_TOKEN = 'Copy auth token', + SHOW_AUTH_TOKEN = 'Show auth token', + VIEW_AUDIT_LOG_DETAILS = 'View audit log details', + FILTER_AUDIT_LOGS = 'Filter audit logs', + COPY_AUDIT_LOG_JSON = 'Copy audit log JSON', + SELECT_PROJECT_GROUP = 'Select project group', + FILTER_DASHBOARD = 'Filter dashboard', + COPILOT_REVIEW_PROFILE = 'Copilot review profile', + SEARCH = 'Search', + FILTER = 'Filter', +} diff --git a/frontend/src/shared/modules/monitoring/types/session.ts b/frontend/src/shared/modules/monitoring/types/session.ts new file mode 100644 index 0000000000..08fb5ff3be --- /dev/null +++ b/frontend/src/shared/modules/monitoring/types/session.ts @@ -0,0 +1,7 @@ +export interface Session { + id?: string; + userId: string; + userEmail: string; + startTime: string; + endTime?: string; +} diff --git a/frontend/src/shared/modules/monitoring/useProductTracking.ts b/frontend/src/shared/modules/monitoring/useProductTracking.ts new file mode 100644 index 0000000000..ed2ae6d6f1 --- /dev/null +++ b/frontend/src/shared/modules/monitoring/useProductTracking.ts @@ -0,0 +1,49 @@ +import { useAuthStore } from '@/modules/auth/store/auth.store'; +import { storeToRefs } from 'pinia'; +import config from '@/config'; +import { createEvent } from './tracking-service'; +import useSessionTracking from './useSessionTracking'; + +const useProductTracking = () => { + const authStore = useAuthStore(); + const { user } = storeToRefs(authStore); + + const shouldTrack = config.env !== 'staging'; + + const trackEvent = ({ + key, + type, + properties, + }: { + key: string; + type: string; + properties?: Record; + }) => { + if (!shouldTrack) { + return; + } + + const { startSession } = useSessionTracking(); + const userSession = sessionStorage.getItem('userSession'); + + if (user.value && userSession) { + createEvent({ + key, + type, + sessionId: userSession, + properties, + userId: user.value.id, + userEmail: user.value.email, + }).catch(() => { + sessionStorage.removeItem('userSession'); + startSession(); + }); + } + }; + + return { + trackEvent, + }; +}; + +export default useProductTracking; diff --git a/frontend/src/shared/modules/monitoring/useSessionTracking.ts b/frontend/src/shared/modules/monitoring/useSessionTracking.ts new file mode 100644 index 0000000000..df59e3709e --- /dev/null +++ b/frontend/src/shared/modules/monitoring/useSessionTracking.ts @@ -0,0 +1,117 @@ +import { useAuthStore } from '@/modules/auth/store/auth.store'; +import { storeToRefs } from 'pinia'; +import { ref } from 'vue'; +import config from '@/config'; +import { createSession, updateSession } from './tracking-service'; + +const useSessionTracking = () => { + const inactivityTimeout = ref(); + + const authStore = useAuthStore(); + const { user } = storeToRefs(authStore); + + const INACTIVITY_PERIOD = 30 * 60 * 1000; + + const shouldTrack: boolean = config.env !== 'staging'; + + const endSession = () => { + if (!shouldTrack) { + return; + } + + const userSession = sessionStorage.getItem('userSession'); + + if (userSession) { + updateSession(userSession, new Date().toISOString()); + sessionStorage.removeItem('userSession'); + } + }; + + const resetInactivityTimeout = () => { + if (!shouldTrack) { + return; + } + + clearTimeout(inactivityTimeout.value); + + inactivityTimeout.value = setTimeout(endSession, INACTIVITY_PERIOD); + }; + + const startSession = async () => { + if (!shouldTrack) { + return Promise.resolve(); + } + + const userSession = sessionStorage.getItem('userSession'); + + if (!userSession) { + if (user.value) { + return createSession({ + startTime: new Date().toISOString(), + userId: user.value.id, + userEmail: user.value.email, + }).then((session) => { + if (session.id) { + sessionStorage.setItem('userSession', session.id); + } + + return Promise.resolve(); + }); + } + } else { + resetInactivityTimeout(); + } + + return Promise.resolve(); + }; + + const onStorageChange = (event: StorageEvent) => { + if (!shouldTrack) { + return; + } + + if (event.key === 'userSession') { + if (event.newValue === null) { + endSession(); + } else { + resetInactivityTimeout(); + } + } + }; + + const attachListeners = () => { + if (!shouldTrack) { + return; + } + + window.addEventListener('mousemove', resetInactivityTimeout); + window.addEventListener('click', resetInactivityTimeout); + window.addEventListener('keypress', resetInactivityTimeout); + window.addEventListener('scroll', resetInactivityTimeout); + window.addEventListener('beforeunload', endSession); + window.addEventListener('storage', onStorageChange); + }; + + const detachListeners = () => { + if (!shouldTrack) { + return; + } + + window.removeEventListener('mousemove', resetInactivityTimeout); + window.removeEventListener('click', resetInactivityTimeout); + window.removeEventListener('keypress', resetInactivityTimeout); + window.removeEventListener('scroll', resetInactivityTimeout); + window.removeEventListener('beforeunload', endSession); + window.removeEventListener('storage', onStorageChange); + }; + + return { + startSession, + endSession, + attachListeners, + detachListeners, + resetInactivityTimeout, + }; +}; + +export default useSessionTracking; diff --git a/frontend/src/shared/modules/permissions/helpers/usePermissions.ts b/frontend/src/shared/modules/permissions/helpers/usePermissions.ts new file mode 100644 index 0000000000..221b5af06a --- /dev/null +++ b/frontend/src/shared/modules/permissions/helpers/usePermissions.ts @@ -0,0 +1,49 @@ +import { useAuthStore } from '@/modules/auth/store/auth.store'; +import { storeToRefs } from 'pinia'; +import { LfPermission } from '@/shared/modules/permissions/types/Permissions'; +import { lfPermissions } from '@/config/permissions'; +import { useLfSegmentsStore } from '@/modules/lf/segments/store'; +import { LfRole } from '@/shared/modules/permissions/types/Roles'; + +export default function usePermissions() { + // Auth store + const authStore = useAuthStore(); + const { + roles, + tenantUser, + } = storeToRefs(authStore); + + // Segment store + const lsSegmentsStore = useLfSegmentsStore(); + const { adminProjectGroups } = storeToRefs(lsSegmentsStore); + + const hasRole = (role: LfRole): boolean => roles.value.includes(role); + + const hasPermission = (permission: LfPermission): boolean => (roles.value || []).some((role) => lfPermissions[role][permission]); + + const hasAccessToProjectGroup = (segmentId: string) => { + if (roles.value.includes(LfRole.admin)) { + return true; + } + if (!tenantUser.value?.adminSegments.length) { + return false; + } + return adminProjectGroups.value.list + .map((p) => p.id) + .includes(segmentId); + }; + + const hasAccessToSegmentId = (segmentId: string) => { + if (roles.value.includes(LfRole.admin)) { + return true; + } + return tenantUser.value?.adminSegments.includes(segmentId); + }; + + return { + hasRole, + hasPermission, + hasAccessToSegmentId, + hasAccessToProjectGroup, + }; +} diff --git a/frontend/src/shared/modules/permissions/router/PermissionGuard.ts b/frontend/src/shared/modules/permissions/router/PermissionGuard.ts new file mode 100644 index 0000000000..3073db8005 --- /dev/null +++ b/frontend/src/shared/modules/permissions/router/PermissionGuard.ts @@ -0,0 +1,21 @@ +import { ToastStore } from '@/shared/message/notification'; +import { LfPermission } from '@/shared/modules/permissions/types/Permissions'; +import usePermissions from '@/shared/modules/permissions/helpers/usePermissions'; +import { useAuthStore } from '@/modules/auth/store/auth.store'; + +export const PermissionGuard = (permission: LfPermission) => async (to, from, next) => { + // Ensure user is loaded + const { ensureLoaded } = useAuthStore(); + await ensureLoaded(); + + const { hasPermission } = usePermissions(); + if (hasPermission(permission)) { + next(); + return true; + } + ToastStore.error('You don\'t have access to this page'); + if (from.matched.length === 0) { + next('/'); + } + return false; +}; diff --git a/frontend/src/shared/modules/permissions/types/Permissions.ts b/frontend/src/shared/modules/permissions/types/Permissions.ts new file mode 100644 index 0000000000..313f90089f --- /dev/null +++ b/frontend/src/shared/modules/permissions/types/Permissions.ts @@ -0,0 +1,86 @@ +export enum LfPermission { + // Tenant + tenantEdit = 'tenantEdit', + tenantDestroy = 'tenantDestroy', + + // Plans + planEdit = 'planEdit', + planRead = 'planRead', + + // User + userEdit = 'userEdit', + userDestroy = 'userDestroy', + userCreate = 'userCreate', + userImport = 'userImport', + userRead = 'userRead', + userAutocomplete = 'userAutocomplete', + + // Audit logs + auditLogRead = 'auditLogRead', + + // Settings + settingsRead = 'settingsRead', + settingsEdit = 'settingsEdit', + + // Integrations + integrationCreate = 'integrationCreate', + integrationEdit = 'integrationEdit', + integrationDestroy = 'integrationDestroy', + integrationRead = 'integrationRead', + integrationAutocomplete = 'integrationAutocomplete', + + // Members + memberImport = 'memberImport', + memberCreate = 'memberCreate', + memberEdit = 'memberEdit', + memberDestroy = 'memberDestroy', + memberRead = 'memberRead', + memberAutocomplete = 'memberAutocomplete', + mergeMembers = 'mergeMembers', + + // Tags + tagRead = 'tagRead', + tagImport = 'tagImport', + tagAutocomplete = 'tagAutocomplete', + tagCreate = 'tagCreate', + tagEdit = 'tagEdit', + tagDestroy = 'tagDestroy', + + // Organizations + organizationImport = 'organizationImport', + organizationCreate = 'organizationCreate', + organizationEdit = 'organizationEdit', + organizationDestroy = 'organizationDestroy', + organizationRead = 'organizationRead', + organizationAutocomplete = 'organizationAutocomplete', + mergeOrganizations = 'mergeOrganizations', + + // Activities + activityImport = 'activityImport', + activityCreate = 'activityCreate', + activityEdit = 'activityEdit', + activityDestroy = 'activityDestroy', + activityRead = 'activityRead', + activityAutocomplete = 'activityAutocomplete', + + // Project groups + projectGroupCreate = 'projectGroupCreate', + projectGroupEdit = 'projectGroupEdit', + projectCreate = 'projectCreate', + projectEdit = 'projectEdit', + subProjectCreate = 'subProjectCreate', + subProjectEdit = 'subProjectEdit', + + // Custom views + customViewsCreate = 'customViewsCreate', + customViewsTenantManage = 'customViewsTenantManage', + + // Custom views + dataQualityRead = 'dataQualityRead', + dataQualityEdit = 'dataQualityEdit', + + // Collections + collectionCreate = 'collectionCreate', + collectionEdit = 'collectionEdit', + collectionDelete = 'collectionDelete', +} diff --git a/frontend/src/shared/modules/permissions/types/Roles.ts b/frontend/src/shared/modules/permissions/types/Roles.ts new file mode 100644 index 0000000000..50b528f8f8 --- /dev/null +++ b/frontend/src/shared/modules/permissions/types/Roles.ts @@ -0,0 +1,5 @@ +export enum LfRole { + admin = 'admin', + readonly = 'readonly', + projectAdmin = 'projectAdmin', +} diff --git a/frontend/src/shared/modules/platform/components/platform-icon.vue b/frontend/src/shared/modules/platform/components/platform-icon.vue new file mode 100644 index 0000000000..f3f445b5cf --- /dev/null +++ b/frontend/src/shared/modules/platform/components/platform-icon.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/frontend/src/shared/modules/platform/components/platform-img.vue b/frontend/src/shared/modules/platform/components/platform-img.vue new file mode 100644 index 0000000000..a22eecc19b --- /dev/null +++ b/frontend/src/shared/modules/platform/components/platform-img.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/frontend/src/shared/modules/platform/components/platform-svg.vue b/frontend/src/shared/modules/platform/components/platform-svg.vue new file mode 100644 index 0000000000..ece67403e4 --- /dev/null +++ b/frontend/src/shared/modules/platform/components/platform-svg.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/frontend/src/shared/modules/platform/components/platform.vue b/frontend/src/shared/modules/platform/components/platform.vue new file mode 100644 index 0000000000..5a0336848e --- /dev/null +++ b/frontend/src/shared/modules/platform/components/platform.vue @@ -0,0 +1,100 @@ + + + + + + + diff --git a/frontend/src/shared/modules/platform/types/Platform.ts b/frontend/src/shared/modules/platform/types/Platform.ts new file mode 100644 index 0000000000..ceff6b9841 --- /dev/null +++ b/frontend/src/shared/modules/platform/types/Platform.ts @@ -0,0 +1,28 @@ +export enum Platform { + ALL = 'all', + GITHUB = 'github', + DISCORD = 'discord', + HACKER_NEWS = 'hackernews', + LINKEDIN = 'linkedin', + TWITTER = 'twitter', + SLACK = 'slack', + DEVTO = 'devto', + INTEGRATION = 'integration', + REDDIT = 'reddit', + STACK_OVERFLOW = 'stackOverflow', + DISCOURSE = 'discourse', + HUBSPOT = 'hubspot', + GIT = 'git', + GROUPS_IO = 'groupsio', + CUSTOM = 'custom', + ENRICHMENT = 'enrichment', + EMAIL = 'email', + EMAILS = 'emails', + PHONE_NUMBERS = 'phoneNumbers', + CRUNCHBASE = 'crunchbase', + CONFLUENCE = 'confluence', + GITLAB = 'gitlab', + JIRA = 'jira', + GITHUB_NANGO = 'github-nango', + GERRIT = 'gerrit', +} diff --git a/frontend/src/shared/modules/project-groups/components/project-groups-tags.vue b/frontend/src/shared/modules/project-groups/components/project-groups-tags.vue new file mode 100644 index 0000000000..96bfcc2cab --- /dev/null +++ b/frontend/src/shared/modules/project-groups/components/project-groups-tags.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/frontend/src/shared/modules/report-issue/component/report-data-issue-modal.vue b/frontend/src/shared/modules/report-issue/component/report-data-issue-modal.vue new file mode 100644 index 0000000000..a8007a26b6 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/component/report-data-issue-modal.vue @@ -0,0 +1,229 @@ + + + + + diff --git a/frontend/src/shared/modules/report-issue/config/entity/organization.ts b/frontend/src/shared/modules/report-issue/config/entity/organization.ts new file mode 100644 index 0000000000..b9af1e1851 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/entity/organization.ts @@ -0,0 +1,14 @@ +import { ReportDataConfig } from '@/shared/modules/report-issue/config'; +import { ReportDataType } from '@/shared/modules/report-issue/constants/report-data-type.enum'; + +const person: ReportDataConfig = { + url: (id: string) => `/organization/${id}/data-issue`, + types: [ + ReportDataType.ORGANIZATION_DETAILS, + ReportDataType.IDENTITY, + ReportDataType.DOMAIN, + ReportDataType.OTHER, + ], +}; + +export default person; diff --git a/frontend/src/shared/modules/report-issue/config/entity/person.ts b/frontend/src/shared/modules/report-issue/config/entity/person.ts new file mode 100644 index 0000000000..e137c0624c --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/entity/person.ts @@ -0,0 +1,16 @@ +import { ReportDataConfig } from '@/shared/modules/report-issue/config'; +import { ReportDataType } from '@/shared/modules/report-issue/constants/report-data-type.enum'; + +const person: ReportDataConfig = { + url: (id: string) => `/member/${id}/data-issue`, + types: [ + ReportDataType.PROFILE_DETAILS, + ReportDataType.PROJECT, + ReportDataType.PROJECT_AFFILIATION, + ReportDataType.WORK_EXPERIENCE, + ReportDataType.IDENTITY, + ReportDataType.OTHER, + ], +}; + +export default person; diff --git a/frontend/src/shared/modules/report-issue/config/index.ts b/frontend/src/shared/modules/report-issue/config/index.ts new file mode 100644 index 0000000000..5cf0589726 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/index.ts @@ -0,0 +1,38 @@ +import { ReportDataEntity } from '@/shared/modules/report-issue/constants/report-data-entity.enum'; + +import { ReportDataType } from '@/shared/modules/report-issue/constants/report-data-type.enum'; +import person from './entity/person'; +import organization from './entity/organization'; +import profileDetails from './type/profile-details/profile-details'; +import projectAffiliation from './type/project-affiliation/project-affiliation'; +import project from './type/project/project'; +import workExperience from './type/work-experience/work-experience'; +import identity from './type/identity/identity'; +import organizationDetails from './type/organization-details/organization-details'; +import domain from './type/domain/domain'; +import other from './type/other/other'; + +export interface ReportDataConfig { + url: (id: string) => string; + types: ReportDataType[]; +} +export interface ReportDataTypeConfig { + description: (attribute: any, entity: any) => string; + display: any | null; +} + +export const reportDataConfig: Record = { + person, + organization, +}; + +export const reportDataTypeDisplay: Record = { + [ReportDataType.PROFILE_DETAILS]: profileDetails, + [ReportDataType.PROJECT]: project, + [ReportDataType.PROJECT_AFFILIATION]: projectAffiliation, + [ReportDataType.WORK_EXPERIENCE]: workExperience, + [ReportDataType.IDENTITY]: identity, + [ReportDataType.ORGANIZATION_DETAILS]: organizationDetails, + [ReportDataType.DOMAIN]: domain, + [ReportDataType.OTHER]: other, +}; diff --git a/frontend/src/shared/modules/report-issue/config/type/domain/domain.ts b/frontend/src/shared/modules/report-issue/config/type/domain/domain.ts new file mode 100644 index 0000000000..5ffefbf9f2 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/domain/domain.ts @@ -0,0 +1,9 @@ +import { ReportDataTypeConfig } from '@/shared/modules/report-issue/config'; +import Domain from './type-domain.vue'; + +const domain: ReportDataTypeConfig = { + description: (attribute: any) => `Domain: ${attribute.value}`, + display: Domain, +}; + +export default domain; diff --git a/frontend/src/shared/modules/report-issue/config/type/domain/type-domain.vue b/frontend/src/shared/modules/report-issue/config/type/domain/type-domain.vue new file mode 100644 index 0000000000..199eb381d5 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/domain/type-domain.vue @@ -0,0 +1,18 @@ + + + diff --git a/frontend/src/shared/modules/report-issue/config/type/identity/identity.ts b/frontend/src/shared/modules/report-issue/config/type/identity/identity.ts new file mode 100644 index 0000000000..c43c20ea50 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/identity/identity.ts @@ -0,0 +1,13 @@ +import { ReportDataTypeConfig } from '@/shared/modules/report-issue/config'; +import { lfIdentities } from '@/config/identities'; +import Identity from './type-identity.vue'; + +const identity: ReportDataTypeConfig = { + description: (identity: any) => { + const platform = lfIdentities[identity.platform]?.name || identity.platform; + return `Identity: ${platform} ${identity.value}`; + }, + display: Identity, +}; + +export default identity; diff --git a/frontend/src/shared/modules/report-issue/config/type/identity/type-identity.vue b/frontend/src/shared/modules/report-issue/config/type/identity/type-identity.vue new file mode 100644 index 0000000000..95287e2bfa --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/identity/type-identity.vue @@ -0,0 +1,40 @@ + + + diff --git a/frontend/src/shared/modules/report-issue/config/type/organization-details/organization-details.ts b/frontend/src/shared/modules/report-issue/config/type/organization-details/organization-details.ts new file mode 100644 index 0000000000..9d48344625 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/organization-details/organization-details.ts @@ -0,0 +1,13 @@ +import { ReportDataTypeConfig } from '@/shared/modules/report-issue/config'; +import { Organization } from '@/modules/organization/types/Organization'; +import useOrganizationHelpers from '@/modules/organization/helpers/organization.helpers'; +import OrganizationDetails from './type-organization-details.vue'; + +const { displayName } = useOrganizationHelpers(); + +export const organizationDetails: ReportDataTypeConfig = { + description: (attribute: any, entity: Organization) => `Organization: ${displayName(entity)}`, + display: OrganizationDetails, +}; + +export default organizationDetails; diff --git a/frontend/src/shared/modules/report-issue/config/type/organization-details/type-organization-details.vue b/frontend/src/shared/modules/report-issue/config/type/organization-details/type-organization-details.vue new file mode 100644 index 0000000000..bcfad40c5a --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/organization-details/type-organization-details.vue @@ -0,0 +1,34 @@ + + + diff --git a/frontend/src/shared/modules/report-issue/config/type/other/other.ts b/frontend/src/shared/modules/report-issue/config/type/other/other.ts new file mode 100644 index 0000000000..05f4a3e87e --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/other/other.ts @@ -0,0 +1,8 @@ +import { ReportDataTypeConfig } from '@/shared/modules/report-issue/config'; + +const other: ReportDataTypeConfig = { + description: () => '', + display: null, +}; + +export default other; diff --git a/frontend/src/shared/modules/report-issue/config/type/profile-details/profile-details.ts b/frontend/src/shared/modules/report-issue/config/type/profile-details/profile-details.ts new file mode 100644 index 0000000000..758cd13af6 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/profile-details/profile-details.ts @@ -0,0 +1,10 @@ +import { ReportDataTypeConfig } from '@/shared/modules/report-issue/config'; +import { Contributor } from '@/modules/contributor/types/Contributor'; +import ProfileDetails from './type-profile-details.vue'; + +export const profileDetails: ReportDataTypeConfig = { + description: (attribute: any, entity: Contributor) => `Person: ${entity.displayName}`, + display: ProfileDetails, +}; + +export default profileDetails; diff --git a/frontend/src/shared/modules/report-issue/config/type/profile-details/type-profile-details.vue b/frontend/src/shared/modules/report-issue/config/type/profile-details/type-profile-details.vue new file mode 100644 index 0000000000..0719e68fc9 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/profile-details/type-profile-details.vue @@ -0,0 +1,27 @@ + + + diff --git a/frontend/src/shared/modules/report-issue/config/type/project-affiliation/project-affiliation.ts b/frontend/src/shared/modules/report-issue/config/type/project-affiliation/project-affiliation.ts new file mode 100644 index 0000000000..f1b3a6d161 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/project-affiliation/project-affiliation.ts @@ -0,0 +1,19 @@ +import { ReportDataTypeConfig } from '@/shared/modules/report-issue/config'; +import ProjectAffiliation from './type-project-affiliation.vue'; + +export const projectAffiliation: ReportDataTypeConfig = { + description: (attribute: any) => { + const affiliations = Object.values(attribute.affiliations).flat(); + + const list: string[] = [ + `Project: ${attribute.name}`, + ]; + if (affiliations.length > 0) { + list.push(`Affiliation: ${affiliations[0].organizationName}`); + } + return list.join('\n'); + }, + display: ProjectAffiliation, +}; + +export default projectAffiliation; diff --git a/frontend/src/shared/modules/report-issue/config/type/project-affiliation/type-project-affiliation.vue b/frontend/src/shared/modules/report-issue/config/type/project-affiliation/type-project-affiliation.vue new file mode 100644 index 0000000000..4314394301 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/project-affiliation/type-project-affiliation.vue @@ -0,0 +1,11 @@ + + + diff --git a/frontend/src/shared/modules/report-issue/config/type/project/project.ts b/frontend/src/shared/modules/report-issue/config/type/project/project.ts new file mode 100644 index 0000000000..678b797cd2 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/project/project.ts @@ -0,0 +1,9 @@ +import { ReportDataTypeConfig } from '@/shared/modules/report-issue/config'; +import Project from './type-project.vue'; + +const project: ReportDataTypeConfig = { + description: (attribute: any) => `Project: ${attribute.name}`, + display: Project, +}; + +export default project; diff --git a/frontend/src/shared/modules/report-issue/config/type/project/type-project.vue b/frontend/src/shared/modules/report-issue/config/type/project/type-project.vue new file mode 100644 index 0000000000..4314394301 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/project/type-project.vue @@ -0,0 +1,11 @@ + + + diff --git a/frontend/src/shared/modules/report-issue/config/type/work-experience/type-work-experience.vue b/frontend/src/shared/modules/report-issue/config/type/work-experience/type-work-experience.vue new file mode 100644 index 0000000000..6ed4624817 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/work-experience/type-work-experience.vue @@ -0,0 +1,32 @@ + + + diff --git a/frontend/src/shared/modules/report-issue/config/type/work-experience/work-experience.ts b/frontend/src/shared/modules/report-issue/config/type/work-experience/work-experience.ts new file mode 100644 index 0000000000..228c740c0b --- /dev/null +++ b/frontend/src/shared/modules/report-issue/config/type/work-experience/work-experience.ts @@ -0,0 +1,39 @@ +import { ReportDataTypeConfig } from '@/shared/modules/report-issue/config'; +import useOrganizationHelpers from '@/modules/organization/helpers/organization.helpers'; +import { Organization } from '@/modules/organization/types/Organization'; +import { dateHelper } from '@/shared/date-helper/date-helper'; +import WorkExperience from './type-work-experience.vue'; + +const { displayName } = useOrganizationHelpers(); + +const getDateRange = (dateStart?: string, dateEnd?: string) => { + const start = dateStart + ? dateHelper(dateStart).utc().format('MMMM YYYY') + : 'Unknown'; + const endDefault = dateStart ? 'Present' : 'Unknown'; + const end = dateEnd + ? dateHelper(dateEnd).utc().format('MMMM YYYY') + : endDefault; + if (start === end) { + return start; + } + return `${start} → ${end}`; +}; + +export const workExperience: ReportDataTypeConfig = { + description: (attribute: Organization) => { + const workExperienceList: string[] = [ + `Work experience: ${displayName(attribute)}`, + ]; + if (attribute.memberOrganizations.title) { + workExperienceList.push(`Title: ${attribute.memberOrganizations.title}`); + } + if (!!attribute.memberOrganizations.dateStart || !!attribute.memberOrganizations.dateEnd) { + workExperienceList.push(`Period: ${getDateRange(attribute.memberOrganizations.dateStart, attribute.memberOrganizations.dateEnd)}`); + } + return workExperienceList.join('\n'); + }, + display: WorkExperience, +}; + +export default workExperience; diff --git a/frontend/src/shared/modules/report-issue/constants/report-data-entity.enum.ts b/frontend/src/shared/modules/report-issue/constants/report-data-entity.enum.ts new file mode 100644 index 0000000000..d2785f1328 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/constants/report-data-entity.enum.ts @@ -0,0 +1,4 @@ +export enum ReportDataEntity { + PERSON = 'person', + ORGANIZATION = 'organization', +} diff --git a/frontend/src/shared/modules/report-issue/constants/report-data-type.enum.ts b/frontend/src/shared/modules/report-issue/constants/report-data-type.enum.ts new file mode 100644 index 0000000000..6bd7c06f27 --- /dev/null +++ b/frontend/src/shared/modules/report-issue/constants/report-data-type.enum.ts @@ -0,0 +1,10 @@ +export enum ReportDataType { + PROFILE_DETAILS = 'Profile details', + PROJECT = 'Project', + PROJECT_AFFILIATION = 'Project affiliation', + WORK_EXPERIENCE = 'Work experience', + IDENTITY = 'Identity', + ORGANIZATION_DETAILS = 'Organization details', + DOMAIN = 'Domain', + OTHER = 'Other', +} diff --git a/frontend/src/shared/modules/saved-views/components/SavedViewManagement.vue b/frontend/src/shared/modules/saved-views/components/SavedViewManagement.vue index 8e3c24860f..28e08104ee 100644 --- a/frontend/src/shared/modules/saved-views/components/SavedViewManagement.vue +++ b/frontend/src/shared/modules/saved-views/components/SavedViewManagement.vue @@ -19,16 +19,17 @@ class="p-2 rounded flex items-center justify-between flex-grow transition hover:bg-gray-50 cursor-grab" >
- + {{ view.name }}
- +
@@ -36,15 +37,16 @@ class="h-6 w-6 flex items-center justify-center ml-1 group cursor-pointer hover:bg-gray-100 rounded" @click="duplicate(view)" > - +
- +
@@ -60,7 +62,13 @@ import { VueDraggableNext } from 'vue-draggable-next'; import { computed } from 'vue'; import ConfirmDialog from '@/shared/dialog/confirm-dialog'; import { SavedViewsService } from '@/shared/modules/saved-views/services/saved-views.service'; -import Message from '@/shared/message/message'; + +import { ToastStore } from '@/shared/message/notification'; +import usePermissions from '@/shared/modules/permissions/helpers/usePermissions'; +import { LfPermission } from '@/shared/modules/permissions/types/Permissions'; +import useProductTracking from '@/shared/modules/monitoring/useProductTracking'; +import { EventType, FeatureEventKey } from '@/shared/modules/monitoring/types/event'; +import LfIcon from '@/ui-kit/icon/Icon.vue'; const props = defineProps<{ config: SavedViewsConfig, @@ -73,6 +81,10 @@ const emit = defineEmits<{(e: 'update:views', value: SavedView[]): any, (e: 'reload'): any, }>(); +const { trackEvent } = useProductTracking(); + +const { hasPermission } = usePermissions(); + const views = computed({ get() { return props.views; @@ -83,6 +95,11 @@ const views = computed({ }); const onListChange = () => { + trackEvent({ + key: FeatureEventKey.REORDER_CUSTOM_VIEW, + type: EventType.FEATURE, + }); + SavedViewsService.updateBulk( views.value.map( (view, viewIndex) => ({ @@ -111,11 +128,16 @@ const remove = (view: SavedView) => { message: isShared ? 'This view will be deleted on all user accounts from this workspace. Are you sure you want to proceed? You can’t undo this action.' : 'Are you sure you want to proceed? You can’t undo this action.', - icon: 'ri-delete-bin-line', + icon: 'fa-trash-can fa-light', cancelButtonText: 'Cancel', confirmButtonText: isShared ? 'Delete shared view' : 'Delete view', } as any) .then(() => { + trackEvent({ + key: FeatureEventKey.DELETE_CUSTOM_VIEW, + type: EventType.FEATURE, + }); + SavedViewsService.delete(view.id) .then(() => { (window as any).analytics.track('Custom view deleted', { @@ -123,10 +145,10 @@ const remove = (view: SavedView) => { name: view.name, }); emit('reload'); - Message.success('View successfully deleted'); + ToastStore.success('View successfully deleted'); }) .catch(() => { - Message.error('There was an error deleting view'); + ToastStore.error('There was an error deleting view'); }); }); }; @@ -134,7 +156,7 @@ const remove = (view: SavedView) => { diff --git a/frontend/src/shared/modules/saved-views/components/SavedViews.vue b/frontend/src/shared/modules/saved-views/components/SavedViews.vue index 303fd25def..a1fe3df94d 100644 --- a/frontend/src/shared/modules/saved-views/components/SavedViews.vue +++ b/frontend/src/shared/modules/saved-views/components/SavedViews.vue @@ -1,13 +1,18 @@