
- Complete NestJS TypeScript implementation with WebSocket support - Direct messaging (DM) and group chat functionality - End-to-end encryption with AES encryption and key pairs - Media file support (images, videos, audio, documents) up to 100MB - Push notifications with Firebase Cloud Messaging integration - Mention alerts and real-time typing indicators - User authentication with JWT and Passport - SQLite database with TypeORM entities and relationships - Comprehensive API documentation with Swagger/OpenAPI - File upload handling with secure access control - Online/offline status tracking and presence management - Message editing, deletion, and reply functionality - Notification management with automatic cleanup - Health check endpoint for monitoring - CORS configuration for cross-origin requests - Environment-based configuration management - Structured for Flutter SDK integration Features implemented: ✅ Real-time messaging with Socket.IO ✅ User registration and authentication ✅ Direct messages and group chats ✅ Media file uploads and management ✅ End-to-end encryption ✅ Push notifications ✅ Mention alerts ✅ Typing indicators ✅ Message read receipts ✅ Online status tracking ✅ File access control ✅ Comprehensive API documentation Ready for Flutter SDK development and production deployment.
455 lines
17 KiB
JavaScript
Executable File
455 lines
17 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
"use strict";
|
|
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
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 __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.loadEsmModule = exports.main = void 0;
|
|
// symbol polyfill must go first
|
|
require("symbol-observable");
|
|
const node_1 = require("@angular-devkit/core/node");
|
|
const schematics_1 = require("@angular-devkit/schematics");
|
|
const tools_1 = require("@angular-devkit/schematics/tools");
|
|
const ansi_colors_1 = __importDefault(require("ansi-colors"));
|
|
const node_fs_1 = require("node:fs");
|
|
const path = __importStar(require("node:path"));
|
|
const yargs_parser_1 = __importStar(require("yargs-parser"));
|
|
/**
|
|
* Parse the name of schematic passed in argument, and return a {collection, schematic} named
|
|
* tuple. The user can pass in `collection-name:schematic-name`, and this function will either
|
|
* return `{collection: 'collection-name', schematic: 'schematic-name'}`, or it will error out
|
|
* and show usage.
|
|
*
|
|
* In the case where a collection name isn't part of the argument, the default is to use the
|
|
* schematics package (@angular-devkit/schematics-cli) as the collection.
|
|
*
|
|
* This logic is entirely up to the tooling.
|
|
*
|
|
* @param str The argument to parse.
|
|
* @return {{collection: string, schematic: (string)}}
|
|
*/
|
|
function parseSchematicName(str) {
|
|
let collection = '@angular-devkit/schematics-cli';
|
|
let schematic = str;
|
|
if (schematic?.includes(':')) {
|
|
const lastIndexOfColon = schematic.lastIndexOf(':');
|
|
[collection, schematic] = [
|
|
schematic.slice(0, lastIndexOfColon),
|
|
schematic.substring(lastIndexOfColon + 1),
|
|
];
|
|
}
|
|
return { collection, schematic };
|
|
}
|
|
function _listSchematics(workflow, collectionName, logger) {
|
|
try {
|
|
const collection = workflow.engine.createCollection(collectionName);
|
|
logger.info(collection.listSchematicNames().join('\n'));
|
|
}
|
|
catch (error) {
|
|
logger.fatal(error instanceof Error ? error.message : `${error}`);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
function _createPromptProvider() {
|
|
return async (definitions) => {
|
|
const questions = definitions.map((definition) => {
|
|
const question = {
|
|
name: definition.id,
|
|
message: definition.message,
|
|
default: definition.default,
|
|
};
|
|
const validator = definition.validator;
|
|
if (validator) {
|
|
question.validate = (input) => validator(input);
|
|
// Filter allows transformation of the value prior to validation
|
|
question.filter = async (input) => {
|
|
for (const type of definition.propertyTypes) {
|
|
let value;
|
|
switch (type) {
|
|
case 'string':
|
|
value = String(input);
|
|
break;
|
|
case 'integer':
|
|
case 'number':
|
|
value = Number(input);
|
|
break;
|
|
default:
|
|
value = input;
|
|
break;
|
|
}
|
|
// Can be a string if validation fails
|
|
const isValid = (await validator(value)) === true;
|
|
if (isValid) {
|
|
return value;
|
|
}
|
|
}
|
|
return input;
|
|
};
|
|
}
|
|
switch (definition.type) {
|
|
case 'confirmation':
|
|
return { ...question, type: 'confirm' };
|
|
case 'list':
|
|
return {
|
|
...question,
|
|
type: definition.multiselect ? 'checkbox' : 'list',
|
|
choices: definition.items &&
|
|
definition.items.map((item) => {
|
|
if (typeof item == 'string') {
|
|
return item;
|
|
}
|
|
else {
|
|
return {
|
|
name: item.label,
|
|
value: item.value,
|
|
};
|
|
}
|
|
}),
|
|
};
|
|
default:
|
|
return { ...question, type: definition.type };
|
|
}
|
|
});
|
|
const { default: inquirer } = await loadEsmModule('inquirer');
|
|
return inquirer.prompt(questions);
|
|
};
|
|
}
|
|
function findUp(names, from) {
|
|
if (!Array.isArray(names)) {
|
|
names = [names];
|
|
}
|
|
const root = path.parse(from).root;
|
|
let currentDir = from;
|
|
while (currentDir && currentDir !== root) {
|
|
for (const name of names) {
|
|
const p = path.join(currentDir, name);
|
|
if ((0, node_fs_1.existsSync)(p)) {
|
|
return p;
|
|
}
|
|
}
|
|
currentDir = path.dirname(currentDir);
|
|
}
|
|
return null;
|
|
}
|
|
/**
|
|
* return package manager' name by lock file
|
|
*/
|
|
function getPackageManagerName() {
|
|
// order by check priority
|
|
const LOCKS = {
|
|
'package-lock.json': 'npm',
|
|
'yarn.lock': 'yarn',
|
|
'pnpm-lock.yaml': 'pnpm',
|
|
};
|
|
const lockPath = findUp(Object.keys(LOCKS), process.cwd());
|
|
if (lockPath) {
|
|
return LOCKS[path.basename(lockPath)];
|
|
}
|
|
return 'npm';
|
|
}
|
|
// eslint-disable-next-line max-lines-per-function
|
|
async function main({ args, stdout = process.stdout, stderr = process.stderr, }) {
|
|
const { cliOptions, schematicOptions, _ } = parseArgs(args);
|
|
// Create a separate instance to prevent unintended global changes to the color configuration
|
|
const colors = ansi_colors_1.default.create();
|
|
/** Create the DevKit Logger used through the CLI. */
|
|
const logger = (0, node_1.createConsoleLogger)(!!cliOptions.verbose, stdout, stderr, {
|
|
info: (s) => s,
|
|
debug: (s) => s,
|
|
warn: (s) => colors.bold.yellow(s),
|
|
error: (s) => colors.bold.red(s),
|
|
fatal: (s) => colors.bold.red(s),
|
|
});
|
|
if (cliOptions.help) {
|
|
logger.info(getUsage());
|
|
return 0;
|
|
}
|
|
/** Get the collection an schematic name from the first argument. */
|
|
const { collection: collectionName, schematic: schematicName } = parseSchematicName(_.shift() || null);
|
|
const isLocalCollection = collectionName.startsWith('.') || collectionName.startsWith('/');
|
|
/** Gather the arguments for later use. */
|
|
const debugPresent = cliOptions.debug !== null;
|
|
const debug = debugPresent ? !!cliOptions.debug : isLocalCollection;
|
|
const dryRunPresent = cliOptions['dry-run'] !== null;
|
|
const dryRun = dryRunPresent ? !!cliOptions['dry-run'] : debug;
|
|
const force = !!cliOptions.force;
|
|
const allowPrivate = !!cliOptions['allow-private'];
|
|
/** Create the workflow scoped to the working directory that will be executed with this run. */
|
|
const workflow = new tools_1.NodeWorkflow(process.cwd(), {
|
|
force,
|
|
dryRun,
|
|
resolvePaths: [process.cwd(), __dirname],
|
|
schemaValidation: true,
|
|
packageManager: getPackageManagerName(),
|
|
});
|
|
/** If the user wants to list schematics, we simply show all the schematic names. */
|
|
if (cliOptions['list-schematics']) {
|
|
return _listSchematics(workflow, collectionName, logger);
|
|
}
|
|
if (!schematicName) {
|
|
logger.info(getUsage());
|
|
return 1;
|
|
}
|
|
if (debug) {
|
|
logger.info(`Debug mode enabled${isLocalCollection ? ' by default for local collections' : ''}.`);
|
|
}
|
|
// Indicate to the user when nothing has been done. This is automatically set to off when there's
|
|
// a new DryRunEvent.
|
|
let nothingDone = true;
|
|
// Logging queue that receives all the messages to show the users. This only get shown when no
|
|
// errors happened.
|
|
let loggingQueue = [];
|
|
let error = false;
|
|
/**
|
|
* Logs out dry run events.
|
|
*
|
|
* All events will always be executed here, in order of discovery. That means that an error would
|
|
* be shown along other events when it happens. Since errors in workflows will stop the Observable
|
|
* from completing successfully, we record any events other than errors, then on completion we
|
|
* show them.
|
|
*
|
|
* This is a simple way to only show errors when an error occur.
|
|
*/
|
|
workflow.reporter.subscribe((event) => {
|
|
nothingDone = false;
|
|
// Strip leading slash to prevent confusion.
|
|
const eventPath = event.path.startsWith('/') ? event.path.slice(1) : event.path;
|
|
switch (event.kind) {
|
|
case 'error':
|
|
error = true;
|
|
const desc = event.description == 'alreadyExist' ? 'already exists' : 'does not exist';
|
|
logger.error(`ERROR! ${eventPath} ${desc}.`);
|
|
break;
|
|
case 'update':
|
|
loggingQueue.push(`${colors.cyan('UPDATE')} ${eventPath} (${event.content.length} bytes)`);
|
|
break;
|
|
case 'create':
|
|
loggingQueue.push(`${colors.green('CREATE')} ${eventPath} (${event.content.length} bytes)`);
|
|
break;
|
|
case 'delete':
|
|
loggingQueue.push(`${colors.yellow('DELETE')} ${eventPath}`);
|
|
break;
|
|
case 'rename':
|
|
const eventToPath = event.to.startsWith('/') ? event.to.slice(1) : event.to;
|
|
loggingQueue.push(`${colors.blue('RENAME')} ${eventPath} => ${eventToPath}`);
|
|
break;
|
|
}
|
|
});
|
|
/**
|
|
* Listen to lifecycle events of the workflow to flush the logs between each phases.
|
|
*/
|
|
workflow.lifeCycle.subscribe((event) => {
|
|
if (event.kind == 'workflow-end' || event.kind == 'post-tasks-start') {
|
|
if (!error) {
|
|
// Flush the log queue and clean the error state.
|
|
loggingQueue.forEach((log) => logger.info(log));
|
|
}
|
|
loggingQueue = [];
|
|
error = false;
|
|
}
|
|
});
|
|
// Show usage of deprecated options
|
|
workflow.registry.useXDeprecatedProvider((msg) => logger.warn(msg));
|
|
// Pass the rest of the arguments as the smart default "argv". Then delete it.
|
|
workflow.registry.addSmartDefaultProvider('argv', (schema) => 'index' in schema ? _[Number(schema['index'])] : _);
|
|
// Add prompts.
|
|
if (cliOptions.interactive && isTTY()) {
|
|
workflow.registry.usePromptProvider(_createPromptProvider());
|
|
}
|
|
/**
|
|
* Execute the workflow, which will report the dry run events, run the tasks, and complete
|
|
* after all is done.
|
|
*
|
|
* The Observable returned will properly cancel the workflow if unsubscribed, error out if ANY
|
|
* step of the workflow failed (sink or task), with details included, and will only complete
|
|
* when everything is done.
|
|
*/
|
|
try {
|
|
await workflow
|
|
.execute({
|
|
collection: collectionName,
|
|
schematic: schematicName,
|
|
options: schematicOptions,
|
|
allowPrivate: allowPrivate,
|
|
debug: debug,
|
|
logger: logger,
|
|
})
|
|
.toPromise();
|
|
if (nothingDone) {
|
|
logger.info('Nothing to be done.');
|
|
}
|
|
else if (dryRun) {
|
|
logger.info(`Dry run enabled${dryRunPresent ? '' : ' by default in debug mode'}. No files written to disk.`);
|
|
}
|
|
return 0;
|
|
}
|
|
catch (err) {
|
|
if (err instanceof schematics_1.UnsuccessfulWorkflowExecution) {
|
|
// "See above" because we already printed the error.
|
|
logger.fatal('The Schematic workflow failed. See above.');
|
|
}
|
|
else if (debug && err instanceof Error) {
|
|
logger.fatal(`An error occured:\n${err.stack}`);
|
|
}
|
|
else {
|
|
logger.fatal(`Error: ${err instanceof Error ? err.message : err}`);
|
|
}
|
|
return 1;
|
|
}
|
|
}
|
|
exports.main = main;
|
|
/**
|
|
* Get usage of the CLI tool.
|
|
*/
|
|
function getUsage() {
|
|
return `
|
|
schematics [collection-name:]schematic-name [options, ...]
|
|
|
|
By default, if the collection name is not specified, use the internal collection provided
|
|
by the Schematics CLI.
|
|
|
|
Options:
|
|
--debug Debug mode. This is true by default if the collection is a relative
|
|
path (in that case, turn off with --debug=false).
|
|
|
|
--allow-private Allow private schematics to be run from the command line. Default to
|
|
false.
|
|
|
|
--dry-run Do not output anything, but instead just show what actions would be
|
|
performed. Default to true if debug is also true.
|
|
|
|
--force Force overwriting files that would otherwise be an error.
|
|
|
|
--list-schematics List all schematics from the collection, by name. A collection name
|
|
should be suffixed by a colon. Example: '@angular-devkit/schematics-cli:'.
|
|
|
|
--no-interactive Disables interactive input prompts.
|
|
|
|
--verbose Show more information.
|
|
|
|
--help Show this message.
|
|
|
|
Any additional option is passed to the Schematics depending on its schema.
|
|
`;
|
|
}
|
|
/** Parse the command line. */
|
|
const booleanArgs = [
|
|
'allow-private',
|
|
'debug',
|
|
'dry-run',
|
|
'force',
|
|
'help',
|
|
'list-schematics',
|
|
'verbose',
|
|
'interactive',
|
|
];
|
|
/** Parse the command line. */
|
|
function parseArgs(args) {
|
|
const { _, ...options } = (0, yargs_parser_1.default)(args, {
|
|
boolean: booleanArgs,
|
|
default: {
|
|
'interactive': true,
|
|
'debug': null,
|
|
'dry-run': null,
|
|
},
|
|
configuration: {
|
|
'dot-notation': false,
|
|
'boolean-negation': true,
|
|
'strip-aliased': true,
|
|
'camel-case-expansion': false,
|
|
},
|
|
});
|
|
// Camelize options as yargs will return the object in kebab-case when camel casing is disabled.
|
|
const schematicOptions = {};
|
|
const cliOptions = {};
|
|
const isCliOptions = (key) => booleanArgs.includes(key);
|
|
for (const [key, value] of Object.entries(options)) {
|
|
if (/[A-Z]/.test(key)) {
|
|
throw new Error(`Unknown argument ${key}. Did you mean ${(0, yargs_parser_1.decamelize)(key)}?`);
|
|
}
|
|
if (isCliOptions(key)) {
|
|
cliOptions[key] = value;
|
|
}
|
|
else {
|
|
schematicOptions[(0, yargs_parser_1.camelCase)(key)] = value;
|
|
}
|
|
}
|
|
return {
|
|
_: _.map((v) => v.toString()),
|
|
schematicOptions,
|
|
cliOptions,
|
|
};
|
|
}
|
|
function isTTY() {
|
|
const isTruthy = (value) => {
|
|
// Returns true if value is a string that is anything but 0 or false.
|
|
return value !== undefined && value !== '0' && value.toUpperCase() !== 'FALSE';
|
|
};
|
|
// If we force TTY, we always return true.
|
|
const force = process.env['NG_FORCE_TTY'];
|
|
if (force !== undefined) {
|
|
return isTruthy(force);
|
|
}
|
|
return !!process.stdout.isTTY && !isTruthy(process.env['CI']);
|
|
}
|
|
if (require.main === module) {
|
|
const args = process.argv.slice(2);
|
|
main({ args })
|
|
.then((exitCode) => (process.exitCode = exitCode))
|
|
.catch((e) => {
|
|
throw e;
|
|
});
|
|
}
|
|
/**
|
|
* Lazily compiled dynamic import loader function.
|
|
*/
|
|
let load;
|
|
/**
|
|
* This uses a dynamic import to load a module which may be ESM.
|
|
* CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript
|
|
* will currently, unconditionally downlevel dynamic import into a require call.
|
|
* require calls cannot load ESM code and will result in a runtime error. To workaround
|
|
* this, a Function constructor is used to prevent TypeScript from changing the dynamic import.
|
|
* Once TypeScript provides support for keeping the dynamic import this workaround can
|
|
* be dropped.
|
|
*
|
|
* @param modulePath The path of the module to load.
|
|
* @returns A Promise that resolves to the dynamically imported module.
|
|
*/
|
|
function loadEsmModule(modulePath) {
|
|
load ??= new Function('modulePath', `return import(modulePath);`);
|
|
return load(modulePath);
|
|
}
|
|
exports.loadEsmModule = loadEsmModule;
|