firstly 0.10.4 → 0.10.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # firstly
2
2
 
3
+ ## 0.10.5
4
+
5
+ ### Patch Changes
6
+
7
+ - [#380](https://github.com/jycouet/firstly/pull/380) [`84d2acf`](https://github.com/jycouet/firstly/commit/84d2acfb2898a83e9334442b72e7ff130b2f3991) Thanks [@renovate](https://github.com/apps/renovate)! - mail: nodemailer 10 (requires Node 20+; remote content fetched over HTTPS now validates TLS certificates).
8
+
9
+ - [#377](https://github.com/jycouet/firstly/pull/377) [`d200b62`](https://github.com/jycouet/firstly/commit/d200b62a027eda358ea4ead9be73bce5974b0e8d) Thanks [@jycouet](https://github.com/jycouet)! - sqlAdmin: server-only code is wrapped in `if (import.meta.env.SSR) { ... }`, so Vite 7 (Rollup) client builds no longer fail on `node:crypto` and the server helpers stay out of the client bundle.
10
+
11
+ - [#378](https://github.com/jycouet/firstly/pull/378) [`525ce42`](https://github.com/jycouet/firstly/commit/525ce424ba0affaec73e08e0c8cb906d20d21859) Thanks [@jycouet](https://github.com/jycouet)! - feedback & carbone: BackendMethod bodies are wrapped in `if (import.meta.env.SSR) { ... }`, so their server code (GitHub GraphQL queries, Carbone calls) no longer ships in the client bundle.
12
+
3
13
  ## 0.10.4
4
14
 
5
15
  ### Patch Changes
@@ -11,123 +11,137 @@ import { Roles_Carbon } from './Roles_Carbon';
11
11
  export class CarboneController {
12
12
  static server;
13
13
  static async uploadTemplate(config) {
14
- const { name, base64, nameForCarbone, deleteOlderTemplatesWithSameName } = config;
15
- // 30 max 1 13
16
- const nameToUse = strTrimMid(nameForCarbone ?? name, { len: 16, midStr: '-' }) + '_' + dateCompact();
17
- const res = await CarboneController.server.fetch({
18
- api: `/template`,
19
- body: JSON.stringify({ name, template: base64 }),
20
- headers: {
21
- 'carbone-template-name': nameToUse,
22
- },
23
- });
24
- const j = await res.json();
25
- const templateId = j.data.templateId;
26
- const newTemplate = await repo(CarboneTemplate).upsert({
27
- where: { id: templateId, name, extension: j.data.templateExtension },
28
- });
29
- await repo(CarboneLog).insert({
30
- templateId,
31
- action: CarbonLogAction.template_upload,
32
- });
33
- // Delete all other templates with the same name
34
- if (deleteOlderTemplatesWithSameName) {
35
- const older = await repo(CarboneTemplate).find({
36
- where: { name: newTemplate.name, id: { $ne: newTemplate.id } },
14
+ if (import.meta.env.SSR) {
15
+ const { name, base64, nameForCarbone, deleteOlderTemplatesWithSameName } = config;
16
+ // 30 max 1 13
17
+ const nameToUse = strTrimMid(nameForCarbone ?? name, { len: 16, midStr: '-' }) + '_' + dateCompact();
18
+ const res = await CarboneController.server.fetch({
19
+ api: `/template`,
20
+ body: JSON.stringify({ name, template: base64 }),
21
+ headers: {
22
+ 'carbone-template-name': nameToUse,
23
+ },
37
24
  });
38
- for (const t of older) {
39
- await CarboneController.deleteTemplate({ templateId: t.id });
25
+ const j = await res.json();
26
+ const templateId = j.data.templateId;
27
+ const newTemplate = await repo(CarboneTemplate).upsert({
28
+ where: { id: templateId, name, extension: j.data.templateExtension },
29
+ });
30
+ await repo(CarboneLog).insert({
31
+ templateId,
32
+ action: CarbonLogAction.template_upload,
33
+ });
34
+ // Delete all other templates with the same name
35
+ if (deleteOlderTemplatesWithSameName) {
36
+ const older = await repo(CarboneTemplate).find({
37
+ where: { name: newTemplate.name, id: { $ne: newTemplate.id } },
38
+ });
39
+ for (const t of older) {
40
+ await CarboneController.deleteTemplate({ templateId: t.id });
41
+ }
40
42
  }
43
+ return;
41
44
  }
45
+ throw new Error('carbone.uploadTemplate: server-only');
42
46
  }
43
47
  static async downloadTemplate(config) {
44
- const { templateName, filename } = config;
45
- let { templateId, templateExtension } = config;
46
- if (templateName) {
47
- const t = await repo(CarboneTemplate).findFirst({ name: templateName });
48
- if (!t) {
49
- throw new Error('Template not found');
48
+ if (import.meta.env.SSR) {
49
+ const { templateName, filename } = config;
50
+ let { templateId, templateExtension } = config;
51
+ if (templateName) {
52
+ const t = await repo(CarboneTemplate).findFirst({ name: templateName });
53
+ if (!t) {
54
+ throw new Error('Template not found');
55
+ }
56
+ templateId = t.id;
57
+ templateExtension = t.extension;
50
58
  }
51
- templateId = t.id;
52
- templateExtension = t.extension;
59
+ const response = await CarboneController.server.fetch({
60
+ api: `/template/${templateId}`,
61
+ method: 'GET',
62
+ });
63
+ await repo(CarboneLog).insert({
64
+ templateId,
65
+ action: CarbonLogAction.template_download,
66
+ });
67
+ const contentType = response.headers.get('content-type');
68
+ const arrayBuffer = await response.arrayBuffer();
69
+ const base64 = Buffer.from(arrayBuffer).toString('base64');
70
+ const filenameToUse = filename ?? templateName ?? `template_${templateId}`;
71
+ return {
72
+ data: base64,
73
+ contentType: contentType || 'application/octet-stream',
74
+ filename: `${filenameToUse}.${templateExtension}`,
75
+ };
53
76
  }
54
- const response = await CarboneController.server.fetch({
55
- api: `/template/${templateId}`,
56
- method: 'GET',
57
- });
58
- await repo(CarboneLog).insert({
59
- templateId,
60
- action: CarbonLogAction.template_download,
61
- });
62
- const contentType = response.headers.get('content-type');
63
- const arrayBuffer = await response.arrayBuffer();
64
- const base64 = Buffer.from(arrayBuffer).toString('base64');
65
- const filenameToUse = filename ?? templateName ?? `template_${templateId}`;
66
- return {
67
- data: base64,
68
- contentType: contentType || 'application/octet-stream',
69
- filename: `${filenameToUse}.${templateExtension}`,
70
- };
77
+ throw new Error('carbone.downloadTemplate: server-only');
71
78
  }
72
79
  static async deleteTemplate(config) {
73
- const { templateId } = config;
74
- await CarboneController.server.fetch({
75
- api: `/template/${templateId}`,
76
- method: 'DELETE',
77
- });
78
- await repo(CarboneLog).insert({
79
- templateId,
80
- action: CarbonLogAction.template_delete,
81
- });
82
- await repo(CarboneTemplate).delete({ id: templateId });
80
+ if (import.meta.env.SSR) {
81
+ const { templateId } = config;
82
+ await CarboneController.server.fetch({
83
+ api: `/template/${templateId}`,
84
+ method: 'DELETE',
85
+ });
86
+ await repo(CarboneLog).insert({
87
+ templateId,
88
+ action: CarbonLogAction.template_delete,
89
+ });
90
+ await repo(CarboneTemplate).delete({ id: templateId });
91
+ return;
92
+ }
93
+ throw new Error('carbone.deleteTemplate: server-only');
83
94
  }
84
95
  static async render(config) {
85
- const { templateName, templateBase64, data, filename } = config;
86
- let { templateId, convertTo } = config;
87
- // eslint-disable-next-line
88
- let mode = '';
89
- if (templateBase64) {
90
- mode = 'templateBase64';
91
- }
92
- else if (templateName) {
93
- mode = 'templateName';
94
- const t = await repo(CarboneTemplate).findFirst({ name: templateName });
95
- if (!t) {
96
- throw new Error('Template not found');
96
+ if (import.meta.env.SSR) {
97
+ const { templateName, templateBase64, data, filename } = config;
98
+ let { templateId, convertTo } = config;
99
+ // eslint-disable-next-line
100
+ let mode = '';
101
+ if (templateBase64) {
102
+ mode = 'templateBase64';
97
103
  }
98
- templateId = t.id;
99
- if (!convertTo) {
100
- convertTo = t.extension;
104
+ else if (templateName) {
105
+ mode = 'templateName';
106
+ const t = await repo(CarboneTemplate).findFirst({ name: templateName });
107
+ if (!t) {
108
+ throw new Error('Template not found');
109
+ }
110
+ templateId = t.id;
111
+ if (!convertTo) {
112
+ convertTo = t.extension;
113
+ }
101
114
  }
102
- }
103
- else {
104
- mode = 'templateId';
105
- }
106
- if (CarboneController.server.test) {
107
- convertTo = 'pdf';
108
- }
109
- const response = templateBase64
110
- ? await CarboneController.server.fetch({
111
- api: `/render/template?download=true`,
112
- body: JSON.stringify({ data, template: templateBase64, convertTo: convertTo ?? 'pdf' }),
113
- })
114
- : await CarboneController.server.fetch({
115
- api: `/render/${templateId}?download=true`,
116
- body: JSON.stringify({ data, convertTo: convertTo ?? 'pdf' }),
115
+ else {
116
+ mode = 'templateId';
117
+ }
118
+ if (CarboneController.server.test) {
119
+ convertTo = 'pdf';
120
+ }
121
+ const response = templateBase64
122
+ ? await CarboneController.server.fetch({
123
+ api: `/render/template?download=true`,
124
+ body: JSON.stringify({ data, template: templateBase64, convertTo: convertTo ?? 'pdf' }),
125
+ })
126
+ : await CarboneController.server.fetch({
127
+ api: `/render/${templateId}?download=true`,
128
+ body: JSON.stringify({ data, convertTo: convertTo ?? 'pdf' }),
129
+ });
130
+ await repo(CarboneLog).insert({
131
+ templateId: mode === 'templateBase64' ? 'templateBase64' : templateId,
132
+ action: CarbonLogAction.render,
117
133
  });
118
- await repo(CarboneLog).insert({
119
- templateId: mode === 'templateBase64' ? 'templateBase64' : templateId,
120
- action: CarbonLogAction.render,
121
- });
122
- const contentType = response.headers.get('content-type');
123
- const arrayBuffer = await response.arrayBuffer();
124
- const base64 = Buffer.from(arrayBuffer).toString('base64');
125
- const filenameToUse = filename ?? `template_${templateId}`;
126
- return {
127
- data: base64,
128
- contentType: contentType || 'application/octet-stream',
129
- filename: `${filenameToUse}.${convertTo}`,
130
- };
134
+ const contentType = response.headers.get('content-type');
135
+ const arrayBuffer = await response.arrayBuffer();
136
+ const base64 = Buffer.from(arrayBuffer).toString('base64');
137
+ const filenameToUse = filename ?? `template_${templateId}`;
138
+ return {
139
+ data: base64,
140
+ contentType: contentType || 'application/octet-stream',
141
+ filename: `${filenameToUse}.${convertTo}`,
142
+ };
143
+ }
144
+ throw new Error('carbone.render: server-only');
131
145
  }
132
146
  }
133
147
  __decorate([
@@ -66,319 +66,340 @@ async function addMetaData(issueId, obj) {
66
66
  }
67
67
  export class FeedbackController {
68
68
  static async getMilestones() {
69
- const data = await getGitHub(`query Milestones(
70
- $repository: String!
71
- $owner: String!
72
- $filter: String
73
- $take: Int = 25
74
- $cursor: String
75
- ) {
76
- repository(name: $repository, owner: $owner) {
77
- milestones(query: $filter, last: $take, after: $cursor, states: OPEN) {
78
- pageInfo {
79
- endCursor
80
- }
81
- nodes {
82
- id
83
- number
84
- title
85
- }
69
+ if (import.meta.env.SSR) {
70
+ const data = await getGitHub(`query Milestones(
71
+ $repository: String!
72
+ $owner: String!
73
+ $filter: String
74
+ $take: Int = 25
75
+ $cursor: String
76
+ ) {
77
+ repository(name: $repository, owner: $owner) {
78
+ milestones(query: $filter, last: $take, after: $cursor, states: OPEN) {
79
+ pageInfo {
80
+ endCursor
81
+ }
82
+ nodes {
83
+ id
84
+ number
85
+ title
86
+ }
87
+ }
88
+ }
89
+ }
90
+ `, {
91
+ repository: remult.context.feedbackOptions.repo.name,
92
+ owner: remult.context.feedbackOptions.repo.owner,
93
+ filter: remult.context.feedbackOptions.milestones?.title_filter ?? '',
94
+ });
95
+ return data.repository.milestones.nodes.map((c) => {
96
+ return {
97
+ ...c,
98
+ title: c.title
99
+ .replaceAll(remult.context.feedbackOptions.milestones?.title_filter ?? '', '')
100
+ .trim(),
101
+ };
102
+ });
86
103
  }
87
- }
88
- }
89
- `, {
90
- repository: remult.context.feedbackOptions.repo.name,
91
- owner: remult.context.feedbackOptions.repo.owner,
92
- filter: remult.context.feedbackOptions.milestones?.title_filter ?? '',
93
- });
94
- return data.repository.milestones.nodes.map((c) => {
95
- return {
96
- ...c,
97
- title: c.title
98
- .replaceAll(remult.context.feedbackOptions.milestones?.title_filter ?? '', '')
99
- .trim(),
100
- };
101
- });
104
+ throw new Error('feedback.getMilestones: server-only');
102
105
  }
103
106
  static async getIssues(milestoneNumber, issueState) {
104
- const issueOrder = issueState === 'CLOSED'
105
- ? { field: 'UPDATED_AT', direction: 'DESC' } // When close, the last issue updated.
106
- : null; // When open take milestone order
107
- const data = await getGitHub(`query Issues(
108
- $repository: String!
109
- $owner: String!
110
- $filters: IssueFilters
111
- $milestoneNumber: Int!
112
- $take: Int = 25
113
- $cursor: String
114
- $issueOrder: IssueOrder
115
- ) {
116
- repository(name: $repository, owner: $owner) {
117
- milestone(number: $milestoneNumber) {
118
- issues(first: $take, after: $cursor, filterBy: $filters, orderBy: $issueOrder) {
119
- nodes {
120
- id
121
- number
122
- titleHTML
123
- state
124
- createdAt
125
- updatedAt
126
- labels(first:10){
127
- nodes {
128
- name
129
- }
107
+ if (import.meta.env.SSR) {
108
+ const issueOrder = issueState === 'CLOSED'
109
+ ? { field: 'UPDATED_AT', direction: 'DESC' } // When close, the last issue updated.
110
+ : null; // When open take milestone order
111
+ const data = await getGitHub(`query Issues(
112
+ $repository: String!
113
+ $owner: String!
114
+ $filters: IssueFilters
115
+ $milestoneNumber: Int!
116
+ $take: Int = 25
117
+ $cursor: String
118
+ $issueOrder: IssueOrder
119
+ ) {
120
+ repository(name: $repository, owner: $owner) {
121
+ milestone(number: $milestoneNumber) {
122
+ issues(first: $take, after: $cursor, filterBy: $filters, orderBy: $issueOrder) {
123
+ nodes {
124
+ id
125
+ number
126
+ titleHTML
127
+ state
128
+ createdAt
129
+ updatedAt
130
+ labels(first:10){
131
+ nodes {
132
+ name
133
+ }
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ `, {
141
+ repository: remult.context.feedbackOptions.repo.name,
142
+ owner: remult.context.feedbackOptions.repo.owner,
143
+ milestoneNumber,
144
+ filters: {
145
+ // GitHub treats `labels: []` as "match nothing", so omit the key when unset.
146
+ labels: remult.context.feedbackOptions.milestones?.labels_filters?.length
147
+ ? remult.context.feedbackOptions.milestones.labels_filters
148
+ : undefined,
149
+ states: [issueState],
150
+ },
151
+ issueOrder,
152
+ });
153
+ return data.repository.milestone.issues.nodes.map((issue) => {
154
+ const labels = issue.labels.nodes.map((label) => label.name);
155
+ const hasWaitingForAnswerLabel = remult.context.feedbackOptions.highlight_label
156
+ ? labels.some((name) => name.includes(remult.context.feedbackOptions.highlight_label))
157
+ : false;
158
+ return {
159
+ id: issue.id,
160
+ number: issue.number,
161
+ titleHTML: issue.titleHTML,
162
+ state: issue.state,
163
+ createdAt: issue.createdAt,
164
+ updatedAt: issue.updatedAt,
165
+ labels,
166
+ highlight: hasWaitingForAnswerLabel,
167
+ };
168
+ });
169
+ }
170
+ throw new Error('feedback.getIssues: server-only');
171
+ }
172
+ static async getIssue(issueNumber) {
173
+ if (import.meta.env.SSR) {
174
+ const data = await getGitHub(`query Issue($repository: String!, $owner: String!, $issueNumber: Int!) {
175
+ repository(name: $repository, owner: $owner) {
176
+ issue(number: $issueNumber) {
177
+ id
178
+ createdAt
179
+ bodyHTML
180
+ state
181
+ title
182
+ labels(first: 25){
183
+ nodes{
184
+ id
185
+ name
186
+ }
187
+ }
188
+ comments(first: 100) {
189
+ nodes {
190
+ id
191
+ isMinimized
192
+ createdAt
193
+ body
194
+ bodyHTML
195
+ reactionGroups {
196
+ content
197
+ reactors(first: 1) {
198
+ totalCount
199
+ }
200
+ }
201
+ }
202
+ }
203
+ }
204
+ }
205
+ }
206
+ `, {
207
+ repository: remult.context.feedbackOptions.repo.name,
208
+ owner: remult.context.feedbackOptions.repo.owner,
209
+ issueNumber,
210
+ });
211
+ const items = [];
212
+ const firstItem = {
213
+ bodyHTML: data.repository.issue.bodyHTML,
214
+ createdAt: data.repository.issue.createdAt,
215
+ public: true,
216
+ title: data.repository.issue.title,
217
+ };
218
+ items.push(firstItem);
219
+ const comments = data.repository.issue.comments.nodes;
220
+ for (let i = 0; i < comments.length; i++) {
221
+ if (comments[i].isMinimized) {
222
+ const parsed = JSON.parse(comments[i].body.replaceAll('<pre>\n', '').replaceAll('\n</pre>', ''));
223
+ const last = items.at(-1);
224
+ last.who = parsed?.author ?? '???';
225
+ last.public = true;
226
+ }
227
+ else {
228
+ const nbEye = comments[i].reactionGroups.find((c) => c.content === 'EYES')?.reactors.totalCount;
229
+ items.push({
230
+ bodyHTML: comments[i].bodyHTML,
231
+ createdAt: new Date(comments[i].createdAt),
232
+ public: nbEye && nbEye > 0 ? true : false,
233
+ title: data.repository.issue.title,
234
+ });
130
235
  }
131
- }
132
236
  }
133
- }
134
- }
135
- }
136
- `, {
137
- repository: remult.context.feedbackOptions.repo.name,
138
- owner: remult.context.feedbackOptions.repo.owner,
139
- milestoneNumber,
140
- filters: {
141
- // GitHub treats `labels: []` as "match nothing", so omit the key when unset.
142
- labels: remult.context.feedbackOptions.milestones?.labels_filters?.length
143
- ? remult.context.feedbackOptions.milestones.labels_filters
144
- : undefined,
145
- states: [issueState],
146
- },
147
- issueOrder,
148
- });
149
- return data.repository.milestone.issues.nodes.map((issue) => {
150
- const labels = issue.labels.nodes.map((label) => label.name);
151
237
  const hasWaitingForAnswerLabel = remult.context.feedbackOptions.highlight_label
152
- ? labels.some((name) => name.includes(remult.context.feedbackOptions.highlight_label))
238
+ ? data.repository.issue.labels.nodes.some((label) => label.name.includes(remult.context.feedbackOptions.highlight_label))
153
239
  : false;
154
- return {
155
- id: issue.id,
156
- number: issue.number,
157
- titleHTML: issue.titleHTML,
158
- state: issue.state,
159
- createdAt: issue.createdAt,
160
- updatedAt: issue.updatedAt,
161
- labels,
240
+ const toRet = {
241
+ id: data.repository.issue.id,
242
+ state: data.repository.issue.state,
243
+ items: items.filter((c) => c.public),
244
+ labels: data.repository.issue.labels.nodes,
162
245
  highlight: hasWaitingForAnswerLabel,
246
+ title: data.repository.issue.title,
163
247
  };
164
- });
165
- }
166
- static async getIssue(issueNumber) {
167
- const data = await getGitHub(`query Issue($repository: String!, $owner: String!, $issueNumber: Int!) {
168
- repository(name: $repository, owner: $owner) {
169
- issue(number: $issueNumber) {
170
- id
171
- createdAt
172
- bodyHTML
173
- state
174
- title
175
- labels(first: 25){
176
- nodes{
177
- id
178
- name
179
- }
180
- }
181
- comments(first: 100) {
182
- nodes {
183
- id
184
- isMinimized
185
- createdAt
186
- body
187
- bodyHTML
188
- reactionGroups {
189
- content
190
- reactors(first: 1) {
191
- totalCount
192
- }
193
- }
194
- }
195
- }
196
- }
197
- }
198
- }
199
- `, {
200
- repository: remult.context.feedbackOptions.repo.name,
201
- owner: remult.context.feedbackOptions.repo.owner,
202
- issueNumber,
203
- });
204
- const items = [];
205
- const firstItem = {
206
- bodyHTML: data.repository.issue.bodyHTML,
207
- createdAt: data.repository.issue.createdAt,
208
- public: true,
209
- title: data.repository.issue.title,
210
- };
211
- items.push(firstItem);
212
- const comments = data.repository.issue.comments.nodes;
213
- for (let i = 0; i < comments.length; i++) {
214
- if (comments[i].isMinimized) {
215
- const parsed = JSON.parse(comments[i].body.replaceAll('<pre>\n', '').replaceAll('\n</pre>', ''));
216
- const last = items.at(-1);
217
- last.who = parsed?.author ?? '???';
218
- last.public = true;
219
- }
220
- else {
221
- const nbEye = comments[i].reactionGroups.find((c) => c.content === 'EYES')?.reactors.totalCount;
222
- items.push({
223
- bodyHTML: comments[i].bodyHTML,
224
- createdAt: new Date(comments[i].createdAt),
225
- public: nbEye && nbEye > 0 ? true : false,
226
- title: data.repository.issue.title,
227
- });
228
- }
248
+ return toRet;
229
249
  }
230
- const hasWaitingForAnswerLabel = remult.context.feedbackOptions.highlight_label
231
- ? data.repository.issue.labels.nodes.some((label) => label.name.includes(remult.context.feedbackOptions.highlight_label))
232
- : false;
233
- const toRet = {
234
- id: data.repository.issue.id,
235
- state: data.repository.issue.state,
236
- items: items.filter((c) => c.public),
237
- labels: data.repository.issue.labels.nodes,
238
- highlight: hasWaitingForAnswerLabel,
239
- title: data.repository.issue.title,
240
- };
241
- return toRet;
250
+ throw new Error('feedback.getIssue: server-only');
242
251
  }
243
252
  static async createIssue(milestoneId, title, body, metadata,
244
253
  /** Extra label names applied on top of `create_label` (unknown names are ignored). */
245
254
  labels = []) {
246
- const repoInfo = await getGitHub(`query RepoInfo(
247
- $repository: String!
248
- $owner: String!
249
- ) {
250
- repository(name: $repository, owner: $owner) {
251
- id
252
- labels(first: 25){
253
- nodes{
254
- id
255
- name
256
- }
257
- }
258
- }
259
- }`, {
260
- repository: remult.context.feedbackOptions.repo.name,
261
- owner: remult.context.feedbackOptions.repo.owner,
262
- });
263
- const repoInfoData = repoInfo.repository;
264
- const wanted = new Set([remult.context.feedbackOptions.create_label, ...labels]);
265
- const labelIds = repoInfoData.labels.nodes.filter((c) => wanted.has(c.name)).map((c) => c.id);
266
- const newIssue = await getGitHub(`mutation CreateIssue($input: CreateIssueInput!) {
267
- createIssue(input: $input) {
268
- issue {
269
- id
270
- number
255
+ if (import.meta.env.SSR) {
256
+ const repoInfo = await getGitHub(`query RepoInfo(
257
+ $repository: String!
258
+ $owner: String!
259
+ ) {
260
+ repository(name: $repository, owner: $owner) {
261
+ id
262
+ labels(first: 25){
263
+ nodes{
264
+ id
265
+ name
266
+ }
267
+ }
268
+ }
269
+ }`, {
270
+ repository: remult.context.feedbackOptions.repo.name,
271
+ owner: remult.context.feedbackOptions.repo.owner,
272
+ });
273
+ const repoInfoData = repoInfo.repository;
274
+ const wanted = new Set([remult.context.feedbackOptions.create_label, ...labels]);
275
+ const labelIds = repoInfoData.labels.nodes.filter((c) => wanted.has(c.name)).map((c) => c.id);
276
+ const newIssue = await getGitHub(`mutation CreateIssue($input: CreateIssueInput!) {
277
+ createIssue(input: $input) {
278
+ issue {
279
+ id
280
+ number
281
+ }
282
+ }
283
+ }
284
+ `, {
285
+ input: {
286
+ repositoryId: repoInfoData.id,
287
+ milestoneId: milestoneId,
288
+ labelIds,
289
+ title: title ?? 'New Feedback (wo title...)',
290
+ body: body,
291
+ },
292
+ });
293
+ const toRet = newIssue.createIssue.issue;
294
+ const feedbackMetadata = remult.context.feedbackOptions.transformMetadata?.({
295
+ user: remult.user,
296
+ metadata,
297
+ }) ?? metadata;
298
+ await addMetaData(toRet.id, feedbackMetadata);
299
+ remult.context.feedbackOptions.saved?.({
300
+ number: toRet.number,
301
+ title: title,
302
+ body,
303
+ metadata: feedbackMetadata,
304
+ });
305
+ return toRet;
271
306
  }
272
- }
273
- }
274
- `, {
275
- input: {
276
- repositoryId: repoInfoData.id,
277
- milestoneId: milestoneId,
278
- labelIds,
279
- title: title ?? 'New Feedback (wo title...)',
280
- body: body,
281
- },
282
- });
283
- const toRet = newIssue.createIssue.issue;
284
- const feedbackMetadata = remult.context.feedbackOptions.transformMetadata?.({
285
- user: remult.user,
286
- metadata,
287
- }) ?? metadata;
288
- await addMetaData(toRet.id, feedbackMetadata);
289
- remult.context.feedbackOptions.saved?.({
290
- number: toRet.number,
291
- title: title,
292
- body,
293
- metadata: feedbackMetadata,
294
- });
295
- return toRet;
307
+ throw new Error('feedback.createIssue: server-only');
296
308
  }
297
309
  static async addCommentOnIssue(issueId, issueNumber, title, body, metadata, labels) {
298
- const inputComment = {
299
- subjectId: issueId,
300
- body,
301
- };
302
- const inputIssue = {
303
- id: issueId,
304
- labelIds: (remult.context.feedbackOptions.highlight_label
305
- ? labels.filter((c) => c.name !== remult.context.feedbackOptions.highlight_label)
306
- : labels).map((c) => c.id),
307
- };
308
- await getGitHub(`mutation AddComment($inputComment: AddCommentInput!, $inputIssue: UpdateIssueInput!) {
309
- addComment(input: $inputComment) {
310
- commentEdge {
311
- node {
312
- id
313
- }
314
- }
315
- }
316
- updateIssue(input: $inputIssue) {
317
- issue {
318
- id
319
- }
310
+ if (import.meta.env.SSR) {
311
+ const inputComment = {
312
+ subjectId: issueId,
313
+ body,
314
+ };
315
+ const inputIssue = {
316
+ id: issueId,
317
+ labelIds: (remult.context.feedbackOptions.highlight_label
318
+ ? labels.filter((c) => c.name !== remult.context.feedbackOptions.highlight_label)
319
+ : labels).map((c) => c.id),
320
+ };
321
+ await getGitHub(`mutation AddComment($inputComment: AddCommentInput!, $inputIssue: UpdateIssueInput!) {
322
+ addComment(input: $inputComment) {
323
+ commentEdge {
324
+ node {
325
+ id
326
+ }
327
+ }
328
+ }
329
+ updateIssue(input: $inputIssue) {
330
+ issue {
331
+ id
332
+ }
333
+ }
334
+ }
335
+ `, {
336
+ inputComment,
337
+ inputIssue,
338
+ });
339
+ const feedbackMetadata = remult.context.feedbackOptions.transformMetadata?.({
340
+ user: remult.user,
341
+ metadata,
342
+ }) ?? metadata;
343
+ await addMetaData(issueId, feedbackMetadata);
344
+ remult.context.feedbackOptions.saved?.({
345
+ number: issueNumber,
346
+ title,
347
+ body,
348
+ metadata: feedbackMetadata,
349
+ });
350
+ return 'done';
320
351
  }
321
- }
322
- `, {
323
- inputComment,
324
- inputIssue,
325
- });
326
- const feedbackMetadata = remult.context.feedbackOptions.transformMetadata?.({
327
- user: remult.user,
328
- metadata,
329
- }) ?? metadata;
330
- await addMetaData(issueId, feedbackMetadata);
331
- remult.context.feedbackOptions.saved?.({
332
- number: issueNumber,
333
- title,
334
- body,
335
- metadata: feedbackMetadata,
336
- });
337
- return 'done';
352
+ throw new Error('feedback.addCommentOnIssue: server-only');
338
353
  }
339
354
  static async close(issueId, labels) {
340
- const inputClose = {
341
- issueId,
342
- };
343
- const inputIssue = {
344
- id: issueId,
345
- labelIds: (remult.context.feedbackOptions.highlight_label
346
- ? labels.filter((c) => c.name !== remult.context.feedbackOptions.highlight_label)
347
- : labels).map((c) => c.id),
348
- };
349
- await getGitHub(`mutation CloseIssue($inputIssue: UpdateIssueInput!, $inputClose: CloseIssueInput!) {
350
- updateIssue(input: $inputIssue) {
351
- issue {
352
- id
353
- }
354
- }
355
- closeIssue(input: $inputClose) {
356
- issue {
357
- id
358
- }
355
+ if (import.meta.env.SSR) {
356
+ const inputClose = {
357
+ issueId,
358
+ };
359
+ const inputIssue = {
360
+ id: issueId,
361
+ labelIds: (remult.context.feedbackOptions.highlight_label
362
+ ? labels.filter((c) => c.name !== remult.context.feedbackOptions.highlight_label)
363
+ : labels).map((c) => c.id),
364
+ };
365
+ await getGitHub(`mutation CloseIssue($inputIssue: UpdateIssueInput!, $inputClose: CloseIssueInput!) {
366
+ updateIssue(input: $inputIssue) {
367
+ issue {
368
+ id
369
+ }
370
+ }
371
+ closeIssue(input: $inputClose) {
372
+ issue {
373
+ id
374
+ }
375
+ }
376
+ }
377
+ `, {
378
+ inputIssue,
379
+ inputClose,
380
+ });
381
+ return 'done';
359
382
  }
360
- }
361
- `, {
362
- inputIssue,
363
- inputClose,
364
- });
365
- return 'done';
383
+ throw new Error('feedback.close: server-only');
366
384
  }
367
385
  static async reOpen(issueId) {
368
- const input = {
369
- issueId,
370
- };
371
- await getGitHub(`mutation ReOpenIssue($input: ReopenIssueInput!) {
372
- reopenIssue(input: $input) {
373
- issue {
374
- id
375
- }
386
+ if (import.meta.env.SSR) {
387
+ const input = {
388
+ issueId,
389
+ };
390
+ await getGitHub(`mutation ReOpenIssue($input: ReopenIssueInput!) {
391
+ reopenIssue(input: $input) {
392
+ issue {
393
+ id
394
+ }
395
+ }
396
+ }
397
+ `, {
398
+ input,
399
+ });
400
+ return 'done';
376
401
  }
377
- }
378
- `, {
379
- input,
380
- });
381
- return 'done';
402
+ throw new Error('feedback.reOpen: server-only');
382
403
  }
383
404
  }
384
405
  __decorate([
@@ -50,6 +50,7 @@ export type SqlTokensOptions = {
50
50
  /** Override the pool used by `read` tokens. Defaults to the Postgres pool behind the data provider. */
51
51
  pool?: SqlTokenPool;
52
52
  };
53
+ export declare const SERVER_ONLY = "sqlAdmin: server-only";
53
54
  export declare const SQL_ADMINS: ("FF_Role.Admin" | "SqlAdmin.Admin")[];
54
55
  export declare class SqlAdminController {
55
56
  /** Optional override set by the `sqlAdmin()` module's `initApi`. Falls back to `SqlDatabase.getDb()`. */
@@ -8,6 +8,8 @@ import { BackendMethod, remult, repo, SqlDatabase } from 'remult';
8
8
  import { FF_Role } from '../core/common';
9
9
  import { Roles_SqlAdmin } from './Roles_SqlAdmin';
10
10
  import { SQL_CAPABILITIES, SQL_TOKEN_TTLS, SqlToken, SqlTokenCall, } from './sqlTokenEntities';
11
+ // Thrown when a server-only body runs on the client (its `import.meta.env.SSR` block is gone there).
12
+ export const SERVER_ONLY = 'sqlAdmin: server-only';
11
13
  export const SQL_ADMINS = [Roles_SqlAdmin.SqlAdmin_Admin, FF_Role.FF_Role_Admin];
12
14
  const CALL_LOG_CMD_MAX = 4000;
13
15
  /** Retention is a housekeeping sweep, not something to pay for on every query. */
@@ -42,21 +44,24 @@ export class SqlAdminController {
42
44
  return SqlAdminController.run(cmd, capabilities);
43
45
  }
44
46
  static async run(cmd, capabilities) {
45
- const db = getDb();
46
- try {
47
- // Anything that is not an explicit list (null, a stray boolean) is a read.
48
- if (Array.isArray(capabilities) && capabilities.includes('write')) {
49
- const start = performance.now();
50
- const rows = (await db.execute(cmd)).rows;
51
- return { rows, rowCount: rows.length, took: performance.now() - start };
47
+ if (import.meta.env.SSR) {
48
+ const db = getDb();
49
+ try {
50
+ // Anything that is not an explicit list (null, a stray boolean) is a read.
51
+ if (Array.isArray(capabilities) && capabilities.includes('write')) {
52
+ const start = performance.now();
53
+ const rows = (await db.execute(cmd)).rows;
54
+ return { rows, rowCount: rows.length, took: performance.now() - start };
55
+ }
56
+ const { readOnlySql } = await import('./server/readOnlySql');
57
+ return await readOnlySql(db, SqlAdminController.options.tokens?.pool ?? poolFrom(db), cmd);
58
+ }
59
+ catch (err) {
60
+ const { enrichSqlError } = await import('./server/sqlError');
61
+ throw await enrichSqlError(db, err, cmd);
52
62
  }
53
- const { readOnlySql } = await import('./server/readOnlySql');
54
- return await readOnlySql(db, SqlAdminController.options.tokens?.pool ?? poolFrom(db), cmd);
55
- }
56
- catch (err) {
57
- const { enrichSqlError } = await import('./server/sqlError');
58
- throw await enrichSqlError(db, err, cmd);
59
63
  }
64
+ throw new Error(SERVER_ONLY);
60
65
  }
61
66
  static async execAsToken(token, cmd) {
62
67
  const o = SqlAdminController.options.tokens;
@@ -115,18 +120,21 @@ export class SqlAdminController {
115
120
  if (!o.capabilities.includes(c))
116
121
  throw new Error(`${c} is not enabled`);
117
122
  }
118
- const { newRawToken, hashToken, tokenHint, randomTokenName } = await import('./server/token');
119
- const prefix = o.prefix ?? 'ffsql_';
120
- const raw = newRawToken(prefix);
121
- await repo(SqlToken).insert({
122
- name: name.trim() || randomTokenName(),
123
- hint: tokenHint(raw, prefix),
124
- tokenHash: hashToken(raw),
125
- userId,
126
- capabilities: unique,
127
- expiresAt: new Date(Date.now() + ttlMs),
128
- });
129
- return raw;
123
+ if (import.meta.env.SSR) {
124
+ const { newRawToken, hashToken, tokenHint, randomTokenName } = await import('./server/token');
125
+ const prefix = o.prefix ?? 'ffsql_';
126
+ const raw = newRawToken(prefix);
127
+ await repo(SqlToken).insert({
128
+ name: name.trim() || randomTokenName(),
129
+ hint: tokenHint(raw, prefix),
130
+ tokenHash: hashToken(raw),
131
+ userId,
132
+ capabilities: unique,
133
+ expiresAt: new Date(Date.now() + ttlMs),
134
+ });
135
+ return raw;
136
+ }
137
+ throw new Error(SERVER_ONLY);
130
138
  }
131
139
  /**
132
140
  * Deletes every token that can no longer be used (expired or revoked), and
@@ -5,7 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  import { BackendMethod, remult, SqlDatabase } from 'remult';
8
- import { SQL_ADMINS } from './SqlAdminController';
8
+ import { SERVER_ONLY, SQL_ADMINS } from './SqlAdminController';
9
9
  export class SqlDriftController {
10
10
  /** Set by the `sqlAdmin()` module's `initApi`. */
11
11
  static options;
@@ -27,24 +27,36 @@ export class SqlDriftController {
27
27
  }
28
28
  // Each apply shares the BackendMethod transaction: any failure rolls every statement back.
29
29
  static async relationIndexes(opts) {
30
- const { db, entities } = SqlDriftController.ctx('relationIndexes');
31
- const { createRelationIndexes } = await import('./server/drift/relationIndexes');
32
- return createRelationIndexes(db, entities, opts);
30
+ if (import.meta.env.SSR) {
31
+ const { db, entities } = SqlDriftController.ctx('relationIndexes');
32
+ const { createRelationIndexes } = await import('./server/drift/relationIndexes');
33
+ return createRelationIndexes(db, entities, opts);
34
+ }
35
+ throw new Error(SERVER_ONLY);
33
36
  }
34
37
  static async primaryKeys(opts) {
35
- const { db, entities } = SqlDriftController.ctx('primaryKeys');
36
- const { reindexPrimaryKeys } = await import('./server/drift/primaryKeys');
37
- return reindexPrimaryKeys(db, entities, opts);
38
+ if (import.meta.env.SSR) {
39
+ const { db, entities } = SqlDriftController.ctx('primaryKeys');
40
+ const { reindexPrimaryKeys } = await import('./server/drift/primaryKeys');
41
+ return reindexPrimaryKeys(db, entities, opts);
42
+ }
43
+ throw new Error(SERVER_ONLY);
38
44
  }
39
45
  static async nullable(opts) {
40
- const { db, entities } = SqlDriftController.ctx('nullable');
41
- const { syncNullable } = await import('./server/drift/nullable');
42
- return syncNullable(db, entities, opts);
46
+ if (import.meta.env.SSR) {
47
+ const { db, entities } = SqlDriftController.ctx('nullable');
48
+ const { syncNullable } = await import('./server/drift/nullable');
49
+ return syncNullable(db, entities, opts);
50
+ }
51
+ throw new Error(SERVER_ONLY);
43
52
  }
44
53
  static async orphanColumns(opts) {
45
- const { db, entities } = SqlDriftController.ctx('orphanColumns');
46
- const { dropColumns } = await import('./server/drift/orphanColumns');
47
- return dropColumns(db, entities, opts);
54
+ if (import.meta.env.SSR) {
55
+ const { db, entities } = SqlDriftController.ctx('orphanColumns');
56
+ const { dropColumns } = await import('./server/drift/orphanColumns');
57
+ return dropColumns(db, entities, opts);
58
+ }
59
+ throw new Error(SERVER_ONLY);
48
60
  }
49
61
  }
50
62
  __decorate([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firstly",
3
- "version": "0.10.4",
3
+ "version": "0.10.5",
4
4
  "type": "module",
5
5
  "description": "Firstly, an opinionated Remult setup!",
6
6
  "bin": {
@@ -41,7 +41,7 @@
41
41
  "clsx": "2.1.1",
42
42
  "cron": "4.4.0",
43
43
  "esm-env": "1.2.2",
44
- "nodemailer": "8.0.5",
44
+ "nodemailer": "10.0.10",
45
45
  "svelte-sonner": "1.2.1",
46
46
  "tailwind-merge": "3.7.0",
47
47
  "tailwindcss": "4.3.0",