Security News
tea.xyz Spam Plagues npm and RubyGems Package Registries
Tea.xyz, a crypto project aimed at rewarding open source contributions, is once again facing backlash due to an influx of spam packages flooding public package registries.
jaypie
Advanced tools
Event-driven fullstack architecture centered around JavaScript, AWS, and the JSON:API specification
Readme
Event-driven fullstack architecture centered around JavaScript, AWS, and the JSON:API specification
"JavaScript on both sides and underneath"
Jaypie is an opinionated approach to application development centered around JavaScript and the JSON:API specification in an event-driven architecture.
Jaypie is suited for applications that require custom infrastructure beyond HTTP requests (e.g., message queues). Without custom infrastructure, fullstack hosts like Vercel or Netlify are recommended.
Jaypie is for building fullstack JavaScript applications.
Jaypie uses the AWS Cloud Development Kit (CDK) to manage infrastructure, which is written in Node.js. This makes managing infrastructure accessible to the fullstack developer without learning a new syntax and living without language constructs like loops and inheritance.
Does NOT use Kubernetes, Docker, Terraform, or the "Serverless" framework.
Jaypie embraces "ejectability," the philosophy that any part of the code can be removed (and therefore replaced) without disturbing the whole.
Jaypie strives to be "mockable-first" meaning all components should be easily tested via default or provided mocks.
npm install jaypie
@jaypie/core
is included in jaypie
. Almost every Jaypie package requires core.
You must install peer dependencies for your project.
Package | Exports | Description |
---|---|---|
@jaypie/aws | getSecret | AWS helpers |
@jaypie/lambda | lambdaHandler | Lambda entry point |
@jaypie/mongoose | connectFromSecretEnv , disconnect , mongoose | MongoDB management |
Matchers, mocks, and utilities to test Jaypie projects.
npm install --save-dev @jaypie/testkit
npm install jaypie @jaypie/lambda
const { InternalError, lambdaHandler, log } = require("jaypie");
export const handler = lambdaHandler(async({event}) => {
// await new Promise(r => setTimeout(r, 2000));
if (event.something === "problem") {
throw new InternalError();
}
// log.debug("Hello World");
return "Hello World";
}, { name: "example"});
This example would then be deployed to AWS via CDK or similar orchestration. See @jaypie/cdk.
import {
getMessages,
getSecret,
sendBatchMessages,
sendMessage,
} from "jaypie";
getMessages(event)
Return an array of message bodies from an SQS event.
import { getMessages } from '@jaypie/aws';
const messages = getMessages(event);
// messages = [{ salutation: "Hello, world!" }, { salutation: "Hola, dushi!" }]
getSecret(secretName: string)
Retrieve a secret from AWS Secrets Manager using the secret name.
import { getSecret } from '@jaypie/aws';
const secret = await getSecret("MongoConnectionStringN0NC3-nSg1bR1sh");
// secret = "mongodb+srv://username:password@env-project.n0nc3.mongodb.net/app?retryWrites=true&w=majority";
sendBatchMessages({ messages, queueUrl })
Batch and send messages to an SQS queue. If more than ten messages are provided, the function will batch them into groups of ten or less (per AWS).
import { sendBatchMessages } from '@jaypie/aws';
const messages = [
{ salutation: "Hello, world!" },
{ salutation: "Hola, dushi!" },
];
const queueUrl = "https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue";
await sendBatchMessages({ messages, queueUrl });
Parameter | Type | Required | Description |
---|---|---|---|
delaySeconds | number | No | Seconds to delay message delivery; default 0 |
messages | Array | Yes | Array of message objects (or strings) |
messageAttributes | object | No | Message attributes |
messageGroupId | string | No | Custom message group for FIFO queues; default provided |
queueUrl | string | Yes | URL of the SQS queue |
sendMessage({ body, queueUrl })
Send a single message to an SQS queue.
import { sendMessage } from '@jaypie/aws';
const body = "Hello, world!";
const queueUrl = "https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue";
const response = await sendMessage({ body, queueUrl });
Parameter | Type | Required | Description |
---|---|---|---|
body | string | Yes | Message body |
delaySeconds | number | No | Seconds to delay message delivery; default 0 |
messageAttributes | object | No | Message attributes |
messageGroupId | string | No | Custom message group for FIFO queues; default provided |
queueUrl | string | Yes | URL of the SQS queue |
import {
CDK,
ERROR,
HTTP,
VALIDATE,
} from "jaypie";
CDK
CDK.ACCOUNT
CDK.ENV
CDK.ROLE
CDK.SERVICE
CDK.TAG
See constants.js in @jaypie/core.
ERROR
Default messages and titles for Jaypie errors.
ERROR.MESSAGE
ERROR.TITLE
See HTTP
for status codes.
HTTP
HTTP.ALLOW.ANY
HTTP.CODE
: OK
, CREATED
, ...HTTP.CONTENT.ANY
HTTP.CONTENT.HTML
HTTP.CONTENT.JSON
HTTP.CONTENT.TEXT
HTTP.HEADER
: ...HTTP.METHOD
: GET
, POST
, ...VALIDATE
VALIDATE.ANY
- DefaultVALIDATE.ARRAY
VALIDATE.CLASS
VALIDATE.FUNCTION
VALIDATE.NUMBER
VALIDATE.NULL
VALIDATE.OBJECT
VALIDATE.STRING
VALIDATE.UNDEFINED
JAYPIE
- for consistency across JaypiePROJECT
- for consistency across projects// See `Error Reference` for full list
const { InternalError } = require("jaypie");
try {
// Code happens...
throw InternalError("Oh, I am slain!");
} catch (error) {
// Is this from a jaypie project?
if(error.isProjectError) {
{
name, // ProjectError
title, // "Internal Server Error"
detail, // "Oh, I am slain"
status, // 500 (HTTP code)
} = error;
} else {
// Not from jaypie
throw error;
}
}
if(error.isProjectError) {
return error.json();
}
const errors = [];
errors.push(BadGatewayError());
errors.push(NotFoundError());
throw MultiError(errors);
Error | Status | Notes |
---|---|---|
BadGatewayError | 502 | Something I need gave me an error |
BadRequestError | 400 | You did something wrong |
ConfigurationError | 500 | "The developer" (probably you) or an associate did something wrong |
ForbiddenError | 403 | You are not allowed |
GatewayTimeoutError | 504 | Something I need is taking too long |
GoneError | 410 | The thing you are looking for was here but is now gone forever |
IllogicalError | 500 | Code is in a state that "should never happen" |
InternalError | 500 | General "something went wrong" |
MethodNotAllowedError | 405 | You tried a good path but the wrong method |
MultiError | Varies | Takes an array of errors |
NotFoundError | 404 | The thing you are looking for is not here and maybe never was |
NotImplementedError | 400 | "The developer" (you again?) didn't finish this part yet - hopefully a temporary message |
RejectedError | 403 | Request filtered prior to processing |
TeapotError | 418 | RFC 2324 section-2.3.2 |
UnavailableError | 503 | The thing you are looking for cannot come to the phone right now |
UnhandledError | 500 | An error that should have been handled wasn't |
UnreachableCodeError | 500 | Should not be possible |
ALWAYS internal to the app, NEVER something the client did
if
/else
that should always return and cover all cases, may throw this as the last else
cloneDeep
lodash.clonedeep
from NPM
import { cloneDeep } from "jaypie";
const original = { a: 1, b: { c: 2 }};
const clone = cloneDeep(original);
envBoolean
Look up a key in process.env
and coerce it into a boolean.
Returns true
for true
(case-insensitive) and 1
for string, boolean, and numeric types.
Returns false
for false
(case-insensitive) and 0
for string, boolean, and numeric types.
Returns undefined
otherwise.
const { envBoolean } = require("jaypie");
process.env.AWESOME = true;
if (envBoolean("AWESOME")) {
console.log("Awesome!");
}
envBoolean
: defaultValue
const { envBoolean } = require("jaypie");
if (envBoolean("AWESOME", { defaultValue: true })) {
console.log("Awesome!");
}
force
Coerce a value into a type or throw an error. Forcing arrays is the primary use case.
import { force } from "jaypie";
argument = force(thing, Array);
argument = force([thing], Array);
// argument = [thing]
force
supports Array, Boolean, Number, Object, and String.
argument = force(argument, Array);
argument = force(argument, Boolean, "true");
argument = force(argument, Number, "12");
argument = force(argument, Object, "key");
argument = force(argument, String, "default");
// Convenience functions
argument = force.array(argument);
argument = force.boolean(argument);
argument = force.number(argument);
argument = force.object(argument, "key");
argument = force.string(argument);
getHeaderFrom
getHeaderFrom(headerKey:string, searchObject:object)
Case-insensitive search inside searchObject
for headerKey
. Also looks in header
and headers
child object of searchObject
, if headerKey
not found at top-level.
placeholders
Lightweight string interpolation
import { placeholders } from "jaypie";
const string = placeholders("Hello, {name}!", { name: "World" });
// string = "Hello, World!"
The code for placeholders was written by Chris Ferdinandi and distributed under the MIT License in 2018-2019. Their web site is https://gomakethings.com
sleep
sleep
is a promise-based setTimeout
that resolves after a specified number of milliseconds. It will NOT run when NODE_ENV
is test
. See sleepAlways
for a version that will run in tests.
import { sleep } from "jaypie";
await sleep(2000);
This is "bad code" because it checks NODE_ENV
during runtime. The "right way" is to let sleep run and mock it in tests, in practice this is needless boilerplate. A fair compromise would be to mock sleep
with @jaypie/testkit
but not all projects include that dependency. Jaypie will trade academically incorrect for human convenience and simplicity.
validate
import { validate, VALIDATE } from "jaypie";
validate(argument, {
type: VALIDATE.ANY,
falsy: false, // When `true`, allows "falsy" values that match the type (e.g., `0`, `""`)
required: true, // When `false`, allows `undefined` as a valid value
throws: true // When `false`, returns `false` instead of throwing error
});
import { validate } from "jaypie";
validate.array(argument);
validate.class(argument);
validate.function(argument);
validate.null(argument);
validate.number(argument);
validate.object(argument);
validate.string(argument);
validate.undefined(argument);
Does not include any, class, or undefined
validate(argument, {
// One of:
type: Array,
type: Function,
type: Number,
type: null,
type: Object,
type: String,
})
The Jaypie handler can be used directly but is more likely to be wrapped in a more specific handler. The Jaypie handler will call lifecycle methods and provide logging. Unhandled errors will be thrown as UnhandledError
.
import { jaypieHandler } from "jaypie";
const handler = jaypieHandler(async(...args) => {
// await new Promise(r => setTimeout(r, 2000));
// log.var({ args });
return "Hello World";
}, { name: "jaypieReference"});
Each function receives the same arguments as the handler.
validate: [async Function]
Returns true
to validate the request. Throw an error or return false
to reject the request.
setup: [async Function]
Called before the handler (e.g., connect to a database). Throw an error to halt execution.
handler: async Function
The main function to handle the request. Throw an error to halt execution.
teardown: [async Function]
Called after the handler (e.g., disconnect from a database). Runs even if setup or handler throws errors.
The Lambda handler wraps the Jaypie handler that is specifically for AWS Lambda. It will call lifecycle methods and provide logging. Unhandled errors will be thrown as UnhandledError
.
const { lambdaHandler } = require("jaypie");
const handler = lambdaHandler(async({event}) => {
// await new Promise(r => setTimeout(r, 2000));
// log.debug("Hello World");
return "Hello World";
}, { name: "lambdaReference"});
import {
log,
} from "jaypie";
import { log } from "jaypie";
log.trace();
log.debug();
log.info();
log.warn();
log.error();
log.fatal();
Uses silent
by default. if process.env.MODULE_LOG_LEVEL
is true
, follows process.env.LOG_LEVEL
. If process.env.MODULE_LOG_LEVEL
is also set, uses that log level.
import { log } from "jaypie";
log.lib().trace();
log.lib({ lib: "myLib" }).trace();
Permanently add the key-value pair to the logger's tags, or at least until log.untag(key)
is called.
import { log } from "jaypie";
log.tag("myTag", "myValue");
log.tag({ myTag: "myValue" });
Remove the key-value pair from the logger's tags.
import { log } from "jaypie";
log.untag("myTag");
log.untag(["myTag1", "myTag2"]);
Log a key-value pair. In the json
format, the key will be tagged as var
and the value will be the value. Logging marker variables this way can be useful for debugging.
import { log } from "jaypie";
log.var("message", "Hello, world");
log.var({ message: "Hello, world" });
const message = "Hello, world";
log.var({ message });
Create a new log object with additional tags
import { log as defaultLogger } from "jaypie";
const log = defaultLogger.with({ customProperty: "customValue" });
import {
connectFromSecretEnv,
disconnect,
mongoose,
} from "jaypie";
connectFromSecretEnv
Jaypie lifecycle method to connect to MongoDB using process.env.MONGO_CONNECTION_STRING
.
import { connectFromSecretEnv, disconnect, lambdaHandler, mongoose } from "jaypie";
const handler = lambdaHandler(async({event}) => {
// mongoose is already connected
return "Hello World";
}, {
name: "lambdaReference"
setup: [connectFromSecretEnv],
teardown: [disconnect],
});
disconnect
Jaypie lifecycle method to disconnect from MongoDB.
import { disconnect, lambdaHandler } from "jaypie";
const handler = lambdaHandler(async({event}) => {
// ...
}, {
teardown: [disconnect],
});
mongoose
mongoose
from NPM
import { mongoose } from "jaypie";
npm install --save-dev @jaypie/testkit
import { restoreLog, spyLog } from "@jaypie/testkit";
import { log } from "@jaypie/core";
beforeEach(() => {
spyLog(log);
});
afterEach(() => {
restoreLog(log);
vi.clearAllMocks();
});
test("log", () => {
log.warn("Danger");
expect(log.warn).toHaveBeenCalled();
expect(log.error).not.toHaveBeenCalled();
});
👺 Logging Conventions:
log.trace
or log.var
during "happy path"log.debug
for edge casesdescribe("Observability", () => {
it("Does not log above trace", async () => {
// Arrange
// TODO: "happy path" setup
// Act
await myNewFunction(); // TODO: add any "happy path" parameters
// Assert
expect(log.debug).not.toHaveBeenCalled();
expect(log.info).not.toHaveBeenCalled();
expect(log.warn).not.toHaveBeenCalled();
expect(log.error).not.toHaveBeenCalled();
expect(log.fatal).not.toHaveBeenCalled();
});
});
👺 Follow the "arrange, act, assert" pattern
testSetup.js
import { matchers as jaypieMatchers } from "@jaypie/testkit";
import * as extendedMatchers from "jest-extended";
import { expect } from "vitest";
expect.extend(extendedMatchers);
expect.extend(jaypieMatchers);
test.spec.js
import { ConfigurationError } from "@jaypie/core";
const error = new ConfigurationError();
const json = error.json();
expect(error).toBeJaypieError();
expect(json).toBeJaypieError();
expect(subject).toBeJaypieError()
Validates instance objects:
try {
throw new Error("Sorpresa!");
} catch (error) {
expect(error).not.toBeJaypieError();
}
Validates plain old JSON:
expect({ errors: [ { status, title, detail } ] }).toBeJaypieError();
Jaypie errors, which are ProjectErrors
, all have a .json()
to convert
expect(subject).toBeValidSchema()
import { jsonApiErrorSchema, jsonApiSchema } from "@jaypie/testkit";
expect(jsonApiErrorSchema).toBeValidSchema();
expect(jsonApiSchema).toBeValidSchema();
expect({ project: "mayhem" }).not.toBeValidSchema();
From jest-json-schema
toBeValidSchema.js (not documented in README)
expect(subject).toMatchSchema(schema)
import { jsonApiErrorSchema, jsonApiSchema } from "@jaypie/testkit";
import { ConfigurationError } from "@jaypie/core";
const error = new ConfigurationError();
const json = error.json();
expect(json).toMatchSchema(jsonApiErrorSchema);
expect(json).not.toMatchSchema(jsonApiSchema);
From jest-json-schema
; see README
import {
jsonApiErrorSchema,
jsonApiSchema,
mockLogFactory,
} from '@jaypie/testkit'
jsonApiErrorSchema
A JSON Schema validator for the JSON:API error schema. Powers the toBeJaypieError
matcher (via toMatchSchema
).
jsonApiSchema
A JSON Schema validator for the JSON:API data schema.
mockLogFactory()
Creates a mock of the log
provided by @jaypie/core
.
import { mockLogFactory } from "@jaypie/testkit";
const log = mockLogFactory();
log.warn("Danger");
expect(log.warn).toHaveBeenCalled();
expect(log.error).not.toHaveBeenCalled();
restoreLog(log)
Restores the log
provided by @jaypie/core
, commonly performed afterEach
with spyLog
in beforeEach
. See example with spyLog
.
spyLog(log)
Spies on the log
provided by @jaypie/core
, commonly performed beforeEach
with restoreLog
in afterEach
.
import { restoreLog, spyLog } from "@jaypie/testkit";
import { log } from "@jaypie/core";
beforeEach(() => {
spyLog(log);
});
afterEach(() => {
restoreLog(log);
vi.clearAllMocks();
});
test("log", () => {
log.warn("Danger");
expect(log.warn).toHaveBeenCalled();
expect(log.error).not.toHaveBeenCalled();
});
@jaypie/cdk
- CDK package@jaypie/express
- Express packageDate | Version | Summary |
---|---|---|
3/19/2024 | 1.0.0 | First publish with @jaypie/core@1.0.0 |
3/15/2024 | 0.1.0 | Initial deploy |
3/15/2024 | 0.0.1 | Initial commit |
Published by Finlayson Studio. All rights reserved
FAQs
Event-driven fullstack architecture centered around JavaScript, AWS, and the JSON:API specification
The npm package jaypie receives a total of 642 weekly downloads. As such, jaypie popularity was classified as not popular.
We found that jaypie demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Tea.xyz, a crypto project aimed at rewarding open source contributions, is once again facing backlash due to an influx of spam packages flooding public package registries.
Security News
As cyber threats become more autonomous, AI-powered defenses are crucial for businesses to stay ahead of attackers who can exploit software vulnerabilities at scale.
Security News
UnitedHealth Group disclosed that the ransomware attack on Change Healthcare compromised protected health information for millions in the U.S., with estimated costs to the company expected to reach $1 billion.