├── packages ├── somod │ ├── .gitignore │ ├── types │ │ └── index.d.ts │ ├── tsconfig.json │ ├── tests │ │ └── utils.ts │ ├── README.md │ ├── src │ │ ├── index.ts │ │ ├── utils │ │ │ └── common.ts │ │ └── commands │ │ │ └── deploy.ts │ ├── jest.config.js │ ├── bin-src │ │ └── build.js │ └── LICENSE ├── create │ ├── .gitignore │ ├── src │ │ ├── index.ts │ │ └── tasks │ │ │ ├── gitInit.ts │ │ │ ├── npmInit.ts │ │ │ ├── createTemplateFiles.ts │ │ │ ├── writeIgnoreFiles.ts │ │ │ └── npmInstall.ts │ ├── tsconfig.json │ ├── bin-src │ │ └── build.js │ ├── README.md │ ├── LICENSE │ └── package.json ├── integration-tests │ ├── .gitignore │ ├── samples │ │ ├── push-notification-ui │ │ │ ├── .npmrc │ │ │ ├── pages │ │ │ │ └── notify.ts │ │ │ ├── lib │ │ │ │ ├── index.ts │ │ │ │ ├── subscribe.ts │ │ │ │ └── notify.ts │ │ │ ├── .env │ │ │ ├── build │ │ │ │ ├── lib │ │ │ │ │ ├── index.js │ │ │ │ │ ├── index.d.ts │ │ │ │ │ ├── subscribe.d.ts │ │ │ │ │ ├── notify.d.ts │ │ │ │ │ ├── subscribe.js │ │ │ │ │ └── notify.js │ │ │ │ ├── ui │ │ │ │ │ ├── pages │ │ │ │ │ │ └── notify.d.ts │ │ │ │ │ └── config.json │ │ │ │ └── parameters.json │ │ │ ├── parameters.json │ │ │ ├── ui │ │ │ │ └── config.yaml │ │ │ ├── parameters.yaml │ │ │ ├── tsconfig.somod.json │ │ │ ├── package.json │ │ │ └── tsconfig.json │ │ ├── push-notification │ │ │ ├── .npmrc │ │ │ ├── pages │ │ │ │ ├── messages.ts │ │ │ │ └── notify.ts │ │ │ ├── .env │ │ │ ├── build │ │ │ │ ├── ui │ │ │ │ │ └── pages │ │ │ │ │ │ ├── messages.d.ts │ │ │ │ │ │ └── messages.js │ │ │ │ ├── serverless │ │ │ │ │ ├── functions │ │ │ │ │ │ ├── segregatemessage.d.ts │ │ │ │ │ │ ├── authorizer.d.ts │ │ │ │ │ │ ├── authorizer.js │ │ │ │ │ │ └── segregatemessage.js │ │ │ │ │ └── template.json │ │ │ │ └── parameters.json │ │ │ ├── parameters.json │ │ │ ├── serverless │ │ │ │ └── functions │ │ │ │ │ ├── authorizer.ts │ │ │ │ │ └── segregatemessage.ts │ │ │ ├── .somod │ │ │ │ └── serverless │ │ │ │ │ └── .functions │ │ │ │ │ ├── push-notification │ │ │ │ │ ├── authorizer.js │ │ │ │ │ └── segregatemessage.js │ │ │ │ │ └── push-notification-service │ │ │ │ │ ├── onconnect.js │ │ │ │ │ ├── ondisconnect.js │ │ │ │ │ ├── sendmessage.js │ │ │ │ │ └── receivemessage.js │ │ │ ├── tsconfig.somod.json │ │ │ ├── parameters.yaml │ │ │ ├── tsconfig.json │ │ │ ├── package.json │ │ │ └── ui │ │ │ │ └── pages │ │ │ │ └── messages.tsx │ │ ├── push-notification-service │ │ │ ├── .npmrc │ │ │ ├── parameters.json │ │ │ ├── build │ │ │ │ ├── serverless │ │ │ │ │ └── functions │ │ │ │ │ │ ├── sendmessage.d.ts │ │ │ │ │ │ ├── receivemessage.d.ts │ │ │ │ │ │ ├── onconnect.d.ts │ │ │ │ │ │ ├── ondisconnect.d.ts │ │ │ │ │ │ └── ondisconnect.js │ │ │ │ └── parameters.json │ │ │ ├── .somod │ │ │ │ └── serverless │ │ │ │ │ └── .functions │ │ │ │ │ └── push-notification-service │ │ │ │ │ ├── onconnect.js │ │ │ │ │ ├── ondisconnect.js │ │ │ │ │ ├── sendmessage.js │ │ │ │ │ └── receivemessage.js │ │ │ ├── tsconfig.somod.json │ │ │ ├── parameters.yaml │ │ │ ├── package.json │ │ │ └── serverless │ │ │ │ └── functions │ │ │ │ ├── ondisconnect.ts │ │ │ │ └── receivemessage.ts │ │ └── README.md │ ├── tsconfig.json │ ├── proxy │ │ ├── stop.js │ │ └── start.js │ ├── jest.config.js │ ├── tests │ │ └── somod │ │ │ └── root.test.ts │ └── package.json ├── lib │ ├── src │ │ ├── tasks │ │ │ ├── extension │ │ │ │ ├── index.ts │ │ │ │ └── bundle.ts │ │ │ ├── tsConfigSomodJson │ │ │ │ ├── index.ts │ │ │ │ └── isValid.ts │ │ │ ├── serverless │ │ │ │ ├── bundleFunctions.ts │ │ │ │ ├── bundleFunctionLayers.ts │ │ │ │ ├── buildServerlessTemplate.ts │ │ │ │ ├── index.ts │ │ │ │ ├── validateServerlessTemplate.ts │ │ │ │ ├── validateSchema.ts │ │ │ │ ├── samDeploy.ts │ │ │ │ ├── validateFunctionExports.ts │ │ │ │ └── prepareSAMTemplate.ts │ │ │ ├── common │ │ │ │ ├── index.ts │ │ │ │ ├── initializeContext.ts │ │ │ │ └── deleteBuildDir.ts │ │ │ ├── packageJson │ │ │ │ ├── index.ts │ │ │ │ └── save.ts │ │ │ ├── parameters │ │ │ │ ├── generateRootParameters.ts │ │ │ │ ├── index.ts │ │ │ │ ├── validateParameterValues.ts │ │ │ │ ├── buildParameters.ts │ │ │ │ └── validateSchema.ts │ │ │ └── nextJs │ │ │ │ ├── nextCommand.ts │ │ │ │ ├── vercelCommand.ts │ │ │ │ ├── buildUiConfigYaml.ts │ │ │ │ ├── validateUiConfigYaml.ts │ │ │ │ ├── buildUiPublic.ts │ │ │ │ ├── validateUiConfigYamlWithSchema.ts │ │ │ │ ├── deletePagesAndPublicDir.ts │ │ │ │ ├── index.ts │ │ │ │ ├── createPublicAssets.ts │ │ │ │ ├── watchRootModulePublicAssets.ts │ │ │ │ ├── validatePageExports.ts │ │ │ │ ├── createPages.ts │ │ │ │ ├── watchRootModulePages.ts │ │ │ │ └── watchRootModulePagesData.ts │ │ ├── utils │ │ │ ├── serverless │ │ │ │ ├── types.ts │ │ │ │ ├── keywords │ │ │ │ │ ├── moduleName.ts │ │ │ │ │ ├── createIf.ts │ │ │ │ │ ├── resourceName.ts │ │ │ │ │ ├── templateResources.ts │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── output.ts │ │ │ │ │ ├── templateOutputs.ts │ │ │ │ │ └── dependsOn.ts │ │ │ │ └── serverlessTemplate │ │ │ │ │ ├── build.ts │ │ │ │ │ └── validate.ts │ │ │ ├── parameters │ │ │ │ ├── types.ts │ │ │ │ ├── build.ts │ │ │ │ ├── namespace.ts │ │ │ │ ├── load.ts │ │ │ │ └── validate.ts │ │ │ ├── ErrorSet.ts │ │ │ ├── keywords │ │ │ │ ├── json-parse.ts │ │ │ │ ├── json-stringify.ts │ │ │ │ ├── ajv-compile.ts │ │ │ │ ├── equals.ts │ │ │ │ ├── if.ts │ │ │ │ ├── key.ts │ │ │ │ ├── and.ts │ │ │ │ ├── or.ts │ │ │ │ └── parameter.ts │ │ │ ├── freeze.ts │ │ │ ├── yamlSchemaValidator.ts │ │ │ ├── packageJson.ts │ │ │ ├── extension │ │ │ │ └── bundle.ts │ │ │ └── nextJs │ │ │ │ └── publicAssets.ts │ │ ├── index.ts │ │ └── findRootDir.ts │ ├── tests │ │ ├── tsconfig.json │ │ ├── utils.ts │ │ ├── findRootDir.test.ts │ │ ├── tasks │ │ │ ├── parameters │ │ │ │ ├── buildParameters.test.ts │ │ │ │ ├── generateRootParameters.test.ts │ │ │ │ └── validateSchema.test.ts │ │ │ ├── nextJs │ │ │ │ ├── buildUiConfigYaml.test.ts │ │ │ │ ├── validateUiConfigYaml.test.ts │ │ │ │ ├── validateUiConfigYamlWithSchema.test.ts │ │ │ │ └── buildUiPublic.test.ts │ │ │ ├── tsConfigSomodJson │ │ │ │ └── isValid.test.ts │ │ │ ├── common │ │ │ │ └── initializeContext.test.ts │ │ │ ├── serverless │ │ │ │ ├── buildServerlessTemplate.test.ts │ │ │ │ └── validateFunctionExports.test.ts │ │ │ └── extension │ │ │ │ └── bundle.test.ts │ │ └── utils │ │ │ ├── serverless │ │ │ └── serverlessTemplate │ │ │ │ └── build.test.ts │ │ │ └── packageJson.test.ts │ ├── README.md │ ├── tsconfig.json │ ├── jest.config.js │ └── package.json ├── middleware │ ├── tests │ │ └── tsconfig.json │ ├── src │ │ ├── index.ts │ │ └── middleware.ts │ ├── README.md │ ├── tsconfig.json │ ├── tsconfig.cjs.json │ ├── jest.config.js │ ├── LICENSE │ └── package.json ├── template │ ├── ui │ │ ├── public │ │ │ └── favicon.ico │ │ ├── config.yaml │ │ └── pages │ │ │ └── _document.tsx │ ├── parameters.yaml │ ├── lib │ │ └── index.ts │ ├── .gitignore │ ├── README.md │ ├── serverless │ │ └── template.yaml │ ├── tsconfig.somod.json │ ├── tsconfig.json │ └── package.json ├── docs │ ├── README.md │ ├── package-lock.json │ ├── src │ │ ├── getting-started │ │ │ ├── build.md │ │ │ ├── develop │ │ │ │ └── add-dependencies.md │ │ │ ├── develop.md │ │ │ ├── deploy.md │ │ │ └── ship.md │ │ ├── reference.md │ │ ├── _files │ │ ├── reference │ │ │ ├── main-concepts │ │ │ │ ├── lib.md │ │ │ │ ├── tsconfig.somod.json.md │ │ │ │ └── module.md │ │ │ └── main-concepts.md │ │ └── getting-started.md │ ├── package.json │ └── LICENSE ├── types │ ├── tsconfig.json │ ├── src │ │ ├── index.ts │ │ ├── Module.ts │ │ ├── Namespace.ts │ │ ├── Context.ts │ │ ├── Middleware.ts │ │ ├── KeywordDefinition.ts │ │ ├── Extension.ts │ │ ├── JsonTemplate.ts │ │ └── ServerlessTemplate.ts │ ├── README.md │ ├── LICENSE │ └── package.json └── schema │ ├── tsconfig.cjs.json │ ├── tsconfig.json │ ├── src │ ├── index.ts │ ├── parameters │ │ └── index.ts │ └── serverless-template │ │ └── resources │ │ ├── custom.ts │ │ └── all.ts │ ├── scripts │ ├── build.js │ └── compile.js │ ├── README.md │ ├── LICENSE │ └── package.json ├── .gitignore ├── .github └── workflows │ ├── test.yml │ └── publish.to.npm.yml ├── .eslintignore ├── README.md ├── LICENSE ├── .prettierignore └── package.json /packages/somod/.gitignore: -------------------------------------------------------------------------------- 1 | bin -------------------------------------------------------------------------------- /packages/create/.gitignore: -------------------------------------------------------------------------------- 1 | bin -------------------------------------------------------------------------------- /packages/integration-tests/.gitignore: -------------------------------------------------------------------------------- 1 | packages 2 | proxy/pid -------------------------------------------------------------------------------- /packages/somod/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "somod-types"; 2 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/extension/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./bundle"; 2 | -------------------------------------------------------------------------------- /packages/lib/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { "compilerOptions": { "noEmit": true } } 2 | -------------------------------------------------------------------------------- /packages/middleware/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { "compilerOptions": { "noEmit": true } } 2 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://localhost:8000 -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://localhost:8000 -------------------------------------------------------------------------------- /packages/middleware/src/index.ts: -------------------------------------------------------------------------------- 1 | export { getMiddlewareHandler } from "./middleware"; 2 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-service/.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://localhost:8000 -------------------------------------------------------------------------------- /packages/integration-tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { "compilerOptions": { "esModuleInterop": true } } 2 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/pages/notify.ts: -------------------------------------------------------------------------------- 1 | export { default } from "../ui/pages/notify"; -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/pages/messages.ts: -------------------------------------------------------------------------------- 1 | export { default } from "../ui/pages/messages"; -------------------------------------------------------------------------------- /packages/lib/src/tasks/tsConfigSomodJson/index.ts: -------------------------------------------------------------------------------- 1 | export { isValid as isValidTsConfigSomodJson } from "./isValid"; 2 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./notify"; 2 | export * from "./subscribe"; 3 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/serverless/bundleFunctions.ts: -------------------------------------------------------------------------------- 1 | export { bundleFunctions } from "../../utils/serverless/bundleFunctions"; 2 | -------------------------------------------------------------------------------- /packages/template/ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somod-dev/somod/HEAD/packages/template/ui/public/favicon.ico -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_PNS_PUBLISH_ENDPOINT="" 2 | NEXT_PUBLIC_PNS_SUBSCRIBE_ENDPOINT="" -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/build/lib/index.js: -------------------------------------------------------------------------------- 1 | export * from "./notify"; 2 | export * from "./subscribe"; 3 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_PNS_PUBLISH_ENDPOINT="" 2 | NEXT_PUBLIC_PNS_SUBSCRIBE_ENDPOINT="" -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/build/lib/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./notify"; 2 | export * from "./subscribe"; 3 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/serverless/bundleFunctionLayers.ts: -------------------------------------------------------------------------------- 1 | export { bundleFunctionLayers } from "../../utils/serverless/bundleFunctionLayers"; 2 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "pns.publish.endpoint": "", 3 | "pns.subscribe.endpoint": "" 4 | } 5 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/pages/notify.ts: -------------------------------------------------------------------------------- 1 | export { default } from "../node_modules/push-notification-ui/build/ui/pages/notify"; -------------------------------------------------------------------------------- /packages/template/parameters.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=./node_modules/somod-schema/schemas/parameters/index.json 2 | 3 | parameters: {} 4 | -------------------------------------------------------------------------------- /packages/template/lib/index.ts: -------------------------------------------------------------------------------- 1 | export const Welcome = (): string => { 2 | return "Welcome to SOMOD Framework. learn more at https://somod.dev"; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/lib/README.md: -------------------------------------------------------------------------------- 1 | # SOMOD lib 2 | 3 | > Library of tasks used in SOMOD CLI 4 | 5 | Not to be used directly, Refer [../../readme](../../README.md) instead. 6 | -------------------------------------------------------------------------------- /packages/somod/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "tests"], 3 | "compilerOptions": { 4 | "lib": ["ESNext"], 5 | "esModuleInterop": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/docs/README.md: -------------------------------------------------------------------------------- 1 | # SOMOD Documentation 2 | 3 | This package contains the reference for SOMOD Framework 4 | 5 | visit https://docs.somod.dev for the online version. 6 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-service/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "pns.publish.endpoint": "", 3 | "pns.subscribe.endpoint": "", 4 | "auth.token.endpoint": "" 5 | } 6 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/build/ui/pages/notify.d.ts: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | declare const Notify: FunctionComponent; 3 | export default Notify; 4 | -------------------------------------------------------------------------------- /packages/somod/tests/utils.ts: -------------------------------------------------------------------------------- 1 | export const mockedFunction = unknown>( 2 | f: T 3 | ): jest.MockedFunction => { 4 | return f as jest.MockedFunction; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/build/lib/subscribe.d.ts: -------------------------------------------------------------------------------- 1 | export declare const subscribe: (cb: (message: unknown) => void, subProtocols?: string | string[]) => Promise; 2 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/build/ui/pages/messages.d.ts: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from "react"; 2 | declare const Messages: FunctionComponent; 3 | export default Messages; 4 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/common/index.ts: -------------------------------------------------------------------------------- 1 | export { compileTypeScript } from "./compileTypeScript"; 2 | export { deleteBuildDir } from "./deleteBuildDir"; 3 | export { initializeContext } from "./initializeContext"; 4 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "pns.publish.endpoint": "", 3 | "pns.subscribe.endpoint": "", 4 | "authorizer.jwt.secret": null, 5 | "auth.token.endpoint": "" 6 | } 7 | -------------------------------------------------------------------------------- /packages/template/ui/config.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=../node_modules/somod-schema/schemas/ui-config/index.json 2 | 3 | env: {} 4 | imageDomains: [] 5 | publicRuntimeConfig: {} 6 | serverRuntimeConfig: {} 7 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-service/build/serverless/functions/sendmessage.d.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBStreamHandler } from "aws-lambda"; 2 | declare const handler: DynamoDBStreamHandler; 3 | export default handler; 4 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/build/serverless/functions/segregatemessage.d.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBStreamHandler } from "aws-lambda"; 2 | declare const handler: DynamoDBStreamHandler; 3 | export default handler; 4 | -------------------------------------------------------------------------------- /packages/template/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /build 3 | /parameters.json 4 | .next 5 | /pages 6 | /public 7 | next-env.d.ts 8 | .env 9 | next.config.js 10 | .aws-sam 11 | samconfig.toml 12 | /template.yaml 13 | package-lock.json -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-service/build/serverless/functions/receivemessage.d.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyHandlerV2 } from "aws-lambda"; 2 | declare const handler: APIGatewayProxyHandlerV2; 3 | export default handler; 4 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/build/ui/config.json: -------------------------------------------------------------------------------- 1 | {"env":{"NEXT_PUBLIC_PNS_PUBLISH_ENDPOINT":{"SOMOD::Parameter":"pns.publish.endpoint"},"NEXT_PUBLIC_PNS_SUBSCRIBE_ENDPOINT":{"SOMOD::Parameter":"pns.subscribe.endpoint"}}} -------------------------------------------------------------------------------- /packages/lib/src/utils/serverless/types.ts: -------------------------------------------------------------------------------- 1 | import { JSONType, ServerlessResource } from "somod-types"; 2 | 3 | export type SAMTemplate = { 4 | Resources: Record; 5 | Outputs?: Record; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-service/build/serverless/functions/onconnect.d.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyWebsocketHandlerV2 } from "aws-lambda"; 2 | declare const handler: APIGatewayProxyWebsocketHandlerV2; 3 | export default handler; 4 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-service/build/serverless/functions/ondisconnect.d.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyWebsocketHandlerV2 } from "aws-lambda"; 2 | declare const handler: APIGatewayProxyWebsocketHandlerV2; 3 | export default handler; 4 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/build/lib/notify.d.ts: -------------------------------------------------------------------------------- 1 | export declare const notify: (data: { 2 | message: string; 3 | audience: { 4 | userId?: string; 5 | groupId?: string; 6 | }; 7 | }) => Promise; 8 | -------------------------------------------------------------------------------- /packages/types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | "rootDir": "./src", 7 | "skipLibCheck": true 8 | }, 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/build/serverless/functions/authorizer.d.ts: -------------------------------------------------------------------------------- 1 | declare const authorizer: (event: any) => { 2 | isAuthorized: boolean; 3 | context: { 4 | id: string; 5 | route: any; 6 | }; 7 | }; 8 | export default authorizer; 9 | -------------------------------------------------------------------------------- /packages/template/README.md: -------------------------------------------------------------------------------- 1 | # somod-template 2 | 3 | > Sample template for [SOMOD](https://somod.dev) projects 4 | 5 | ## Usage 6 | 7 | Direct usage of this package is not encouraged. 8 | 9 | Use the [create-somod](https://npmjs.com/package/create-somod) utility instead. 10 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/README.md: -------------------------------------------------------------------------------- 1 | # Sample SOMOD Modules 2 | 3 | ## Install 4 | 5 | The sample modules have dependencies on `somod` and are also interdependent 6 | 7 | > So It is advised to start the npm proxy before running `npm i` inside these modules for development purposes. 8 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/packageJson/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | update as updatePackageJson, 3 | updateSodaruModuleKey as updateSodaruModuleKeyInPackageJson 4 | } from "./update"; 5 | export { save as savePackageJson } from "./save"; 6 | export { validate as validatePackageJson } from "./validate"; 7 | -------------------------------------------------------------------------------- /packages/docs/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "somod-docs", 3 | "version": "2.1.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "somod-docs", 9 | "version": "2.1.1", 10 | "license": "MIT" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/types/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Module"; 2 | export * from "./JsonTemplate"; 3 | export * from "./KeywordDefinition"; 4 | export * from "./Extension"; 5 | export * from "./ServerlessTemplate"; 6 | export * from "./Middleware"; 7 | export * from "./Context"; 8 | export * from "./Namespace"; 9 | -------------------------------------------------------------------------------- /packages/create/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Command, executeCommand } from "nodejs-cli-runner"; 2 | import { CreateSomodAction, decorateCommand } from "./action"; 3 | 4 | const command = new Command("create-somod"); 5 | decorateCommand(command); 6 | command.action(CreateSomodAction); 7 | 8 | executeCommand(command); 9 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/serverless/functions/authorizer.ts: -------------------------------------------------------------------------------- 1 | const authorizer = event => { 2 | return { 3 | isAuthorized: true, 4 | context: { 5 | id: "dummy", 6 | route: event.request.routekey 7 | } 8 | }; 9 | }; 10 | 11 | export default authorizer; 12 | -------------------------------------------------------------------------------- /packages/lib/src/utils/parameters/types.ts: -------------------------------------------------------------------------------- 1 | // must match the Parameter in @somod/parameters-schema 2 | 3 | import { JSONSchema7 } from "decorated-ajv"; 4 | 5 | export type Parameters = { 6 | parameters?: Record; 7 | }; 8 | 9 | export type ParameterValues = Record; 10 | -------------------------------------------------------------------------------- /packages/middleware/README.md: -------------------------------------------------------------------------------- 1 | # somod-middleware 2 | 3 | > Serverless Lambda function middleware utility for [SOMOD](https://somod.dev) projects 4 | 5 | ## Usage 6 | 7 | Direct usage of this package is not encouraged. 8 | 9 | Refer to https://docs.somod.dev/reference/main-concepts/serverless/middlewares for usage 10 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/.somod/serverless/.functions/push-notification/authorizer.js: -------------------------------------------------------------------------------- 1 | import { getMiddlewareHandler } from "somod-middleware"; 2 | import lambdaFn from "../../../../build/serverless/functions/authorizer"; 3 | const handler = getMiddlewareHandler(lambdaFn, []); 4 | export default handler; 5 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/ui/config.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=../../../../schema/schemas/ui-config/index.json 2 | 3 | env: 4 | NEXT_PUBLIC_PNS_PUBLISH_ENDPOINT: 5 | SOMOD::Parameter: pns.publish.endpoint 6 | NEXT_PUBLIC_PNS_SUBSCRIBE_ENDPOINT: 7 | SOMOD::Parameter: pns.subscribe.endpoint 8 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/.somod/serverless/.functions/push-notification/segregatemessage.js: -------------------------------------------------------------------------------- 1 | import { getMiddlewareHandler } from "somod-middleware"; 2 | import lambdaFn from "../../../../build/serverless/functions/segregatemessage"; 3 | const handler = getMiddlewareHandler(lambdaFn, []); 4 | export default handler; 5 | -------------------------------------------------------------------------------- /packages/somod/README.md: -------------------------------------------------------------------------------- 1 | # SOMOD 2 | 3 | > **S**erverless **O**ptimized **MOD**ule 4 | 5 | SOMOD is a framework to create reusable micro applications using serverless technologies. The framework helps to develop, build and merge multiple micro applications to create a complete solution. 6 | 7 | For usage and guides visit https://somod.dev 8 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-service/.somod/serverless/.functions/push-notification-service/onconnect.js: -------------------------------------------------------------------------------- 1 | import { getMiddlewareHandler } from "somod-middleware"; 2 | import lambdaFn from "../../../../build/serverless/functions/onconnect"; 3 | const handler = getMiddlewareHandler(lambdaFn, []); 4 | export default handler; 5 | -------------------------------------------------------------------------------- /packages/middleware/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "rootDir": "./src", 5 | "module": "ES6", 6 | "declaration": true, 7 | "moduleResolution": "node", 8 | "lib": ["ESNext"], 9 | "esModuleInterop": true, 10 | "skipLibCheck": true 11 | }, 12 | "include": ["src"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-service/.somod/serverless/.functions/push-notification-service/ondisconnect.js: -------------------------------------------------------------------------------- 1 | import { getMiddlewareHandler } from "somod-middleware"; 2 | import lambdaFn from "../../../../build/serverless/functions/ondisconnect"; 3 | const handler = getMiddlewareHandler(lambdaFn, []); 4 | export default handler; 5 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-service/.somod/serverless/.functions/push-notification-service/sendmessage.js: -------------------------------------------------------------------------------- 1 | import { getMiddlewareHandler } from "somod-middleware"; 2 | import lambdaFn from "../../../../build/serverless/functions/sendmessage"; 3 | const handler = getMiddlewareHandler(lambdaFn, []); 4 | export default handler; 5 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/build/parameters.json: -------------------------------------------------------------------------------- 1 | {"parameters":{"pns.publish.endpoint":{"type":"string","description":"Notification service management API endpoint","format":"url","default":""},"pns.subscribe.endpoint":{"type":"string","description":"Notification service websocket API endpoint","format":"url","default":""}}} -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/build/serverless/functions/authorizer.js: -------------------------------------------------------------------------------- 1 | var authorizer = function (event) { 2 | return { 3 | isAuthorized: true, 4 | context: { 5 | id: "dummy", 6 | route: event.request.routekey 7 | } 8 | }; 9 | }; 10 | export default authorizer; 11 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/parameters/generateRootParameters.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "somod-types"; 2 | import { generate } from "../../utils/parameters/generate"; 3 | 4 | export const generateRootParameters = async ( 5 | context: IContext, 6 | override = false 7 | ): Promise => { 8 | await generate(context, override); 9 | }; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | 4 | /packages/lib/coverage 5 | 6 | packages/schema/compiled 7 | packages/schema/schemas 8 | 9 | packages/integration-tests/samples/**/.aws-sam 10 | packages/integration-tests/samples/**/.next 11 | packages/integration-tests/samples/**/samconfig.toml 12 | packages/integration-tests/samples/**/next-env.d.ts 13 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-service/.somod/serverless/.functions/push-notification-service/receivemessage.js: -------------------------------------------------------------------------------- 1 | import { getMiddlewareHandler } from "somod-middleware"; 2 | import lambdaFn from "../../../../build/serverless/functions/receivemessage"; 3 | const handler = getMiddlewareHandler(lambdaFn, []); 4 | export default handler; 5 | -------------------------------------------------------------------------------- /packages/lib/tests/utils.ts: -------------------------------------------------------------------------------- 1 | export { 2 | createFiles, 3 | createTempDir, 4 | deleteDir, 5 | readFiles, 6 | unixStylePath 7 | } from "nodejs-file-utils"; 8 | 9 | export const mockedFunction = unknown>( 10 | f: T 11 | ): jest.MockedFunction => { 12 | return f as jest.MockedFunction; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/middleware/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist/cjs", 4 | "rootDir": "./src", 5 | "module": "CommonJS", 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "lib": ["ESNext"], 9 | "esModuleInterop": true, 10 | "skipLibCheck": true 11 | }, 12 | "include": ["src"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/schema/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist/cjs", 4 | "rootDir": "./src", 5 | "target": "ES5", 6 | "module": "CommonJS", 7 | "moduleResolution": "node", 8 | "lib": ["ESNext", "DOM"], 9 | "importHelpers": true, 10 | "esModuleInterop": true 11 | }, 12 | "include": ["src"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/.somod/serverless/.functions/push-notification-service/onconnect.js: -------------------------------------------------------------------------------- 1 | import { getMiddlewareHandler } from "somod-middleware"; 2 | import lambdaFn from "../../../../node_modules/push-notification-service/build/serverless/functions/onconnect"; 3 | const handler = getMiddlewareHandler(lambdaFn, []); 4 | export default handler; 5 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/.somod/serverless/.functions/push-notification-service/ondisconnect.js: -------------------------------------------------------------------------------- 1 | import { getMiddlewareHandler } from "somod-middleware"; 2 | import lambdaFn from "../../../../node_modules/push-notification-service/build/serverless/functions/ondisconnect"; 3 | const handler = getMiddlewareHandler(lambdaFn, []); 4 | export default handler; 5 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/.somod/serverless/.functions/push-notification-service/sendmessage.js: -------------------------------------------------------------------------------- 1 | import { getMiddlewareHandler } from "somod-middleware"; 2 | import lambdaFn from "../../../../node_modules/push-notification-service/build/serverless/functions/sendmessage"; 3 | const handler = getMiddlewareHandler(lambdaFn, []); 4 | export default handler; 5 | -------------------------------------------------------------------------------- /packages/create/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "rootDir": "./src", 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "moduleResolution": "node", 8 | "lib": ["ESNext"], 9 | "importHelpers": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true 12 | }, 13 | "include": ["src"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/.somod/serverless/.functions/push-notification-service/receivemessage.js: -------------------------------------------------------------------------------- 1 | import { getMiddlewareHandler } from "somod-middleware"; 2 | import lambdaFn from "../../../../node_modules/push-notification-service/build/serverless/functions/receivemessage"; 3 | const handler = getMiddlewareHandler(lambdaFn, []); 4 | export default handler; 5 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/parameters/index.ts: -------------------------------------------------------------------------------- 1 | export { validateSchema as validateParametersWithSchema } from "./validateSchema"; 2 | export { buildParameters } from "./buildParameters"; 3 | export { generateRootParameters } from "./generateRootParameters"; 4 | export { updateParametersFromSAM } from "./updateParametersFromSAM"; 5 | export { validateParameterValues } from "./validateParameterValues"; 6 | -------------------------------------------------------------------------------- /packages/lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "declaration": true, 5 | "module": "ES2020", 6 | "target": "ES5", 7 | "rootDir": "./src", 8 | "lib": ["ESNext"], 9 | "esModuleInterop": true, 10 | "moduleResolution": "Node", 11 | "importHelpers": true, 12 | "skipLibCheck": true 13 | }, 14 | "include": ["src"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/schema/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "rootDir": "./src", 5 | "declaration": true, 6 | "target": "ES5", 7 | "module": "ES6", 8 | "moduleResolution": "node", 9 | "lib": ["ESNext"], 10 | "importHelpers": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true 13 | }, 14 | "include": ["src"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/integration-tests/proxy/stop.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const { readFile } = require("fs"); 3 | const { join } = require("path"); 4 | const { exec } = require("child_process"); 5 | 6 | readFile(join(__dirname, "pid"), "utf8", (err, pid) => { 7 | exec("kill -9 " + pid, err => { 8 | if (err) { 9 | console.error(err); 10 | process.exitCode = 1; 11 | } 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/lib/jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | testMatch: ["**/tests/**/**.test.ts"], 6 | collectCoverageFrom: ["src/**/*.ts"], 7 | coverageReporters: ["text", "lcov"], 8 | transform: { 9 | "^.+\\.tsx?$": "ts-jest" // for ts & tsx files 10 | }, 11 | modulePathIgnorePatterns: ["/dist/"] 12 | }; 13 | -------------------------------------------------------------------------------- /packages/middleware/jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | testMatch: ["**/tests/**/**.test.ts"], 6 | collectCoverageFrom: ["src/**/*.ts"], 7 | coverageReporters: ["text", "lcov"], 8 | transform: { 9 | "^.+\\.tsx?$": "ts-jest" // for ts & tsx files 10 | }, 11 | modulePathIgnorePatterns: ["/dist/"] 12 | }; 13 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/common/initializeContext.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "../../utils/context"; 2 | 3 | export const initializeContext = async ( 4 | dir: string, 5 | isUI: boolean, 6 | isServerless: boolean, 7 | isDebugMode: boolean 8 | ) => { 9 | const context = await Context.getInstance( 10 | dir, 11 | isUI, 12 | isServerless, 13 | isDebugMode 14 | ); 15 | return context; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/lib/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./findRootDir"; 2 | 3 | export * from "./tasks/packageJson"; 4 | 5 | export * from "./tasks/tsConfigSomodJson"; 6 | 7 | export * from "./tasks/common"; 8 | 9 | export * from "./tasks/nextJs"; 10 | 11 | export * from "./tasks/serverless"; 12 | 13 | export * from "./tasks/parameters"; 14 | 15 | export * from "./tasks/extension"; 16 | 17 | export * from "./utils/constants"; 18 | -------------------------------------------------------------------------------- /packages/template/serverless/template.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=../node_modules/somod-schema/schemas/serverless-template/index.json 2 | 3 | Resources: 4 | SampleParameter: 5 | Type: AWS::SSM::Parameter 6 | Properties: 7 | Name: Hello 8 | Description: "TODO: This is a sample resource, Delete this and add the valid resources for your module" 9 | Type: String 10 | Value: Good Luck 11 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/packageJson/save.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import { file_packageJson } from "../../utils/constants"; 3 | import { saveJsonFileStore } from "nodejs-file-utils"; 4 | import { IContext } from "somod-types"; 5 | 6 | export const save = async (context: IContext): Promise => { 7 | const packageJsonPath = join(context.dir, file_packageJson); 8 | await saveJsonFileStore(packageJsonPath); 9 | }; 10 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/build/parameters.json: -------------------------------------------------------------------------------- 1 | {"parameters":{"pns.publish.endpoint":{"type":"string","description":"Notification service management API endpoint","format":"url","default":""},"pns.subscribe.endpoint":{"type":"string","description":"Notification service websocket API endpoint","format":"url","default":""},"authorizer.jwt.secret":{"type":"string","description":"Secret for signing authorization JWT token"}}} -------------------------------------------------------------------------------- /packages/integration-tests/jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | testMatch: ["**/tests/**/**.test.ts"], 6 | collectCoverageFrom: ["src/**/*.ts"], 7 | coverageReporters: ["text", "lcov"], 8 | testTimeout: 30000, 9 | transform: { 10 | "^.+\\.tsx?$": "ts-jest" // for ts & tsx files 11 | }, 12 | modulePathIgnorePatterns: ["/dist/"] 13 | }; 14 | -------------------------------------------------------------------------------- /packages/lib/src/utils/ErrorSet.ts: -------------------------------------------------------------------------------- 1 | export default class ErrorSet extends Error { 2 | private _errors: Error[] = []; 3 | constructor(errors: Error[]) { 4 | super(errors.map(e => e.message).join("\n")); 5 | 6 | this._errors = errors; 7 | 8 | // Set the prototype explicitly. 9 | Object.setPrototypeOf(this, new.target.prototype); 10 | } 11 | 12 | get errors(): Error[] { 13 | return [...this._errors]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/parameters/validateParameterValues.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "somod-types"; 2 | import { loadAllParameterValues } from "../../utils/parameters/load"; 3 | import { validateParameterValues as validate } from "../../utils/parameters/validate"; 4 | 5 | export const validateParameterValues = async (context: IContext) => { 6 | const parameterValues = await loadAllParameterValues(context); 7 | await validate(context, parameterValues); 8 | }; 9 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/nextJs/nextCommand.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | import { childProcess } from "nodejs-cli-runner"; 4 | 5 | export const nextCommand = async ( 6 | dir: string, 7 | args: string[] 8 | ): Promise => { 9 | await childProcess( 10 | dir, 11 | process.platform === "win32" ? "npx.cmd" : "npx", 12 | ["next", ...args], 13 | { show: "on", return: "off" }, 14 | { show: "on", return: "off" } 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/nextJs/vercelCommand.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | import { childProcess } from "nodejs-cli-runner"; 4 | 5 | export const vercelCommand = async ( 6 | dir: string, 7 | args: string[] 8 | ): Promise => { 9 | await childProcess( 10 | dir, 11 | process.platform === "win32" ? "vercel.cmd" : "vercel", 12 | args, 13 | { show: "on", return: "off" }, 14 | { show: "on", return: "off" } 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/integration-tests/proxy/start.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const { spawn } = require("child_process"); 3 | const { join } = require("path"); 4 | const { writeFile } = require("fs"); 5 | 6 | const proxyServer = spawn(process.argv[0], [join(__dirname, "npm-proxy.js")], { 7 | detached: true, 8 | stdio: "ignore" 9 | }); 10 | 11 | writeFile(join(__dirname, "pid"), proxyServer.pid + "", () => { 12 | // don't do anything 13 | }); 14 | 15 | proxyServer.unref(); 16 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-service/build/parameters.json: -------------------------------------------------------------------------------- 1 | {"parameters":{"pns.publish.endpoint":{"type":"string","description":"Notification service management API endpoint","format":"url","default":""},"pns.subscribe.endpoint":{"type":"string","description":"Notification service websocket API endpoint","format":"url","default":""},"auth.token.endpoint":{"type":"string","description":"Notification service websocket API endpoint","format":"url","default":""}}} -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-service/tsconfig.somod.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowUmdGlobalAccess": true, 4 | "outDir": "build", 5 | "declaration": true, 6 | "target": "ES5", 7 | "module": "ESNext", 8 | "rootDir": "./", 9 | "moduleResolution": "Node", 10 | "esModuleInterop": true, 11 | "importHelpers": true, 12 | "skipLibCheck": true 13 | }, 14 | "include": ["lib", "serverless"] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - develop 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 22 16 | registry-url: https://registry.npmjs.org/ 17 | - run: npm i -g esbuild 18 | - run: npm install 19 | - run: npm test 20 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/parameters/buildParameters.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | import { join } from "path"; 3 | import { IContext } from "somod-types"; 4 | import { file_parametersYaml } from "../../utils/constants"; 5 | import { build } from "../../utils/parameters/build"; 6 | 7 | export const buildParameters = async (context: IContext): Promise => { 8 | if (existsSync(join(context.dir, file_parametersYaml))) { 9 | await build(context); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/parameters.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=../../../schema/schemas/parameters/index.json 2 | 3 | parameters: 4 | pns.publish.endpoint: 5 | type: string 6 | description: Notification service management API endpoint 7 | format: url 8 | default: "" 9 | pns.subscribe.endpoint: 10 | type: string 11 | description: Notification service websocket API endpoint 12 | format: url 13 | default: "" 14 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/tsconfig.somod.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowUmdGlobalAccess": true, 4 | "outDir": "build", 5 | "declaration": true, 6 | "target": "ES5", 7 | "module": "ESNext", 8 | "rootDir": "./", 9 | "moduleResolution": "Node", 10 | "esModuleInterop": true, 11 | "importHelpers": true, 12 | "skipLibCheck": true, 13 | "jsx": "react-jsx" 14 | }, 15 | "include": ["lib", "ui"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/tsconfig.somod.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowUmdGlobalAccess": true, 4 | "outDir": "build", 5 | "declaration": true, 6 | "target": "ES5", 7 | "module": "ESNext", 8 | "rootDir": "./", 9 | "moduleResolution": "Node", 10 | "esModuleInterop": true, 11 | "importHelpers": true, 12 | "skipLibCheck": true, 13 | "jsx": "react-jsx" 14 | }, 15 | "include": ["lib", "serverless", "ui"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/tsConfigSomodJson/isValid.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | import { join } from "path"; 3 | import { IContext } from "somod-types"; 4 | import { file_tsConfigSomodJson } from "../../utils/constants"; 5 | import { validate } from "../../utils/tsConfigSomodJson"; 6 | 7 | export const isValid = async (context: IContext): Promise => { 8 | const tsConfigPath = join(context.dir, file_tsConfigSomodJson); 9 | if (existsSync(tsConfigPath)) { 10 | await validate(context); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/extension/bundle.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | import { join } from "path"; 3 | import { IContext } from "somod-types"; 4 | import { file_extensionTs } from "../../utils/constants"; 5 | import { bundle } from "../../utils/extension/bundle"; 6 | 7 | export const bundleExtension = async (context: IContext, verbose = false) => { 8 | const extensionFilePath = join(context.dir, file_extensionTs); 9 | if (existsSync(extensionFilePath)) { 10 | await bundle(context, verbose); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/nextJs/buildUiConfigYaml.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | import { join } from "path"; 3 | import { IContext } from "somod-types"; 4 | import { file_configYaml, path_ui } from "../../utils/constants"; 5 | import { build } from "../../utils/nextJs/config"; 6 | 7 | export const buildUiConfigYaml = async (context: IContext): Promise => { 8 | const configYamlPath = join(context.dir, path_ui, file_configYaml); 9 | if (existsSync(configYamlPath)) { 10 | await build(context.dir); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /packages/template/tsconfig.somod.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowUmdGlobalAccess": true, 4 | "outDir": "build", 5 | "declaration": true, 6 | "target": "ES5", 7 | "module": "ESNext", 8 | "rootDir": "./", 9 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 10 | "moduleResolution": "Node", 11 | "esModuleInterop": true, 12 | "importHelpers": true, 13 | "skipLibCheck": true, 14 | "jsx": "react-jsx" 15 | }, 16 | "include": ["lib", "ui", "serverless"], 17 | "exclude": [] 18 | } 19 | -------------------------------------------------------------------------------- /packages/somod/src/index.ts: -------------------------------------------------------------------------------- 1 | import { rootCommand } from "nodejs-cli-runner"; 2 | import buildCommand from "./commands/build"; 3 | import deployCommand from "./commands/deploy"; 4 | import prepareCommand from "./commands/prepare"; 5 | import startCommand from "./commands/start"; 6 | import parametersCommand from "./commands/parameters"; 7 | 8 | const program = rootCommand("somod", [ 9 | buildCommand, 10 | prepareCommand, 11 | deployCommand, 12 | startCommand, 13 | parametersCommand 14 | ]); 15 | 16 | export default program; 17 | -------------------------------------------------------------------------------- /packages/create/src/tasks/gitInit.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | import { join } from "path"; 3 | import { childProcess } from "nodejs-cli-runner"; 4 | 5 | export const gitInit = async (dir: string, verbose: boolean) => { 6 | if (!existsSync(join(dir, ".git"))) { 7 | await childProcess( 8 | dir, 9 | process.platform === "win32" ? "git.exe" : "git", 10 | ["init"], 11 | { show: verbose ? "on" : "error", return: "off" }, 12 | { show: verbose ? "on" : "error", return: "off" } 13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /packages/lib/src/findRootDir.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | import { dirname, join } from "path"; 3 | import { file_packageJson } from "./utils/constants"; 4 | 5 | export const findRootDir = () => { 6 | let cwd = process.cwd(); 7 | while (!existsSync(join(cwd, file_packageJson))) { 8 | const parentDir = dirname(cwd); 9 | if (parentDir == cwd) { 10 | throw new Error( 11 | "fatal: not a npm package (or any of the parent directories)" 12 | ); 13 | } 14 | cwd = parentDir; 15 | } 16 | return cwd; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/nextJs/validateUiConfigYaml.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "somod-types"; 2 | import { existsSync } from "fs"; 3 | import { join } from "path"; 4 | import { file_configYaml, path_ui } from "../../utils/constants"; 5 | import { validate } from "../../utils/nextJs/config"; 6 | 7 | export const validateUiConfigYaml = async ( 8 | context: IContext 9 | ): Promise => { 10 | const configYamlPath = join(context.dir, path_ui, file_configYaml); 11 | if (existsSync(configYamlPath)) { 12 | await validate(context); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /packages/create/src/tasks/npmInit.ts: -------------------------------------------------------------------------------- 1 | import { childProcess } from "nodejs-cli-runner"; 2 | 3 | export const npmInit = async ( 4 | dir: string, 5 | verbose: boolean, 6 | prompt: boolean 7 | ) => { 8 | const args = []; 9 | if (!prompt) { 10 | args.push("--yes"); 11 | } 12 | await childProcess( 13 | dir, 14 | process.platform === "win32" ? "npm.cmd" : "npm", 15 | ["init", ...args], 16 | { show: prompt || verbose ? "on" : "error", return: "off" }, 17 | { show: prompt || verbose ? "on" : "error", return: "off" } 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/template/ui/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | const Document = () => { 4 | return ( 5 | 6 | 7 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default Document; 21 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/serverless/buildServerlessTemplate.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | import { join } from "path"; 3 | import { IContext } from "somod-types"; 4 | import { file_templateYaml, path_serverless } from "../../utils/constants"; 5 | import { build } from "../../utils/serverless/serverlessTemplate/build"; 6 | 7 | export const buildServerlessTemplate = async (context: IContext) => { 8 | const templatePath = join(context.dir, path_serverless, file_templateYaml); 9 | if (existsSync(templatePath)) { 10 | await build(context.dir); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/serverless/index.ts: -------------------------------------------------------------------------------- 1 | export { validateSchema as validateServerlessTemplateWithSchema } from "./validateSchema"; 2 | export { validateServerlessTemplate } from "./validateServerlessTemplate"; 3 | export { buildServerlessTemplate } from "./buildServerlessTemplate"; 4 | export { bundleFunctions } from "./bundleFunctions"; 5 | export { bundleFunctionLayers } from "./bundleFunctionLayers"; 6 | export { prepareSAMTemplate } from "./prepareSAMTemplate"; 7 | export { samDeploy } from "./samDeploy"; 8 | export { validateFunctionExports } from "./validateFunctionExports"; 9 | -------------------------------------------------------------------------------- /packages/types/src/Module.ts: -------------------------------------------------------------------------------- 1 | export type Module = Readonly<{ 2 | name: string; 3 | version: string; 4 | packageLocation: string; 5 | root?: boolean; 6 | }>; 7 | 8 | export type ModuleNode = Readonly<{ 9 | module: Module; 10 | parents: ModuleNode[]; 11 | children: ModuleNode[]; 12 | }>; 13 | 14 | export interface IModuleHandler { 15 | get rootModuleName(): string; 16 | 17 | getModule(moduleName: string): ModuleNode; 18 | 19 | /** 20 | * @returns the list of modules , all parents are listed before children 21 | */ 22 | get list(): ModuleNode[]; 23 | } 24 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/parameters.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=../../../schema/schemas/parameters/index.json 2 | 3 | parameters: 4 | pns.publish.endpoint: 5 | type: string 6 | description: Notification service management API endpoint 7 | format: url 8 | default: "" 9 | pns.subscribe.endpoint: 10 | type: string 11 | description: Notification service websocket API endpoint 12 | format: url 13 | default: "" 14 | authorizer.jwt.secret: 15 | type: string 16 | description: Secret for signing authorization JWT token 17 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/common/deleteBuildDir.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import rimraf from "rimraf"; 3 | import { IContext } from "somod-types"; 4 | import { path_build } from "../../utils/constants"; 5 | 6 | export const deleteBuildDir = (context: IContext): Promise => { 7 | return new Promise((resolve, reject) => { 8 | rimraf(join(context.dir, path_build), { disableGlob: true }, err => { 9 | /* istanbul ignore if reason: its ok here */ 10 | if (err) { 11 | reject(err); 12 | } else { 13 | resolve(); 14 | } 15 | }); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/nextJs/buildUiPublic.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | import { join } from "path"; 3 | import { path_build, path_public, path_ui } from "../../utils/constants"; 4 | import { copyDirectory } from "nodejs-file-utils"; 5 | import { IContext } from "somod-types"; 6 | 7 | export const buildUiPublic = async (context: IContext): Promise => { 8 | const source = join(context.dir, path_ui, path_public); 9 | const target = join(context.dir, path_build, path_ui, path_public); 10 | if (existsSync(source)) { 11 | await copyDirectory(source, target); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | 4 | packages/schema/compiled 5 | packages/schema/schemas 6 | 7 | packages/template/.aws-sam 8 | packages/template/.next 9 | packages/template/.somod 10 | packages/template/build 11 | packages/template/parameters.json 12 | packages/template/tsconfig.json 13 | packages/template/pages 14 | packages/template/public 15 | packages/template/next-env.d.ts 16 | packages/template/.env 17 | packages/template/next.config.js 18 | packages/template/samconfig.toml 19 | packages/template/template.yaml 20 | packages/template/tsconfig.somod.json 21 | 22 | packages/integration-tests/samples 23 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/build/lib/subscribe.js: -------------------------------------------------------------------------------- 1 | export var subscribe = function (cb, subProtocols) { 2 | return new Promise(function (resolve, reject) { 3 | var ws = new WebSocket(process.env.NEXT_PUBLIC_PNS_SUBSCRIBE_ENDPOINT, subProtocols); 4 | ws.addEventListener("open", function () { 5 | ws.addEventListener("message", function (e) { 6 | cb(e.data); 7 | }); 8 | resolve(ws); 9 | }); 10 | ws.addEventListener("error", function (e) { 11 | reject(e); 12 | }); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-service/parameters.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=../../../schema/schemas/parameters/index.json 2 | 3 | parameters: 4 | pns.publish.endpoint: 5 | type: string 6 | description: Notification service management API endpoint 7 | format: url 8 | default: "" 9 | pns.subscribe.endpoint: 10 | type: string 11 | description: Notification service websocket API endpoint 12 | format: url 13 | default: "" 14 | auth.token.endpoint: 15 | type: string 16 | description: Notification service websocket API endpoint 17 | format: url 18 | default: "" 19 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "push-notification-ui", 3 | "version": "1.0.0", 4 | "somod": "1.17.2", 5 | "description": "Sample somod module for integration testing", 6 | "module": "build/lib/index.js", 7 | "typings": "build/lib/index.d.ts", 8 | "files": [ 9 | "build" 10 | ], 11 | "sideEffects": false, 12 | "scripts": { 13 | "build": "npx somod build --ui -v" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^18.0.27", 17 | "react": "^18.2.0", 18 | "next": "^14.0.3", 19 | "react-dom": "^18.2.0", 20 | "somod": "*" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/types/src/Namespace.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "./Context"; 2 | import { Module } from "./Module"; 3 | 4 | export type Namespace = { 5 | name: string; 6 | values: string[]; 7 | }; 8 | 9 | export type ModuleNamespace = { 10 | name: string; 11 | module: string; 12 | value: string; 13 | }; 14 | 15 | export type NamespaceLoader = ( 16 | module: Module, 17 | context: IContext // context at this time will have only moduleHandler and extensionHandler initialized 18 | ) => Promise; 19 | 20 | export interface INamespaceHandler { 21 | get names(): string[]; 22 | get(name: string): ModuleNamespace[]; 23 | } 24 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/lib/subscribe.ts: -------------------------------------------------------------------------------- 1 | export const subscribe = ( 2 | cb: (message: unknown) => void, 3 | subProtocols?: string | string[] 4 | ): Promise => { 5 | return new Promise((resolve, reject) => { 6 | const ws = new WebSocket( 7 | process.env.NEXT_PUBLIC_PNS_SUBSCRIBE_ENDPOINT, 8 | subProtocols 9 | ); 10 | ws.addEventListener("open", () => { 11 | ws.addEventListener("message", e => { 12 | cb(e.data); 13 | }); 14 | resolve(ws); 15 | }); 16 | ws.addEventListener("error", e => { 17 | reject(e); 18 | }); 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/parameters/validateSchema.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import { file_parametersYaml } from "../../utils/constants"; 3 | import { yamlSchemaValidator } from "../../utils/yamlSchemaValidator"; 4 | import parametersValidator from "somod-schema/compiled/parameters"; 5 | import { IContext } from "somod-types"; 6 | import { existsSync } from "fs"; 7 | 8 | export const validateSchema = async (context: IContext): Promise => { 9 | const parametersPath = join(context.dir, file_parametersYaml); 10 | if (existsSync(parametersPath)) { 11 | await yamlSchemaValidator(parametersValidator, parametersPath); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/lib/notify.ts: -------------------------------------------------------------------------------- 1 | export const notify = async (data: { 2 | message: string; 3 | audience: { 4 | userId?: string; 5 | groupId?: string; 6 | }; 7 | }): Promise => { 8 | const res = await fetch( 9 | process.env.NEXT_PUBLIC_PNS_PUBLISH_ENDPOINT + "/notify", 10 | { 11 | method: "POST", 12 | body: JSON.stringify(data), 13 | headers: { "Content-Type": "application/json" } 14 | } 15 | ); 16 | if (res.status != 200) { 17 | throw new Error("Notify failed : " + res.status); 18 | } 19 | const result = await res.json(); 20 | return result.messageId; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/schema/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./common/keywords"; 2 | export * from "./serverless-template/components/expression"; 3 | export * from "./serverless-template/components/keywords"; 4 | export * from "./serverless-template/resources/all"; 5 | export * from "./serverless-template/resources/custom"; 6 | export * from "./serverless-template/resources/function"; 7 | export * from "./serverless-template/resources/functionLayer"; 8 | export * from "./ui-config/index"; 9 | export { default as parameters } from "./parameters/index"; 10 | export { default as serverlessTemplate } from "./serverless-template/index"; 11 | export { default as uiConfig } from "./ui-config/index"; 12 | -------------------------------------------------------------------------------- /packages/types/src/Context.ts: -------------------------------------------------------------------------------- 1 | import { IModuleHandler } from "./Module"; 2 | import { IServerlessTemplateHandler } from "./ServerlessTemplate"; 3 | import { IExtensionHandler } from "./Extension"; 4 | import { INamespaceHandler } from "./Namespace"; 5 | 6 | export interface IContext { 7 | get dir(): string; 8 | get moduleHandler(): IModuleHandler; 9 | get namespaceHandler(): INamespaceHandler; 10 | get serverlessTemplateHandler(): IServerlessTemplateHandler; 11 | get isUI(): boolean; 12 | get isServerless(): boolean; 13 | get isDebugMode(): boolean; 14 | get extensionHandler(): IExtensionHandler; 15 | 16 | getModuleHash(moduleName: string): string; 17 | } 18 | -------------------------------------------------------------------------------- /packages/types/src/Middleware.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "aws-lambda"; 2 | 3 | export interface IMiddlewareContext { 4 | set(key: string, value: unknown): void; 5 | 6 | get(key: string): unknown; 7 | } 8 | 9 | export type EventWithMiddlewareContext> = 10 | TEvent & { 11 | somodMiddlewareContext: IMiddlewareContext; 12 | }; 13 | 14 | export type Middleware< 15 | TEvent extends Record = Record, 16 | TResult = unknown 17 | > = ( 18 | next: () => Promise, 19 | event: Readonly>, 20 | context: Readonly 21 | ) => Promise; 22 | -------------------------------------------------------------------------------- /packages/lib/src/utils/keywords/json-parse.ts: -------------------------------------------------------------------------------- 1 | import { KeywordDefinition } from "somod-types"; 2 | 3 | export const keywordJsonParse: KeywordDefinition = { 4 | keyword: "SOMOD::JsonParse", 5 | getValidator: async () => (keyword, node) => { 6 | const errors: Error[] = []; 7 | if (Object.keys(node.properties).length > 1) { 8 | errors.push( 9 | new Error(`Object with ${keyword} must not have additional properties`) 10 | ); 11 | } 12 | 13 | return errors; 14 | }, 15 | getProcessor: async () => (keyword, node, value) => { 16 | return { 17 | type: "object", 18 | value: JSON.parse(value) 19 | }; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /packages/lib/src/utils/keywords/json-stringify.ts: -------------------------------------------------------------------------------- 1 | import { KeywordDefinition } from "somod-types"; 2 | 3 | export const keywordJsonStringify: KeywordDefinition = { 4 | keyword: "SOMOD::JsonStringify", 5 | getValidator: async () => (keyword, node) => { 6 | const errors: Error[] = []; 7 | if (Object.keys(node.properties).length > 1) { 8 | errors.push( 9 | new Error(`Object with ${keyword} must not have additional properties`) 10 | ); 11 | } 12 | 13 | return errors; 14 | }, 15 | getProcessor: async () => (keyword, node, value) => { 16 | return { 17 | type: "object", 18 | value: JSON.stringify(value) 19 | }; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/nextJs/validateUiConfigYamlWithSchema.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import { file_configYaml, path_ui } from "../../utils/constants"; 3 | import { yamlSchemaValidator } from "../../utils/yamlSchemaValidator"; 4 | import uiConfigValidator from "somod-schema/compiled/ui-config"; 5 | import { IContext } from "somod-types"; 6 | import { existsSync } from "fs"; 7 | 8 | export const validateUiConfigYamlWithSchema = async ( 9 | context: IContext 10 | ): Promise => { 11 | const configFilePath = join(context.dir, path_ui, file_configYaml); 12 | if (existsSync(configFilePath)) { 13 | await yamlSchemaValidator(uiConfigValidator, configFilePath); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /packages/somod/jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | testMatch: ["**/tests/**/**.test.ts"], 6 | collectCoverageFrom: ["src/**/*.ts"], 7 | coverageReporters: ["text", "lcov"], 8 | transformIgnorePatterns: ["node_modules/(?!@somod)"], 9 | transform: { 10 | "^.+\\.tsx?$": "ts-jest", // for ts & tsx files 11 | "^.+\\.jsx?$": [ 12 | "babel-jest", 13 | { plugins: ["@babel/plugin-transform-modules-commonjs"] } 14 | ] // for js & jsx files 15 | }, 16 | modulePathIgnorePatterns: ["/dist/"], 17 | moduleNameMapper: { 18 | "somod-lib": "/node_modules/somod-lib/dist/index.js" 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /packages/template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "incremental": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "Node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve" 21 | }, 22 | "include": [ 23 | "next-env.d.ts", 24 | "**/*.ts", 25 | "**/*.tsx" 26 | ], 27 | "exclude": [ 28 | "node_modules" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/serverless/validateServerlessTemplate.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | import { join } from "path"; 3 | import { IContext } from "somod-types"; 4 | import { file_templateYaml, path_serverless } from "../../utils/constants"; 5 | import { validateServerlessTemplate as _validateServerlessTemplate } from "../../utils/serverless/serverlessTemplate/validate"; 6 | 7 | export const validateServerlessTemplate = async ( 8 | context: IContext 9 | ): Promise => { 10 | const templateYamlPath = join( 11 | context.dir, 12 | path_serverless, 13 | file_templateYaml 14 | ); 15 | if (existsSync(templateYamlPath)) { 16 | await _validateServerlessTemplate(context); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /packages/lib/src/utils/freeze.ts: -------------------------------------------------------------------------------- 1 | import { isArray, isPlainObject } from "lodash"; 2 | 3 | export const freeze = (obj: T, deep = false): T => { 4 | if (!deep) { 5 | return Object.freeze(obj); 6 | } else { 7 | const visited: unknown[] = []; 8 | const queue: unknown[] = [obj]; 9 | 10 | while (queue.length > 0) { 11 | const item = queue.shift(); 12 | if (!visited.includes(item)) { 13 | visited.push(Object.freeze(item)); 14 | if (isPlainObject(item)) { 15 | queue.push(...Object.values(item)); 16 | } else if (isArray(item)) { 17 | queue.push(...item); 18 | } 19 | } 20 | } 21 | return visited[0] as T; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /.github/workflows/publish.to.npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: 22 15 | registry-url: https://registry.npmjs.org/ 16 | - run: npm i -g esbuild 17 | - run: npm install 18 | - run: npm test 19 | - run: npx mono-repo publish -v 20 | env: 21 | NODE_AUTH_TOKEN: ${{secrets.NPM_PUBLIC_TOKEN}} 22 | OTLP_KEY_NAME: ${{secrets.OTLP_KEY_NAME}} 23 | OTLP_KEY_VALUE: ${{secrets.OTLP_KEY_VALUE}} 24 | OTLP_URL: ${{secrets.OTLP_URL}} 25 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "push-notification-service", 3 | "version": "1.0.0", 4 | "somod": "1.15.10", 5 | "description": "Sample somod module for integration testing", 6 | "module": "build/lib/index.js", 7 | "typings": "build/lib/index.d.ts", 8 | "files": [ 9 | "build" 10 | ], 11 | "sideEffects": false, 12 | "scripts": { 13 | "build": "npx somod build --serverless -v" 14 | }, 15 | "dependencies": { 16 | "node-fetch": "^3.3.0", 17 | "uuid": "^9.0.0" 18 | }, 19 | "devDependencies": { 20 | "@types/aws-lambda": "^8.10.110", 21 | "aws-sdk": "^2.1055.0", 22 | "somod": "*", 23 | "somod-middleware": "*" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/schema/scripts/build.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const { getCompiledValidator } = require("decorated-ajv"); 3 | 4 | const { writeFileSync, mkdirSync } = require("fs"); 5 | 6 | const { join } = require("path"); 7 | 8 | const schemasDirectory = join(__dirname, "../schemas"); 9 | mkdirSync(schemasDirectory, { recursive: true }); 10 | 11 | function build(type) { 12 | const schema = require("../dist/cjs/" + type + "/index"); 13 | mkdirSync(join(schemasDirectory, type), { recursive: true }); 14 | writeFileSync( 15 | join(schemasDirectory, type, "index.json"), 16 | JSON.stringify(schema.default, null, 2) 17 | ); 18 | } 19 | 20 | build("parameters"); 21 | build("serverless-template"); 22 | build("ui-config"); 23 | -------------------------------------------------------------------------------- /packages/types/src/KeywordDefinition.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "./Context"; 2 | import { JSONType, KeywordProcessor, KeywordValidator } from "./JsonTemplate"; 3 | 4 | export type GetValidator = ( 5 | rootModuleName: string, 6 | context: IContext 7 | ) => Promise>; 8 | 9 | export type GetProcessor = ( 10 | roodModuleName: string, 11 | context: IContext 12 | ) => Promise>; 13 | 14 | export type KeywordDefinition = { 15 | keyword: string; 16 | getValidator: GetValidator; 17 | getProcessor: GetProcessor; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/types/README.md: -------------------------------------------------------------------------------- 1 | # Type Definitions in SOMOD 2 | 3 | > Typescript type definitions from SOMOD 4 | 5 | > DONT INSTALL THIS PACKAGE DIRECTLY. THIS WILL BE PART OF SOMOD 6 | 7 | ## Usage 8 | 9 | SOMOD CLI uses this internally. 10 | 11 | These types are re-exported from `somod` package. It is recommended to import these types from `somod` for creating plugins 12 | 13 | ```typescript 14 | import { Plugin } from "somod"; 15 | 16 | const myPlugin: Plugin = { 17 | // ... 18 | }; 19 | 20 | export default myPlugin; 21 | ``` 22 | 23 | ## Support 24 | 25 | This project is a part of Open Source Intitiative from [Sodaru Technologies](https://sodaru.com) 26 | 27 | Write an email to opensource@sodaru.com for queries on this project 28 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/serverless/validateSchema.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import { file_templateYaml, path_serverless } from "../../utils/constants"; 3 | import { yamlSchemaValidator } from "../../utils/yamlSchemaValidator"; 4 | import serverlessTemplateValidator from "somod-schema/compiled/serverless-template"; 5 | import { IContext } from "somod-types"; 6 | import { existsSync } from "fs"; 7 | 8 | export const validateSchema = async (context: IContext): Promise => { 9 | const templateYamlPath = join( 10 | context.dir, 11 | path_serverless, 12 | file_templateYaml 13 | ); 14 | if (existsSync(templateYamlPath)) { 15 | await yamlSchemaValidator(serverlessTemplateValidator, templateYamlPath); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "preserve", 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "strict": false, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "incremental": true, 16 | "esModuleInterop": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true 21 | }, 22 | "include": [ 23 | "next-env.d.ts", 24 | "**/*.ts", 25 | "**/*.tsx" 26 | ], 27 | "exclude": [ 28 | "node_modules" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "preserve", 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "strict": false, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "incremental": true, 16 | "esModuleInterop": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true 21 | }, 22 | "include": [ 23 | "next-env.d.ts", 24 | "**/*.ts", 25 | "**/*.tsx" 26 | ], 27 | "exclude": [ 28 | "node_modules" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SOMOD 2 | 3 | > **S**erverless **O**ptimized **MOD**ule 4 | 5 | SOMOD is a framework to create reusable micro applications using serverless technologies. The framework helps to develop, build and merge multiple micro applications to create a complete solution. 6 | 7 | For usage and guides visit https://somod.dev 8 | 9 | This GitHub repository is a mono-repo containing `somod`, `somod-docs`, `create-somod`, and other utility packages. 10 | 11 | ## Issues 12 | 13 | The project issues, features, and milestones are maintained in this GitHub repo. 14 | 15 | Create issues or feature requests at https://github.com/somod-dev/somod/issues 16 | 17 | ## Contributions 18 | 19 | Please read our [CONTRIBUTING](./CONTRIBUTING.md) guide before contributing to this project. 20 | -------------------------------------------------------------------------------- /packages/docs/src/getting-started/build.md: -------------------------------------------------------------------------------- 1 | ```YAML 2 | title: Build SOMOD Module 3 | meta: 4 | description: 5 | Build the SOMOD Module to make it ready for sharing. 6 | ``` 7 | 8 | # Build SOMOD Module 9 | 10 | --- 11 | 12 | After creating the SOMOD module, the module has to be built. The build step validates and creates files ready for sharing inside the `build` directory. 13 | 14 | Use the following command to build the SOMOD module. 15 | 16 | ``` 17 | npx somod build -v 18 | ``` 19 | 20 | The [SOMOD CLI](/reference/cli) guide contains the complete details of this command. 21 | 22 | After the build, The SOMOD module is shipped to the NPM Registry for distribution. In the [Next Chapter](/getting-started/ship), let us understand how to ship the module to npm registries. 23 | -------------------------------------------------------------------------------- /packages/docs/src/reference.md: -------------------------------------------------------------------------------- 1 | ```YAML 2 | title: SOMOD Reference 3 | meta: 4 | description: 5 | This documentation contains manual pages for the main concepts, CLI tool, and extnsion framework of the SOMOD 6 | 7 | ``` 8 | 9 | # SOMOD Reference 10 | 11 | --- 12 | 13 | > **If you are new to SOMOD** 14 | > Follow the [Getting Started](/getting-started) guide to get an handson on the usage of SOMOD 15 | 16 | SOMOD (aka **S**erverless **O**ptimized **MOD**ules) provides a CLI toolset for modularizing the code for serverless infrastructure. 17 | 18 | This documentation contains manual pages for the main concepts, CLI tool, and extension framework of the SOMOD. 19 | 20 | Let us start with understanding the main concepts of SOMOD in the [next chapter](/reference/main-concepts) 21 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/nextJs/deletePagesAndPublicDir.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import rimraf from "rimraf"; 3 | import { IContext } from "somod-types"; 4 | import { path_pages, path_public } from "../../utils/constants"; 5 | 6 | const rimrafAsync = (dirToBeDeleted: string) => { 7 | return new Promise((resolve, reject) => { 8 | rimraf(dirToBeDeleted, { disableGlob: true }, err => { 9 | /* istanbul ignore if reason: its ok here */ 10 | if (err) { 11 | reject(err); 12 | } else { 13 | resolve(); 14 | } 15 | }); 16 | }); 17 | }; 18 | 19 | export const deletePagesAndPublicDir = async (context: IContext) => { 20 | await rimrafAsync(join(context.dir, path_pages)); 21 | await rimrafAsync(join(context.dir, path_public)); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/lib/src/utils/keywords/ajv-compile.ts: -------------------------------------------------------------------------------- 1 | import { getCompiledValidator } from "decorated-ajv"; 2 | import { JSONObjectType, KeywordDefinition } from "somod-types"; 3 | 4 | export const keywordAjvCompile: KeywordDefinition = { 5 | keyword: "SOMOD::AjvCompile", 6 | 7 | getValidator: async () => (keyword, node) => { 8 | const errors: Error[] = []; 9 | if (Object.keys(node.properties).length > 1) { 10 | errors.push( 11 | new Error(`Object with ${keyword} must not have additional properties`) 12 | ); 13 | } 14 | return errors; 15 | }, 16 | 17 | getProcessor: async () => async (keyword, node, value) => { 18 | return { 19 | type: "object", 20 | value: await getCompiledValidator(value) 21 | }; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /packages/lib/src/utils/parameters/build.ts: -------------------------------------------------------------------------------- 1 | import { mkdir, writeFile } from "fs/promises"; 2 | import { readYamlFileStore } from "nodejs-file-utils"; 3 | import { dirname, join } from "path"; 4 | import { IContext } from "somod-types"; 5 | import { 6 | file_parametersJson, 7 | file_parametersYaml, 8 | path_build 9 | } from "../constants"; 10 | 11 | export const build = async (context: IContext): Promise => { 12 | const parameters = 13 | (await readYamlFileStore(join(context.dir, file_parametersYaml))) || {}; 14 | const parametersBuildPath = join( 15 | context.dir, 16 | path_build, 17 | file_parametersJson 18 | ); 19 | await mkdir(dirname(parametersBuildPath), { recursive: true }); 20 | await writeFile(parametersBuildPath, JSON.stringify(parameters)); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/lib/src/utils/serverless/keywords/moduleName.ts: -------------------------------------------------------------------------------- 1 | import { KeywordDefinition } from "somod-types"; 2 | 3 | export const keywordModuleName: KeywordDefinition = { 4 | keyword: "SOMOD::ModuleName", 5 | 6 | getValidator: async () => (keyword, node, value) => { 7 | const errors: Error[] = []; 8 | 9 | if (Object.keys(node.properties).length > 1) { 10 | errors.push( 11 | new Error(`Object with ${keyword} must not have additional properties`) 12 | ); 13 | } else if (value !== true) { 14 | errors.push(new Error(`${keyword} value must equal to true`)); 15 | } 16 | 17 | return errors; 18 | }, 19 | 20 | getProcessor: async moduleName => () => { 21 | return { 22 | type: "object", 23 | value: moduleName 24 | }; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /packages/lib/src/utils/parameters/namespace.ts: -------------------------------------------------------------------------------- 1 | import { IContext, Module, NamespaceLoader } from "somod-types"; 2 | import { namespace_parameter } from "../constants"; 3 | import { loadParameters } from "./load"; 4 | 5 | export const loadParameterNamespaces: NamespaceLoader = async ( 6 | module: Module 7 | ) => { 8 | const parameters = await loadParameters(module); 9 | return [ 10 | { 11 | name: namespace_parameter, 12 | values: Object.keys(parameters?.parameters || {}) 13 | } 14 | ]; 15 | }; 16 | 17 | export const getParameterToModuleMap = (context: IContext) => { 18 | const parameters = context.namespaceHandler.get(namespace_parameter); 19 | const parameterToModuleMap = Object.fromEntries( 20 | parameters.map(p => [p.value, p.module]) 21 | ); 22 | 23 | return parameterToModuleMap; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/schema/README.md: -------------------------------------------------------------------------------- 1 | # JSON Schema in SOMOD 2 | 3 | > JSON Schema for `parameters.yaml`, `ui/config.yaml` and `serverless/template.yaml` files in SOMOD 4 | 5 | ## Install 6 | 7 | ``` 8 | npm i somod-schema; 9 | ``` 10 | 11 | ## Usage 12 | 13 | SOMOD CLI uses this schema internally to validate the file during build. 14 | 15 | Use this package in the custom plugin to extend the default schema to work with more contraints. 16 | 17 | ```typescript 18 | import { parameters, serverlessTemplate, uiConfig } from "somod-schema"; 19 | 20 | // parameters, serverlessTemplate, uiConfig are objects of JSONSchema7 type 21 | ``` 22 | 23 | ## Support 24 | 25 | This project is a part of Open Source Intitiative from [Sodaru Technologies](https://sodaru.com) 26 | 27 | Write an email to opensource@sodaru.com for queries on this project 28 | -------------------------------------------------------------------------------- /packages/somod/src/utils/common.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "nodejs-cli-runner"; 2 | 3 | export type SOMODCommandTypeOptions = { 4 | ui: boolean; 5 | serverless: boolean; 6 | }; 7 | 8 | export type DebugModeOptions = { 9 | debug: boolean; 10 | }; 11 | 12 | export const addSOMODCommandTypeOptions = (command: Command) => { 13 | command.option("--ui", "only ui"); 14 | command.option("--serverless", "only serverless"); 15 | }; 16 | 17 | export const addDebugOptions = (command: Command) => { 18 | command.option("-d, --debug", "Enable Debug mode"); 19 | }; 20 | 21 | export const getSOMODCommandTypeOptions = ( 22 | options: SOMODCommandTypeOptions 23 | ): SOMODCommandTypeOptions => { 24 | const ui = !options.serverless || options.ui; 25 | const serverless = !options.ui || options.serverless; 26 | return { ui, serverless }; 27 | }; 28 | -------------------------------------------------------------------------------- /packages/lib/src/utils/yamlSchemaValidator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CompiledValidateFunction, 3 | validate as jsonValidator 4 | } from "decorated-ajv"; 5 | import { existsSync } from "fs"; 6 | import { readYamlFileStore } from "nodejs-file-utils"; 7 | 8 | export const yamlSchemaValidator = async ( 9 | schemaValidator: CompiledValidateFunction, 10 | yamlFilePath: string 11 | ): Promise => { 12 | if (existsSync(yamlFilePath)) { 13 | const yamlContentAsJson = await readYamlFileStore(yamlFilePath); 14 | const violations = await jsonValidator(schemaValidator, yamlContentAsJson); 15 | if (violations.length > 0) { 16 | throw new Error( 17 | `${yamlFilePath} has following errors\n${violations 18 | .map(v => " " + (v.path + " " + v.message).trim()) 19 | .join("\n")}` 20 | ); 21 | } 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /packages/somod/bin-src/build.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const { join, dirname } = require("path"); 3 | const { mkdir, writeFile, chmod } = require("fs/promises"); 4 | 5 | const template = `#!/usr/bin/env node 6 | /* eslint-disable */ 7 | require("cli-opentelemetry").tele( 8 | "somod", 9 | require("path").join(__dirname, "../dist/index.js"), 10 | "${process.env.GITHUB_REF_NAME || ""}", 11 | "${process.env.OTLP_URL || ""}", 12 | { "${process.env.OTLP_KEY_NAME || ""}": "${ 13 | process.env.OTLP_KEY_VALUE || "" 14 | }" }, 15 | 2 * 60 * 1000 // 2 minutes 16 | ); 17 | `; 18 | 19 | async function build() { 20 | const binFilePath = join(__dirname, "../bin/somod.js"); 21 | await mkdir(dirname(binFilePath), { recursive: true }); 22 | await writeFile(binFilePath, template); 23 | await chmod(binFilePath, 0755); 24 | } 25 | 26 | build(); 27 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "push-notification", 3 | "version": "1.0.0", 4 | "somod": "1.17.2", 5 | "description": "Sample somod module for integration testing", 6 | "module": "build/lib/index.js", 7 | "typings": "build/lib/index.d.ts", 8 | "files": [ 9 | "build" 10 | ], 11 | "sideEffects": false, 12 | "scripts": { 13 | "build": "npx somod build -v" 14 | }, 15 | "dependencies": { 16 | "push-notification-service": "1.0.0", 17 | "push-notification-ui": "1.0.0" 18 | }, 19 | "devDependencies": { 20 | "@types/aws-lambda": "^8.10.110", 21 | "next": "^14.0.3", 22 | "react-dom": "^18.2.0", 23 | "aws-sdk": "^2.1055.0", 24 | "@types/react": "^18.0.27", 25 | "react": "^18.2.0", 26 | "somod": "*", 27 | "somod-middleware": "*" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/lib/src/utils/serverless/keywords/createIf.ts: -------------------------------------------------------------------------------- 1 | import { KeywordDefinition } from "somod-types"; 2 | import { getPath } from "../../jsonTemplate"; 3 | 4 | export const keywordCreateIf: KeywordDefinition = { 5 | keyword: "SOMOD::CreateIf", 6 | 7 | getValidator: async () => (keyword, node) => { 8 | const errors: Error[] = []; 9 | 10 | const path = getPath(node); 11 | if (!(path.length == 2 && path[0] == "Resources")) { 12 | errors.push(new Error(`${keyword} is allowed only as Resource Property`)); 13 | } 14 | 15 | //NOTE: structure of the value is validated by serverless-schema 16 | 17 | return errors; 18 | }, 19 | 20 | getProcessor: async () => (keyword, node, value) => { 21 | return { 22 | type: "keyword", 23 | value: value ? {} : { Condition: "SkipCreation" } 24 | }; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /packages/lib/src/utils/packageJson.ts: -------------------------------------------------------------------------------- 1 | import { join, normalize } from "path"; 2 | import { file_packageJson } from "./constants"; 3 | import { 4 | readJsonFileStore, 5 | updateJsonFileStore, 6 | unixStylePath 7 | } from "nodejs-file-utils"; 8 | 9 | export const read = async (dir: string): Promise> => { 10 | const packageJsonPath = join(dir, file_packageJson); 11 | const packageJsonContent = await readJsonFileStore(packageJsonPath); 12 | return packageJsonContent; 13 | }; 14 | 15 | export const update = ( 16 | dir: string, 17 | packageJson: Record 18 | ): void => { 19 | const packageJsonPath = join(dir, file_packageJson); 20 | updateJsonFileStore(packageJsonPath, packageJson); 21 | }; 22 | 23 | export const packageJsonPath = (dir: string): string => { 24 | return unixStylePath(normalize(join(dir, file_packageJson))); 25 | }; 26 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/serverless/samDeploy.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | import { childProcess } from "nodejs-cli-runner"; 4 | 5 | export const samDeploy = async ( 6 | dir: string, 7 | verbose = false, 8 | guided = false 9 | ): Promise => { 10 | await childProcess( 11 | dir, 12 | process.platform === "win32" ? "sam.cmd" : "sam", 13 | ["build"], 14 | { show: verbose ? "on" : "error", return: "off" }, 15 | { show: verbose ? "on" : "error", return: "off" } 16 | ); 17 | 18 | const deployArgs = ["deploy"]; 19 | if (guided) { 20 | deployArgs.push("--guided"); 21 | } 22 | 23 | await childProcess( 24 | dir, 25 | process.platform === "win32" ? "sam.cmd" : "sam", 26 | deployArgs, 27 | { show: verbose || guided ? "on" : "error", return: "off" }, 28 | { show: verbose || guided ? "on" : "error", return: "off" } 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-service/serverless/functions/ondisconnect.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyWebsocketHandlerV2 } from "aws-lambda"; 2 | import { DynamoDB } from "aws-sdk"; 3 | 4 | const ddb = new DynamoDB.DocumentClient({ 5 | apiVersion: "2012-08-10", 6 | region: process.env.AWS_REGION 7 | }); 8 | 9 | const handler: APIGatewayProxyWebsocketHandlerV2 = async event => { 10 | try { 11 | await ddb 12 | .delete({ 13 | TableName: process.env.TABLE_NAME as string, 14 | Key: { 15 | connectionId: event.requestContext.connectionId 16 | } 17 | }) 18 | .promise(); 19 | } catch (err) { 20 | return { 21 | statusCode: 500, 22 | body: "Failed to disconnect: " + JSON.stringify(err) 23 | }; 24 | } 25 | 26 | return { statusCode: 200, body: "Disconnected." }; 27 | }; 28 | 29 | export default handler; 30 | -------------------------------------------------------------------------------- /packages/lib/src/utils/serverless/serverlessTemplate/build.ts: -------------------------------------------------------------------------------- 1 | import { mkdir, writeFile } from "fs/promises"; 2 | import { readYamlFileStore } from "nodejs-file-utils"; 3 | import { dirname, join } from "path"; 4 | import { 5 | file_templateJson, 6 | file_templateYaml, 7 | path_build, 8 | path_serverless 9 | } from "../../constants"; 10 | 11 | /** 12 | * Creates `build/serverless/template.json` from `serverless/template.yaml`. 13 | * 14 | */ 15 | export const build = async (dir: string) => { 16 | const sourcePath = join(dir, path_serverless, file_templateYaml); 17 | const template = await readYamlFileStore(sourcePath); 18 | const targetFilePath = join( 19 | dir, 20 | path_build, 21 | path_serverless, 22 | file_templateJson 23 | ); 24 | await mkdir(dirname(targetFilePath), { recursive: true }); 25 | await writeFile(targetFilePath, JSON.stringify(template)); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/ui/pages/messages.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, useReducer, useEffect } from "react"; 2 | import { subscribe } from "push-notification-ui"; 3 | 4 | const Messages: FunctionComponent = () => { 5 | const [messages, addMessage] = useReducer( 6 | (state: unknown[], data: unknown) => { 7 | return [...state, data]; 8 | }, 9 | [] 10 | ); 11 | 12 | useEffect(() => { 13 | subscribe(message => { 14 | addMessage(message); 15 | }); 16 | }, []); 17 | 18 | return ( 19 |
20 |

