@workjournal/shared 2.19.0 → 2.27.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.
- package/dist/__tests__/address.test.d.ts +2 -0
- package/dist/__tests__/address.test.d.ts.map +1 -0
- package/dist/__tests__/address.test.js +211 -0
- package/dist/__tests__/address.test.js.map +1 -0
- package/dist/__tests__/export-formats.test.d.ts +2 -0
- package/dist/__tests__/export-formats.test.d.ts.map +1 -0
- package/dist/__tests__/export-formats.test.js +43 -0
- package/dist/__tests__/export-formats.test.js.map +1 -0
- package/dist/__tests__/notification-levels.test.d.ts +2 -0
- package/dist/__tests__/notification-levels.test.d.ts.map +1 -0
- package/dist/__tests__/notification-levels.test.js +54 -0
- package/dist/__tests__/notification-levels.test.js.map +1 -0
- package/dist/address.d.ts +81 -0
- package/dist/address.d.ts.map +1 -0
- package/dist/address.js +192 -0
- package/dist/address.js.map +1 -0
- package/dist/constants.d.ts +38 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +38 -0
- package/dist/constants.js.map +1 -1
- package/dist/export-formats.d.ts +53 -0
- package/dist/export-formats.d.ts.map +1 -0
- package/dist/export-formats.js +26 -0
- package/dist/export-formats.js.map +1 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/notification-levels.d.ts +53 -0
- package/dist/notification-levels.d.ts.map +1 -0
- package/dist/notification-levels.js +68 -0
- package/dist/notification-levels.js.map +1 -0
- package/dist/tiers.d.ts +7 -1
- package/dist/tiers.d.ts.map +1 -1
- package/dist/tiers.js +11 -0
- package/dist/tiers.js.map +1 -1
- package/dist/types.d.ts +127 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"address.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/address.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ADDRESS_FAILURE_MESSAGES, formatAddress, formatEntryAddress, formatJournalAddress, formatWorkspaceAddress, hasAddressScheme, isAddress, MAX_ENTRY_INDEX, parseAddress, } from '../address.js';
|
|
3
|
+
import { MAX_SLUG_LENGTH } from '../slug.js';
|
|
4
|
+
function parsed(input) {
|
|
5
|
+
const result = parseAddress(input);
|
|
6
|
+
if (!result.ok)
|
|
7
|
+
throw new Error(`expected ${input} to parse, got ${result.reason}`);
|
|
8
|
+
return result.address;
|
|
9
|
+
}
|
|
10
|
+
function reason(input) {
|
|
11
|
+
const result = parseAddress(input);
|
|
12
|
+
if (result.ok)
|
|
13
|
+
throw new Error(`expected ${input} to fail, got ${formatAddress(result.address)}`);
|
|
14
|
+
return result.reason;
|
|
15
|
+
}
|
|
16
|
+
describe('parseAddress — valid forms', () => {
|
|
17
|
+
it('parses a workspace address', () => {
|
|
18
|
+
expect(parsed('wj:acme')).toEqual({ kind: 'workspace', workspaceSlug: 'acme' });
|
|
19
|
+
});
|
|
20
|
+
it('parses a journal address', () => {
|
|
21
|
+
expect(parsed('wj:acme.eng-notes')).toEqual({
|
|
22
|
+
kind: 'journal',
|
|
23
|
+
workspaceSlug: 'acme',
|
|
24
|
+
journalSlug: 'eng-notes',
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
it('parses an entry address', () => {
|
|
28
|
+
expect(parsed('wj:acme.eng-notes#42')).toEqual({
|
|
29
|
+
kind: 'entry',
|
|
30
|
+
workspaceSlug: 'acme',
|
|
31
|
+
journalSlug: 'eng-notes',
|
|
32
|
+
index: 42,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
it('accepts numeric slugs', () => {
|
|
36
|
+
expect(parsed('wj:2026.q1#1')).toEqual({
|
|
37
|
+
kind: 'entry',
|
|
38
|
+
workspaceSlug: '2026',
|
|
39
|
+
journalSlug: 'q1',
|
|
40
|
+
index: 1,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
it('accepts the maximum index', () => {
|
|
44
|
+
expect(parsed(`wj:acme.eng#${MAX_ENTRY_INDEX}`)).toMatchObject({ index: MAX_ENTRY_INDEX });
|
|
45
|
+
});
|
|
46
|
+
// The SQL backfill truncates AFTER trimming hyphens, so a live slug can end
|
|
47
|
+
// in `-`; the OpenAPI param pattern permits it too. Rejecting these would
|
|
48
|
+
// make an already-existing journal unaddressable.
|
|
49
|
+
it('accepts trailing and doubled hyphens, matching the OpenAPI slug pattern', () => {
|
|
50
|
+
expect(parsed('wj:acme-.eng--notes#1')).toMatchObject({
|
|
51
|
+
workspaceSlug: 'acme-',
|
|
52
|
+
journalSlug: 'eng--notes',
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
it('accepts slugs at the length cap', () => {
|
|
56
|
+
const slug = 'a'.repeat(MAX_SLUG_LENGTH);
|
|
57
|
+
expect(parsed(`wj:${slug}.${slug}#1`)).toMatchObject({
|
|
58
|
+
workspaceSlug: slug,
|
|
59
|
+
journalSlug: slug,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('parseAddress — normalisation', () => {
|
|
64
|
+
it('trims surrounding whitespace', () => {
|
|
65
|
+
expect(parsed(' wj:acme.eng-notes#7 ')).toMatchObject({ index: 7 });
|
|
66
|
+
});
|
|
67
|
+
it('lowercases scheme and slugs', () => {
|
|
68
|
+
expect(parsed('WJ:Acme.Eng-Notes#42')).toEqual({
|
|
69
|
+
kind: 'entry',
|
|
70
|
+
workspaceSlug: 'acme',
|
|
71
|
+
journalSlug: 'eng-notes',
|
|
72
|
+
index: 42,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
it('round-trips through formatAddress in canonical form', () => {
|
|
76
|
+
expect(formatAddress(parsed(' WJ:Acme.Eng-Notes#42 '))).toBe('wj:acme.eng-notes#42');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe('parseAddress — rejections', () => {
|
|
80
|
+
it.each([
|
|
81
|
+
['acme.eng-notes#42', 'missing-scheme'],
|
|
82
|
+
['https://workjournal.pro/acme', 'missing-scheme'],
|
|
83
|
+
['acme/eng-notes/42', 'missing-scheme'],
|
|
84
|
+
['', 'missing-scheme'],
|
|
85
|
+
['wj:', 'empty-workspace'],
|
|
86
|
+
['wj:.eng-notes', 'empty-workspace'],
|
|
87
|
+
['wj:acme.', 'empty-journal'],
|
|
88
|
+
['wj:acme.eng-notes#', 'empty-index'],
|
|
89
|
+
['wj:acme#42', 'index-without-journal'],
|
|
90
|
+
['wj:shared-with-me', 'virtual-workspace'],
|
|
91
|
+
['wj:shared-with-me.eng-notes#1', 'virtual-workspace'],
|
|
92
|
+
['wj:settings', 'reserved-slug'],
|
|
93
|
+
['wj:acme.entries', 'reserved-slug'],
|
|
94
|
+
['wj:acme.eng-notes#0', 'invalid-index'],
|
|
95
|
+
['wj:acme.eng-notes#042', 'invalid-index'],
|
|
96
|
+
['wj:acme.eng-notes#-1', 'invalid-index'],
|
|
97
|
+
['wj:acme.eng-notes#4.2', 'invalid-index'],
|
|
98
|
+
['wj:acme.eng-notes#abc', 'invalid-index'],
|
|
99
|
+
[`wj:acme.eng-notes#${MAX_ENTRY_INDEX + 1}`, 'invalid-index'],
|
|
100
|
+
['wj:acme.eng.notes#1', 'invalid-slug'],
|
|
101
|
+
['wj:acme.eng-notes#1#2', 'invalid-slug'],
|
|
102
|
+
['wj:-acme', 'invalid-slug'],
|
|
103
|
+
['wj:acme_notes', 'invalid-slug'],
|
|
104
|
+
['wj:acme notes', 'invalid-slug'],
|
|
105
|
+
['wj:café', 'invalid-slug'],
|
|
106
|
+
])('rejects %s as %s', (input, expected) => {
|
|
107
|
+
expect(reason(input)).toBe(expected);
|
|
108
|
+
});
|
|
109
|
+
it('rejects slugs over the length cap', () => {
|
|
110
|
+
const tooLong = 'a'.repeat(MAX_SLUG_LENGTH + 1);
|
|
111
|
+
expect(reason(`wj:${tooLong}`)).toBe('invalid-slug');
|
|
112
|
+
expect(reason(`wj:acme.${tooLong}`)).toBe('invalid-slug');
|
|
113
|
+
});
|
|
114
|
+
it('has a message for every failure reason', () => {
|
|
115
|
+
const reasons = [
|
|
116
|
+
'missing-scheme',
|
|
117
|
+
'empty-workspace',
|
|
118
|
+
'empty-journal',
|
|
119
|
+
'empty-index',
|
|
120
|
+
'invalid-slug',
|
|
121
|
+
'reserved-slug',
|
|
122
|
+
'virtual-workspace',
|
|
123
|
+
'invalid-index',
|
|
124
|
+
'index-without-journal',
|
|
125
|
+
];
|
|
126
|
+
for (const r of reasons) {
|
|
127
|
+
expect(ADDRESS_FAILURE_MESSAGES[r]).toBeTruthy();
|
|
128
|
+
}
|
|
129
|
+
expect(Object.keys(ADDRESS_FAILURE_MESSAGES).sort()).toEqual([...reasons].sort());
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe('formatAddress', () => {
|
|
133
|
+
it('formats each kind', () => {
|
|
134
|
+
expect(formatAddress({ kind: 'workspace', workspaceSlug: 'acme' })).toBe('wj:acme');
|
|
135
|
+
expect(formatAddress({ kind: 'journal', workspaceSlug: 'acme', journalSlug: 'eng-notes' })).toBe('wj:acme.eng-notes');
|
|
136
|
+
expect(formatAddress({
|
|
137
|
+
kind: 'entry',
|
|
138
|
+
workspaceSlug: 'acme',
|
|
139
|
+
journalSlug: 'eng-notes',
|
|
140
|
+
index: 42,
|
|
141
|
+
})).toBe('wj:acme.eng-notes#42');
|
|
142
|
+
});
|
|
143
|
+
it('exposes loose-slug convenience formatters', () => {
|
|
144
|
+
expect(formatWorkspaceAddress('acme')).toBe('wj:acme');
|
|
145
|
+
expect(formatJournalAddress('acme', 'eng-notes')).toBe('wj:acme.eng-notes');
|
|
146
|
+
expect(formatEntryAddress('acme', 'eng-notes', 42)).toBe('wj:acme.eng-notes#42');
|
|
147
|
+
});
|
|
148
|
+
it('normalises slug case so output is always canonical', () => {
|
|
149
|
+
expect(formatAddress({ kind: 'journal', workspaceSlug: 'Acme', journalSlug: 'Eng' })).toBe('wj:acme.eng');
|
|
150
|
+
expect(formatEntryAddress(' ACME ', 'ENG', 7)).toBe('wj:acme.eng#7');
|
|
151
|
+
});
|
|
152
|
+
// formatAddress is exported and reachable from untyped JS; its contract says
|
|
153
|
+
// the output is canonical, so it must refuse to emit an unparseable string.
|
|
154
|
+
it.each([0, -1, 1.5, Number.NaN, MAX_ENTRY_INDEX + 1])('rejects index %s', (index) => {
|
|
155
|
+
expect(() => formatAddress({ kind: 'entry', workspaceSlug: 'acme', journalSlug: 'eng', index })).toThrow(RangeError);
|
|
156
|
+
});
|
|
157
|
+
it.each(['', 'has space', 'has.dot', 'has#hash', '-leading'])('rejects slug %j', (slug) => {
|
|
158
|
+
expect(() => formatAddress({ kind: 'workspace', workspaceSlug: slug })).toThrow(TypeError);
|
|
159
|
+
});
|
|
160
|
+
// `parseAddress` rejects reserved slugs in both positions, so formatAddress
|
|
161
|
+
// emitting one would break the round-trip it documents (#782 follow-up).
|
|
162
|
+
it.each([
|
|
163
|
+
'new',
|
|
164
|
+
'settings',
|
|
165
|
+
'journals',
|
|
166
|
+
'entries',
|
|
167
|
+
'shared-with-me',
|
|
168
|
+
])('rejects reserved workspace slug %j', (slug) => {
|
|
169
|
+
expect(() => formatAddress({ kind: 'workspace', workspaceSlug: slug })).toThrow(TypeError);
|
|
170
|
+
});
|
|
171
|
+
it.each([
|
|
172
|
+
'new',
|
|
173
|
+
'settings',
|
|
174
|
+
'invites',
|
|
175
|
+
'shared-with-me',
|
|
176
|
+
])('rejects reserved journal slug %j', (slug) => {
|
|
177
|
+
expect(() => formatAddress({ kind: 'journal', workspaceSlug: 'acme', journalSlug: slug })).toThrow(TypeError);
|
|
178
|
+
expect(() => formatEntryAddress('acme', slug, 1)).toThrow(TypeError);
|
|
179
|
+
});
|
|
180
|
+
it('never emits a string parseAddress would reject', () => {
|
|
181
|
+
// The contract, stated as a property: anything formatAddress returns must
|
|
182
|
+
// parse back. Reserved slugs were the hole — they formatted fine and then
|
|
183
|
+
// failed to parse.
|
|
184
|
+
for (const slug of ['new', 'shared-with-me', 'settings']) {
|
|
185
|
+
expect(() => formatWorkspaceAddress(slug)).toThrow(TypeError);
|
|
186
|
+
expect(parseAddress(`wj:${slug}`).ok).toBe(false);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
it('output of formatAddress always parses back', () => {
|
|
190
|
+
const formatted = formatEntryAddress('Acme', 'Eng-Notes', 42);
|
|
191
|
+
expect(parseAddress(formatted).ok).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
it('round-trips every valid kind', () => {
|
|
194
|
+
for (const input of ['wj:acme', 'wj:acme.eng-notes', 'wj:acme.eng-notes#42']) {
|
|
195
|
+
expect(formatAddress(parsed(input))).toBe(input);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
describe('isAddress / hasAddressScheme', () => {
|
|
200
|
+
it('isAddress is true only for parseable input', () => {
|
|
201
|
+
expect(isAddress('wj:acme.eng-notes#42')).toBe(true);
|
|
202
|
+
expect(isAddress('wj:acme.eng-notes#0')).toBe(false);
|
|
203
|
+
expect(isAddress('acme')).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
it('hasAddressScheme is true for malformed addresses too', () => {
|
|
206
|
+
expect(hasAddressScheme('wj:acme.eng-notes#0')).toBe(true);
|
|
207
|
+
expect(hasAddressScheme(' WJ:whatever ')).toBe(true);
|
|
208
|
+
expect(hasAddressScheme('acme')).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
//# sourceMappingURL=address.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"address.test.js","sourceRoot":"","sources":["../../src/__tests__/address.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export-formats.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/export-formats.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { WS_API_PATHS } from '../constants.js';
|
|
3
|
+
import { EXPORT_FORMAT_IDS, EXPORT_FORMATS, getExportFormat, isExportFormatId, } from '../export-formats.js';
|
|
4
|
+
import { tierNamesWithFeature } from '../tiers.js';
|
|
5
|
+
describe('EXPORT_FORMATS', () => {
|
|
6
|
+
it('lists every format exactly once', () => {
|
|
7
|
+
expect(EXPORT_FORMAT_IDS).toEqual(['json', 'md', 'csv', 'epub']);
|
|
8
|
+
expect(new Set(EXPORT_FORMAT_IDS).size).toBe(EXPORT_FORMATS.length);
|
|
9
|
+
});
|
|
10
|
+
it('gates EPUB behind can_export_epub and leaves the text formats open', () => {
|
|
11
|
+
expect(getExportFormat('epub').feature).toBe('can_export_epub');
|
|
12
|
+
for (const id of ['json', 'md', 'csv']) {
|
|
13
|
+
expect(getExportFormat(id).feature).toBeUndefined();
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
it('recognises only registered ids', () => {
|
|
17
|
+
expect(isExportFormatId('epub')).toBe(true);
|
|
18
|
+
expect(isExportFormatId('pdf')).toBe(false);
|
|
19
|
+
expect(isExportFormatId(undefined)).toBe(false);
|
|
20
|
+
expect(isExportFormatId('toString')).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
describe('tierNamesWithFeature', () => {
|
|
24
|
+
const tier = (code, name, sort_order, epub) => ({ code, name, sort_order, can_export_epub: epub });
|
|
25
|
+
it('names the purchasable tiers with the flag, in upgrade order, without ADMIN', () => {
|
|
26
|
+
const tiers = [
|
|
27
|
+
tier('PRO', 'Pro', 3, true),
|
|
28
|
+
tier('ADMIN', 'Admin', 4, true),
|
|
29
|
+
tier('FREE', 'Free', 1, false),
|
|
30
|
+
tier('PLUS', 'Plus', 2, true),
|
|
31
|
+
];
|
|
32
|
+
expect(tierNamesWithFeature(tiers, 'can_export_epub')).toEqual(['Plus', 'Pro']);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe('WS_API_PATHS export link helpers', () => {
|
|
36
|
+
it('builds the journal-scoped mint path', () => {
|
|
37
|
+
expect(WS_API_PATHS.exportLink('acme', 'eng')).toBe('/v1/workspaces/acme/journals/eng/export-link');
|
|
38
|
+
});
|
|
39
|
+
it('builds the token download path', () => {
|
|
40
|
+
expect(WS_API_PATHS.exportDownload('abc_-1')).toBe('/v1/exports/abc_-1');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
//# sourceMappingURL=export-formats.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export-formats.test.js","sourceRoot":"","sources":["../../src/__tests__/export-formats.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-levels.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/notification-levels.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { isConfigurableNotificationType, isNotificationLevel, NOTIFICATION_TYPE_GROUPS, NOTIFICATION_TYPE_OPTIONS, resolveNotificationLevel, } from '../notification-levels.js';
|
|
5
|
+
describe('NOTIFICATION_TYPE_OPTIONS', () => {
|
|
6
|
+
it('has unique types, each in a known group', () => {
|
|
7
|
+
const types = NOTIFICATION_TYPE_OPTIONS.map((o) => o.type);
|
|
8
|
+
expect(new Set(types).size).toBe(types.length);
|
|
9
|
+
const groups = new Set(NOTIFICATION_TYPE_GROUPS.map((g) => g.group));
|
|
10
|
+
for (const o of NOTIFICATION_TYPE_OPTIONS)
|
|
11
|
+
expect(groups.has(o.group)).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
it('scopes member.* to the workspace and everything else to journals', () => {
|
|
14
|
+
for (const o of NOTIFICATION_TYPE_OPTIONS) {
|
|
15
|
+
expect(o.scope).toBe(o.type.startsWith('member.') ? 'workspace' : 'journal');
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
describe('resolveNotificationLevel', () => {
|
|
20
|
+
it('defaults to list for missing keys and malformed maps', () => {
|
|
21
|
+
expect(resolveNotificationLevel(undefined, 'entry.created')).toBe('list');
|
|
22
|
+
expect(resolveNotificationLevel(null, 'entry.created')).toBe('list');
|
|
23
|
+
expect(resolveNotificationLevel([], 'entry.created')).toBe('list');
|
|
24
|
+
expect(resolveNotificationLevel({}, 'entry.created')).toBe('list');
|
|
25
|
+
expect(resolveNotificationLevel({ 'entry.created': 'loud' }, 'entry.created')).toBe('list');
|
|
26
|
+
});
|
|
27
|
+
it('returns the stored level for configurable types', () => {
|
|
28
|
+
expect(resolveNotificationLevel({ 'entry.updated': 'hide' }, 'entry.updated')).toBe('hide');
|
|
29
|
+
expect(resolveNotificationLevel({ 'member.joined': 'push' }, 'member.joined')).toBe('push');
|
|
30
|
+
});
|
|
31
|
+
it('ignores stored levels for types outside the registry', () => {
|
|
32
|
+
expect(isConfigurableNotificationType('tag.created')).toBe(false);
|
|
33
|
+
expect(resolveNotificationLevel({ 'tag.created': 'hide' }, 'tag.created')).toBe('list');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('isNotificationLevel', () => {
|
|
37
|
+
it('accepts exactly hide / list / push', () => {
|
|
38
|
+
expect(['hide', 'list', 'push'].every(isNotificationLevel)).toBe(true);
|
|
39
|
+
expect(isNotificationLevel('show')).toBe(false);
|
|
40
|
+
expect(isNotificationLevel(null)).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('patch_my_notification_levels SQL allow-list', () => {
|
|
44
|
+
it('matches NOTIFICATION_TYPE_OPTIONS exactly', () => {
|
|
45
|
+
// The RPC is callable directly through PostgREST, so it re-validates keys
|
|
46
|
+
// against its own copy of the registry (#805). Fail if they drift.
|
|
47
|
+
const sql = readFileSync(fileURLToPath(new URL('../../../../supabase/migrations/20261005000001_workspace_user_settings.sql', import.meta.url)), 'utf8');
|
|
48
|
+
const block = sql.match(/notification-types:begin([\s\S]*?)notification-types:end/)?.[1] ?? '';
|
|
49
|
+
const sqlTypes = [...block.matchAll(/'([a-z_]+\.[a-z_]+)'/g)].map((m) => m[1]);
|
|
50
|
+
expect(new Set(sqlTypes)).toEqual(new Set(NOTIFICATION_TYPE_OPTIONS.map((o) => o.type)));
|
|
51
|
+
expect(sqlTypes).toHaveLength(NOTIFICATION_TYPE_OPTIONS.length);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
//# sourceMappingURL=notification-levels.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-levels.test.js","sourceRoot":"","sources":["../../src/__tests__/notification-levels.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `wj:` address standard — one notation for naming a workspace, a journal,
|
|
3
|
+
* or an entry across every surface. See `docs/addressing.md` for the spec.
|
|
4
|
+
*
|
|
5
|
+
* wj:acme → workspace
|
|
6
|
+
* wj:acme.eng-notes → journal
|
|
7
|
+
* wj:acme.eng-notes#42 → entry
|
|
8
|
+
*
|
|
9
|
+
* This module is the single implementation. No surface may hand-roll the
|
|
10
|
+
* grammar — `connector-app.ts` used to carry its own `ID_PATTERN` and drifting
|
|
11
|
+
* copies are exactly what this replaces.
|
|
12
|
+
*/
|
|
13
|
+
export declare const ADDRESS_SCHEME: "wj:";
|
|
14
|
+
/**
|
|
15
|
+
* `entries.index` is a Postgres INTEGER. Rejecting above the ceiling at parse
|
|
16
|
+
* time keeps an over-large address from becoming a confusing 400 from the API.
|
|
17
|
+
*/
|
|
18
|
+
export declare const MAX_ENTRY_INDEX = 2147483647;
|
|
19
|
+
export type WjAddress = {
|
|
20
|
+
kind: 'workspace';
|
|
21
|
+
workspaceSlug: string;
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'journal';
|
|
24
|
+
workspaceSlug: string;
|
|
25
|
+
journalSlug: string;
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'entry';
|
|
28
|
+
workspaceSlug: string;
|
|
29
|
+
journalSlug: string;
|
|
30
|
+
index: number;
|
|
31
|
+
};
|
|
32
|
+
export type AddressParseFailure = 'missing-scheme' | 'empty-workspace' | 'empty-journal' | 'empty-index' | 'invalid-slug' | 'reserved-slug' | 'virtual-workspace' | 'invalid-index' | 'index-without-journal';
|
|
33
|
+
export type ParseAddressResult = {
|
|
34
|
+
ok: true;
|
|
35
|
+
address: WjAddress;
|
|
36
|
+
} | {
|
|
37
|
+
ok: false;
|
|
38
|
+
reason: AddressParseFailure;
|
|
39
|
+
};
|
|
40
|
+
/** Human-readable explanation per failure reason, for CLI/MCP error messages. */
|
|
41
|
+
export declare const ADDRESS_FAILURE_MESSAGES: Readonly<Record<AddressParseFailure, string>>;
|
|
42
|
+
/**
|
|
43
|
+
* Parse a `wj:` address.
|
|
44
|
+
*
|
|
45
|
+
* Liberal in what it accepts — surrounding whitespace is trimmed and the input
|
|
46
|
+
* is lowercased, so `WJ:Acme.Eng-Notes#42` parses — but the parsed result is
|
|
47
|
+
* always canonical, so `formatAddress(parse(x))` is a normalising round-trip.
|
|
48
|
+
*
|
|
49
|
+
* Returns a `reason` rather than throwing (or returning bare `null`) so callers
|
|
50
|
+
* can render something the user can act on: "uses a reserved slug" is
|
|
51
|
+
* actionable where "malformed" is not.
|
|
52
|
+
*/
|
|
53
|
+
export declare function parseAddress(input: string): ParseAddressResult;
|
|
54
|
+
/**
|
|
55
|
+
* Render an address in canonical form. Inverse of `parseAddress`.
|
|
56
|
+
*
|
|
57
|
+
* Normalises slug case and rejects anything that would produce an unparseable
|
|
58
|
+
* string. The type system stops most misuse, but `formatAddress` is exported
|
|
59
|
+
* and reachable from untyped JS, and its documented contract is that the
|
|
60
|
+
* output is *canonical* — silently emitting `wj:Acme.eng#0` would break that
|
|
61
|
+
* for every consumer that later parses it back.
|
|
62
|
+
*/
|
|
63
|
+
export declare function formatAddress(address: WjAddress): string;
|
|
64
|
+
/** Convenience formatters for callers holding loose slugs rather than a union. */
|
|
65
|
+
export declare function formatWorkspaceAddress(workspaceSlug: string): string;
|
|
66
|
+
export declare function formatJournalAddress(workspaceSlug: string, journalSlug: string): string;
|
|
67
|
+
export declare function formatEntryAddress(workspaceSlug: string, journalSlug: string, index: number): string;
|
|
68
|
+
/**
|
|
69
|
+
* Cheap check for "does this look like an address" — used to decide whether to
|
|
70
|
+
* treat a CLI positional as an address or as a bare slug. Note this returns
|
|
71
|
+
* false for a malformed address; use `parseAddress` when you need to tell a
|
|
72
|
+
* non-address apart from a broken one.
|
|
73
|
+
*/
|
|
74
|
+
export declare function isAddress(input: string): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* True when the input is *trying* to be an address, valid or not. The CLI uses
|
|
77
|
+
* this to route a `wj:`-prefixed positional into the address parser so a typo
|
|
78
|
+
* reports the real reason instead of being treated as a workspace slug.
|
|
79
|
+
*/
|
|
80
|
+
export declare function hasAddressScheme(input: string): boolean;
|
|
81
|
+
//# sourceMappingURL=address.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;GAWG;AAEH,eAAO,MAAM,cAAc,EAAG,KAAc,CAAC;AAE7C;;;GAGG;AACH,eAAO,MAAM,eAAe,aAAa,CAAC;AAsB1C,MAAM,MAAM,SAAS,GAClB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhF,MAAM,MAAM,mBAAmB,GAC5B,gBAAgB,GAChB,iBAAiB,GACjB,eAAe,GACf,aAAa,GACb,cAAc,GACd,eAAe,GACf,mBAAmB,GACnB,eAAe,GACf,uBAAuB,CAAC;AAE3B,MAAM,MAAM,kBAAkB,GAC3B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,SAAS,CAAA;CAAE,GAChC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,mBAAmB,CAAA;CAAE,CAAC;AAE9C,iFAAiF;AACjF,eAAO,MAAM,wBAAwB,EAAE,QAAQ,CAAC,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAWlF,CAAC;AAUF;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB,CAwD9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,SAAS,GAAG,MAAM,CAkBxD;AAiBD,kFAAkF;AAClF,wBAAgB,sBAAsB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED,wBAAgB,oBAAoB,CAAC,aAAa,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAEvF;AAED,wBAAgB,kBAAkB,CACjC,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,GACX,MAAM,CAER;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEvD"}
|
package/dist/address.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { SHARED_WITH_ME_SLUG } from './constants.js';
|
|
2
|
+
import { isReservedSlug, MAX_SLUG_LENGTH } from './slug.js';
|
|
3
|
+
/**
|
|
4
|
+
* The `wj:` address standard — one notation for naming a workspace, a journal,
|
|
5
|
+
* or an entry across every surface. See `docs/addressing.md` for the spec.
|
|
6
|
+
*
|
|
7
|
+
* wj:acme → workspace
|
|
8
|
+
* wj:acme.eng-notes → journal
|
|
9
|
+
* wj:acme.eng-notes#42 → entry
|
|
10
|
+
*
|
|
11
|
+
* This module is the single implementation. No surface may hand-roll the
|
|
12
|
+
* grammar — `connector-app.ts` used to carry its own `ID_PATTERN` and drifting
|
|
13
|
+
* copies are exactly what this replaces.
|
|
14
|
+
*/
|
|
15
|
+
export const ADDRESS_SCHEME = 'wj:';
|
|
16
|
+
/**
|
|
17
|
+
* `entries.index` is a Postgres INTEGER. Rejecting above the ceiling at parse
|
|
18
|
+
* time keeps an over-large address from becoming a confusing 400 from the API.
|
|
19
|
+
*/
|
|
20
|
+
export const MAX_ENTRY_INDEX = 2147483647;
|
|
21
|
+
/**
|
|
22
|
+
* Slug shape shared by both address positions. Deliberately identical to the
|
|
23
|
+
* OpenAPI `pattern` already documented on the workspace/journal slug params
|
|
24
|
+
* (`^[a-z0-9][a-z0-9-]{0,39}$`) — the length half is applied separately below.
|
|
25
|
+
*
|
|
26
|
+
* Note this is looser than `slugify()`'s own output: it tolerates a trailing
|
|
27
|
+
* hyphen and doubled hyphens. That is deliberate. The SQL backfill in
|
|
28
|
+
* `20260422000002_add_slug_columns` truncates with `substring(... FOR 30)`
|
|
29
|
+
* *after* trimming hyphens, so a live slug can legitimately end in `-`.
|
|
30
|
+
* Rejecting those here would make an existing journal unaddressable.
|
|
31
|
+
*
|
|
32
|
+
* What the parser actually depends on is narrower: a slug can never contain
|
|
33
|
+
* `.`, `#`, or `:`. That is what makes the grammar unambiguous, and it is the
|
|
34
|
+
* invariant the DB CHECK constraint enforces.
|
|
35
|
+
*/
|
|
36
|
+
const SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
37
|
+
/** Positive integer, no leading zeros. Mirrors the old connector-app pattern. */
|
|
38
|
+
const INDEX_PATTERN = /^[1-9]\d*$/;
|
|
39
|
+
/** Human-readable explanation per failure reason, for CLI/MCP error messages. */
|
|
40
|
+
export const ADDRESS_FAILURE_MESSAGES = {
|
|
41
|
+
'missing-scheme': `Address must start with "${ADDRESS_SCHEME}"`,
|
|
42
|
+
'empty-workspace': 'Address is missing a workspace slug',
|
|
43
|
+
'empty-journal': 'Address has a "." but no journal slug',
|
|
44
|
+
'empty-index': 'Address has a "#" but no entry number',
|
|
45
|
+
'invalid-slug': 'Slugs must start with a lowercase letter or digit and contain only lowercase letters, digits, and hyphens',
|
|
46
|
+
'reserved-slug': 'Address uses a reserved slug',
|
|
47
|
+
'virtual-workspace': `"${SHARED_WITH_ME_SLUG}" is a virtual listing, not an addressable workspace — use the journal's real workspace slug`,
|
|
48
|
+
'invalid-index': `Entry number must be a positive integer with no leading zeros, at most ${MAX_ENTRY_INDEX}`,
|
|
49
|
+
'index-without-journal': 'Address has an entry number but no journal slug',
|
|
50
|
+
};
|
|
51
|
+
function isValidSlug(value) {
|
|
52
|
+
return value.length <= MAX_SLUG_LENGTH && SLUG_PATTERN.test(value);
|
|
53
|
+
}
|
|
54
|
+
function fail(reason) {
|
|
55
|
+
return { ok: false, reason };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Parse a `wj:` address.
|
|
59
|
+
*
|
|
60
|
+
* Liberal in what it accepts — surrounding whitespace is trimmed and the input
|
|
61
|
+
* is lowercased, so `WJ:Acme.Eng-Notes#42` parses — but the parsed result is
|
|
62
|
+
* always canonical, so `formatAddress(parse(x))` is a normalising round-trip.
|
|
63
|
+
*
|
|
64
|
+
* Returns a `reason` rather than throwing (or returning bare `null`) so callers
|
|
65
|
+
* can render something the user can act on: "uses a reserved slug" is
|
|
66
|
+
* actionable where "malformed" is not.
|
|
67
|
+
*/
|
|
68
|
+
export function parseAddress(input) {
|
|
69
|
+
const normalized = input.trim().toLowerCase();
|
|
70
|
+
if (!normalized.startsWith(ADDRESS_SCHEME))
|
|
71
|
+
return fail('missing-scheme');
|
|
72
|
+
const rest = normalized.slice(ADDRESS_SCHEME.length);
|
|
73
|
+
// Split on the LAST `#` and the FIRST `.`. Safe because neither character
|
|
74
|
+
// can occur inside a slug — a second `.` or `#` lands inside a segment and
|
|
75
|
+
// is rejected by the slug check rather than silently re-splitting.
|
|
76
|
+
const hashAt = rest.lastIndexOf('#');
|
|
77
|
+
const head = hashAt === -1 ? rest : rest.slice(0, hashAt);
|
|
78
|
+
const indexPart = hashAt === -1 ? null : rest.slice(hashAt + 1);
|
|
79
|
+
const dotAt = head.indexOf('.');
|
|
80
|
+
const workspacePart = dotAt === -1 ? head : head.slice(0, dotAt);
|
|
81
|
+
const journalPart = dotAt === -1 ? null : head.slice(dotAt + 1);
|
|
82
|
+
if (indexPart !== null && journalPart === null)
|
|
83
|
+
return fail('index-without-journal');
|
|
84
|
+
if (workspacePart.length === 0)
|
|
85
|
+
return fail('empty-workspace');
|
|
86
|
+
if (!isValidSlug(workspacePart))
|
|
87
|
+
return fail('invalid-slug');
|
|
88
|
+
// Checked before the general reserved test so the caller gets the specific
|
|
89
|
+
// reason — `shared-with-me` is in RESERVED_SLUGS too, but it fails for a
|
|
90
|
+
// different reason than a route-collision name would.
|
|
91
|
+
if (workspacePart === SHARED_WITH_ME_SLUG)
|
|
92
|
+
return fail('virtual-workspace');
|
|
93
|
+
if (isReservedSlug(workspacePart))
|
|
94
|
+
return fail('reserved-slug');
|
|
95
|
+
if (journalPart === null) {
|
|
96
|
+
return { ok: true, address: { kind: 'workspace', workspaceSlug: workspacePart } };
|
|
97
|
+
}
|
|
98
|
+
if (journalPart.length === 0)
|
|
99
|
+
return fail('empty-journal');
|
|
100
|
+
if (!isValidSlug(journalPart))
|
|
101
|
+
return fail('invalid-slug');
|
|
102
|
+
if (isReservedSlug(journalPart))
|
|
103
|
+
return fail('reserved-slug');
|
|
104
|
+
if (indexPart === null) {
|
|
105
|
+
return {
|
|
106
|
+
ok: true,
|
|
107
|
+
address: { kind: 'journal', workspaceSlug: workspacePart, journalSlug: journalPart },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (indexPart.length === 0)
|
|
111
|
+
return fail('empty-index');
|
|
112
|
+
if (!INDEX_PATTERN.test(indexPart))
|
|
113
|
+
return fail('invalid-index');
|
|
114
|
+
const index = Number(indexPart);
|
|
115
|
+
if (index > MAX_ENTRY_INDEX)
|
|
116
|
+
return fail('invalid-index');
|
|
117
|
+
return {
|
|
118
|
+
ok: true,
|
|
119
|
+
address: {
|
|
120
|
+
kind: 'entry',
|
|
121
|
+
workspaceSlug: workspacePart,
|
|
122
|
+
journalSlug: journalPart,
|
|
123
|
+
index,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Render an address in canonical form. Inverse of `parseAddress`.
|
|
129
|
+
*
|
|
130
|
+
* Normalises slug case and rejects anything that would produce an unparseable
|
|
131
|
+
* string. The type system stops most misuse, but `formatAddress` is exported
|
|
132
|
+
* and reachable from untyped JS, and its documented contract is that the
|
|
133
|
+
* output is *canonical* — silently emitting `wj:Acme.eng#0` would break that
|
|
134
|
+
* for every consumer that later parses it back.
|
|
135
|
+
*/
|
|
136
|
+
export function formatAddress(address) {
|
|
137
|
+
const workspaceSlug = normalizeSlugOrThrow(address.workspaceSlug, 'workspaceSlug');
|
|
138
|
+
if (address.kind === 'workspace') {
|
|
139
|
+
return `${ADDRESS_SCHEME}${workspaceSlug}`;
|
|
140
|
+
}
|
|
141
|
+
const journalSlug = normalizeSlugOrThrow(address.journalSlug, 'journalSlug');
|
|
142
|
+
if (address.kind === 'journal') {
|
|
143
|
+
return `${ADDRESS_SCHEME}${workspaceSlug}.${journalSlug}`;
|
|
144
|
+
}
|
|
145
|
+
const { index } = address;
|
|
146
|
+
if (!Number.isInteger(index) || index < 1 || index > MAX_ENTRY_INDEX) {
|
|
147
|
+
throw new RangeError(`formatAddress: index must be an integer in 1..${MAX_ENTRY_INDEX}, got ${String(index)}`);
|
|
148
|
+
}
|
|
149
|
+
return `${ADDRESS_SCHEME}${workspaceSlug}.${journalSlug}#${index}`;
|
|
150
|
+
}
|
|
151
|
+
function normalizeSlugOrThrow(slug, field) {
|
|
152
|
+
const normalized = typeof slug === 'string' ? slug.trim().toLowerCase() : '';
|
|
153
|
+
if (!isValidSlug(normalized)) {
|
|
154
|
+
throw new TypeError(`formatAddress: ${field} is not a valid slug: ${JSON.stringify(slug)}`);
|
|
155
|
+
}
|
|
156
|
+
// `parseAddress` rejects reserved slugs in both positions, so emitting one
|
|
157
|
+
// would produce a string that cannot be parsed back — breaking the canonical
|
|
158
|
+
// round-trip this function documents. `shared-with-me` is in RESERVED_SLUGS,
|
|
159
|
+
// so this covers the virtual-workspace case too.
|
|
160
|
+
if (isReservedSlug(normalized)) {
|
|
161
|
+
throw new TypeError(`formatAddress: ${field} is a reserved slug: ${JSON.stringify(slug)}`);
|
|
162
|
+
}
|
|
163
|
+
return normalized;
|
|
164
|
+
}
|
|
165
|
+
/** Convenience formatters for callers holding loose slugs rather than a union. */
|
|
166
|
+
export function formatWorkspaceAddress(workspaceSlug) {
|
|
167
|
+
return formatAddress({ kind: 'workspace', workspaceSlug });
|
|
168
|
+
}
|
|
169
|
+
export function formatJournalAddress(workspaceSlug, journalSlug) {
|
|
170
|
+
return formatAddress({ kind: 'journal', workspaceSlug, journalSlug });
|
|
171
|
+
}
|
|
172
|
+
export function formatEntryAddress(workspaceSlug, journalSlug, index) {
|
|
173
|
+
return formatAddress({ kind: 'entry', workspaceSlug, journalSlug, index });
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Cheap check for "does this look like an address" — used to decide whether to
|
|
177
|
+
* treat a CLI positional as an address or as a bare slug. Note this returns
|
|
178
|
+
* false for a malformed address; use `parseAddress` when you need to tell a
|
|
179
|
+
* non-address apart from a broken one.
|
|
180
|
+
*/
|
|
181
|
+
export function isAddress(input) {
|
|
182
|
+
return parseAddress(input).ok;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* True when the input is *trying* to be an address, valid or not. The CLI uses
|
|
186
|
+
* this to route a `wj:`-prefixed positional into the address parser so a typo
|
|
187
|
+
* reports the real reason instead of being treated as a workspace slug.
|
|
188
|
+
*/
|
|
189
|
+
export function hasAddressScheme(input) {
|
|
190
|
+
return input.trim().toLowerCase().startsWith(ADDRESS_SCHEME);
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=address.js.map
|