-
Notifications
You must be signed in to change notification settings - Fork 9
feat: ingestion #64
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: ingestion #64
Changes from 1 commit
93d867d
4a88385
ce3f652
d4185f7
826dd3e
bd4d3be
1f86af3
9cf1517
9a2ca88
0298f7c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| { | ||
| "extends": ["../../../.eslintrc.base.json"], | ||
| "ignorePatterns": ["!**/*"], | ||
| "overrides": [ | ||
| { | ||
| "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], | ||
| "rules": {} | ||
| }, | ||
| { | ||
| "files": ["*.ts", "*.tsx"], | ||
| "rules": {} | ||
| }, | ||
| { | ||
| "files": ["*.js", "*.jsx"], | ||
| "rules": {} | ||
| }, | ||
| { | ||
| "files": ["*.json"], | ||
| "parser": "jsonc-eslint-parser", | ||
| "rules": { | ||
| "@nx/dependency-checks": "error" | ||
| } | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # ingestion | ||
|
|
||
| This library was generated with [Nx](https://nx.dev). | ||
|
|
||
| ## Building | ||
|
|
||
| Run `nx build ingestion` to build the library. | ||
|
|
||
| ## Running unit tests | ||
|
|
||
| Run `nx test ingestion` to execute the unit tests via [Jest](https://jestjs.io). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| /* eslint-disable */ | ||
| export default { | ||
| displayName: 'ingestion', | ||
| preset: '../../../jest.preset.js', | ||
| testEnvironment: 'node', | ||
| transform: { | ||
| '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }], | ||
| }, | ||
| moduleFileExtensions: ['ts', 'js', 'html'], | ||
| coverageDirectory: '../../../coverage/libs/report/ingestion', | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "name": "@herodevs/report-ingestion", | ||
| "version": "0.0.1", | ||
| "dependencies": { | ||
| "tslib": "^2.3.0" | ||
| }, | ||
| "type": "commonjs", | ||
| "main": "./src/index.js", | ||
| "typings": "./src/index.d.ts", | ||
| "private": true | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "name": "report-ingestion", | ||
| "$schema": "../../../node_modules/nx/schemas/project-schema.json", | ||
| "sourceRoot": "libs/report/ingestion/src", | ||
| "projectType": "library", | ||
| "tags": [], | ||
| "targets": { | ||
| "build": { | ||
| "executor": "@nx/js:tsc", | ||
| "outputs": ["{options.outputPath}"], | ||
| "options": { | ||
| "outputPath": "dist/libs/report/ingestion", | ||
| "main": "libs/report/ingestion/src/index.ts", | ||
| "tsConfig": "libs/report/ingestion/tsconfig.lib.json", | ||
| "assets": ["libs/report/ingestion/*.md"] | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './lib/ingestion'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import { readFileSync, statSync } from 'node:fs'; | ||
| import { promptToProceedUploadFile } from './prompts'; | ||
| import { findManifestFile } from './send-manifest'; | ||
|
|
||
| jest.mock('node:fs', () => ({ | ||
| readFileSync: jest.fn(), | ||
| statSync: jest.fn(), | ||
| })); | ||
|
|
||
| jest.mock('./prompts', () => ({ | ||
| promptToProceedUploadFile: jest.fn(), | ||
| })); | ||
|
|
||
| describe('Telemetry Functions', () => { | ||
| it('should find manifest files correctly', async () => { | ||
| const mockFileName = 'package.json'; | ||
| const mockFileData = '{"name": "test package"}'; | ||
| const mockFileStat = { size: 1024 }; | ||
|
|
||
| (statSync as jest.Mock).mockReturnValue(mockFileStat); | ||
| (readFileSync as jest.Mock).mockReturnValue(mockFileData); | ||
| (promptToProceedUploadFile as jest.Mock).mockResolvedValue(true); | ||
|
|
||
| const result = await findManifestFile(); | ||
|
|
||
| expect(result).toEqual({ name: mockFileName, data: mockFileData }); | ||
| expect(promptToProceedUploadFile).toHaveBeenCalledWith(mockFileName); | ||
| }); | ||
|
|
||
| it('should warn if manifest file is empty', async () => { | ||
| const mockFileName = 'package.json'; | ||
| const mockFileStat = { size: 0 }; | ||
| (statSync as jest.Mock).mockReturnValue(mockFileStat); | ||
| const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); | ||
| const result = await findManifestFile(); | ||
|
|
||
| expect(result).toBeUndefined(); | ||
| expect(consoleWarnSpy).toHaveBeenCalledWith(`File ${mockFileName} is empty`); | ||
| consoleWarnSpy.mockRestore(); | ||
| }); | ||
|
|
||
| it('should warn if manifest file is too large', async () => { | ||
| const mockFileStat = { size: 6e6 }; // 6MB file, larger than the 5MB max size | ||
| (statSync as jest.Mock).mockReturnValue(mockFileStat); | ||
| const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); | ||
| const result = await findManifestFile(); | ||
| const mockFileName = 'package.json'; | ||
| expect(result).toBeUndefined(); | ||
| expect(consoleWarnSpy).toHaveBeenCalledWith(`File ${mockFileName} is too large`); | ||
| consoleWarnSpy.mockRestore(); | ||
| }); | ||
|
|
||
| it('should not proceed with upload if user rejects', async () => { | ||
| const mockFileName = 'package.json'; | ||
| const mockFileStat = { size: 1024 }; | ||
| (statSync as jest.Mock).mockReturnValue(mockFileStat); | ||
| (promptToProceedUploadFile as jest.Mock).mockResolvedValue(false); | ||
| const result = await findManifestFile(); | ||
| expect(result).toBeUndefined(); | ||
| expect(promptToProceedUploadFile).toHaveBeenCalledWith(mockFileName); | ||
| }); | ||
|
|
||
| it('should return undefined if no manifest file is found', async () => { | ||
| (statSync as jest.Mock).mockReturnValueOnce(undefined); | ||
| const result = await findManifestFile(); | ||
| expect(result).toBeUndefined(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| import { input, confirm } from '@inquirer/prompts'; | ||
| import { askConsent, promptClientName, promptToProceedUploadFile } from './prompts'; | ||
|
|
||
| jest.mock('@inquirer/prompts', () => ({ | ||
| input: jest.fn(), | ||
| confirm: jest.fn(), | ||
| })); | ||
|
|
||
| describe('askConsent', () => { | ||
| it('should return true if args.consent is true', async () => { | ||
| const args = { consent: true } as any; | ||
| const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); | ||
|
|
||
| const result = await askConsent(args); | ||
|
|
||
| expect(consoleSpy).toHaveBeenCalledWith( | ||
| 'Data may contain sensitive data, please review before sharing it.' | ||
| ); | ||
| expect(result).toBe(true); | ||
|
|
||
| consoleSpy.mockRestore(); | ||
| }); | ||
|
|
||
| it('should prompt for consent and return true if user agrees', async () => { | ||
| const args = { consent: false } as any; | ||
| (confirm as jest.Mock).mockResolvedValue(true); | ||
|
|
||
| const result = await askConsent(args); | ||
|
|
||
| expect(confirm).toHaveBeenCalledWith({ | ||
| message: 'Data may contain sensitive data, please review before sharing it. Continue?', | ||
| }); | ||
| expect(result).toBe(true); | ||
| }); | ||
|
|
||
| it('should prompt for consent and return false if user disagrees', async () => { | ||
| const args = { consent: false } as any; | ||
| (confirm as jest.Mock).mockResolvedValue(false); | ||
|
|
||
| const result = await askConsent(args); | ||
|
|
||
| expect(confirm).toHaveBeenCalledWith({ | ||
| message: 'Data may contain sensitive data, please review before sharing it. Continue?', | ||
| }); | ||
| expect(result).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe('promptClientName', () => { | ||
| it('should return the entered name if valid', async () => { | ||
| const mockName = 'John Doe'; | ||
| (input as jest.Mock).mockResolvedValue(mockName); | ||
|
|
||
| const result = await promptClientName(); | ||
|
|
||
| expect(input).toHaveBeenCalledWith({ | ||
| message: 'Please enter your name:', | ||
| validate: expect.any(Function), | ||
| }); | ||
| expect(result).toBe(mockName); | ||
| }); | ||
|
|
||
| it('should validate the input and reject empty names', async () => { | ||
| const validateFn = (input as jest.Mock).mock.calls[0][0].validate; | ||
marco-ippolito marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| expect(validateFn('')).toBe('Name cannot be empty!'); | ||
| expect(validateFn(' ')).toBe('Name cannot be empty!'); | ||
| expect(validateFn('Valid Name')).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| describe('promptToProceedUploadFile', () => { | ||
| it('should return true if the user confirms the upload', async () => { | ||
| const fileName = 'test-file.txt'; | ||
| (confirm as jest.Mock).mockResolvedValue(true); | ||
|
|
||
| const result = await promptToProceedUploadFile(fileName); | ||
|
|
||
| expect(result).toBe(true); | ||
| expect(confirm).toHaveBeenCalledWith({ | ||
| message: `Found ${fileName}, this file will be uploaded. Continue?`, | ||
| }); | ||
| }); | ||
|
|
||
| it('should return false if the user denies the upload', async () => { | ||
| const fileName = 'test-file.txt'; | ||
| (confirm as jest.Mock).mockResolvedValue(false); | ||
|
|
||
| const result = await promptToProceedUploadFile(fileName); | ||
|
|
||
| expect(result).toBe(false); | ||
| expect(confirm).toHaveBeenCalledWith({ | ||
| message: `Found ${fileName}, this file will be uploaded. Continue?`, | ||
| }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import { type ArgumentsCamelCase, type CommandModule } from 'yargs'; | ||
| import { askConsent, promptClientName, promptToProceedUploadFile } from './prompts'; | ||
marco-ippolito marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
marco-ippolito marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| import { findManifestFile, getClientToken, sendManifest } from './send-manifest'; | ||
| import { type Options } from './types'; | ||
|
|
||
| export const reportIngestionCommand: CommandModule<object, Options> = { | ||
| command: 'ingestion', | ||
| describe: 'send manifest files information', | ||
| aliases: ['ingest', 'i'], | ||
| builder: { | ||
| consent: { | ||
| describe: 'Agree to understanding that sensitive data may be outputted', | ||
marco-ippolito marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| required: false, | ||
| default: false, | ||
| boolean: true, | ||
| }, | ||
| }, | ||
| handler: run, | ||
| }; | ||
|
|
||
| async function run(args: ArgumentsCamelCase<Options>): Promise<void> { | ||
| const consent = await askConsent(args); | ||
marco-ippolito marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (!consent) { | ||
| return; | ||
| } | ||
| // Prompt the user to insert their name | ||
| const clientName = await promptClientName(); | ||
| // First we need to get a short lived token | ||
| const oid = await getClientToken(clientName); | ||
|
|
||
| const manifest = await findManifestFile(); | ||
| if (!manifest) { | ||
| console.log('No manifest files found'); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional suggestion: It might be helpful to mention what files we were looking for. That might give them an idea of what they did wrong (for example, if they ran the command in the wrong directory).
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, we should log |
||
| return; | ||
| } | ||
|
|
||
| await sendManifest(oid, manifest, { clientName }); | ||
| console.log('Manifest sent correctly!'); | ||
marco-ippolito marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { confirm, input } from '@inquirer/prompts'; | ||
| import { ArgumentsCamelCase } from 'yargs'; | ||
| import { type Options } from './types'; | ||
|
|
||
| export async function askConsent(args: ArgumentsCamelCase<Options>): Promise<boolean> { | ||
| const consentPrompt = 'Data may contain sensitive data, please review before sharing it.'; | ||
marco-ippolito marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if (!args.consent) { | ||
| const answer = await confirm({ | ||
| message: `${consentPrompt} Continue?`, | ||
| }); | ||
| if (!answer) { | ||
| return false; | ||
| } | ||
| } else { | ||
| console.log(consentPrompt); | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| export async function promptClientName() { | ||
| const name = await input({ | ||
| message: 'Please enter your name:', | ||
marco-ippolito marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| validate: (value) => (value.trim() === '' ? 'Name cannot be empty!' : true), | ||
| }); | ||
| return name; | ||
| } | ||
|
|
||
| export async function promptToProceedUploadFile(fileName: string): Promise<boolean> { | ||
| const consentPrompt = `Found ${fileName}, this file will be uploaded.`; | ||
| const answer = await confirm({ | ||
| message: `${consentPrompt} Continue?`, | ||
| }); | ||
| if (!answer) { | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { gql } from '@apollo/client/core'; | ||
|
|
||
| export const TELEMETRY_INITIALIZE_MUTATION = gql` | ||
| mutation Telemetry($clientName: String!) { | ||
| telemetry { | ||
| initialize(input: { context: { client: { id: $clientName } } }) { | ||
| success | ||
| oid | ||
| message | ||
| } | ||
| } | ||
| } | ||
| `; | ||
|
|
||
| export const TELEMETRY_REPORT_MUTATION = gql` | ||
| mutation Report($key: String!, $report: JSON!, $metadata: JSON) { | ||
| telemetry { | ||
| report(input: { key: $key, report: $report, metadata: $metadata }) { | ||
| txId | ||
| success | ||
| message | ||
| diagnostics | ||
| } | ||
| } | ||
| } | ||
| `; | ||
marco-ippolito marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Uh oh!
There was an error while loading. Please reload this page.