Messages

21 |
    22 | {messages.map((message, i) => ( 23 |
  • 24 |
    {JSON.stringify(message, null, 2)}
    25 |
  • 26 | ))} 27 |
28 |
29 | ); 30 | }; 31 | 32 | export default Messages; 33 | -------------------------------------------------------------------------------- /packages/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "somod-docs", 3 | "version": "2.1.1", 4 | "description": "Documentation for SOMOD", 5 | "files": [ 6 | "src" 7 | ], 8 | "homepage": "https://github.com/somod-dev/somod/tree/main/packages/docs#readme", 9 | "author": "Raghavendra K R ", 10 | "contributors": [ 11 | "Raghavendra K R ", 12 | "Lokesh G C ", 13 | "Sukhesh M G " 14 | ], 15 | "bugs": { 16 | "url": "https://github.com/somod-dev/somod/issues" 17 | }, 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "https//github.com/somod-dev/somod.git" 22 | }, 23 | "scripts": { 24 | "test": "echo 'No Tests'" 25 | }, 26 | "keywords": [ 27 | "somod", 28 | "doc", 29 | "docs", 30 | "serverless", 31 | "nextjs", 32 | "aws-sam", 33 | "sodaru" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /packages/create/bin-src/build.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const { join, dirname } = require("path"); 3 | const { mkdir, writeFile, chmod } = require("fs/promises"); 4 | 5 | const template = `#!/usr/bin/env node 6 | /* eslint-disable */ 7 | process.env.CREATE_SOMOD_CLI_PATH = require("path").dirname(__dirname); 8 | 9 | require("cli-opentelemetry").tele( 10 | "create-somod", 11 | require("path").join(__dirname, "../dist/index.js"), 12 | "${process.env.GITHUB_REF_NAME || ""}", 13 | "${process.env.OTLP_URL || ""}", 14 | { "${process.env.OTLP_KEY_NAME || ""}": "${ 15 | process.env.OTLP_KEY_VALUE || "" 16 | }" }, 17 | 3 * 60 * 1000 // 3 minutes 18 | ); 19 | `; 20 | 21 | async function build() { 22 | const binFilePath = join(__dirname, "../bin/create-somod.js"); 23 | await mkdir(dirname(binFilePath), { recursive: true }); 24 | await writeFile(binFilePath, template); 25 | await chmod(binFilePath, 0755); 26 | } 27 | 28 | build(); 29 | -------------------------------------------------------------------------------- /packages/create/src/tasks/createTemplateFiles.ts: -------------------------------------------------------------------------------- 1 | import { copyFile } from "fs/promises"; 2 | import { copyDirectory } from "nodejs-file-utils"; 3 | import { join } from "path"; 4 | 5 | export const createTemplateFiles = async ( 6 | dir: string, 7 | serverless: boolean, 8 | ui: boolean 9 | ) => { 10 | const templatePath = join(dir, "node_modules/somod-template"); 11 | 12 | await copyDirectory(join(templatePath, "lib"), join(dir, "lib")); 13 | await copyFile( 14 | join(templatePath, "parameters.yaml"), 15 | join(dir, "parameters.yaml") 16 | ); 17 | await copyFile( 18 | join(templatePath, "tsconfig.somod.json"), 19 | join(dir, "tsconfig.somod.json") 20 | ); 21 | if (serverless) { 22 | await copyDirectory( 23 | join(templatePath, "serverless"), 24 | join(dir, "serverless") 25 | ); 26 | } 27 | if (ui) { 28 | await copyDirectory(join(templatePath, "ui"), join(dir, "ui")); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /packages/schema/src/parameters/index.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7 } from "decorated-ajv"; 2 | 3 | const parameters: JSONSchema7 = { 4 | $schema: "http://json-schema.org/draft-07/schema", 5 | $id: "https://somod.json-schema.sodaru.com/parameters/index.json", 6 | title: "JSON Schema for Parameters in SOMOD", 7 | type: "object", 8 | additionalProperties: false, 9 | properties: { 10 | parameters: { 11 | type: "object", 12 | additionalProperties: { $ref: "http://json-schema.org/draft-07/schema" }, 13 | propertyNames: { 14 | pattern: "^[a-zA-Z][a-zA-Z0-9-_.]*$", 15 | maxLength: 128, 16 | minLength: 4, 17 | errorMessage: { 18 | pattern: 19 | "Parameter Name must contain only alphaNumerics, dot(.), hyphen(-), and underscore(_). must start with alphabet" 20 | } 21 | }, 22 | maxProperties: 64 23 | } 24 | } 25 | }; 26 | 27 | export default parameters; 28 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/nextJs/index.ts: -------------------------------------------------------------------------------- 1 | export { watchRootModulePages } from "./watchRootModulePages"; 2 | export { watchRootModulePagesData } from "./watchRootModulePagesData"; 3 | export { watchRootModulePublicAssets } from "./watchRootModulePublicAssets"; 4 | export { nextCommand } from "./nextCommand"; 5 | export { vercelCommand } from "./vercelCommand"; 6 | export { createPages } from "./createPages"; 7 | export { createPublicAssets } from "./createPublicAssets"; 8 | export { validateUiConfigYaml } from "./validateUiConfigYaml"; 9 | export { validateUiConfigYamlWithSchema } from "./validateUiConfigYamlWithSchema"; 10 | export { buildUiConfigYaml } from "./buildUiConfigYaml"; 11 | export { generateNextConfig } from "./generateNextConfig"; 12 | export { buildUiPublic } from "./buildUiPublic"; 13 | export { validatePageExports } from "./validatePageExports"; 14 | export { validatePageData } from "./validatePageData"; 15 | export { deletePagesAndPublicDir } from "./deletePagesAndPublicDir"; 16 | -------------------------------------------------------------------------------- /packages/schema/scripts/compile.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const { getCompiledValidator } = require("decorated-ajv"); 3 | 4 | const { writeFileSync, mkdirSync } = require("fs"); 5 | 6 | const { join } = require("path"); 7 | const { exit } = require("process"); 8 | 9 | const compiledDirectory = join(__dirname, "../compiled"); 10 | mkdirSync(compiledDirectory, { recursive: true }); 11 | 12 | function compile(type) { 13 | const schema = require("../schemas/" + type + "/index.json"); 14 | return new Promise((resolve, reject) => { 15 | getCompiledValidator(schema).then(compiledSchema => { 16 | writeFileSync(join(compiledDirectory, type + ".js"), compiledSchema); 17 | resolve(); 18 | }, reject); 19 | }); 20 | } 21 | 22 | Promise.all([ 23 | compile("parameters"), 24 | compile("serverless-template"), 25 | compile("ui-config") 26 | ]).then( 27 | () => { 28 | // do nothing 29 | }, 30 | e => { 31 | console.error(e); 32 | exit(1); 33 | } 34 | ); 35 | -------------------------------------------------------------------------------- /packages/schema/src/serverless-template/resources/custom.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7 } from "decorated-ajv"; 2 | 3 | export const customResourceTypePattern = "^Custom::[A-Z][a-zA-Z0-9]{0,63}$"; 4 | 5 | export const customResource: JSONSchema7 = { 6 | type: "object", 7 | required: ["Type", "Properties"], 8 | anyOf: [ 9 | { 10 | type: "object", 11 | required: ["SOMOD::Extend"] 12 | }, 13 | { 14 | type: "object", 15 | properties: { 16 | Properties: { 17 | type: "object", 18 | required: ["ServiceToken"] 19 | } 20 | } 21 | } 22 | ], 23 | errorMessage: { 24 | anyOf: "When not extended, Properties must have ServiceToken" 25 | }, 26 | properties: { 27 | Type: { type: "string", pattern: customResourceTypePattern }, 28 | Properties: { 29 | type: "object", 30 | properties: { 31 | ServiceToken: { 32 | $ref: "#/definitions/somodRef" 33 | } 34 | } 35 | } 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/build/ui/pages/messages.js: -------------------------------------------------------------------------------- 1 | import { __assign, __spreadArray } from "tslib"; 2 | import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; 3 | import { useReducer, useEffect } from "react"; 4 | import { subscribe } from "push-notification-ui"; 5 | var Messages = function () { 6 | var _a = useReducer(function (state, data) { 7 | return __spreadArray(__spreadArray([], state, true), [data], false); 8 | }, []), messages = _a[0], addMessage = _a[1]; 9 | useEffect(function () { 10 | subscribe(function (message) { 11 | addMessage(message); 12 | }); 13 | }, []); 14 | return (_jsxs("div", __assign({ style: { fontFamily: "Roboto, Arial, sans-serif" } }, { children: [_jsx("h1", { children: "Messages" }), _jsx("ul", { children: messages.map(function (message, i) { return (_jsx("li", { children: _jsx("pre", { children: JSON.stringify(message, null, 2) }) }, i)); }) })] }))); 15 | }; 16 | export default Messages; 17 | -------------------------------------------------------------------------------- /packages/docs/src/_files: -------------------------------------------------------------------------------- 1 | overview 2 | getting-started 3 | getting-started/setup 4 | getting-started/develop 5 | getting-started/develop/add-dependencies 6 | getting-started/develop/serverless 7 | getting-started/develop/ui 8 | getting-started/build 9 | getting-started/ship 10 | getting-started/reuse 11 | getting-started/deploy 12 | reference 13 | reference/main-concepts 14 | reference/main-concepts/module 15 | reference/main-concepts/package.json 16 | reference/main-concepts/directory-structure 17 | reference/main-concepts/lib 18 | reference/main-concepts/serverless 19 | reference/main-concepts/serverless/template.yaml 20 | reference/main-concepts/serverless/functions 21 | reference/main-concepts/serverless/middlewares 22 | reference/main-concepts/ui 23 | reference/main-concepts/ui/config.yaml 24 | reference/main-concepts/yaml-processing 25 | reference/main-concepts/parameters 26 | reference/main-concepts/namespaces 27 | reference/main-concepts/tsconfig.somod.json 28 | reference/main-concepts/extensions 29 | reference/cli 30 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-ui/build/lib/notify.js: -------------------------------------------------------------------------------- 1 | import { __awaiter, __generator } from "tslib"; 2 | export var notify = function (data) { return __awaiter(void 0, void 0, void 0, function () { 3 | var res, result; 4 | return __generator(this, function (_a) { 5 | switch (_a.label) { 6 | case 0: return [4 /*yield*/, fetch(process.env.NEXT_PUBLIC_PNS_PUBLISH_ENDPOINT + "/notify", { 7 | method: "POST", 8 | body: JSON.stringify(data), 9 | headers: { "Content-Type": "application/json" } 10 | })]; 11 | case 1: 12 | res = _a.sent(); 13 | if (res.status != 200) { 14 | throw new Error("Notify failed : " + res.status); 15 | } 16 | return [4 /*yield*/, res.json()]; 17 | case 2: 18 | result = _a.sent(); 19 | return [2 /*return*/, result.messageId]; 20 | } 21 | }); 22 | }); }; 23 | -------------------------------------------------------------------------------- /packages/lib/src/utils/extension/bundle.ts: -------------------------------------------------------------------------------- 1 | import { build as esbuild } from "esbuild"; 2 | import { join } from "path"; 3 | import { IContext } from "somod-types"; 4 | import { file_extensionJs, file_extensionTs, path_build } from "../constants"; 5 | import { read } from "../packageJson"; 6 | 7 | export const bundle = async (context: IContext, verbose = false) => { 8 | const dir = context.dir; 9 | const sourceFile = join(dir, file_extensionTs); 10 | const targetFile = join(dir, path_build, file_extensionJs); 11 | 12 | const packageJson = await read(dir); 13 | await esbuild({ 14 | sourcemap: "inline", 15 | platform: "node", 16 | external: ["tslib"], 17 | minify: true, 18 | target: [ 19 | "node" + context.serverlessTemplateHandler.functionNodeRuntimeVersion 20 | ], 21 | logLevel: verbose ? "verbose" : "silent", 22 | ...((packageJson.ExtensionBuildOptions as Record) || {}), 23 | bundle: true, 24 | entryPoints: [sourceFile], 25 | outfile: targetFile 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/create/README.md: -------------------------------------------------------------------------------- 1 | # create-somod 2 | 3 | > [SOMOD](https://somod.dev) project creation utility 4 | 5 | Initialize the SOMOD project 6 | 7 | ## Usage 8 | 9 | ```bash 10 | npx create-somod 11 | ``` 12 | 13 | ### help 14 | 15 | ```bash 16 | npx create-somod -h 17 | ``` 18 | 19 | ```bash 20 | Usage: create-somod [options] [modName] 21 | 22 | Arguments: 23 | modName module Directory name to create project 24 | (default: "my-module") 25 | 26 | Options: 27 | -v, --verbose enable verbose 28 | --ui Initialize only UI 29 | --serverless Initialize only Serverless 30 | --version somod SDK Version 31 | --template-version somod-template Version 32 | --no-git Skip git initialization 33 | --no-prettier Skip prettier initialization 34 | --no-eslint Skip eslint initialization 35 | --no-files Skip Sample files 36 | --npm-prompt Prompt for input during npm init 37 | -h, --help display help for command 38 | 39 | ``` 40 | -------------------------------------------------------------------------------- /packages/lib/src/utils/keywords/equals.ts: -------------------------------------------------------------------------------- 1 | import { JSONArrayType, KeywordDefinition } from "somod-types"; 2 | import { isArray, isEqual } from "lodash"; 3 | 4 | export const keywordEquals: KeywordDefinition = { 5 | keyword: "SOMOD::Equals", 6 | getValidator: async () => (keyword, node, value) => { 7 | const errors: Error[] = []; 8 | if (Object.keys(node.properties).length > 1) { 9 | errors.push( 10 | new Error(`Object with ${keyword} must not have additional properties`) 11 | ); 12 | } 13 | 14 | if (!isArray(value)) { 15 | errors.push(new Error(`${keyword} value must be array`)); 16 | } else if (value.length != 2) { 17 | errors.push( 18 | new Error(`${keyword} value must be array matching [Value1, Value2]`) 19 | ); 20 | } 21 | return errors; 22 | }, 23 | 24 | getProcessor: async () => (keyword, node, value) => { 25 | const result = isEqual(value[0], value[1]); 26 | return { 27 | type: "object", 28 | value: result 29 | }; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /packages/lib/src/utils/serverless/keywords/resourceName.ts: -------------------------------------------------------------------------------- 1 | import { JSONType, KeywordDefinition } from "somod-types"; 2 | 3 | export const keyword = "SOMOD::ResourceName"; 4 | 5 | export const keywordResourceName: KeywordDefinition = { 6 | keyword: "SOMOD::ResourceName", 7 | 8 | getValidator: async () => (keyword, node, value) => { 9 | const errors: Error[] = []; 10 | 11 | if (Object.keys(node.properties).length > 1) { 12 | errors.push( 13 | new Error(`Object with ${keyword} must not have additional properties`) 14 | ); 15 | } else if (typeof value != "string") { 16 | errors.push(new Error(`${keyword} value must be string`)); 17 | } 18 | 19 | return errors; 20 | }, 21 | 22 | getProcessor: async (moduleName, context) => { 23 | return (keyword, node, value) => { 24 | return { 25 | type: "object", 26 | value: context.serverlessTemplateHandler.getSAMResourceName( 27 | moduleName, 28 | value 29 | ) as JSONType 30 | }; 31 | }; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /packages/create/src/tasks/writeIgnoreFiles.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from "fs/promises"; 2 | import { join } from "path"; 3 | 4 | export const writeIgnoreFiles = async ( 5 | dir: string, 6 | serverless: boolean, 7 | ui: boolean, 8 | eslint: boolean, 9 | prettier: boolean 10 | ) => { 11 | const ignoreFiles = [ 12 | "node_modules", 13 | ".somod", 14 | "/build", 15 | "/parameters.json", 16 | "tsconfig.json" 17 | ]; 18 | if (serverless) { 19 | ignoreFiles.push(".aws-sam", "samconfig.toml", "/template.yaml"); 20 | } 21 | if (ui) { 22 | ignoreFiles.push( 23 | ".next", 24 | "/pages", 25 | "/public", 26 | "next-env.d.ts", 27 | ".env", 28 | "next.config.js" 29 | ); 30 | } 31 | 32 | const ignoreContent = ignoreFiles.join("\n"); 33 | 34 | await writeFile(join(dir, ".gitignore"), ignoreContent); 35 | if (eslint) { 36 | await writeFile(join(dir, ".eslintignore"), ignoreContent); 37 | } 38 | if (prettier) { 39 | await writeFile(join(dir, ".prettierignore"), ignoreContent); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /packages/create/src/tasks/npmInstall.ts: -------------------------------------------------------------------------------- 1 | import { childProcess } from "nodejs-cli-runner"; 2 | 3 | export const npmInstall = async ( 4 | dir: string, 5 | version: string, 6 | templateVersion: string, 7 | serverless: boolean, 8 | ui: boolean, 9 | eslint: boolean, 10 | prettier: boolean 11 | ) => { 12 | const args: string[] = [ 13 | "install", 14 | "somod@" + version, 15 | "somod-template@" + templateVersion 16 | ]; 17 | if (serverless) { 18 | args.push("@types/aws-lambda", "aws-sdk", "somod-middleware"); 19 | } 20 | if (ui) { 21 | args.push("@types/react", "react", "next", "react-dom"); 22 | } 23 | if (eslint) { 24 | args.push("eslint-config-sodaru"); 25 | if (ui) { 26 | args.push("eslint-config-next"); 27 | } 28 | } 29 | if (prettier) { 30 | args.push("prettier-config-sodaru"); 31 | } 32 | args.push("--save-dev"); 33 | 34 | await childProcess( 35 | dir, 36 | process.platform === "win32" ? "npm.cmd" : "npm", 37 | args, 38 | { show: "on", return: "off" }, 39 | { show: "on", return: "off" } 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /packages/lib/src/utils/keywords/if.ts: -------------------------------------------------------------------------------- 1 | import { JSONArrayType, KeywordDefinition } from "somod-types"; 2 | import { isArray } from "lodash"; 3 | 4 | export const keywordIf: KeywordDefinition = { 5 | keyword: "SOMOD::If", 6 | getValidator: async () => (keyword, node, value) => { 7 | const errors: Error[] = []; 8 | if (Object.keys(node.properties).length > 1) { 9 | errors.push( 10 | new Error(`Object with ${keyword} must not have additional properties`) 11 | ); 12 | } 13 | 14 | if (!isArray(value)) { 15 | errors.push(new Error(`${keyword} value must be array`)); 16 | } else if (value.length < 2 || value.length > 3) { 17 | errors.push( 18 | new Error( 19 | `${keyword} value must be array matching [Condition, ValueIfTrue, ValueIfFalse]` 20 | ) 21 | ); 22 | } 23 | return errors; 24 | }, 25 | 26 | getProcessor: async () => (keyword, node, value) => { 27 | const result = value[0] ? value[1] : value[2]; 28 | return { 29 | type: "object", 30 | value: result 31 | }; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /packages/lib/src/utils/keywords/key.ts: -------------------------------------------------------------------------------- 1 | import { JSONArrayType, JSONObjectType, KeywordDefinition } from "somod-types"; 2 | import { isArray } from "lodash"; 3 | 4 | export const keywordKey: KeywordDefinition< 5 | [JSONObjectType | JSONArrayType, string | number] 6 | > = { 7 | keyword: "SOMOD::Key", 8 | 9 | getValidator: async () => (keyword, node, value) => { 10 | const errors: Error[] = []; 11 | if (Object.keys(node.properties).length > 1) { 12 | errors.push( 13 | new Error(`Object with ${keyword} must not have additional properties`) 14 | ); 15 | } 16 | 17 | if (!isArray(value)) { 18 | errors.push(new Error(`${keyword} value must be array`)); 19 | } else if (value.length != 2) { 20 | errors.push( 21 | new Error( 22 | `${keyword} value must be array matching [ArrayOrObject, IndexOrPropertyName]` 23 | ) 24 | ); 25 | } 26 | return errors; 27 | }, 28 | 29 | getProcessor: async () => (keyword, node, value) => { 30 | return { 31 | type: "object", 32 | value: value[0][value[1]] 33 | }; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /packages/lib/tests/findRootDir.test.ts: -------------------------------------------------------------------------------- 1 | import { createFiles, createTempDir, deleteDir } from "./utils"; 2 | import { join } from "path"; 3 | import { findRootDir } from "../src"; 4 | 5 | describe("test findRootDir", () => { 6 | let dir: string; 7 | const originalCwd = process.cwd; 8 | 9 | beforeEach(() => { 10 | dir = createTempDir("test-somod-lib"); 11 | process.cwd = () => dir; 12 | }); 13 | 14 | afterEach(() => { 15 | process.cwd = originalCwd; 16 | deleteDir(dir); 17 | }); 18 | 19 | test("for invalid directory", () => { 20 | expect(() => findRootDir()).toThrowError( 21 | new Error("fatal: not a npm package (or any of the parent directories)") 22 | ); 23 | }); 24 | 25 | test("for valid directory at the root", () => { 26 | createFiles(dir, { "package.json": "" }); 27 | expect(findRootDir()).toEqual(dir); 28 | }); 29 | 30 | test("for valid directory at the child", () => { 31 | createFiles(dir, { "a/package.json": "", "a/b/c/": "" }); 32 | process.cwd = () => join(dir, "a/b/c"); 33 | expect(findRootDir()).toEqual(join(dir, "a")); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/lib/src/utils/keywords/and.ts: -------------------------------------------------------------------------------- 1 | import { JSONArrayType, KeywordDefinition } from "somod-types"; 2 | import { isArray } from "lodash"; 3 | 4 | export const keywordAnd: KeywordDefinition = { 5 | keyword: "SOMOD::And", 6 | 7 | getValidator: async () => (keyword, node, value) => { 8 | const errors: Error[] = []; 9 | if (Object.keys(node.properties).length > 1) { 10 | errors.push( 11 | new Error(`Object with ${keyword} must not have additional properties`) 12 | ); 13 | } 14 | 15 | if (!isArray(value)) { 16 | errors.push(new Error(`${keyword} value must be array`)); 17 | } else if (value.length < 1) { 18 | errors.push(new Error(`${keyword} value must contain atleast 1 value`)); 19 | } 20 | return errors; 21 | }, 22 | 23 | getProcessor: async () => (keyword, node, value) => { 24 | let result = true; 25 | for (const v of value) { 26 | result = result && !!v; 27 | if (result === false) { 28 | break; 29 | } 30 | } 31 | return { 32 | type: "object", 33 | value: result 34 | }; 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /packages/lib/src/utils/keywords/or.ts: -------------------------------------------------------------------------------- 1 | import { JSONArrayType, KeywordDefinition } from "somod-types"; 2 | import { isArray } from "lodash"; 3 | 4 | export const keywordOr: KeywordDefinition = { 5 | keyword: "SOMOD::Or", 6 | 7 | getValidator: async () => (keyword, node, value) => { 8 | const errors: Error[] = []; 9 | if (Object.keys(node.properties).length > 1) { 10 | errors.push( 11 | new Error(`Object with ${keyword} must not have additional properties`) 12 | ); 13 | } 14 | 15 | if (!isArray(value)) { 16 | errors.push(new Error(`${keyword} value must be array`)); 17 | } else if (value.length < 1) { 18 | errors.push(new Error(`${keyword} value must contain atleast 1 value`)); 19 | } 20 | return errors; 21 | }, 22 | 23 | getProcessor: async () => (keyword, node, value) => { 24 | let result = false; 25 | for (const v of value) { 26 | result = result || !!v; 27 | if (result === true) { 28 | break; 29 | } 30 | } 31 | return { 32 | type: "object", 33 | value: result 34 | }; 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sodaru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "somod-template", 3 | "version": "2.1.1", 4 | "description": "Template SOMOD Module", 5 | "module": "build/lib/index.js", 6 | "typings": "build/lib/index.d.ts", 7 | "files": [ 8 | "build", 9 | "lib", 10 | "serverless", 11 | "ui", 12 | "parameters.yaml", 13 | "tsconfig.somod.json" 14 | ], 15 | "sideEffects": false, 16 | "somod": "1.15.10", 17 | "scripts": { 18 | "test": "echo 'No Tests'" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/somod-dev/somod.git" 23 | }, 24 | "author": "Raghavendra K R ", 25 | "contributors": [ 26 | "Raghavendra K R ", 27 | "Lokesh G C ", 28 | "Sukhesh M G " 29 | ], 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/somod-dev/somod/issues" 33 | }, 34 | "homepage": "https://github.com/somod-dev/somod/tree/main/packages/somod-template#readme", 35 | "devDependencies": { 36 | "next": "^12.0.0 || ^13.0.0 || ^14.0.3", 37 | "somod": "^2.1.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/create/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sodaru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/docs/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sodaru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/serverless/functions/segregatemessage.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDB } from "aws-sdk"; 2 | import { DynamoDBRecord, DynamoDBStreamHandler } from "aws-lambda"; 3 | 4 | const ddb = new DynamoDB.DocumentClient({ 5 | apiVersion: "2012-08-10", 6 | region: process.env.AWS_REGION 7 | }); 8 | 9 | const handleRecord = async (record: DynamoDBRecord) => { 10 | const message = DynamoDB.Converter.unmarshall( 11 | record.dynamodb?.NewImage || {} 12 | ) as { 13 | messageId: string; 14 | message: unknown; 15 | audience: { userId?: string; groupId?: string }; 16 | }; 17 | 18 | if (typeof message.message["type"] === "string") { 19 | ddb.put({ 20 | TableName: process.env.MESSAGE_TYPE_TABLE_NAME, 21 | Item: { 22 | type: message.message["type"], 23 | messageId: message.messageId 24 | } 25 | }); 26 | } 27 | }; 28 | 29 | const handler: DynamoDBStreamHandler = async event => { 30 | for (const record of event.Records) { 31 | if (record.eventName == "INSERT") { 32 | await handleRecord(record); 33 | } 34 | } 35 | }; 36 | 37 | export default handler; 38 | -------------------------------------------------------------------------------- /packages/schema/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sodaru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/somod/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sodaru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/types/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sodaru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/nextJs/createPublicAssets.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import { IContext } from "somod-types"; 3 | import { path_build, path_public, path_ui } from "../../utils/constants"; 4 | import { 5 | linkAsset, 6 | getPublicAssetToModuleMap 7 | } from "../../utils/nextJs/publicAssets"; 8 | 9 | export const createPublicAssets = async (context: IContext): Promise => { 10 | const allPublicAssets = getPublicAssetToModuleMap(context); 11 | 12 | await Promise.all( 13 | Object.keys(allPublicAssets).map(async publicAsset => { 14 | const moduleName = allPublicAssets[publicAsset]; 15 | const moduleNode = context.moduleHandler.getModule(moduleName); 16 | const packageLocation = moduleNode.module.packageLocation; 17 | 18 | const sourcePublicAssetPath = join( 19 | packageLocation, 20 | moduleNode.module.root ? "" : path_build, 21 | path_ui, 22 | path_public, 23 | publicAsset 24 | ); 25 | 26 | const publicAssetPath = join(context.dir, path_public, publicAsset); 27 | 28 | await linkAsset(sourcePublicAssetPath, publicAssetPath); 29 | }) 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/middleware/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sodaru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/serverless/validateFunctionExports.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | import { listFiles } from "nodejs-file-utils"; 3 | import { join } from "path"; 4 | import { IContext } from "somod-types"; 5 | import { path_functions, path_serverless } from "../../utils/constants"; 6 | import ErrorSet from "../../utils/ErrorSet"; 7 | import { get as getExports } from "../../utils/exports"; 8 | 9 | export const validateFunctionExports = async ( 10 | context: IContext 11 | ): Promise => { 12 | const errors: Error[] = []; 13 | const functionsDir = join(context.dir, path_serverless, path_functions); 14 | if (existsSync(functionsDir)) { 15 | const functions = await listFiles(functionsDir, ".ts"); 16 | 17 | functions.forEach(_function => { 18 | const exports = getExports(join(functionsDir, _function)); 19 | if (!exports.default) { 20 | errors.push( 21 | new Error( 22 | `${path_serverless}/${path_functions}/${_function} must have a default export` 23 | ) 24 | ); 25 | } 26 | }); 27 | } 28 | 29 | if (errors.length > 0) { 30 | throw new ErrorSet(errors); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /packages/docs/src/getting-started/develop/add-dependencies.md: -------------------------------------------------------------------------------- 1 | ```YAML 2 | title: Add dependency modules to SOMOD 3 | meta: 4 | description: 5 | Reusability is the core of SOMOD. By adding available modules as dependencies, one can import the complete functionality of added modules into the current solution. 6 | 7 | ``` 8 | 9 | # Add dependency modules 10 | 11 | --- 12 | 13 | Reusability is the core of SOMOD. By adding available modules as dependencies, one can import the complete functionality of added modules into the current solution. 14 | 15 | ## Example:- 16 | 17 | Add [somod-http-api-gateway](https://www.npmjs.com/package/somod-http-api-gateway) as a dependency to get the API gateway initialized. 18 | 19 | By running `npm install somod-http-api-gateway`, you get the infrastructure code ready for creating api gateway in AWS 20 | 21 | ## Module Catalog 22 | 23 | Browse our [module catalog](/catalog) to find out readily available modules. You can add any number of modules from the catalog or your SOMOD modules as dependencies. 24 | 25 | In the [Next Chapter](/getting-started/develop/serverless), let us understand how to add a backend code for creating api with persistent storage. 26 | -------------------------------------------------------------------------------- /packages/docs/src/getting-started/develop.md: -------------------------------------------------------------------------------- 1 | ```YAML 2 | title: Develop SOMOD Module 3 | meta: 4 | description: 5 | Create Infrastructure, BackEnd, and FrontEnd code in SOMOD Module 6 | 7 | ``` 8 | 9 | # Develop SOMOD Module 10 | 11 | --- 12 | 13 | > ### Prerequisits 14 | > 15 | > Before continuing this guild, create a new NPM package and set it up for somod module. Refer the [Setup](/getting-started/setup) guide on how to setup a SOMOD module. 16 | 17 | Developing a SOMOD module involves 3 steps. 18 | 19 | 1. Add required modules as dependencies 20 | 21 | The fundamental features of SOMOD are Reusability & Extendability. By adding the readily available modules as dependencies we can get the complete functionality of the dependency modules in our current module. 22 | 23 | 2. Create Infrastructure and BackEnd Code in a serverless framework 24 | 25 | The serverless infrastructure and the backend code are created in the `serverless` directory 26 | 27 | 3. Create the User Interface 28 | 29 | Create the NextJs Pages which are part of this module under the `ui/pages` directory 30 | 31 | Let us start by adding the dependency in the [next chapter](/getting-started/develop/add-dependencies) 32 | -------------------------------------------------------------------------------- /packages/types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "somod-types", 3 | "version": "2.1.1", 4 | "description": "Type Definitions for somod", 5 | "typings": "dist/index.d.ts", 6 | "files": [ 7 | "dist" 8 | ], 9 | "homepage": "https://github.com/somod-dev/somod/tree/main/packages/types#readme", 10 | "author": "Raghavendra K R ", 11 | "contributors": [ 12 | "Raghavendra K R ", 13 | "Lokesh G C ", 14 | "Sukhesh M G " 15 | ], 16 | "bugs": { 17 | "url": "https://github.com/somod-dev/somod" 18 | }, 19 | "license": "MIT", 20 | "repository": { 21 | "type": "git", 22 | "url": "https//github.com/somod-dev/somod.git" 23 | }, 24 | "scripts": { 25 | "clean": "npx rimraf dist", 26 | "tsc": "npx tsc", 27 | "build": "npm run clean && npm run tsc", 28 | "pretest": "$CI || npm run build", 29 | "test": "echo 'No tests defined'" 30 | }, 31 | "keywords": [ 32 | "somod", 33 | "typescript", 34 | "ts", 35 | "sodaru" 36 | ], 37 | "devDependencies": { 38 | "@types/aws-lambda": "^8.10.109", 39 | "rimraf": "^3.0.2", 40 | "typescript": "^4.5.5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/lib/src/utils/serverless/keywords/templateResources.ts: -------------------------------------------------------------------------------- 1 | import { getPath } from "../../jsonTemplate"; 2 | import { JSONType, KeywordDefinition } from "somod-types"; 3 | 4 | type Resources = Record; 5 | 6 | export const keywordTemplateResources: KeywordDefinition = { 7 | keyword: "Resources", 8 | 9 | getValidator: async () => { 10 | return () => { 11 | // no validation as of now 12 | return []; 13 | }; 14 | }, 15 | 16 | getProcessor: async (moduleName, context) => { 17 | return (keyword, node, value) => { 18 | if (getPath(node).length == 0) { 19 | return { 20 | type: "keyword", 21 | value: { 22 | [keyword]: Object.fromEntries( 23 | Object.keys(value).map(p => [ 24 | context.serverlessTemplateHandler.getSAMResourceLogicalId( 25 | moduleName, 26 | p 27 | ), 28 | value[p] 29 | ]) 30 | ) 31 | } 32 | }; 33 | } 34 | return { 35 | type: "keyword", 36 | value: { 37 | [keyword]: value 38 | } 39 | }; 40 | }; 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /packages/lib/tests/tasks/parameters/buildParameters.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createFiles, 3 | createTempDir, 4 | deleteDir, 5 | mockedFunction 6 | } from "../../utils"; 7 | import { build } from "../../../src/utils/parameters/build"; 8 | import { buildParameters } from "../../../src"; 9 | import { IContext } from "somod-types"; 10 | 11 | jest.mock("../../../src/utils/parameters/build", () => { 12 | return { 13 | __esModule: true, 14 | build: jest.fn() 15 | }; 16 | }); 17 | 18 | describe("test Task buildParameters", () => { 19 | let dir: string = null; 20 | 21 | beforeEach(async () => { 22 | dir = createTempDir("test-somod-lib"); 23 | mockedFunction(build).mockReset(); 24 | }); 25 | 26 | afterEach(() => { 27 | deleteDir(dir); 28 | }); 29 | 30 | test("for no parameters.yaml", async () => { 31 | await expect(buildParameters({ dir } as IContext)).resolves.toBeUndefined(); 32 | expect(build).toHaveBeenCalledTimes(0); 33 | }); 34 | 35 | test("for valid parameters.yaml", async () => { 36 | createFiles(dir, { "parameters.yaml": "" }); 37 | await expect(buildParameters({ dir } as IContext)).resolves.toBeUndefined(); 38 | expect(build).toHaveBeenCalledTimes(1); 39 | expect(build).toHaveBeenCalledWith({ dir }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/lib/tests/tasks/nextJs/buildUiConfigYaml.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createFiles, 3 | createTempDir, 4 | deleteDir, 5 | mockedFunction 6 | } from "../../utils"; 7 | import { build } from "../../../src/utils/nextJs/config"; 8 | import { buildUiConfigYaml } from "../../../src"; 9 | import { IContext } from "somod-types"; 10 | 11 | jest.mock("../../../src/utils/nextJs/config", () => { 12 | return { 13 | __esModule: true, 14 | build: jest.fn() 15 | }; 16 | }); 17 | 18 | describe("test Task buildUiConfigYaml", () => { 19 | let dir: string = null; 20 | 21 | beforeEach(async () => { 22 | dir = createTempDir("test-somod-lib"); 23 | mockedFunction(build).mockReset(); 24 | }); 25 | 26 | afterEach(() => { 27 | deleteDir(dir); 28 | }); 29 | 30 | test("for no config.yaml", async () => { 31 | await expect( 32 | buildUiConfigYaml({ dir } as IContext) 33 | ).resolves.toBeUndefined(); 34 | expect(build).toHaveBeenCalledTimes(0); 35 | }); 36 | 37 | test("for valid config.yaml", async () => { 38 | createFiles(dir, { "ui/config.yaml": "" }); 39 | await expect( 40 | buildUiConfigYaml({ dir } as IContext) 41 | ).resolves.toBeUndefined(); 42 | expect(build).toHaveBeenCalledTimes(1); 43 | expect(build).toHaveBeenCalledWith(dir); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/serverless/prepareSAMTemplate.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "somod-types"; 2 | import { join } from "path"; 3 | import { file_templateYaml } from "../../utils/constants"; 4 | import { prepareSamTemplate } from "../../utils/serverless/serverlessTemplate/prepare"; 5 | import { saveYamlFileStore, updateYamlFileStore } from "nodejs-file-utils"; 6 | 7 | export const prepareSAMTemplate = async (context: IContext): Promise => { 8 | const samTemplate = await prepareSamTemplate(context); 9 | 10 | if (samTemplate.Resources && Object.keys(samTemplate.Resources).length > 0) { 11 | const completeSamTemplate = { 12 | AWSTemplateFormatVersion: "2010-09-09", 13 | Transform: "AWS::Serverless-2016-10-31", 14 | Globals: { 15 | Function: { 16 | Runtime: `nodejs${context.serverlessTemplateHandler.functionNodeRuntimeVersion}.x`, 17 | Handler: "index.default", 18 | Architectures: ["arm64"] 19 | } 20 | }, 21 | Conditions: { 22 | SkipCreation: { "Fn::Equals": ["1", "0"] } 23 | }, 24 | ...samTemplate 25 | }; 26 | 27 | const templateYamlPath = join(context.dir, file_templateYaml); 28 | 29 | updateYamlFileStore(templateYamlPath, completeSamTemplate); 30 | await saveYamlFileStore(templateYamlPath); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /packages/lib/src/utils/parameters/load.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | import { readJsonFileStore, readYamlFileStore } from "nodejs-file-utils"; 3 | import { join } from "path"; 4 | import { IContext, Module } from "somod-types"; 5 | import { 6 | file_parametersJson, 7 | file_parametersYaml, 8 | path_build 9 | } from "../constants"; 10 | import { Parameters, ParameterValues } from "./types"; 11 | 12 | export const loadParameters = async (module: Module): Promise => { 13 | try { 14 | const parameters = await (module.root 15 | ? readYamlFileStore(join(module.packageLocation, file_parametersYaml)) 16 | : readJsonFileStore( 17 | join(module.packageLocation, path_build, file_parametersJson) 18 | )); 19 | 20 | return parameters; 21 | } catch (e) { 22 | if (e.message.startsWith("ENOENT: no such file or directory, open")) { 23 | return {}; 24 | } else { 25 | throw e; 26 | } 27 | } 28 | }; 29 | 30 | export const loadAllParameterValues = async ( 31 | context: IContext 32 | ): Promise => { 33 | const parameterValuesPath = join(context.dir, file_parametersJson); 34 | const parameterValues = existsSync(parameterValuesPath) 35 | ? await readJsonFileStore(parameterValuesPath) 36 | : {}; 37 | return parameterValues; 38 | }; 39 | -------------------------------------------------------------------------------- /packages/schema/src/serverless-template/resources/all.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7 } from "decorated-ajv"; 2 | 3 | export const allResource: JSONSchema7 = { 4 | allOf: [ 5 | { 6 | type: "object", 7 | required: ["Type", "Properties"], 8 | properties: { 9 | Type: { type: "string" }, 10 | Properties: { 11 | type: "object" 12 | } 13 | } 14 | }, 15 | { $ref: "#/definitions/somodExtend" }, 16 | { $ref: "#/definitions/somodAccess" }, 17 | { $ref: "#/definitions/somodDependsOn" }, 18 | { $ref: "#/definitions/somodOutput" }, 19 | { $ref: "#/definitions/somodCreateIf" }, 20 | { 21 | if: { type: "object", required: ["SOMOD::Extend"] }, 22 | then: { 23 | type: "object", 24 | propertyNames: { 25 | not: { 26 | enum: [ 27 | "SOMOD::Access", 28 | "SOMOD::DependsOn", 29 | "SOMOD::Output", 30 | "SOMOD::CreateIf" 31 | ] 32 | }, 33 | errorMessage: 34 | "Extended Resource can not have Access, DependsOn, Output or CreateIf keywords" 35 | } 36 | }, 37 | errorMessage: { 38 | if: "Extended Resource can not have Access, DependsOn, Output or CreateIf keywords" 39 | } 40 | } 41 | ] 42 | }; 43 | -------------------------------------------------------------------------------- /packages/somod/src/commands/deploy.ts: -------------------------------------------------------------------------------- 1 | import { CommonOptions, taskRunner, Command, Option } from "nodejs-cli-runner"; 2 | import { file_templateYaml, findRootDir, samDeploy } from "somod-lib"; 3 | import { addDebugOptions, DebugModeOptions } from "../utils/common"; 4 | import { BuildAction } from "./build"; 5 | import { PrepareAction } from "./prepare"; 6 | 7 | type DeployOptions = CommonOptions & 8 | DebugModeOptions & { 9 | guided: boolean; 10 | }; 11 | 12 | export const DeployAction = async ({ 13 | verbose, 14 | guided, 15 | debug 16 | }: DeployOptions): Promise => { 17 | const dir = findRootDir(); 18 | 19 | await BuildAction({ verbose, ui: false, serverless: true, debug }); 20 | 21 | await PrepareAction({ verbose, ui: false, serverless: true, debug }); 22 | 23 | await taskRunner( 24 | `Deploying ${file_templateYaml}`, 25 | samDeploy, 26 | { verbose, progressIndicator: false }, 27 | dir, 28 | verbose, 29 | guided 30 | ); 31 | }; 32 | 33 | const deployCommand = new Command("deploy"); 34 | 35 | deployCommand.action(DeployAction); 36 | 37 | deployCommand.addOption( 38 | new Option( 39 | "-g, --guided", 40 | "guided will assist in configuring backend parameters in apply state" 41 | ) 42 | ); 43 | addDebugOptions(deployCommand); 44 | 45 | export default deployCommand; 46 | -------------------------------------------------------------------------------- /packages/lib/src/utils/nextJs/publicAssets.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | import { copyFile, mkdir } from "fs/promises"; 3 | import { listFiles } from "nodejs-file-utils"; 4 | import { dirname, join } from "path"; 5 | import { IContext, NamespaceLoader } from "somod-types"; 6 | import { 7 | namespace_public, 8 | path_build, 9 | path_public, 10 | path_ui 11 | } from "../constants"; 12 | 13 | export const linkAsset = async (from: string, to: string) => { 14 | await mkdir(dirname(to), { recursive: true }); 15 | await copyFile(from, to); 16 | }; 17 | 18 | export const loadPublicAssetNamespaces: NamespaceLoader = async module => { 19 | const baseDir = join( 20 | module.packageLocation, 21 | module.root ? "" : path_build, 22 | path_ui, 23 | path_public 24 | ); 25 | const publicAssets: string[] = []; 26 | 27 | if (existsSync(baseDir)) { 28 | publicAssets.push(...(await listFiles(baseDir))); 29 | } 30 | 31 | return [{ name: namespace_public, values: publicAssets }]; 32 | }; 33 | 34 | export const getPublicAssetToModuleMap = (context: IContext) => { 35 | const publicAssets = context.namespaceHandler.get(namespace_public); 36 | 37 | const publicAssetToModuleMap = Object.fromEntries( 38 | publicAssets.map(p => [p.value, p.module]) 39 | ); 40 | 41 | return publicAssetToModuleMap; 42 | }; 43 | -------------------------------------------------------------------------------- /packages/lib/tests/tasks/parameters/generateRootParameters.test.ts: -------------------------------------------------------------------------------- 1 | import { createTempDir, deleteDir, mockedFunction } from "../../utils"; 2 | import { generate } from "../../../src/utils/parameters/generate"; 3 | import { generateRootParameters } from "../../../src"; 4 | import { IContext } from "somod-types"; 5 | 6 | jest.mock("../../../src/utils/parameters/generate", () => { 7 | return { 8 | __esModule: true, 9 | generate: jest.fn() 10 | }; 11 | }); 12 | 13 | describe("test Task generateRootParameters", () => { 14 | let dir: string = null; 15 | 16 | beforeEach(async () => { 17 | dir = createTempDir("test-somod-lib"); 18 | mockedFunction(generate).mockReset(); 19 | }); 20 | 21 | afterEach(() => { 22 | deleteDir(dir); 23 | }); 24 | 25 | test("for no override", async () => { 26 | await expect( 27 | generateRootParameters({ dir } as IContext) 28 | ).resolves.toBeUndefined(); 29 | expect(generate).toHaveBeenCalledTimes(1); 30 | expect(generate).toHaveBeenCalledWith({ dir }, false); 31 | }); 32 | 33 | test("for override", async () => { 34 | await expect( 35 | generateRootParameters({ dir } as IContext, true) 36 | ).resolves.toBeUndefined(); 37 | expect(generate).toHaveBeenCalledTimes(1); 38 | expect(generate).toHaveBeenCalledWith({ dir }, true); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/lib/tests/tasks/nextJs/validateUiConfigYaml.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createFiles, 3 | createTempDir, 4 | deleteDir, 5 | mockedFunction 6 | } from "../../utils"; 7 | import { validate } from "../../../src/utils/nextJs/config"; 8 | import { validateUiConfigYaml } from "../../../src"; 9 | import { IContext } from "somod-types"; 10 | 11 | jest.mock("../../../src/utils/nextJs/config", () => { 12 | return { 13 | __esModule: true, 14 | validate: jest.fn() 15 | }; 16 | }); 17 | 18 | describe("test Task validateUiConfigYaml", () => { 19 | let dir: string = null; 20 | 21 | beforeEach(async () => { 22 | dir = createTempDir("test-somod-lib"); 23 | mockedFunction(validate).mockReset(); 24 | }); 25 | 26 | afterEach(() => { 27 | deleteDir(dir); 28 | }); 29 | 30 | test("for no config.yaml", async () => { 31 | await expect( 32 | validateUiConfigYaml({ dir } as IContext) 33 | ).resolves.toBeUndefined(); 34 | expect(validate).toHaveBeenCalledTimes(0); 35 | }); 36 | 37 | test("for valid config.yaml", async () => { 38 | createFiles(dir, { "ui/config.yaml": "" }); 39 | await expect( 40 | validateUiConfigYaml({ dir } as IContext) 41 | ).resolves.toBeUndefined(); 42 | expect(validate).toHaveBeenCalledTimes(1); 43 | expect(validate).toHaveBeenCalledWith({ dir }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/lib/tests/tasks/tsConfigSomodJson/isValid.test.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "somod-types"; 2 | import { isValidTsConfigSomodJson } from "../../../src"; 3 | import { validate } from "../../../src/utils/tsConfigSomodJson"; 4 | import { 5 | createFiles, 6 | createTempDir, 7 | deleteDir, 8 | mockedFunction 9 | } from "../../utils"; 10 | 11 | jest.mock("../../../src/utils/tsConfigSomodJson", () => { 12 | return { 13 | __esModule: true, 14 | validate: jest.fn() 15 | }; 16 | }); 17 | 18 | describe("Test task isValidTsConfigSomodJson", () => { 19 | let dir: string = null; 20 | 21 | beforeEach(() => { 22 | dir = createTempDir("test-somod-lib"); 23 | }); 24 | 25 | afterEach(() => { 26 | deleteDir(dir); 27 | mockedFunction(validate).mockReset(); 28 | }); 29 | 30 | test("for no existing file", async () => { 31 | await expect( 32 | isValidTsConfigSomodJson({ dir } as IContext) 33 | ).resolves.toBeUndefined(); 34 | expect(validate).toHaveBeenCalledTimes(0); 35 | }); 36 | 37 | test("for existing file", async () => { 38 | createFiles(dir, { "tsconfig.somod.json": "" }); 39 | await expect( 40 | isValidTsConfigSomodJson({ dir } as IContext) 41 | ).resolves.toBeUndefined(); 42 | expect(validate).toHaveBeenCalledTimes(1); 43 | expect(validate).toHaveBeenCalledWith({ dir }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/nextJs/watchRootModulePublicAssets.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdtempSync } from "fs"; 2 | import { tmpdir } from "os"; 3 | import { join } from "path"; 4 | import { path_public, path_ui } from "../../utils/constants"; 5 | import { copyDirectory } from "nodejs-file-utils"; 6 | import watch from "../../utils/watch"; 7 | import { sync as rimrafSync } from "rimraf"; 8 | import { linkAsset } from "../../utils/nextJs/publicAssets"; 9 | 10 | const createTempDir = (): string => { 11 | return mkdtempSync(join(tmpdir(), "sodaruPackageManagerLib-")); 12 | }; 13 | 14 | export const watchRootModulePublicAssets = async ( 15 | dir: string 16 | ): Promise<() => void> => { 17 | const backupDir = createTempDir(); 18 | 19 | const publicAssetsDir = join(dir, path_public); 20 | 21 | if (existsSync(publicAssetsDir)) { 22 | await copyDirectory(publicAssetsDir, backupDir); 23 | } 24 | const closeWatch = watch( 25 | join(dir, path_ui, path_public), 26 | publicAssetsDir, 27 | backupDir, 28 | file => { 29 | linkAsset( 30 | join(dir, path_ui, path_public, file), 31 | join(dir, path_public, file) 32 | ).catch(err => { 33 | // eslint-disable-next-line no-console 34 | console.error(err); 35 | }); 36 | } 37 | ); 38 | return () => { 39 | rimrafSync(backupDir); 40 | closeWatch(); 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/nextJs/validatePageExports.ts: -------------------------------------------------------------------------------- 1 | import { listFiles } from "nodejs-file-utils"; 2 | import { existsSync } from "fs"; 3 | import { join } from "path"; 4 | import { path_pages, path_pagesData, path_ui } from "../../utils/constants"; 5 | import { get as getExports } from "../../utils/exports"; 6 | import ErrorSet from "../../utils/ErrorSet"; 7 | import { IContext } from "somod-types"; 8 | 9 | export const validatePageExports = async (context: IContext): Promise => { 10 | const errors: Error[] = []; 11 | const pagesDir = join(context.dir, path_ui, path_pages); 12 | if (existsSync(pagesDir)) { 13 | const pages = await listFiles(pagesDir, ".tsx"); 14 | 15 | pages.forEach(page => { 16 | const exports = getExports(join(pagesDir, page)); 17 | if (!exports.default) { 18 | errors.push( 19 | new Error( 20 | `${path_ui}/${path_pages}/${page} must have a default export` 21 | ) 22 | ); 23 | } 24 | if (exports.named.length > 0) { 25 | errors.push( 26 | new Error( 27 | `${path_ui}/${path_pages}/${page} must not contain named exports. All data-fetching methods must be defined under ${path_ui}/${path_pagesData}` 28 | ) 29 | ); 30 | } 31 | }); 32 | } 33 | 34 | if (errors.length > 0) { 35 | throw new ErrorSet(errors); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package-lock.json 4 | 5 | packages/lib/coverage 6 | packages/lib/tests/tasks/build/testData 7 | 8 | packages/schema/compiled 9 | packages/schema/schemas 10 | 11 | packages/template/.aws-sam 12 | packages/template/.next 13 | packages/template/.somod 14 | packages/template/build 15 | packages/template/parameters.json 16 | packages/template/tsconfig.json 17 | packages/template/pages 18 | packages/template/public 19 | packages/template/next-env.d.ts 20 | packages/template/.env 21 | packages/template/next.config.js 22 | packages/template/samconfig.toml 23 | packages/template/template.yaml 24 | packages/template/tsconfig.somod.json 25 | 26 | packages/integration-tests/packages 27 | 28 | packages/integration-tests/samples/**/build 29 | packages/integration-tests/samples/**/.aws-sam 30 | packages/integration-tests/samples/**/.next 31 | packages/integration-tests/samples/**/.somod 32 | packages/integration-tests/samples/**/samconfig.toml 33 | packages/integration-tests/samples/**/tsconfig.json 34 | 35 | packages/integration-tests/samples/push-notification-service/template.yaml 36 | packages/integration-tests/samples/push-notification-ui/pages/notify.ts 37 | packages/integration-tests/samples/push-notification/pages/messages.ts 38 | packages/integration-tests/samples/push-notification/pages/notify.ts 39 | packages/integration-tests/samples/push-notification/template.yaml 40 | -------------------------------------------------------------------------------- /packages/integration-tests/tests/somod/root.test.ts: -------------------------------------------------------------------------------- 1 | import { createFiles, createTempDir, deleteDir } from "nodejs-file-utils"; 2 | import { execPromise, execute } from "../utils"; 3 | 4 | describe("Test the somod command", () => { 5 | let dir: string; 6 | 7 | beforeAll(async () => { 8 | dir = createTempDir("test-somod-somod"); 9 | createFiles(dir, { 10 | ".npmrc": "registry=http://localhost:8000", 11 | "package.json": JSON.stringify({ name: "sample", version: "1.0.0" }) 12 | }); 13 | await execPromise("npm i somod", dir); 14 | }, 180000); 15 | 16 | afterAll(() => { 17 | deleteDir(dir); 18 | }); 19 | 20 | test("help", async () => { 21 | const result = await execute( 22 | dir, 23 | process.platform == "win32" ? "npx.cmd" : "npx", 24 | ["somod", "-h"], 25 | { return: "on", show: "off" }, 26 | { return: "on", show: "off" } 27 | ); 28 | expect(result).toMatchInlineSnapshot(` 29 | Object { 30 | "stderr": "", 31 | "stdout": "Usage: somod [options] [command] 32 | 33 | Options: 34 | --version Print version of somod 35 | -h, --help display help for command 36 | 37 | Commands: 38 | build [options] 39 | prepare [options] 40 | deploy [options] 41 | start [options] 42 | parameters [options] 43 | ", 44 | } 45 | `); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /packages/lib/tests/utils/serverless/serverlessTemplate/build.test.ts: -------------------------------------------------------------------------------- 1 | import { createFiles, createTempDir, deleteDir } from "../../../utils"; 2 | import { dump } from "js-yaml"; 3 | import { build } from "../../../../src/utils/serverless/serverlessTemplate/build"; 4 | import { readFile } from "fs/promises"; 5 | import { join } from "path"; 6 | import { ServerlessTemplate } from "somod-types"; 7 | 8 | describe("test util serverlessTemplate.build", () => { 9 | let dir: string; 10 | 11 | beforeEach(() => { 12 | dir = createTempDir("test-somod-lib"); 13 | }); 14 | 15 | afterEach(() => { 16 | deleteDir(dir); 17 | }); 18 | 19 | test("successfully builds", async () => { 20 | const serverlessTemplate: ServerlessTemplate = { 21 | Resources: { 22 | R1: { 23 | Type: "T1", 24 | Properties: {} 25 | }, 26 | R2: { 27 | Type: "T2", 28 | Properties: { P1: "", P2: true } 29 | }, 30 | R3: { 31 | Type: "T1", 32 | Properties: { P1: "", P2: true } 33 | } 34 | } 35 | }; 36 | createFiles(dir, { 37 | "serverless/template.yaml": dump(serverlessTemplate) 38 | }); 39 | 40 | await expect(build(dir)).resolves.toBeUndefined(); 41 | 42 | await expect( 43 | readFile(join(dir, "build/serverless/template.json"), "utf8") 44 | ).resolves.toEqual(JSON.stringify(serverlessTemplate)); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/nextJs/createPages.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import { IContext } from "somod-types"; 3 | import { 4 | path_build, 5 | path_pages, 6 | path_pagesData, 7 | path_ui 8 | } from "../../utils/constants"; 9 | import { 10 | addPageExtention, 11 | linkPage, 12 | getPageToModuleMap 13 | } from "../../utils/nextJs/pages"; 14 | 15 | export const createPages = async (context: IContext): Promise => { 16 | const allPages = getPageToModuleMap(context); 17 | 18 | await Promise.all( 19 | Object.keys(allPages).map(async page => { 20 | const moduleName = allPages[page]; 21 | const moduleNode = context.moduleHandler.getModule(moduleName); 22 | const packageLocation = moduleNode.module.packageLocation; 23 | 24 | const sourcePagePath = addPageExtention( 25 | join( 26 | packageLocation, 27 | moduleNode.module.root ? "" : path_build, 28 | path_ui, 29 | path_pages, 30 | page 31 | ) 32 | ); 33 | 34 | const sourcePageDataPath = join( 35 | packageLocation, 36 | moduleNode.module.root ? "" : path_build, 37 | path_ui, 38 | path_pagesData, 39 | page + (moduleNode.module.root ? ".ts" : ".js") 40 | ); 41 | 42 | const pagePath = join(context.dir, path_pages, page + ".ts"); 43 | await linkPage(sourcePagePath, sourcePageDataPath, pagePath); 44 | }) 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-service/build/serverless/functions/ondisconnect.js: -------------------------------------------------------------------------------- 1 | import { __awaiter, __generator } from "tslib"; 2 | import { DynamoDB } from "aws-sdk"; 3 | var ddb = new DynamoDB.DocumentClient({ 4 | apiVersion: "2012-08-10", 5 | region: process.env.AWS_REGION 6 | }); 7 | var handler = function (event) { return __awaiter(void 0, void 0, void 0, function () { 8 | var err_1; 9 | return __generator(this, function (_a) { 10 | switch (_a.label) { 11 | case 0: 12 | _a.trys.push([0, 2, , 3]); 13 | return [4 /*yield*/, ddb 14 | .delete({ 15 | TableName: process.env.TABLE_NAME, 16 | Key: { 17 | connectionId: event.requestContext.connectionId 18 | } 19 | }) 20 | .promise()]; 21 | case 1: 22 | _a.sent(); 23 | return [3 /*break*/, 3]; 24 | case 2: 25 | err_1 = _a.sent(); 26 | return [2 /*return*/, { 27 | statusCode: 500, 28 | body: "Failed to disconnect: " + JSON.stringify(err_1) 29 | }]; 30 | case 3: return [2 /*return*/, { statusCode: 200, body: "Disconnected." }]; 31 | } 32 | }); 33 | }); }; 34 | export default handler; 35 | -------------------------------------------------------------------------------- /packages/docs/src/reference/main-concepts/lib.md: -------------------------------------------------------------------------------- 1 | ```YAML 2 | title: lib directory in SOMOD Module | SOMOD 3 | meta: 4 | description: 5 | The lib directory contains pure javascript code exported from a SOMOD module. 6 | ``` 7 | 8 | # SOMOD's `lib` Directory 9 | 10 | --- 11 | 12 | Among all directories in SOMOD, lib is the simplest one. The Developer can write the reusable code in the lib directory. The build command compiles typescript in the lib directory into build/lib, generating javascript and type definitions. 13 | 14 | As discussed in the [package.json](/reference/main-concepts/package.json) structure, the module property has a "build/lib/index.js" value. So the code exported from "lib/index.ts" is available for other modules to import. 15 | 16 | ```bash 17 | 18 | project-root 19 | | 20 | +-- build/ 21 | | +-- lib/ 22 | | +-- other.js 23 | | +-- index.js <-------+ 24 | | ^ | 25 | | | | 26 | | 'BUILD' 'module in package.json refers to this' 27 | +-- lib/ | | 28 | | +-- other.ts | | 29 | | +-- index.ts ---+ | 30 | | | 31 | +-- package.json -----------------+ 32 | 33 | ``` 34 | 35 | In the [next section](/reference/main-concepts/serverless), let's explore what our `serverless` directory contains. 36 | -------------------------------------------------------------------------------- /packages/docs/src/getting-started/deploy.md: -------------------------------------------------------------------------------- 1 | ```YAML 2 | title: Deploy SOMOD Module 3 | meta: 4 | description: 5 | Deploy AWS SAM and NextJs Project prepared by SOMOD. 6 | ``` 7 | 8 | # Deploy SOMOD Module 9 | 10 | --- 11 | 12 | SOMOD prepare command creates deployable AWS SAM Project and NextJs Project. 13 | 14 | ``` 15 | npx somod prepare 16 | ``` 17 | 18 | - AWS SAM Project 19 | 20 | For the SAM Project, SOMOD creates the `template.yaml` file and the `.somod/` directory at the root of the module. 21 | 22 | The [AWS SAM Deployment](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-deploying.html) guide explains the steps for AWS SAM Project deployment. 23 | 24 | - NextJs Project 25 | 26 | For the NextJs Project, SOMOD creates the `pages/`, `public/` directories, `.env`, and `next.config.js` files at the root of the module. 27 | 28 | The [NextJs Deployment](https://nextjs.org/docs/deployment) guide explains the different options for deploying the NextJs project. 29 | 30 | ## What Next? 31 | 32 | Browse our [module catalog](/catalog) to find out readily available modules. 33 | 34 | Create new modules for your business use cases, and add the common modules as dependencies. 35 | 36 | You can also submit your modules to the catalog. Read more about contributing to the module catalog [here](/catalog/contribute). 37 | 38 | Find the advanced features of SOMOD and other reference material on the [Reference](/reference) page. 39 | -------------------------------------------------------------------------------- /packages/docs/src/reference/main-concepts.md: -------------------------------------------------------------------------------- 1 | ```YAML 2 | title: Understand the Concepts behind SOMOD | SOMOD 3 | meta: 4 | description: 5 | A Single framework to Develop, Build, Reuse, and Deploy Serverless Applications 6 | ``` 7 | 8 | # Main Concepts of **SOMOD** 9 | 10 | --- 11 | 12 | SOMOD makes it easy to **Develop**, **Build**, **Reuse** and **Deploy** Serverless Applications 13 | 14 | In this guide, we will examine the concepts used in SOMOD projects. Once you master them, you can create complex applications from smaller reusable modules. 15 | 16 | This reference is a step-by-step guide to explaining the main concepts of SOMOD. You can find the list of all concepts in the left navigation bar under [Reference](/reference)/[Main Concepts](/reference/main-concepts). 17 | 18 | > This guide is designed for people who prefer learning concepts **step by step**. 19 | > If you prefer to learn by doing, check out our [Getting Started](/getting-started) guide. 20 | > You might find this and getting started guide complementary to each other. 21 | 22 | ## Assumptions 23 | 24 | SOMOD is a **serverless** framework developed using `typescript` and `npm` packaging, so we will assume you have a basic understanding of [Typescript](https://www.typescriptlang.org/), [NPM](https://npmjs.com), and [Serverless](https://aws.amazon.com/serverless/). 25 | 26 | ## Let's Get Started! 27 | 28 | Let's start by understanding the building block of SOMOD, the [Module](/reference/main-concepts/module). 29 | -------------------------------------------------------------------------------- /packages/lib/src/utils/serverless/serverlessTemplate/validate.ts: -------------------------------------------------------------------------------- 1 | import { IContext, KeywordValidator } from "somod-types"; 2 | import ErrorSet from "../../ErrorSet"; 3 | import { parseJson, validateKeywords } from "../../jsonTemplate"; 4 | import { getBaseKeywords } from "./serverlessTemplate"; 5 | 6 | /** 7 | * Validate the `serverless/template.yaml` at the root module. 8 | * 9 | * Assumption is that `serverless/template.yaml` is present in root module 10 | */ 11 | export const validateServerlessTemplate = async (context: IContext) => { 12 | const keywords = [...getBaseKeywords()]; 13 | 14 | context.extensionHandler.serverlessTemplateKeywords.forEach( 15 | serverlessTemplateKeywords => { 16 | keywords.push(...serverlessTemplateKeywords.value); 17 | } 18 | ); 19 | 20 | const keywordValidators: Record = {}; 21 | 22 | await Promise.all( 23 | keywords.map(async keyword => { 24 | const validator = await keyword.getValidator( 25 | context.moduleHandler.rootModuleName, 26 | context 27 | ); 28 | 29 | keywordValidators[keyword.keyword] = validator; 30 | }) 31 | ); 32 | 33 | const errors = await validateKeywords( 34 | parseJson( 35 | context.serverlessTemplateHandler.getTemplate( 36 | context.moduleHandler.rootModuleName 37 | ).template 38 | ), 39 | keywordValidators 40 | ); 41 | 42 | if (errors.length > 0) { 43 | throw new ErrorSet(errors); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /packages/middleware/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "somod-middleware", 3 | "version": "2.1.1", 4 | "description": "Middleware library for SOMOD serverless functions", 5 | "keywords": [ 6 | "middleware", 7 | "lambda", 8 | "serverless-function", 9 | "somod", 10 | "serverless-optimized-modules", 11 | "reuse serverless", 12 | "sodaru", 13 | "developer" 14 | ], 15 | "author": "Raghavendra K R ", 16 | "contributors": [ 17 | "Raghavendra K R ", 18 | "Lokesh G C ", 19 | "Sukhesh M G " 20 | ], 21 | "homepage": "https://github.com/somod-dev/somod/tree/main/packages/middleware#readme", 22 | "license": "MIT", 23 | "main": "./dist/cjs/index.js", 24 | "module": "./dist/index.js", 25 | "typings": "./dist/index.d.ts", 26 | "files": [ 27 | "dist" 28 | ], 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/somod-dev/somod.git" 32 | }, 33 | "scripts": { 34 | "clean": "npx rimraf dist", 35 | "tsc": "npx tsc", 36 | "tsc:cjs": "npx tsc --project tsconfig.cjs.json", 37 | "build": "npm run clean && npm run tsc && npm run tsc:cjs", 38 | "pretest": "$CI || npm run build", 39 | "test": "npx jest" 40 | }, 41 | "devDependencies": { 42 | "@types/aws-lambda": "^8.10.109", 43 | "@types/jest": "^27.5.1", 44 | "jest": "28.1.0", 45 | "ts-jest": "28.0.2", 46 | "typescript": "^4.5.5", 47 | "somod-types": "^2.1.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/docs/src/getting-started/ship.md: -------------------------------------------------------------------------------- 1 | ```YAML 2 | title: Ship SOMOD Module 3 | meta: 4 | description: 5 | Ship the SOMOD module so that other modules can reuse it. 6 | ``` 7 | 8 | # Ship SOMOD Module 9 | 10 | --- 11 | 12 | Because every SOMOD module is an NPM Package in itself, shipping the SOMOD module is the same as any other NPM Package. 13 | 14 | Run this command to publish to the NPM registry 15 | 16 | ``` 17 | npm publish 18 | ``` 19 | 20 | > Make sure that the build step is run before running the publish. If not done, the shipped tar file contains no build. 21 | 22 | ## Example 23 | 24 | The `example user management` module that we created in [Develop](/getting-started/develop) guide is shipped to NPM Public Registry at https://www.npmjs.com/package/somod-example-user-management 25 | 26 | > ## NPM Registries 27 | > 28 | > An NPM Registry is web-based storage of artifacts whose APIs are compatible with the NPM CLI. 29 | > 30 | > https://npmjs.com is a public registry holding all public packages. 31 | > SOMOD recommends only the public SOMOD modules be pushed to this registry. Any other modules, private, > getting-started, or examples to be pushed to private NPM registries 32 | > 33 | > [GitHub](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry) > and [GitLab](https://docs.gitlab.com/ee/user/packages/npm_registry/) provide a way to have your own private NPM > registry. 34 | 35 | In the [Next](/getting-started/reuse) chapter, let us understand how to reuse a shipped SOMOD module. 36 | -------------------------------------------------------------------------------- /packages/types/src/Extension.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "./Context"; 2 | import { KeywordDefinition } from "./KeywordDefinition"; 3 | import { NamespaceLoader } from "./Namespace"; 4 | 5 | export type LifecycleHook = (context: IContext) => Promise; 6 | 7 | export type Extension = { 8 | prebuild?: LifecycleHook; 9 | build?: LifecycleHook; 10 | preprepare?: LifecycleHook; 11 | prepare?: LifecycleHook; 12 | 13 | namespaceLoader?: NamespaceLoader; 14 | uiConfigKeywords?: KeywordDefinition[]; 15 | serverlessTemplateKeywords?: KeywordDefinition[]; 16 | 17 | functionLayers?: string[]; 18 | functionMiddlewares?: string[]; 19 | }; 20 | 21 | export type ExtensionValue = { 22 | extension: string; 23 | value: T; 24 | }; 25 | 26 | /** 27 | * Interface for handling the extensions, all getters return the values ordered from child to parent module 28 | */ 29 | export interface IExtensionHandler { 30 | get prebuildHooks(): ExtensionValue[]; 31 | get buildHooks(): ExtensionValue[]; 32 | get preprepareHooks(): ExtensionValue[]; 33 | get prepareHooks(): ExtensionValue[]; 34 | 35 | get namespaceLoaders(): ExtensionValue[]; 36 | get uiConfigKeywords(): ExtensionValue[]; 37 | get serverlessTemplateKeywords(): ExtensionValue[]; 38 | 39 | get functionLayers(): ExtensionValue[]; 40 | get functionMiddlewares(): ExtensionValue[]; 41 | 42 | get(key: string): ExtensionValue[]; 43 | } 44 | -------------------------------------------------------------------------------- /packages/lib/tests/tasks/parameters/validateSchema.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createFiles, 3 | createTempDir, 4 | deleteDir, 5 | mockedFunction 6 | } from "../../utils"; 7 | import { yamlSchemaValidator } from "../../../src/utils/yamlSchemaValidator"; 8 | import { validateParametersWithSchema } from "../../../src"; 9 | import { join } from "path"; 10 | import { IContext } from "somod-types"; 11 | 12 | jest.mock("../../../src/utils/yamlSchemaValidator", () => { 13 | return { 14 | __esModule: true, 15 | yamlSchemaValidator: jest.fn() 16 | }; 17 | }); 18 | 19 | describe("test Task validateParametersWithSchema", () => { 20 | let dir: string = null; 21 | 22 | beforeEach(async () => { 23 | dir = createTempDir("test-somod-lib"); 24 | mockedFunction(yamlSchemaValidator).mockReset(); 25 | }); 26 | 27 | afterEach(() => { 28 | deleteDir(dir); 29 | }); 30 | 31 | test("for no parameters.yaml", async () => { 32 | await expect( 33 | validateParametersWithSchema({ dir } as IContext) 34 | ).resolves.toBeUndefined(); 35 | expect(yamlSchemaValidator).toHaveBeenCalledTimes(0); 36 | }); 37 | 38 | test("for valid parameters.yaml", async () => { 39 | createFiles(dir, { "parameters.yaml": "" }); 40 | await expect( 41 | validateParametersWithSchema({ dir } as IContext) 42 | ).resolves.toBeUndefined(); 43 | expect(yamlSchemaValidator).toHaveBeenCalledTimes(1); 44 | expect(yamlSchemaValidator).toHaveBeenCalledWith( 45 | expect.any(Function), 46 | join(dir, "parameters.yaml") 47 | ); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/lib/src/utils/keywords/parameter.ts: -------------------------------------------------------------------------------- 1 | import { KeywordDefinition, KeywordObjectReplacement } from "somod-types"; 2 | import { file_parametersYaml } from "../constants"; 3 | import { loadAllParameterValues } from "../parameters/load"; 4 | import { getParameterToModuleMap } from "../parameters/namespace"; 5 | 6 | export const keywordParameter: KeywordDefinition = { 7 | keyword: "SOMOD::Parameter", 8 | 9 | getValidator: async (moduleName, context) => { 10 | const parameters = Object.keys(getParameterToModuleMap(context)); 11 | return (keyword, node, value) => { 12 | const errors: Error[] = []; 13 | if (Object.keys(node.properties).length > 1) { 14 | errors.push( 15 | new Error( 16 | `Object with ${keyword} must not have additional properties` 17 | ) 18 | ); 19 | } 20 | 21 | if (typeof value != "string") { 22 | errors.push(new Error(`${keyword} value must be string`)); 23 | } else if (!parameters.includes(value)) { 24 | errors.push( 25 | new Error( 26 | `parameter ${value} referenced by ${keyword} does not exist. Define ${value} in /${file_parametersYaml}` 27 | ) 28 | ); 29 | } 30 | return errors; 31 | }; 32 | }, 33 | 34 | getProcessor: async (moduleName, context) => { 35 | const parameters = await loadAllParameterValues(context); 36 | return (keyword, node, value) => { 37 | return { 38 | type: "object", 39 | value: parameters[value] 40 | } as KeywordObjectReplacement; 41 | }; 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/nextJs/watchRootModulePages.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdtempSync } from "fs"; 2 | import { tmpdir } from "os"; 3 | import { join } from "path"; 4 | import { path_pages, path_pagesData, path_ui } from "../../utils/constants"; 5 | import { copyDirectory } from "nodejs-file-utils"; 6 | import { linkPage, removeExtension } from "../../utils/nextJs/pages"; 7 | import watch from "../../utils/watch"; 8 | import { sync as rimrafSync } from "rimraf"; 9 | 10 | const createTempDir = (): string => { 11 | return mkdtempSync(join(tmpdir(), "somod-")); 12 | }; 13 | 14 | export const watchRootModulePages = async ( 15 | dir: string 16 | ): Promise<() => void> => { 17 | const backupDir = createTempDir(); 18 | 19 | const pagesDir = join(dir, path_pages); 20 | 21 | if (existsSync(pagesDir)) { 22 | await copyDirectory(pagesDir, backupDir); 23 | } 24 | const closeWatch = watch( 25 | join(dir, path_ui, path_pages), 26 | pagesDir, 27 | backupDir, 28 | (sourcePage, destinationPage) => { 29 | linkPage( 30 | join(dir, path_ui, path_pages, sourcePage), 31 | join( 32 | dir, 33 | path_ui, 34 | path_pagesData, 35 | sourcePage.substring(0, sourcePage.lastIndexOf(".")) + ".ts" 36 | ), 37 | join(dir, path_pages, destinationPage) 38 | ).catch(err => { 39 | // eslint-disable-next-line no-console 40 | console.error(err); 41 | }); 42 | }, 43 | page => { 44 | return removeExtension(page) + ".ts"; 45 | } 46 | ); 47 | return () => { 48 | rimrafSync(backupDir); 49 | closeWatch(); 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /packages/lib/src/utils/parameters/validate.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7, validate } from "decorated-ajv"; 2 | import { uniq } from "lodash"; 3 | import { IContext } from "somod-types"; 4 | import { file_parametersJson } from "../constants"; 5 | import { loadParameters } from "./load"; 6 | import { getParameterToModuleMap } from "./namespace"; 7 | import { Parameters, ParameterValues } from "./types"; 8 | 9 | export const validateParameterValues = async ( 10 | context: IContext, 11 | parameterValues: ParameterValues 12 | ) => { 13 | const parameterToModuleNameMap = getParameterToModuleMap(context); 14 | 15 | const moduleNames = uniq(Object.values(parameterToModuleNameMap)); 16 | const moduleParameters: Record = {}; 17 | 18 | await Promise.all( 19 | moduleNames.map(async moduleName => { 20 | const moduleNode = context.moduleHandler.getModule(moduleName); 21 | moduleParameters[moduleName] = await loadParameters(moduleNode.module); 22 | }) 23 | ); 24 | 25 | const parametersSchema: JSONSchema7 = { type: "object", properties: {} }; 26 | for (const parameterName in parameterToModuleNameMap) { 27 | const moduleName = parameterToModuleNameMap[parameterName]; 28 | parametersSchema.properties[parameterName] = 29 | moduleParameters[moduleName].parameters[parameterName]; 30 | } 31 | const violations = await validate(parametersSchema, parameterValues); 32 | if (violations.length > 0) { 33 | throw new Error( 34 | `${file_parametersJson} has following errors\n${violations 35 | .map(v => " " + (v.path + " " + v.message).trim()) 36 | .join("\n")}` 37 | ); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /packages/lib/tests/tasks/common/initializeContext.test.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "somod-types"; 2 | import { initializeContext } from "../../../src"; 3 | import { Context } from "../../../src/utils/context"; 4 | import { mockedFunction } from "../../utils"; 5 | 6 | jest.mock("../../../src/utils/context", () => { 7 | return { 8 | __esModule: true, 9 | Context: { getInstance: jest.fn() } 10 | }; 11 | }); 12 | 13 | describe("Test Task initializeContext", () => { 14 | afterEach(() => { 15 | mockedFunction(Context.getInstance).mockReset(); 16 | }); 17 | test("for successfull initialization", async () => { 18 | mockedFunction(Context.getInstance).mockResolvedValue({ 19 | dir: "Success" 20 | } as IContext); 21 | 22 | await expect( 23 | initializeContext("sample", true, false, true) 24 | ).resolves.toEqual({ 25 | dir: "Success" 26 | }); 27 | expect(Context.getInstance).toHaveBeenCalledTimes(1); 28 | expect(Context.getInstance).toHaveBeenCalledWith( 29 | "sample", 30 | true, 31 | false, 32 | true 33 | ); 34 | }); 35 | 36 | test("for failed initialization", async () => { 37 | mockedFunction(Context.getInstance).mockRejectedValue( 38 | new Error("There is an error in context initialization") 39 | ); 40 | 41 | await expect( 42 | initializeContext("sample", false, false, false) 43 | ).rejects.toEqual(new Error("There is an error in context initialization")); 44 | expect(Context.getInstance).toHaveBeenCalledTimes(1); 45 | expect(Context.getInstance).toHaveBeenCalledWith( 46 | "sample", 47 | false, 48 | false, 49 | false 50 | ); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/lib/tests/tasks/nextJs/validateUiConfigYamlWithSchema.test.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import { IContext } from "somod-types"; 3 | import { validateUiConfigYamlWithSchema } from "../../../src"; 4 | import { createFiles, createTempDir, deleteDir } from "../../utils"; 5 | 6 | describe("Test Task validateUiConfigYamlWithSchema", () => { 7 | let dir: string = null; 8 | 9 | beforeEach(() => { 10 | dir = createTempDir("test-somod-lib"); 11 | }); 12 | 13 | afterEach(() => { 14 | deleteDir(dir); 15 | }); 16 | 17 | test("For no ui directory", async () => { 18 | await expect( 19 | validateUiConfigYamlWithSchema({ dir } as IContext) 20 | ).resolves.toBeUndefined(); 21 | }); 22 | 23 | test("For no config.yaml", async () => { 24 | createFiles(dir, { 25 | "ui/": "" 26 | }); 27 | await expect( 28 | validateUiConfigYamlWithSchema({ dir } as IContext) 29 | ).resolves.toBeUndefined(); 30 | }); 31 | 32 | test("For empty config.yaml", async () => { 33 | createFiles(dir, { 34 | "ui/config.yaml": "" 35 | }); 36 | await expect( 37 | validateUiConfigYamlWithSchema({ dir } as IContext) 38 | ).rejects.toEqual( 39 | new Error( 40 | join(dir, "ui/config.yaml") + " has following errors\n must be object" 41 | ) 42 | ); 43 | }, 20000); 44 | 45 | test("For config.yaml with only env", async () => { 46 | createFiles(dir, { 47 | "ui/config.yaml": ` 48 | env: 49 | MY_ENV_VAR: 50 | SOMOD::Parameter: myparameter 51 | ` 52 | }); 53 | await expect( 54 | validateUiConfigYamlWithSchema({ dir } as IContext) 55 | ).resolves.toBeUndefined(); 56 | }, 20000); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/types/src/JsonTemplate.ts: -------------------------------------------------------------------------------- 1 | export type JSONBaseNode = Readonly<{ 2 | parent?: { 3 | key: number | string; 4 | node: JSONArrayNode | JSONObjectNode; 5 | }; 6 | }>; 7 | 8 | export type JSONPrimitiveNode = Readonly<{ 9 | type: "primitive"; 10 | value: JSONPrimitiveType; 11 | }> & 12 | JSONBaseNode; 13 | 14 | export type JSONArrayNode = Readonly<{ 15 | type: "array"; 16 | items: JSONNode[]; 17 | }> & 18 | JSONBaseNode; 19 | 20 | export type JSONObjectNode = Readonly<{ 21 | type: "object"; 22 | properties: Record; 23 | }> & 24 | JSONBaseNode; 25 | 26 | export type JSONNode = JSONPrimitiveNode | JSONArrayNode | JSONObjectNode; 27 | 28 | export type JSONPrimitiveType = string | boolean | number | null; 29 | 30 | export type JSONArrayType = JSONType[]; 31 | 32 | export type JSONObjectType = { 33 | [property: string]: JSONType; 34 | }; 35 | 36 | export type JSONType = JSONPrimitiveType | JSONArrayType | JSONObjectType; 37 | 38 | export type KeywordValidator = ( 39 | keyword: string, 40 | node: JSONObjectNode, 41 | value: T 42 | ) => Error[] | Promise; 43 | 44 | export type KeywordObjectReplacement = { 45 | type: "object"; 46 | value: JSONType; 47 | level?: number; 48 | }; 49 | 50 | export type KeywordKeywordReplacement = { 51 | type: "keyword"; 52 | value: JSONObjectType; 53 | }; 54 | 55 | export type KeywordReplacement = 56 | | KeywordObjectReplacement 57 | | KeywordKeywordReplacement; 58 | 59 | export type KeywordProcessor = ( 60 | keyword: string, 61 | node: JSONObjectNode, 62 | value: T 63 | ) => KeywordReplacement | Promise; 64 | -------------------------------------------------------------------------------- /packages/lib/src/utils/serverless/keywords/access.ts: -------------------------------------------------------------------------------- 1 | import { KeywordDefinition, ServerlessResource } from "somod-types"; 2 | import { getPath } from "../../jsonTemplate"; 3 | 4 | type Access = "module" | "scope" | "public"; 5 | 6 | export const keywordAccess: KeywordDefinition = { 7 | keyword: "SOMOD::Access", 8 | 9 | getValidator: async () => (keyword, node) => { 10 | const errors: Error[] = []; 11 | 12 | const path = getPath(node); 13 | if (!(path.length == 2 && path[0] == "Resources")) { 14 | errors.push(new Error(`${keyword} is allowed only as Resource Property`)); 15 | } 16 | 17 | //NOTE: structure of the value is validated by serverless-schema 18 | 19 | return errors; 20 | }, 21 | 22 | getProcessor: async () => () => ({ 23 | type: "keyword", 24 | value: {} 25 | }) 26 | }; 27 | 28 | export const checkAccess = ( 29 | resource: ServerlessResource, 30 | accessedModule: string, 31 | accessedResource: string, 32 | fromModule: string, 33 | referenceType: "Referenced" | "Extended" | "Depended" = "Referenced" 34 | ) => { 35 | const access = (resource[keywordAccess.keyword] || "scope") as Access; 36 | 37 | if (access == "module" && fromModule != accessedModule) { 38 | throw new Error( 39 | `${referenceType} module resource {${accessedModule}, ${accessedResource}} can not be accessed (has "module" access).` 40 | ); 41 | } 42 | 43 | if ( 44 | access == "scope" && 45 | fromModule.split("/")[0] != accessedModule.split("/")[0] 46 | ) { 47 | throw new Error( 48 | `Referenced module resource {${accessedModule}, ${accessedResource}} can not be accessed (has "scope" access).` 49 | ); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /packages/docs/src/reference/main-concepts/tsconfig.somod.json.md: -------------------------------------------------------------------------------- 1 | ```YAML 2 | title: Typescript Configuration in SOMOD Module | SOMOD 3 | meta: 4 | description: 5 | SOMOD's typescript configuration helps to compile the source code into the distributable format. 6 | ``` 7 | 8 | # Typescript Configuration in SOMOD Module 9 | 10 | --- 11 | 12 | SOMOD's typescript configuration helps to compile the source code into the distributable format. 13 | 14 | SOMOD uses a predefined set of typescript definitions in the `tsconfig.somod.json` file. 15 | 16 | The content of the `tsconfig.somod.json` file is as follows 17 | 18 | ```json 19 | { 20 | "compilerOptions": { 21 | "allowUmdGlobalAccess": true, 22 | "outDir": "build", 23 | "declaration": true, 24 | "target": "ES5", 25 | "module": "ESNext", 26 | "rootDir": "./", 27 | "moduleResolution": "Node", 28 | "esModuleInterop": true, 29 | "importHelpers": true, 30 | "skipLibCheck": true, 31 | "jsx": "react-jsx" 32 | }, 33 | "include": ["lib", "serverless", "ui"] 34 | } 35 | ``` 36 | 37 | Any extra configurations are allowed, but these configurations must be present and not altered. 38 | 39 | > - Typescript compilation is skipped if `tsconfig.somod.json` is not present 40 | > - When build is run with `--ui` option, the typescript compilation includes `lib` and `ui` only 41 | > - When build is run with `--serverless` option, the typescript compilation includes `lib` and `serverless` only 42 | 43 | Till now this guide explained the main concepts and project structure in the SOMOD module. In the [next chapter](/reference/main-concepts/extensions), let us understand how to extend the capabilities of SOMOD using extensions. 44 | -------------------------------------------------------------------------------- /packages/lib/tests/tasks/serverless/buildServerlessTemplate.test.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "somod-types"; 2 | import { buildServerlessTemplate } from "../../../src"; 3 | import { build } from "../../../src/utils/serverless/serverlessTemplate/build"; 4 | import { 5 | createFiles, 6 | createTempDir, 7 | deleteDir, 8 | mockedFunction 9 | } from "../../utils"; 10 | 11 | jest.mock("../../../src/utils/serverless/serverlessTemplate/build", () => { 12 | return { 13 | __exModule: true, 14 | build: jest.fn() 15 | }; 16 | }); 17 | 18 | describe("Test Task buildServerlessTemplate", () => { 19 | let dir: string = null; 20 | 21 | beforeEach(async () => { 22 | dir = createTempDir("test-somod-lib"); 23 | }); 24 | 25 | afterEach(() => { 26 | deleteDir(dir); 27 | mockedFunction(build).mockReset(); 28 | }); 29 | 30 | test("For no serverless directory", async () => { 31 | await expect( 32 | buildServerlessTemplate({ dir } as IContext) 33 | ).resolves.toBeUndefined(); 34 | expect(build).toHaveBeenCalledTimes(0); 35 | }); 36 | 37 | test("For no template", async () => { 38 | createFiles(dir, { 39 | "serverless/": "" 40 | }); 41 | await expect( 42 | buildServerlessTemplate({ dir } as IContext) 43 | ).resolves.toBeUndefined(); 44 | expect(build).toHaveBeenCalledTimes(0); 45 | }); 46 | 47 | test("For a template", async () => { 48 | createFiles(dir, { 49 | "serverless/template.yaml": "" 50 | }); 51 | await expect( 52 | buildServerlessTemplate({ dir } as IContext) 53 | ).resolves.toBeUndefined(); 54 | expect(build).toHaveBeenCalledTimes(1); 55 | expect(build).toHaveBeenCalledWith(dir); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/schema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "somod-schema", 3 | "version": "2.1.1", 4 | "description": "Schema to validate yaml files in somod modules", 5 | "keywords": [ 6 | "schema", 7 | "serverless", 8 | "aws-sam", 9 | "sodaru", 10 | "somod", 11 | "modules", 12 | "ui-config", 13 | "parameters" 14 | ], 15 | "main": "./dist/cjs/index.js", 16 | "module": "dist/index.js", 17 | "typings": "dist/index.d.ts", 18 | "files": [ 19 | "dist", 20 | "compiled", 21 | "schemas" 22 | ], 23 | "author": "Raghavendra K R ", 24 | "contributors": [ 25 | "Raghavendra K R ", 26 | "Lokesh G C ", 27 | "Sukhesh M G " 28 | ], 29 | "homepage": "https://github.com/somod-dev/somod/tree/main/packages/schema#readme", 30 | "license": "MIT", 31 | "scripts": { 32 | "clean": "npx rimraf dist && npx rimraf compiled && npx rimraf schemas", 33 | "tsc": "npx tsc", 34 | "tsc-cjs": "npx tsc --project tsconfig.cjs.json", 35 | "buildSchema": "node ./scripts/build.js", 36 | "compileSchema": "node ./scripts/compile.js", 37 | "build": "npm run clean && npm run tsc && npm run tsc-cjs && npm run buildSchema && npm run compileSchema", 38 | "pretest": "$CI || npm run build", 39 | "test": "echo 'No tests here'" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "https//github.com/somod-dev/somod.git" 44 | }, 45 | "bugs": { 46 | "url": "https://github.com/somod-dev/somod/issues" 47 | }, 48 | "devDependencies": { 49 | "decorated-ajv": "^1.1.0", 50 | "rimraf": "^3.0.2", 51 | "tslib": "^2.4.0", 52 | "typescript": "^4.5.5" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "somod-sdk", 3 | "version": "2.1.1", 4 | "description": "SOMOD (Serverless Optimized MODule). Serverless Framework to create reusable micro applications", 5 | "scripts": { 6 | "postinstall": "npx mono-repo install -v && npx mono-repo run build -v", 7 | "eslint": "npx eslint ./ --no-error-on-unmatched-pattern", 8 | "prettier": "npx prettier --check --ignore-unknown --no-error-on-unmatched-pattern ./**/*", 9 | "check-sanity": "npm run prettier && npm run eslint && npx mono-repo validate --version-match", 10 | "pretest": "npm run check-sanity", 11 | "test": "npx mono-repo run test -v", 12 | "version": "npx mono-repo version -v %npm_config_packages%", 13 | "postversion": "git push --follow-tags" 14 | }, 15 | "eslintConfig": { 16 | "extends": [ 17 | "sodaru" 18 | ] 19 | }, 20 | "prettier": "prettier-config-sodaru", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/somod-dev/somod.git" 24 | }, 25 | "keywords": [ 26 | "sodaru", 27 | "somod", 28 | "nextjs-package", 29 | "serverless-package", 30 | "sodaru-module-package" 31 | ], 32 | "author": "Raghavendra K R ", 33 | "contributors": [ 34 | "Raghavendra K R ", 35 | "Lokesh G C ", 36 | "Sukhesh M G " 37 | ], 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/somod-dev/somod/issues" 41 | }, 42 | "homepage": "https://github.com/somod-dev/somod#readme", 43 | "private": true, 44 | "devDependencies": { 45 | "eslint-config-sodaru": "^1.0.0", 46 | "npm-mono-repo": "^1.2.0", 47 | "prettier-config-sodaru": "^1.0.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/lib/tests/tasks/serverless/validateFunctionExports.test.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "somod-types"; 2 | import { validateFunctionExports } from "../../../src"; 3 | import { createFiles, createTempDir, deleteDir } from "../../utils"; 4 | 5 | describe("Test task validateFunctionExports", () => { 6 | let dir: string = null; 7 | 8 | beforeEach(() => { 9 | dir = createTempDir("test-somod-lib"); 10 | }); 11 | 12 | afterEach(() => { 13 | deleteDir(dir); 14 | }); 15 | 16 | test("for no functions directory", async () => { 17 | createFiles(dir, { "serverless/": "" }); 18 | await expect( 19 | validateFunctionExports({ dir } as IContext) 20 | ).resolves.toBeUndefined(); 21 | }); 22 | 23 | test("for empty functions directory", async () => { 24 | createFiles(dir, { "serverless/functions/": "" }); 25 | await expect( 26 | validateFunctionExports({ dir } as IContext) 27 | ).resolves.toBeUndefined(); 28 | }); 29 | 30 | test("for invalid file", async () => { 31 | createFiles(dir, { "serverless/functions/aaaa.ts": "

cfd

" }); 32 | await expect( 33 | validateFunctionExports({ dir } as IContext) 34 | ).rejects.toMatchObject({ 35 | message: expect.stringContaining( 36 | "serverless/functions/aaaa.ts must have a default export" 37 | ) 38 | }); 39 | }); 40 | 41 | test("for all valid functions", async () => { 42 | createFiles(dir, { 43 | "serverless/functions/home.ts": 44 | "export default function Home() {}; export const a = 10;", 45 | "serverless/functions/about/me.ts": "export default function AboutMe() {}" 46 | }); 47 | 48 | await expect( 49 | validateFunctionExports({ dir } as IContext) 50 | ).resolves.toBeUndefined(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/docs/src/reference/main-concepts/module.md: -------------------------------------------------------------------------------- 1 | ```YAML 2 | title: SOMOD Module | SOMOD 3 | meta: 4 | description: 5 | Modules are the building blocks of SOMOD architecture. SOMOD combines Infrastructure, Backend, and Frontend code in a single reusable package called Module 6 | ``` 7 | 8 | # Module 9 | 10 | --- 11 | 12 | SOMOD combines Infrastructure, Backend, and Frontend code in a single reusable package called the **Module**. Modules are packaged, distributed, and reused using NPM. 13 | 14 | Start by creating a simple module, then add it as an npm dependency in another module, and so on. SOMOD integrates all dependency modules to create AWS SAM Project and NextJs Project. 15 | 16 | > - You can deploy the **AWS SAM** Project using SAM CLI. To know more about AWS SAM, visit their documentation [here](https://aws.amazon.com/serverless/sam/). 17 | > - **NextJs** is an all-in-one framework to develop awesome UI using React. You can deploy NextJs projects to Managed Server on Vercel. NextJs [documents](https://nextjs.org/) other ways of deploying too. 18 | 19 | ## Structure of a Module 20 | 21 | To facilitate the integration and working of the Module, SOMOD defines the **directory structure** of the SOMOD Module project. 22 | 23 | Initialize the SOMOD project by simply running 24 | 25 | ```bash 26 | npx create-somod 27 | ``` 28 | 29 | > NPM zips all or selected files and directories of the project into a `tar.gz` file for distribution. NPM mandates and defines the content structure of `package.json`, the rest of the content is the developer's choice. Along with the content of package.json, SOMOD defines the directory structure of whole project. 30 | 31 | Let's understand the structure of `package.json` in the [next section](/reference/main-concepts/package.json). 32 | -------------------------------------------------------------------------------- /packages/create/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-somod", 3 | "version": "2.1.1", 4 | "description": "SOMOD Project creator", 5 | "keywords": [ 6 | "somod", 7 | "serverless-optimized-modules", 8 | "reuse serverless", 9 | "sodaru", 10 | "developer", 11 | "dev", 12 | "somod init" 13 | ], 14 | "author": "Raghavendra K R ", 15 | "contributors": [ 16 | "Raghavendra K R ", 17 | "Lokesh G C ", 18 | "Sukhesh M G " 19 | ], 20 | "homepage": "https://github.com/somod-dev/somod/tree/main/packages/create#readme", 21 | "license": "MIT", 22 | "main": "./dist/index.js", 23 | "typings": "./dist/index.d.ts", 24 | "bin": { 25 | "create-somod": "./bin/create-somod.js" 26 | }, 27 | "files": [ 28 | "bin", 29 | "dist", 30 | ".create-somod.js", 31 | "template" 32 | ], 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/somod-dev/somod.git" 36 | }, 37 | "scripts": { 38 | "clean": "npx rimraf dist", 39 | "bundle": "npx esbuild src/index.ts --bundle --outdir=dist --platform=node --minify", 40 | "emit-types": "npx tsc", 41 | "build": "npm run clean && npm run bundle && npm run emit-types", 42 | "pretest": "$CI || npm run build", 43 | "test": "echo 'No Tests'", 44 | "prepack": "node bin-src/build.js" 45 | }, 46 | "dependencies": { 47 | "cli-opentelemetry": "^1.2.3" 48 | }, 49 | "devDependencies": { 50 | "@types/lodash": "^4.14.179", 51 | "@types/node": "^17.0.21 || ^18.0.0", 52 | "esbuild": "^0.25.5", 53 | "lodash": "^4.17.21", 54 | "nodejs-cli-runner": "^1.2.2", 55 | "nodejs-file-utils": "^1.0.1", 56 | "tslib": "^2.4.0", 57 | "typescript": "^4.5.5" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/integration-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "somod-integration-tests", 3 | "version": "2.1.1", 4 | "description": "Integration tests SOMDO (Serverless Optimized MODule) - The Serverless Framework. THIS REPO IS NOT PUBLISHED TO NPM", 5 | "keywords": [ 6 | "sodaru", 7 | "somod", 8 | "serverless", 9 | "aws-sam", 10 | "nextjs", 11 | "react", 12 | "micro-app", 13 | "package", 14 | "builder" 15 | ], 16 | "author": "Raghavendra K R ", 17 | "contributors": [ 18 | "Raghavendra K R ", 19 | "Lokesh G C ", 20 | "Sukhesh M G " 21 | ], 22 | "homepage": "https://github.com/somod-dev/somod/tree/main/packages/integration-tests#readme", 23 | "license": "MIT", 24 | "private": true, 25 | "repository": { 26 | "type": "git", 27 | "url": "https//github.com/somod-dev/somod.git" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/somod-dev/somod/issues" 31 | }, 32 | "scripts": { 33 | "proxy:start": "node proxy/start.js", 34 | "proxy:stop": "node proxy/stop.js", 35 | "proxy:stop-catch": "npm run proxy:stop && exit 1", 36 | "pretest": "npm run proxy:start", 37 | "integration:test": "npx jest || npm run proxy:stop-catch", 38 | "test": "echo 'its ok'", 39 | "posttest": "npm run proxy:stop" 40 | }, 41 | "devDependencies": { 42 | "@types/jest": "^27.5.1", 43 | "@types/node": "^17.0.21 || ^18.0.0", 44 | "jest": "28.1.0", 45 | "jest-environment-steps": "^1.1.0", 46 | "jest-summarizing-reporter": "^1.1.4", 47 | "nodejs-cli-runner": "^1.2.2", 48 | "nodejs-file-utils": "^1.0.1", 49 | "ts-jest": "28.0.2", 50 | "typescript": "^4.5.5", 51 | "somod": "^2.1.1", 52 | "create-somod": "^2.1.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/types/src/ServerlessTemplate.ts: -------------------------------------------------------------------------------- 1 | import { JSONType } from "./JsonTemplate"; 2 | 3 | export type ServerlessResource = Readonly< 4 | { 5 | Type: string; 6 | Properties: Record; 7 | } & Record 8 | >; 9 | 10 | export type ServerlessTemplate = Readonly<{ 11 | Resources: Readonly>; 12 | Outputs?: Readonly>; 13 | }>; 14 | 15 | export type ModuleServerlessTemplate = Readonly<{ 16 | module: string; 17 | template: ServerlessTemplate; 18 | }>; 19 | 20 | export type ResourcePropertySourceNode = Readonly<{ 21 | module: string; 22 | resource: string; 23 | children: Record; 24 | }>; 25 | 26 | export interface IServerlessTemplateHandler { 27 | /** 28 | * Returns null if no template found for given module 29 | */ 30 | getTemplate(module: string): ModuleServerlessTemplate | null; 31 | 32 | listTemplates(): ModuleServerlessTemplate[]; 33 | 34 | /** 35 | * Returns the resource merged with all extended resources 36 | */ 37 | getResource( 38 | module: string, 39 | resource: string 40 | ): { 41 | resource: ServerlessResource; 42 | propertySourceMap: ResourcePropertySourceNode; 43 | } | null; 44 | 45 | getResourcePropertySource( 46 | propertyPath: (string | number)[], 47 | propertyModuleMap: ResourcePropertySourceNode 48 | ): { module: string; resource: string; depth: number }; 49 | 50 | get functionNodeRuntimeVersion(): string; 51 | 52 | getSAMResourceLogicalId(moduleName: string, somodResourceId: string): string; 53 | getSAMResourceName(moduleName: string, somodResourceName: string): JSONType; 54 | getSAMOutputName(parameterName: string): string; 55 | getParameterNameFromSAMOutputName(samOutputName: string): string; 56 | } 57 | -------------------------------------------------------------------------------- /packages/lib/src/tasks/nextJs/watchRootModulePagesData.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, writeFileSync } from "fs"; 2 | import { join, normalize, relative } from "path"; 3 | import Watchpack from "watchpack"; 4 | import { path_pages, path_pagesData, path_ui } from "../../utils/constants"; 5 | import { addPageExtention } from "../../utils/nextJs/pages"; 6 | 7 | export const watchRootModulePagesData = async ( 8 | dir: string 9 | ): Promise<() => void> => { 10 | const watchDir = join(dir, path_ui, path_pagesData); 11 | const wp = new Watchpack({}); 12 | const normalizedWatchDir = normalize(watchDir); 13 | let watchingParent = false; 14 | let dirWatch = normalizedWatchDir; 15 | while (!existsSync(dirWatch) && dirWatch.length > 0) { 16 | dirWatch = join(dirWatch, ".."); 17 | } 18 | watchingParent = dirWatch != normalizedWatchDir; 19 | wp.watch([], [dirWatch]); 20 | 21 | wp.on("change", (filePath: string) => { 22 | if (watchingParent) { 23 | if (filePath == normalizedWatchDir) { 24 | watchingParent = false; 25 | wp.watch([], [normalizedWatchDir]); 26 | } 27 | return; 28 | } 29 | const pageDataRelativePath = relative(watchDir, filePath); 30 | const page = pageDataRelativePath.substring( 31 | 0, 32 | pageDataRelativePath.lastIndexOf(".") 33 | ); 34 | try { 35 | const pageFullPath = addPageExtention( 36 | join(dir, path_ui, path_pages, page) 37 | ); 38 | writeFileSync(pageFullPath, readFileSync(pageFullPath, "utf8")); // this triggers page update 39 | } catch (e) { 40 | if (e?.message?.startsWith("Could not find supported extention for")) { 41 | // ignore if corresponding page does not exist 42 | } else { 43 | throw e; 44 | } 45 | } 46 | }); 47 | 48 | return () => { 49 | wp.close(); 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /packages/lib/src/utils/serverless/keywords/output.ts: -------------------------------------------------------------------------------- 1 | import { KeywordDefinition, ServerlessResource } from "somod-types"; 2 | import { getPath } from "../../jsonTemplate"; 3 | 4 | type Output = { 5 | default?: boolean; 6 | attributes?: string[]; 7 | }; 8 | 9 | export const keywordOutput: KeywordDefinition = { 10 | keyword: "SOMOD::Output", 11 | 12 | getValidator: async () => { 13 | return (keyword, node) => { 14 | const errors: Error[] = []; 15 | 16 | const path = getPath(node); 17 | if (!(path.length == 2 && path[0] == "Resources")) { 18 | errors.push( 19 | new Error(`${keyword} is allowed only as Resource Property`) 20 | ); 21 | } 22 | 23 | //NOTE: structure of the value is validated by serverless-schema 24 | 25 | return errors; 26 | }; 27 | }, 28 | 29 | getProcessor: async () => () => { 30 | return { 31 | type: "keyword", 32 | value: {} 33 | }; 34 | } 35 | }; 36 | 37 | export const checkOutput = ( 38 | resource: ServerlessResource, 39 | referencedModule: string, 40 | referencedResource: string, 41 | referencedAttribute?: string 42 | ) => { 43 | const outputDefinitionInTargetResource = resource[ 44 | keywordOutput.keyword 45 | ] as Output; 46 | 47 | if (referencedAttribute === undefined) { 48 | if (outputDefinitionInTargetResource?.default === false) { 49 | throw new Error( 50 | `default must be true in ${keywordOutput.keyword} of ${referencedResource} resource in ${referencedModule}.` 51 | ); 52 | } 53 | } else if ( 54 | !outputDefinitionInTargetResource?.attributes?.includes(referencedAttribute) 55 | ) { 56 | throw new Error( 57 | `attributes must have ${referencedAttribute} in ${keywordOutput.keyword} of ${referencedResource} resource in ${referencedModule}.` 58 | ); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /packages/lib/tests/utils/packageJson.test.ts: -------------------------------------------------------------------------------- 1 | import { createFiles, createTempDir, deleteDir } from "../utils"; 2 | import { read, update } from "../../src/utils/packageJson"; 3 | import { join } from "path"; 4 | 5 | describe("Test Util packageJson.read", () => { 6 | let dir: string = null; 7 | 8 | beforeEach(() => { 9 | dir = createTempDir("test-somod-lib"); 10 | }); 11 | 12 | afterEach(() => { 13 | deleteDir(dir); 14 | }); 15 | test("for non existing file", async () => { 16 | expect.assertions(1); 17 | await expect(read(dir)).rejects.toMatchObject({ 18 | message: expect.stringContaining( 19 | "no such file or directory, open '" + join(dir, "package.json") + "'" 20 | ) 21 | }); 22 | }); 23 | 24 | test("for invalid json file", async () => { 25 | expect.assertions(1); 26 | createFiles(dir, { "package.json": "" }); 27 | await expect(read(dir)).rejects.toMatchObject({ 28 | message: expect.stringContaining("Unexpected end of JSON input") 29 | }); 30 | }); 31 | 32 | test("for valid file", async () => { 33 | expect.assertions(1); 34 | createFiles(dir, { 35 | "package.json": JSON.stringify({ name: "test-package" }) 36 | }); 37 | await expect(read(dir)).resolves.toEqual({ 38 | name: "test-package" 39 | }); 40 | }); 41 | }); 42 | 43 | describe("Test Util packageJson.update", () => { 44 | let dir: string = null; 45 | 46 | beforeEach(() => { 47 | dir = createTempDir("test-somod-lib"); 48 | }); 49 | 50 | afterEach(() => { 51 | deleteDir(dir); 52 | }); 53 | 54 | test("for valid file", async () => { 55 | createFiles(dir, { 56 | "package.json": JSON.stringify({ name: "test-package" }) 57 | }); 58 | expect(update(dir, { name: "some-package" })).toBeUndefined(); 59 | await expect(read(dir)).resolves.toEqual({ name: "some-package" }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/build/serverless/functions/segregatemessage.js: -------------------------------------------------------------------------------- 1 | import { __awaiter, __generator } from "tslib"; 2 | import { DynamoDB } from "aws-sdk"; 3 | var ddb = new DynamoDB.DocumentClient({ 4 | apiVersion: "2012-08-10", 5 | region: process.env.AWS_REGION 6 | }); 7 | var handleRecord = function (record) { return __awaiter(void 0, void 0, void 0, function () { 8 | var message; 9 | var _a; 10 | return __generator(this, function (_b) { 11 | message = DynamoDB.Converter.unmarshall(((_a = record.dynamodb) === null || _a === void 0 ? void 0 : _a.NewImage) || {}); 12 | if (typeof message.message["type"] === "string") { 13 | ddb.put({ 14 | TableName: process.env.MESSAGE_TYPE_TABLE_NAME, 15 | Item: { 16 | type: message.message["type"], 17 | messageId: message.messageId 18 | } 19 | }); 20 | } 21 | return [2 /*return*/]; 22 | }); 23 | }); }; 24 | var handler = function (event) { return __awaiter(void 0, void 0, void 0, function () { 25 | var _i, _a, record; 26 | return __generator(this, function (_b) { 27 | switch (_b.label) { 28 | case 0: 29 | _i = 0, _a = event.Records; 30 | _b.label = 1; 31 | case 1: 32 | if (!(_i < _a.length)) return [3 /*break*/, 4]; 33 | record = _a[_i]; 34 | if (!(record.eventName == "INSERT")) return [3 /*break*/, 3]; 35 | return [4 /*yield*/, handleRecord(record)]; 36 | case 2: 37 | _b.sent(); 38 | _b.label = 3; 39 | case 3: 40 | _i++; 41 | return [3 /*break*/, 1]; 42 | case 4: return [2 /*return*/]; 43 | } 44 | }); 45 | }); }; 46 | export default handler; 47 | -------------------------------------------------------------------------------- /packages/docs/src/getting-started.md: -------------------------------------------------------------------------------- 1 | ```YAML 2 | title: Getting Started with SOMOD | SOMOD - Serverless Optimized Modules 3 | meta: 4 | description: 5 | This page is an overview of the SOMOD Documentation and its resources. 6 | ``` 7 | 8 | # Getting Started with SOMOD 9 | 10 | --- 11 | 12 | This page is an overview of the SOMOD Documentation and its resources. 13 | 14 | > **SOMOD** (Serverless Optimized MODules) is CLI Tool to Develop, Build, and Reuse NPM packages which contains Infrastructure, BackEnd, and FrontEnd code 15 | 16 | - **Try SOMOD** 17 | 18 | Try the SOMOD by running the following command 19 | 20 | ``` 21 | npx create-somod my-first-somod-module 22 | ``` 23 | 24 | This command initializes an NPM package called `my-first-somod-module` along with the required configuration and sample files 25 | 26 | **To see the results**, run the following commands 27 | 28 | ``` 29 | cd my-first-somod-module 30 | npx somod start --dev 31 | ``` 32 | 33 | and visit http://localhost:3000 to see the first page created using SOMOD. 34 | 35 | - **Learn SOMOD** 36 | 37 | People come to SOMOD from different backgrounds and with different learning styles. Whether you prefer a more theoretical or a practical approach, we hope you’ll find this section helpful. 38 | 39 | If you prefer to learn by doing, start with our [practical](/getting-started/setup) guide with an example. 40 | If you prefer to learn concepts step by step, start with our [reference](/reference) guide. 41 | 42 | Like any unfamiliar technology, SOMOD does have a learning curve. With practice and some patience, you will get the hang of it. 43 | 44 | - **Contributing to SOMOD** 45 | 46 | We love your contributions to this project. Be it a problem in the documentation or an issue with the code or a fix to the issue. 47 | 48 | Please use the [SOMOD GitHub](https://github.com/somod-dev/somod) repo to contribute to this project. 49 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification-service/serverless/functions/receivemessage.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyHandlerV2 } from "aws-lambda"; 2 | import { DynamoDB } from "aws-sdk"; 3 | import { v1 } from "uuid"; 4 | 5 | const ddb = new DynamoDB({ 6 | apiVersion: "2012-08-10", 7 | region: process.env.AWS_REGION 8 | }); 9 | 10 | const TABLE_NAME = process.env.TABLE_NAME as string; 11 | const API_KEY = process.env.API_KEY as string; 12 | 13 | const handler: APIGatewayProxyHandlerV2 = async event => { 14 | // authentication 15 | if (event.headers["authorization"] !== API_KEY) { 16 | return { statusCode: 401 }; 17 | } 18 | 19 | // sanity 20 | const body = JSON.parse(event.body || "{}"); 21 | if (body?.message === undefined || body?.audience === undefined) { 22 | return { 23 | statusCode: 400, 24 | body: JSON.stringify({ 25 | error: "must have 'message' and 'audience' properties in the body" 26 | }), 27 | headers: { "Content-Type": "application/json" } 28 | }; 29 | } 30 | if ( 31 | body.audience?.userId === undefined && 32 | body.audience?.groupId === undefined 33 | ) { 34 | return { 35 | statusCode: 400, 36 | body: JSON.stringify({ 37 | error: "audience property must have 'userId' or 'groupId'" 38 | }), 39 | headers: { "Content-Type": "application/json" } 40 | }; 41 | } 42 | // TODO: typecheck for userId and groupId 43 | 44 | const messageId = v1(); 45 | 46 | await ddb 47 | .putItem({ 48 | TableName: TABLE_NAME, 49 | Item: DynamoDB.Converter.marshall({ 50 | messageId, 51 | message: body.message, 52 | audience: body.audience 53 | }) 54 | }) 55 | .promise(); 56 | 57 | return { 58 | statusCode: 200, 59 | body: JSON.stringify({ messageId }), 60 | headers: { "Content-Type": "application/json" } 61 | }; 62 | }; 63 | 64 | export default handler; 65 | -------------------------------------------------------------------------------- /packages/integration-tests/samples/push-notification/build/serverless/template.json: -------------------------------------------------------------------------------- 1 | {"Resources":{"MessageTypeTable":{"Type":"AWS::DynamoDB::Table","Properties":{"AttributeDefinitions":[{"AttributeName":"type","AttributeType":"S"},{"AttributeName":"messageId","AttributeType":"S"}],"KeySchema":[{"AttributeName":"type","KeyType":"HASH"}],"GlobalSecondaryIndexes":[{"IndexName":"byMessageId","KeySchema":[{"AttributeName":"messageId","KeyType":"HASH"}],"Projection":{"ProjectionType":"ALL"}}],"BillingMode":"PAY_PER_REQUEST","SSESpecification":{"SSEEnabled":true}}},"MessageSegrgationFunction":{"Type":"AWS::Serverless::Function","Properties":{"CodeUri":{"SOMOD::Function":{"type":"DynamoDB","name":"segregatemessage"}},"MemorySize":256,"Environment":{"Variables":{"MESSAGE_TYPE_TABLE_NAME":{"SOMOD::Ref":{"resource":"MessageTypeTable"}}}},"Events":{"StreamFromMessagesTable":{"Type":"DynamoDB","Properties":{"Stream":{"SOMOD::Ref":{"module":"push-notification-service","resource":"MessagesTable","attribute":"StreamArn"}},"BatchSize":10,"StartingPosition":"LATEST","MaximumRetryAttempts":3,"ParallelizationFactor":10}}},"Policies":[{"DynamoDBCrudPolicy":{"TableName":{"SOMOD::Ref":{"resource":"MessageTypeTable"}}}}],"Timeout":300}},"AuthorizerLambda":{"Type":"AWS::Serverless::Function","SOMOD::Output":{"default":true,"attributes":["Arn"]},"Properties":{"CodeUri":{"SOMOD::Function":{"type":"HttpApiAuthorizer","name":"authorizer"}},"Environment":{"Variables":{"secret":{"SOMOD::Parameter":"authorizer.jwt.secret"}}}}},"AuthorizerHttpApi":{"Type":"AWS::Serverless::HttpApi","SOMOD::Extend":{"module":"push-notification-service","resource":"ManagementApi"},"Properties":{"Auth":{"Authorizers":{"LambdaAuthorizer":{"AuthorizerPayloadFormatVersion":"2.0","EnableFunctionDefaultPermissions":true,"EnableSimpleResponses":true,"FunctionArn":{"SOMOD::Ref":{"module":"push-notification","resource":"AuthorizerLambda","attribute":"Arn"}},"Identity":{"Headers":["Authorization"]}}},"DefaultAuthorizer":"LambdaAuthorizer"}}}}} -------------------------------------------------------------------------------- /packages/lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "somod-lib", 3 | "version": "2.1.1", 4 | "description": "SOMOD library", 5 | "author": "Raghavendra K R ", 6 | "contributors": [ 7 | "Raghavendra K R ", 8 | "Lokesh G C ", 9 | "Sukhesh M G " 10 | ], 11 | "homepage": "https://github.com/somod-dev/somod/tree/main/packages/lib#readme", 12 | "license": "MIT", 13 | "module": "dist/index.js", 14 | "typings": "dist/index.d.ts", 15 | "files": [ 16 | "dist" 17 | ], 18 | "sideEffects": false, 19 | "private": true, 20 | "repository": { 21 | "type": "git", 22 | "url": "https//github.com/somod-dev/somod.git" 23 | }, 24 | "scripts": { 25 | "clean": "npx rimraf dist", 26 | "typescript": "tsc", 27 | "build": "npm run clean && npm run typescript", 28 | "pretest": "$CI || npm run build", 29 | "test": "npx jest" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/somod-dev/somod/issues" 33 | }, 34 | "dependencies": { 35 | "@aws-sdk/client-cloudformation": "^3.121.0", 36 | "@typescript-eslint/typescript-estree": ">=5.22.0", 37 | "decorated-ajv": "^1.1.0", 38 | "esbuild": "^0.25.5", 39 | "graph-dsa": "^1.0.0", 40 | "js-yaml": "^4.1.0", 41 | "json-object-merge": "^1.2.0", 42 | "lodash": "^4.17.21", 43 | "nodejs-cli-runner": "^1.2.2", 44 | "nodejs-file-utils": "^1.0.1", 45 | "rimraf": "^3.0.2", 46 | "tslib": "^2.4.0", 47 | "watchpack": "^2.2.0" 48 | }, 49 | "devDependencies": { 50 | "@types/jest": "^27.5.1", 51 | "@types/js-yaml": "^4.0.3", 52 | "@types/lodash": "^4.14.179", 53 | "@types/node": "^17.0.21 || ^18.0.0", 54 | "@types/rimraf": "^3.0.2", 55 | "jest": "28.1.0", 56 | "jest-summarizing-reporter": "^1.1.4", 57 | "ts-jest": "28.0.2", 58 | "typescript": "^4.5.5", 59 | "somod-schema": "^2.1.1", 60 | "somod-types": "^2.1.1" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/lib/tests/tasks/extension/bundle.test.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "somod-types"; 2 | import { bundleExtension } from "../../../src"; 3 | import { bundle } from "../../../src/utils/extension/bundle"; 4 | import { 5 | createFiles, 6 | createTempDir, 7 | deleteDir, 8 | mockedFunction 9 | } from "../../utils"; 10 | 11 | jest.mock("../../../src/utils/extension/bundle", () => { 12 | return { 13 | __esModule: true, 14 | bundle: jest.fn() 15 | }; 16 | }); 17 | 18 | describe("Test Task bundleExtension", () => { 19 | let dir: string; 20 | beforeEach(() => { 21 | dir = createTempDir("test-somod-lib"); 22 | }); 23 | 24 | afterEach(() => { 25 | deleteDir(dir); 26 | mockedFunction(bundle).mockReset(); 27 | }); 28 | 29 | test("for no extension file", async () => { 30 | mockedFunction(bundle).mockResolvedValue(); 31 | 32 | await expect( 33 | bundleExtension({ dir: "sample" } as IContext, true) 34 | ).resolves.toBeUndefined(); 35 | expect(bundle).toHaveBeenCalledTimes(0); 36 | }); 37 | 38 | test("for successfull initialization", async () => { 39 | createFiles(dir, { "extension.ts": "" }); 40 | mockedFunction(bundle).mockResolvedValue(); 41 | 42 | await expect( 43 | bundleExtension({ dir } as IContext, true) 44 | ).resolves.toBeUndefined(); 45 | expect(bundle).toHaveBeenCalledTimes(1); 46 | expect(bundle).toHaveBeenCalledWith({ dir } as IContext, true); 47 | }); 48 | 49 | test("for failed initialization", async () => { 50 | createFiles(dir, { "extension.ts": "" }); 51 | mockedFunction(bundle).mockRejectedValue( 52 | new Error("There is an error in bundling extension") 53 | ); 54 | 55 | await expect(bundleExtension({ dir } as IContext)).rejects.toEqual( 56 | new Error("There is an error in bundling extension") 57 | ); 58 | expect(bundle).toHaveBeenCalledTimes(1); 59 | expect(bundle).toHaveBeenCalledWith({ dir } as IContext, false); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/lib/src/utils/serverless/keywords/templateOutputs.ts: -------------------------------------------------------------------------------- 1 | import { file_parametersYaml } from "../../constants"; 2 | import { getPath } from "../../jsonTemplate"; 3 | import { getParameterToModuleMap } from "../../parameters/namespace"; 4 | import { JSONType, KeywordDefinition } from "somod-types"; 5 | 6 | type Outputs = Record; 7 | 8 | export const keywordTemplateOutputs: KeywordDefinition = { 9 | keyword: "Outputs", 10 | 11 | getValidator: async (moduleName, context) => { 12 | const parameters = Object.keys(getParameterToModuleMap(context)); 13 | return (keyword, node, value) => { 14 | const errors: Error[] = []; 15 | 16 | const path = getPath(node); 17 | if (path.length == 0) { 18 | // consider as keyword only if found at the top of the template 19 | 20 | //NOTE: structure of the value is validated by serverless-schema 21 | 22 | Object.keys(value).forEach(parameter => { 23 | if (!parameters.includes(parameter)) { 24 | errors.push( 25 | new Error( 26 | `parameter ${parameter} referenced by ${keyword} does not exist. Define ${parameter} in /${file_parametersYaml}` 27 | ) 28 | ); 29 | } 30 | }); 31 | } 32 | 33 | return errors; 34 | }; 35 | }, 36 | 37 | getProcessor: async (moduleName, context) => { 38 | return (keyword, node, value) => { 39 | if (getPath(node).length == 0) { 40 | return { 41 | type: "keyword", 42 | value: { 43 | [keyword]: Object.fromEntries( 44 | Object.keys(value).map(p => [ 45 | context.serverlessTemplateHandler.getSAMOutputName(p), 46 | { Value: value[p], Description: `Value for ${p}` } 47 | ]) 48 | ) 49 | } 50 | }; 51 | } 52 | return { 53 | type: "keyword", 54 | value: { 55 | [keyword]: value 56 | } 57 | }; 58 | }; 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /packages/lib/tests/tasks/nextJs/buildUiPublic.test.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | import { readFile } from "fs/promises"; 3 | import { join } from "path"; 4 | import { IContext } from "somod-types"; 5 | import { buildUiPublic } from "../../../src"; 6 | import { createFiles, createTempDir, deleteDir } from "../../utils"; 7 | 8 | describe("Test Task buildUiPublic", () => { 9 | let dir: string = null; 10 | 11 | beforeEach(() => { 12 | dir = createTempDir("test-somod-lib"); 13 | }); 14 | 15 | afterEach(() => { 16 | deleteDir(dir); 17 | }); 18 | 19 | test("for no ui dir", async () => { 20 | await expect(buildUiPublic({ dir } as IContext)).resolves.toBeUndefined(); 21 | expect(existsSync(join(dir, "build"))).toBeFalsy(); 22 | }); 23 | 24 | test("for no public dir", async () => { 25 | createFiles(dir, { "ui/": "" }); 26 | await expect(buildUiPublic({ dir } as IContext)).resolves.toBeUndefined(); 27 | expect(existsSync(join(dir, "build"))).toBeFalsy(); 28 | }); 29 | 30 | test("for empty public dir", async () => { 31 | createFiles(dir, { "ui/public/": "" }); 32 | await expect(buildUiPublic({ dir } as IContext)).resolves.toBeUndefined(); 33 | expect(existsSync(join(dir, "build/public"))).toBeFalsy(); 34 | }); 35 | 36 | test("for valid public dir", async () => { 37 | createFiles(dir, { 38 | "ui/public/a.css": "a {color: #090909}", 39 | "ui/public/b.css": "a {margin: 5}", 40 | "ui/public/f1/a.css": "a {top: 5}" 41 | }); 42 | await expect(buildUiPublic({ dir } as IContext)).resolves.toBeUndefined(); 43 | await expect( 44 | readFile(join(dir, "build/ui/public/a.css"), { encoding: "utf8" }) 45 | ).resolves.toEqual("a {color: #090909}"); 46 | await expect( 47 | readFile(join(dir, "build/ui/public/b.css"), { encoding: "utf8" }) 48 | ).resolves.toEqual("a {margin: 5}"); 49 | 50 | await expect( 51 | readFile(join(dir, "build/ui/public/f1/a.css"), { encoding: "utf8" }) 52 | ).resolves.toEqual("a {top: 5}"); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/lib/src/utils/serverless/keywords/dependsOn.ts: -------------------------------------------------------------------------------- 1 | import { getPath } from "../../jsonTemplate"; 2 | import { checkAccess } from "./access"; 3 | import { JSONObjectNode, KeywordDefinition } from "somod-types"; 4 | import { getReferencedResource } from "./ref"; 5 | 6 | export type DependsOn = { module?: string; resource: string }[]; 7 | 8 | const validateKeywordPosition = (node: JSONObjectNode) => { 9 | const path = getPath(node); 10 | if (!(path.length == 2 && path[0] == "Resources")) { 11 | throw new Error( 12 | `${keywordDependsOn.keyword} is allowed only as Resource Property` 13 | ); 14 | } 15 | return path; 16 | }; 17 | 18 | export const keywordDependsOn: KeywordDefinition = { 19 | keyword: "SOMOD::DependsOn", 20 | 21 | getValidator: async (moduleName, context) => { 22 | return (keyword, node, value) => { 23 | const errors: Error[] = []; 24 | 25 | try { 26 | validateKeywordPosition(node); 27 | 28 | value.map(v => { 29 | try { 30 | const resource = getReferencedResource( 31 | context.serverlessTemplateHandler, 32 | v.module || moduleName, 33 | v.resource, 34 | "Depended" 35 | ); 36 | 37 | checkAccess( 38 | resource.resource, 39 | v.module || moduleName, 40 | v.resource, 41 | moduleName, 42 | "Depended" 43 | ); 44 | } catch (e) { 45 | errors.push(e); 46 | } 47 | }); 48 | } catch (e) { 49 | errors.push(e); 50 | } 51 | 52 | return errors; 53 | }; 54 | }, 55 | 56 | getProcessor: async (moduleName, context) => { 57 | return (keyword, node, value) => ({ 58 | type: "keyword", 59 | value: { 60 | DependsOn: value.map(v => 61 | context.serverlessTemplateHandler.getSAMResourceLogicalId( 62 | v.module || moduleName, 63 | v.resource 64 | ) 65 | ) 66 | } 67 | }); 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /packages/middleware/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { Context, Handler } from "aws-lambda"; 2 | import { 3 | Middleware, 4 | IMiddlewareContext, 5 | EventWithMiddlewareContext 6 | } from "somod-types"; 7 | 8 | export class MiddlewareContext implements IMiddlewareContext { 9 | private context: Record = {}; 10 | 11 | constructor() { 12 | Object.setPrototypeOf(this, new.target.prototype); 13 | } 14 | 15 | set(key: string, value: unknown) { 16 | this.context[key] = value; 17 | } 18 | 19 | get(key: string) { 20 | return this.context[key]; 21 | } 22 | } 23 | 24 | const executeLambda = async < 25 | TEvent = Record, 26 | TResult = unknown 27 | >( 28 | lambda: Handler, 29 | event: TEvent, 30 | context: Context 31 | ): Promise => { 32 | let lambdaResult: void | Promise | TResult = lambda( 33 | event, 34 | context, 35 | null 36 | ); 37 | 38 | while (typeof lambdaResult?.["then"] === "function") { 39 | lambdaResult = await lambdaResult; 40 | } 41 | 42 | return lambdaResult as TResult; 43 | }; 44 | 45 | export const getMiddlewareHandler = < 46 | TEvent extends Record = Record, 47 | TResult = unknown 48 | >( 49 | lambda: Handler, 50 | middlewares: Middleware[] 51 | ): Handler => { 52 | return async (event, context) => { 53 | const eventWithContext = { 54 | ...event, 55 | somodMiddlewareContext: new MiddlewareContext() 56 | } as EventWithMiddlewareContext; 57 | 58 | let step = middlewares.length; 59 | const next = async () => { 60 | if (step > 0) { 61 | const middleware = middlewares[--step]; 62 | const result = await middleware(next, eventWithContext, context); 63 | return result; 64 | } else { 65 | const lambdaResult = await executeLambda( 66 | lambda, 67 | eventWithContext, 68 | context 69 | ); 70 | return lambdaResult; 71 | } 72 | }; 73 | 74 | return await next(); 75 | }; 76 | }; 77 | --------------------------------------------------------------------------------