@prairielearn/flash 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # `@prairielearn/flash`
2
+
3
+ Adds support for flash messages to Express applications.
4
+
5
+ ## Usage
6
+
7
+ `req.session` must exist; [`express-session`](https://www.npmjs.com/package/express-session) is the most common way to use sessions in Express.
8
+
9
+ ```ts
10
+ import express from 'express';
11
+ import session from 'express-session';
12
+ import { flashMiddleware } from '@prairielearn/flash';
13
+
14
+ const app = express();
15
+
16
+ app.use(
17
+ session({
18
+ // See https://www.npmjs.com/package/express-session for more information
19
+ // about configuring the session middleware.
20
+ secret: 'secret',
21
+ resave: false,
22
+ saveUninitialized: true,
23
+ })
24
+ );
25
+ app.use(flashMiddleware());
26
+ ```
27
+
28
+ Now, you can use the `flash()` function exported by `@prairielearn/flash` to read/write flash messages.
29
+
30
+ ```ts
31
+ import { flash } from '@prairielearn/flash';
32
+
33
+ app.get('/set-flash', (req, res) => {
34
+ flash('notice', 'Your preferences have been updated.');
35
+ flash('success', 'Course created successfully.');
36
+ flash('warning', 'Syncing completed with 5 warnings.');
37
+ flash('error', 'Group must have fewer than 10 members.');
38
+
39
+ res.redirect('/display-flash');
40
+ });
41
+
42
+ app.get('/display-flash', (req, res) => {
43
+ const messages = flash();
44
+ res.json(messages);
45
+ });
46
+ ```
47
+
48
+ The `flash()` function has three behaviors:
49
+
50
+ - `flash(type: string, message: string)`: Set a message with the given type.
51
+ - `flash(type: string): FlashMessage[]`: Get all messages with the given type.
52
+ - `flash(types: string[]): FlashMessage[]`: Get all messages with any of the given types.
53
+ - `flash(): FlashMessage[]`: Get all messages.
54
+
55
+ Once a message is read, it is immediately removed from the persisted list of messages. A message is _only_ removed once it is read; it will be persisted indefinitely if it iw not.
@@ -0,0 +1,11 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ export type FlashMessageType = 'notice' | 'success' | 'warning' | 'error';
3
+ export interface FlashMessage {
4
+ type: FlashMessageType;
5
+ message: string;
6
+ }
7
+ export declare function flashMiddleware(): (req: Request, _res: Response, next: NextFunction) => void;
8
+ export declare function flash(): FlashMessage[];
9
+ export declare function flash(type: FlashMessageType): FlashMessage[];
10
+ export declare function flash(type: FlashMessageType[]): FlashMessage[];
11
+ export declare function flash(type: FlashMessageType, message: string): void;
package/dist/index.js ADDED
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.flash = exports.flashMiddleware = void 0;
4
+ const node_async_hooks_1 = require("node:async_hooks");
5
+ const als = new node_async_hooks_1.AsyncLocalStorage();
6
+ function flashMiddleware() {
7
+ return (req, _res, next) => {
8
+ const flashStorage = makeFlashStorage(req);
9
+ als.run(flashStorage, () => next());
10
+ };
11
+ }
12
+ exports.flashMiddleware = flashMiddleware;
13
+ function flash(type, message) {
14
+ const flashStorage = als.getStore();
15
+ if (!flashStorage) {
16
+ throw new Error('flash() must be called within a request');
17
+ }
18
+ if (Array.isArray(type)) {
19
+ const messages = type.flatMap((type) => flashStorage.get(type));
20
+ type.forEach((t) => flashStorage.clear(t));
21
+ return messages;
22
+ }
23
+ if (type != null && message != null) {
24
+ flashStorage.add(type, message);
25
+ return;
26
+ }
27
+ if (type != null) {
28
+ const message = flashStorage.get(type);
29
+ flashStorage.clear(type);
30
+ return message;
31
+ }
32
+ const messages = flashStorage.getAll();
33
+ flashStorage.clearAll();
34
+ return messages;
35
+ }
36
+ exports.flash = flash;
37
+ function makeFlashStorage(req) {
38
+ if (!req.session) {
39
+ throw new Error('@prairielearn/flash requires session support');
40
+ }
41
+ const session = req.session;
42
+ return {
43
+ add(type, message) {
44
+ session.flash ??= [];
45
+ session.flash.push({ type, message });
46
+ },
47
+ get(type) {
48
+ const messages = session.flash ?? [];
49
+ return messages.filter((message) => message.type === type);
50
+ },
51
+ getAll() {
52
+ return session.flash ?? [];
53
+ },
54
+ clear(type) {
55
+ session.flash = session.flash?.filter((message) => message.type !== type) ?? [];
56
+ },
57
+ clearAll() {
58
+ session.flash = [];
59
+ },
60
+ };
61
+ }
62
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,uDAAqD;AAGrD,MAAM,GAAG,GAAG,IAAI,oCAAiB,EAAgB,CAAC;AASlD,SAAgB,eAAe;IAC7B,OAAO,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB,EAAE,EAAE;QAC1D,MAAM,YAAY,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;QAC3C,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;IACtC,CAAC,CAAC;AACJ,CAAC;AALD,0CAKC;AAMD,SAAgB,KAAK,CAAC,IAA4C,EAAE,OAAgB;IAClF,MAAM,YAAY,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;IACpC,IAAI,CAAC,YAAY,EAAE;QACjB,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;KAC5D;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;QACvB,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;QAChE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3C,OAAO,QAAQ,CAAC;KACjB;IAED,IAAI,IAAI,IAAI,IAAI,IAAI,OAAO,IAAI,IAAI,EAAE;QACnC,YAAY,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAChC,OAAO;KACR;IAED,IAAI,IAAI,IAAI,IAAI,EAAE;QAChB,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACvC,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACzB,OAAO,OAAO,CAAC;KAChB;IAED,MAAM,QAAQ,GAAG,YAAY,CAAC,MAAM,EAAE,CAAC;IACvC,YAAY,CAAC,QAAQ,EAAE,CAAC;IACxB,OAAO,QAAQ,CAAC;AAClB,CAAC;AA1BD,sBA0BC;AAUD,SAAS,gBAAgB,CAAC,GAAY;IACpC,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE;QAChB,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;KACjE;IAED,MAAM,OAAO,GAAG,GAAG,CAAC,OAAc,CAAC;IAEnC,OAAO;QACL,GAAG,CAAC,IAAsB,EAAE,OAAe;YACzC,OAAO,CAAC,KAAK,KAAK,EAAE,CAAC;YACrB,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QACxC,CAAC;QACD,GAAG,CAAC,IAAsB;YACxB,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;YACrC,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAqB,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;QAC3E,CAAC;QACD,MAAM;YACJ,OAAO,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;QAC7B,CAAC;QACD,KAAK,CAAC,IAAsB;YAC1B,OAAO,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAqB,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;QAChG,CAAC;QACD,QAAQ;YACN,OAAO,CAAC,KAAK,GAAG,EAAE,CAAC;QACrB,CAAC;KACqB,CAAC;AAC3B,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const index_1 = require("./index");
5
+ describe('flash', () => {
6
+ it('throws an error if no session present', () => {
7
+ chai_1.assert.throw(() => {
8
+ (0, index_1.flashMiddleware)()({}, {}, () => { });
9
+ });
10
+ });
11
+ it('throws an error when middleware is not used', () => {
12
+ chai_1.assert.throw(() => {
13
+ (0, index_1.flash)('notice', 'Hello world');
14
+ });
15
+ });
16
+ it('adds a flash', () => {
17
+ const req = {
18
+ session: {},
19
+ };
20
+ const res = {};
21
+ (0, index_1.flashMiddleware)()(req, res, () => {
22
+ (0, index_1.flash)('notice', 'hello world');
23
+ chai_1.assert.sameDeepMembers((0, index_1.flash)(), [{ type: 'notice', message: 'hello world' }]);
24
+ });
25
+ });
26
+ it('stores multiples flashes with the same type', () => {
27
+ const req = {
28
+ session: {},
29
+ };
30
+ const res = {};
31
+ (0, index_1.flashMiddleware)()(req, res, () => {
32
+ (0, index_1.flash)('notice', 'hello world');
33
+ (0, index_1.flash)('notice', 'goodbye world');
34
+ chai_1.assert.sameDeepMembers((0, index_1.flash)(), [
35
+ { type: 'notice', message: 'hello world' },
36
+ { type: 'notice', message: 'goodbye world' },
37
+ ]);
38
+ });
39
+ });
40
+ it('returns flash message for a given type', () => {
41
+ const req = {
42
+ session: {},
43
+ };
44
+ const res = {};
45
+ (0, index_1.flashMiddleware)()(req, res, () => {
46
+ (0, index_1.flash)('notice', 'hello world');
47
+ (0, index_1.flash)('error', 'goodbye world');
48
+ chai_1.assert.sameDeepMembers((0, index_1.flash)('notice'), [{ type: 'notice', message: 'hello world' }]);
49
+ chai_1.assert.sameDeepMembers((0, index_1.flash)('error'), [{ type: 'error', message: 'goodbye world' }]);
50
+ });
51
+ });
52
+ it('returns all flashes', () => {
53
+ const req = {
54
+ session: {},
55
+ };
56
+ const res = {};
57
+ (0, index_1.flashMiddleware)()(req, res, () => {
58
+ (0, index_1.flash)('notice', 'hello world');
59
+ (0, index_1.flash)('error', 'goodbye world');
60
+ chai_1.assert.sameDeepMembers((0, index_1.flash)(), [
61
+ { type: 'notice', message: 'hello world' },
62
+ { type: 'error', message: 'goodbye world' },
63
+ ]);
64
+ });
65
+ });
66
+ it('clears flash after it has been read', () => {
67
+ const req = {
68
+ session: {},
69
+ };
70
+ const res = {};
71
+ (0, index_1.flashMiddleware)()(req, res, () => {
72
+ (0, index_1.flash)('notice', 'hello world');
73
+ (0, index_1.flash)('error', 'goodbye world');
74
+ chai_1.assert.sameDeepMembers((0, index_1.flash)('notice'), [{ type: 'notice', message: 'hello world' }]);
75
+ chai_1.assert.sameDeepMembers((0, index_1.flash)('error'), [{ type: 'error', message: 'goodbye world' }]);
76
+ chai_1.assert.deepEqual((0, index_1.flash)('notice'), []);
77
+ chai_1.assert.deepEqual((0, index_1.flash)('error'), []);
78
+ chai_1.assert.isEmpty((0, index_1.flash)());
79
+ });
80
+ });
81
+ });
82
+ //# sourceMappingURL=index.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@prairielearn/flash",
3
+ "version": "1.0.0",
4
+ "main": "dist/index.js",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/PrairieLearn/PrairieLearn.git",
8
+ "directory": "packages/flash"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch --preserveWatchOutput",
13
+ "test": "mocha --no-config --require ts-node/register src/index.test.ts"
14
+ },
15
+ "devDependencies": {
16
+ "@prairielearn/tsconfig": "^0.0.0",
17
+ "@types/express": "^4.17.17",
18
+ "@types/express-session": "^1.17.7",
19
+ "@types/node": "^18.16.16",
20
+ "mocha": "^10.2.0",
21
+ "ts-node": "^10.9.1",
22
+ "typescript": "^5.1.3"
23
+ }
24
+ }
@@ -0,0 +1,7 @@
1
+ import 'express-session';
2
+
3
+ declare module 'express-session' {
4
+ interface SessionData {
5
+ flash?: Record<string, string>;
6
+ }
7
+ }
@@ -0,0 +1,98 @@
1
+ import { assert } from 'chai';
2
+
3
+ import { flashMiddleware, flash } from './index';
4
+
5
+ describe('flash', () => {
6
+ it('throws an error if no session present', () => {
7
+ assert.throw(() => {
8
+ flashMiddleware()({} as any, {} as any, () => {});
9
+ });
10
+ });
11
+
12
+ it('throws an error when middleware is not used', () => {
13
+ assert.throw(() => {
14
+ flash('notice', 'Hello world');
15
+ });
16
+ });
17
+
18
+ it('adds a flash', () => {
19
+ const req = {
20
+ session: {},
21
+ } as any;
22
+ const res = {} as any;
23
+
24
+ flashMiddleware()(req, res, () => {
25
+ flash('notice', 'hello world');
26
+
27
+ assert.sameDeepMembers(flash(), [{ type: 'notice', message: 'hello world' }]);
28
+ });
29
+ });
30
+
31
+ it('stores multiples flashes with the same type', () => {
32
+ const req = {
33
+ session: {},
34
+ } as any;
35
+ const res = {} as any;
36
+
37
+ flashMiddleware()(req, res, () => {
38
+ flash('notice', 'hello world');
39
+ flash('notice', 'goodbye world');
40
+
41
+ assert.sameDeepMembers(flash(), [
42
+ { type: 'notice', message: 'hello world' },
43
+ { type: 'notice', message: 'goodbye world' },
44
+ ]);
45
+ });
46
+ });
47
+
48
+ it('returns flash message for a given type', () => {
49
+ const req = {
50
+ session: {},
51
+ } as any;
52
+ const res = {} as any;
53
+
54
+ flashMiddleware()(req, res, () => {
55
+ flash('notice', 'hello world');
56
+ flash('error', 'goodbye world');
57
+
58
+ assert.sameDeepMembers(flash('notice'), [{ type: 'notice', message: 'hello world' }]);
59
+ assert.sameDeepMembers(flash('error'), [{ type: 'error', message: 'goodbye world' }]);
60
+ });
61
+ });
62
+
63
+ it('returns all flashes', () => {
64
+ const req = {
65
+ session: {},
66
+ } as any;
67
+ const res = {} as any;
68
+
69
+ flashMiddleware()(req, res, () => {
70
+ flash('notice', 'hello world');
71
+ flash('error', 'goodbye world');
72
+
73
+ assert.sameDeepMembers(flash(), [
74
+ { type: 'notice', message: 'hello world' },
75
+ { type: 'error', message: 'goodbye world' },
76
+ ]);
77
+ });
78
+ });
79
+
80
+ it('clears flash after it has been read', () => {
81
+ const req = {
82
+ session: {},
83
+ } as any;
84
+ const res = {} as any;
85
+
86
+ flashMiddleware()(req, res, () => {
87
+ flash('notice', 'hello world');
88
+ flash('error', 'goodbye world');
89
+
90
+ assert.sameDeepMembers(flash('notice'), [{ type: 'notice', message: 'hello world' }]);
91
+ assert.sameDeepMembers(flash('error'), [{ type: 'error', message: 'goodbye world' }]);
92
+
93
+ assert.deepEqual(flash('notice'), []);
94
+ assert.deepEqual(flash('error'), []);
95
+ assert.isEmpty(flash());
96
+ });
97
+ });
98
+ });
package/src/index.ts ADDED
@@ -0,0 +1,86 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { Request, Response, NextFunction } from 'express';
3
+
4
+ const als = new AsyncLocalStorage<FlashStorage>();
5
+
6
+ export type FlashMessageType = 'notice' | 'success' | 'warning' | 'error';
7
+
8
+ export interface FlashMessage {
9
+ type: FlashMessageType;
10
+ message: string;
11
+ }
12
+
13
+ export function flashMiddleware() {
14
+ return (req: Request, _res: Response, next: NextFunction) => {
15
+ const flashStorage = makeFlashStorage(req);
16
+ als.run(flashStorage, () => next());
17
+ };
18
+ }
19
+
20
+ export function flash(): FlashMessage[];
21
+ export function flash(type: FlashMessageType): FlashMessage[];
22
+ export function flash(type: FlashMessageType[]): FlashMessage[];
23
+ export function flash(type: FlashMessageType, message: string): void;
24
+ export function flash(type?: FlashMessageType | FlashMessageType[], message?: string) {
25
+ const flashStorage = als.getStore();
26
+ if (!flashStorage) {
27
+ throw new Error('flash() must be called within a request');
28
+ }
29
+
30
+ if (Array.isArray(type)) {
31
+ const messages = type.flatMap((type) => flashStorage.get(type));
32
+ type.forEach((t) => flashStorage.clear(t));
33
+ return messages;
34
+ }
35
+
36
+ if (type != null && message != null) {
37
+ flashStorage.add(type, message);
38
+ return;
39
+ }
40
+
41
+ if (type != null) {
42
+ const message = flashStorage.get(type);
43
+ flashStorage.clear(type);
44
+ return message;
45
+ }
46
+
47
+ const messages = flashStorage.getAll();
48
+ flashStorage.clearAll();
49
+ return messages;
50
+ }
51
+
52
+ interface FlashStorage {
53
+ add(type: FlashMessageType, message: string): void;
54
+ get(type: FlashMessageType): FlashMessage[] | null;
55
+ getAll(): FlashMessage[];
56
+ clear(type: FlashMessageType): void;
57
+ clearAll(): void;
58
+ }
59
+
60
+ function makeFlashStorage(req: Request): FlashStorage {
61
+ if (!req.session) {
62
+ throw new Error('@prairielearn/flash requires session support');
63
+ }
64
+
65
+ const session = req.session as any;
66
+
67
+ return {
68
+ add(type: FlashMessageType, message: string) {
69
+ session.flash ??= [];
70
+ session.flash.push({ type, message });
71
+ },
72
+ get(type: FlashMessageType) {
73
+ const messages = session.flash ?? [];
74
+ return messages.filter((message: FlashMessage) => message.type === type);
75
+ },
76
+ getAll() {
77
+ return session.flash ?? [];
78
+ },
79
+ clear(type: FlashMessageType) {
80
+ session.flash = session.flash?.filter((message: FlashMessage) => message.type !== type) ?? [];
81
+ },
82
+ clearAll() {
83
+ session.flash = [];
84
+ },
85
+ } satisfies FlashStorage;
86
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "@prairielearn/tsconfig/tsconfig.package.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "types": ["mocha", "node", "express-session"],
7
+ }
8
+ }