@trypattern/sdk 0.1.20 → 0.1.22

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.
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { appTitle } from './appTitle.ts';
3
3
  export { appTitle };
4
- export { ASSERTION_SECONDS, MEDIA_WRITE, NEXT_PAGE_HEADER, TABLES_READ, assertionKey, signAssertion } from './patternAccess.ts';
4
+ export { ASSERTION_SECONDS, MEDIA_WRITE, NEXT_PAGE_HEADER, TABLES_READ, TABLES_WRITE, assertionKey, signAssertion } from './patternAccess.ts';
5
5
  export type { AssertionKey, AssertionScope } from './patternAccess.ts';
6
6
  export declare const DASHBOARD_URL = "https://onpattern.app";
7
7
  export declare const APPS_DOMAIN = "onpattern.app";
@@ -320,6 +320,94 @@ export declare const databaseRows: z.ZodObject<{
320
320
  next: z.ZodNullable<z.ZodString>;
321
321
  }, z.core.$strip>;
322
322
  export type DatabaseRows = z.output<typeof databaseRows>;
323
+ export declare const CHANGE_SET_ROWS = 300;
324
+ export declare const dataChange: z.ZodDiscriminatedUnion<[z.ZodObject<{
325
+ op: z.ZodLiteral<"add">;
326
+ table: z.ZodString;
327
+ row: z.ZodRecord<z.ZodString, z.ZodUnknown>;
328
+ }, z.core.$strip>, z.ZodObject<{
329
+ op: z.ZodLiteral<"update">;
330
+ table: z.ZodString;
331
+ id: z.ZodString;
332
+ before: z.ZodRecord<z.ZodString, z.ZodUnknown>;
333
+ after: z.ZodRecord<z.ZodString, z.ZodUnknown>;
334
+ }, z.core.$strip>, z.ZodObject<{
335
+ op: z.ZodLiteral<"remove">;
336
+ table: z.ZodString;
337
+ id: z.ZodString;
338
+ before: z.ZodRecord<z.ZodString, z.ZodUnknown>;
339
+ }, z.core.$strip>], "op">;
340
+ export type DataChange = z.output<typeof dataChange>;
341
+ export declare const changeOutcome: z.ZodObject<{
342
+ status: z.ZodEnum<{
343
+ failed: "failed";
344
+ applied: "applied";
345
+ skipped: "skipped";
346
+ }>;
347
+ reason: z.ZodNullable<z.ZodString>;
348
+ row: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
349
+ }, z.core.$strip>;
350
+ export type ChangeOutcome = z.output<typeof changeOutcome>;
351
+ export declare const changeReply: z.ZodObject<{
352
+ outcomes: z.ZodArray<z.ZodObject<{
353
+ status: z.ZodEnum<{
354
+ failed: "failed";
355
+ applied: "applied";
356
+ skipped: "skipped";
357
+ }>;
358
+ reason: z.ZodNullable<z.ZodString>;
359
+ row: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
360
+ }, z.core.$strip>>;
361
+ }, z.core.$strip>;
362
+ export declare const changeSetStatus: z.ZodEnum<{
363
+ declined: "declined";
364
+ waiting: "waiting";
365
+ applied: "applied";
366
+ expired: "expired";
367
+ undone: "undone";
368
+ }>;
369
+ export declare const changeSet: z.ZodObject<{
370
+ id: z.ZodString;
371
+ status: z.ZodEnum<{
372
+ declined: "declined";
373
+ waiting: "waiting";
374
+ applied: "applied";
375
+ expired: "expired";
376
+ undone: "undone";
377
+ }>;
378
+ title: z.ZodString;
379
+ asked: z.ZodString;
380
+ device: z.ZodString;
381
+ createdAt: z.ZodString;
382
+ decidedAt: z.ZodNullable<z.ZodString>;
383
+ undoneAt: z.ZodNullable<z.ZodString>;
384
+ changes: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
385
+ op: z.ZodLiteral<"add">;
386
+ table: z.ZodString;
387
+ row: z.ZodRecord<z.ZodString, z.ZodUnknown>;
388
+ }, z.core.$strip>, z.ZodObject<{
389
+ op: z.ZodLiteral<"update">;
390
+ table: z.ZodString;
391
+ id: z.ZodString;
392
+ before: z.ZodRecord<z.ZodString, z.ZodUnknown>;
393
+ after: z.ZodRecord<z.ZodString, z.ZodUnknown>;
394
+ }, z.core.$strip>, z.ZodObject<{
395
+ op: z.ZodLiteral<"remove">;
396
+ table: z.ZodString;
397
+ id: z.ZodString;
398
+ before: z.ZodRecord<z.ZodString, z.ZodUnknown>;
399
+ }, z.core.$strip>], "op">>;
400
+ outcomes: z.ZodNullable<z.ZodArray<z.ZodObject<{
401
+ status: z.ZodEnum<{
402
+ failed: "failed";
403
+ applied: "applied";
404
+ skipped: "skipped";
405
+ }>;
406
+ reason: z.ZodNullable<z.ZodString>;
407
+ row: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
408
+ }, z.core.$strip>>>;
409
+ }, z.core.$strip>;
410
+ export type ChangeSet = z.output<typeof changeSet>;
323
411
  export declare const publishServices: {
324
412
  readonly apps: {
325
413
  readonly list: NoInfer<import("./services.ts").ServiceCall<"/apps", {}, z.ZodArray<z.ZodObject<{
@@ -647,6 +735,272 @@ export declare const publishServices: {
647
735
  next: z.ZodNullable<z.ZodString>;
648
736
  }, z.core.$strip>>>;
649
737
  };
738
+ readonly changes: {
739
+ readonly list: NoInfer<import("./services.ts").ServiceCall<"/apps/:name/changes", {}, z.ZodArray<z.ZodObject<{
740
+ id: z.ZodString;
741
+ status: z.ZodEnum<{
742
+ declined: "declined";
743
+ waiting: "waiting";
744
+ applied: "applied";
745
+ expired: "expired";
746
+ undone: "undone";
747
+ }>;
748
+ title: z.ZodString;
749
+ asked: z.ZodString;
750
+ device: z.ZodString;
751
+ createdAt: z.ZodString;
752
+ decidedAt: z.ZodNullable<z.ZodString>;
753
+ undoneAt: z.ZodNullable<z.ZodString>;
754
+ changes: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
755
+ op: z.ZodLiteral<"add">;
756
+ table: z.ZodString;
757
+ row: z.ZodRecord<z.ZodString, z.ZodUnknown>;
758
+ }, z.core.$strip>, z.ZodObject<{
759
+ op: z.ZodLiteral<"update">;
760
+ table: z.ZodString;
761
+ id: z.ZodString;
762
+ before: z.ZodRecord<z.ZodString, z.ZodUnknown>;
763
+ after: z.ZodRecord<z.ZodString, z.ZodUnknown>;
764
+ }, z.core.$strip>, z.ZodObject<{
765
+ op: z.ZodLiteral<"remove">;
766
+ table: z.ZodString;
767
+ id: z.ZodString;
768
+ before: z.ZodRecord<z.ZodString, z.ZodUnknown>;
769
+ }, z.core.$strip>], "op">>;
770
+ outcomes: z.ZodNullable<z.ZodArray<z.ZodObject<{
771
+ status: z.ZodEnum<{
772
+ failed: "failed";
773
+ applied: "applied";
774
+ skipped: "skipped";
775
+ }>;
776
+ reason: z.ZodNullable<z.ZodString>;
777
+ row: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
778
+ }, z.core.$strip>>>;
779
+ }, z.core.$strip>>>>;
780
+ readonly read: NoInfer<import("./services.ts").ServiceCall<"/apps/:name/changes/:id", {}, z.ZodObject<{
781
+ id: z.ZodString;
782
+ status: z.ZodEnum<{
783
+ declined: "declined";
784
+ waiting: "waiting";
785
+ applied: "applied";
786
+ expired: "expired";
787
+ undone: "undone";
788
+ }>;
789
+ title: z.ZodString;
790
+ asked: z.ZodString;
791
+ device: z.ZodString;
792
+ createdAt: z.ZodString;
793
+ decidedAt: z.ZodNullable<z.ZodString>;
794
+ undoneAt: z.ZodNullable<z.ZodString>;
795
+ changes: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
796
+ op: z.ZodLiteral<"add">;
797
+ table: z.ZodString;
798
+ row: z.ZodRecord<z.ZodString, z.ZodUnknown>;
799
+ }, z.core.$strip>, z.ZodObject<{
800
+ op: z.ZodLiteral<"update">;
801
+ table: z.ZodString;
802
+ id: z.ZodString;
803
+ before: z.ZodRecord<z.ZodString, z.ZodUnknown>;
804
+ after: z.ZodRecord<z.ZodString, z.ZodUnknown>;
805
+ }, z.core.$strip>, z.ZodObject<{
806
+ op: z.ZodLiteral<"remove">;
807
+ table: z.ZodString;
808
+ id: z.ZodString;
809
+ before: z.ZodRecord<z.ZodString, z.ZodUnknown>;
810
+ }, z.core.$strip>], "op">>;
811
+ outcomes: z.ZodNullable<z.ZodArray<z.ZodObject<{
812
+ status: z.ZodEnum<{
813
+ failed: "failed";
814
+ applied: "applied";
815
+ skipped: "skipped";
816
+ }>;
817
+ reason: z.ZodNullable<z.ZodString>;
818
+ row: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
819
+ }, z.core.$strip>>>;
820
+ }, z.core.$strip>>>;
821
+ readonly create: NoInfer<import("./services.ts").ServiceCall<"/apps/:name/changes", {
822
+ asked: z.ZodString;
823
+ changes: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
824
+ op: z.ZodLiteral<"add">;
825
+ table: z.ZodString;
826
+ row: z.ZodRecord<z.ZodString, z.ZodUnknown>;
827
+ }, z.core.$strip>, z.ZodObject<{
828
+ op: z.ZodLiteral<"update">;
829
+ table: z.ZodString;
830
+ id: z.ZodString;
831
+ before: z.ZodRecord<z.ZodString, z.ZodUnknown>;
832
+ after: z.ZodRecord<z.ZodString, z.ZodUnknown>;
833
+ }, z.core.$strip>, z.ZodObject<{
834
+ op: z.ZodLiteral<"remove">;
835
+ table: z.ZodString;
836
+ id: z.ZodString;
837
+ before: z.ZodRecord<z.ZodString, z.ZodUnknown>;
838
+ }, z.core.$strip>], "op">>;
839
+ }, z.ZodObject<{
840
+ id: z.ZodString;
841
+ status: z.ZodEnum<{
842
+ declined: "declined";
843
+ waiting: "waiting";
844
+ applied: "applied";
845
+ expired: "expired";
846
+ undone: "undone";
847
+ }>;
848
+ title: z.ZodString;
849
+ asked: z.ZodString;
850
+ device: z.ZodString;
851
+ createdAt: z.ZodString;
852
+ decidedAt: z.ZodNullable<z.ZodString>;
853
+ undoneAt: z.ZodNullable<z.ZodString>;
854
+ changes: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
855
+ op: z.ZodLiteral<"add">;
856
+ table: z.ZodString;
857
+ row: z.ZodRecord<z.ZodString, z.ZodUnknown>;
858
+ }, z.core.$strip>, z.ZodObject<{
859
+ op: z.ZodLiteral<"update">;
860
+ table: z.ZodString;
861
+ id: z.ZodString;
862
+ before: z.ZodRecord<z.ZodString, z.ZodUnknown>;
863
+ after: z.ZodRecord<z.ZodString, z.ZodUnknown>;
864
+ }, z.core.$strip>, z.ZodObject<{
865
+ op: z.ZodLiteral<"remove">;
866
+ table: z.ZodString;
867
+ id: z.ZodString;
868
+ before: z.ZodRecord<z.ZodString, z.ZodUnknown>;
869
+ }, z.core.$strip>], "op">>;
870
+ outcomes: z.ZodNullable<z.ZodArray<z.ZodObject<{
871
+ status: z.ZodEnum<{
872
+ failed: "failed";
873
+ applied: "applied";
874
+ skipped: "skipped";
875
+ }>;
876
+ reason: z.ZodNullable<z.ZodString>;
877
+ row: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
878
+ }, z.core.$strip>>>;
879
+ }, z.core.$strip>>>;
880
+ readonly approve: NoInfer<import("./services.ts").ServiceCall<"/apps/:name/changes/:id/approve", {}, z.ZodObject<{
881
+ id: z.ZodString;
882
+ status: z.ZodEnum<{
883
+ declined: "declined";
884
+ waiting: "waiting";
885
+ applied: "applied";
886
+ expired: "expired";
887
+ undone: "undone";
888
+ }>;
889
+ title: z.ZodString;
890
+ asked: z.ZodString;
891
+ device: z.ZodString;
892
+ createdAt: z.ZodString;
893
+ decidedAt: z.ZodNullable<z.ZodString>;
894
+ undoneAt: z.ZodNullable<z.ZodString>;
895
+ changes: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
896
+ op: z.ZodLiteral<"add">;
897
+ table: z.ZodString;
898
+ row: z.ZodRecord<z.ZodString, z.ZodUnknown>;
899
+ }, z.core.$strip>, z.ZodObject<{
900
+ op: z.ZodLiteral<"update">;
901
+ table: z.ZodString;
902
+ id: z.ZodString;
903
+ before: z.ZodRecord<z.ZodString, z.ZodUnknown>;
904
+ after: z.ZodRecord<z.ZodString, z.ZodUnknown>;
905
+ }, z.core.$strip>, z.ZodObject<{
906
+ op: z.ZodLiteral<"remove">;
907
+ table: z.ZodString;
908
+ id: z.ZodString;
909
+ before: z.ZodRecord<z.ZodString, z.ZodUnknown>;
910
+ }, z.core.$strip>], "op">>;
911
+ outcomes: z.ZodNullable<z.ZodArray<z.ZodObject<{
912
+ status: z.ZodEnum<{
913
+ failed: "failed";
914
+ applied: "applied";
915
+ skipped: "skipped";
916
+ }>;
917
+ reason: z.ZodNullable<z.ZodString>;
918
+ row: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
919
+ }, z.core.$strip>>>;
920
+ }, z.core.$strip>>>;
921
+ readonly decline: NoInfer<import("./services.ts").ServiceCall<"/apps/:name/changes/:id/decline", {}, z.ZodObject<{
922
+ id: z.ZodString;
923
+ status: z.ZodEnum<{
924
+ declined: "declined";
925
+ waiting: "waiting";
926
+ applied: "applied";
927
+ expired: "expired";
928
+ undone: "undone";
929
+ }>;
930
+ title: z.ZodString;
931
+ asked: z.ZodString;
932
+ device: z.ZodString;
933
+ createdAt: z.ZodString;
934
+ decidedAt: z.ZodNullable<z.ZodString>;
935
+ undoneAt: z.ZodNullable<z.ZodString>;
936
+ changes: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
937
+ op: z.ZodLiteral<"add">;
938
+ table: z.ZodString;
939
+ row: z.ZodRecord<z.ZodString, z.ZodUnknown>;
940
+ }, z.core.$strip>, z.ZodObject<{
941
+ op: z.ZodLiteral<"update">;
942
+ table: z.ZodString;
943
+ id: z.ZodString;
944
+ before: z.ZodRecord<z.ZodString, z.ZodUnknown>;
945
+ after: z.ZodRecord<z.ZodString, z.ZodUnknown>;
946
+ }, z.core.$strip>, z.ZodObject<{
947
+ op: z.ZodLiteral<"remove">;
948
+ table: z.ZodString;
949
+ id: z.ZodString;
950
+ before: z.ZodRecord<z.ZodString, z.ZodUnknown>;
951
+ }, z.core.$strip>], "op">>;
952
+ outcomes: z.ZodNullable<z.ZodArray<z.ZodObject<{
953
+ status: z.ZodEnum<{
954
+ failed: "failed";
955
+ applied: "applied";
956
+ skipped: "skipped";
957
+ }>;
958
+ reason: z.ZodNullable<z.ZodString>;
959
+ row: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
960
+ }, z.core.$strip>>>;
961
+ }, z.core.$strip>>>;
962
+ readonly undo: NoInfer<import("./services.ts").ServiceCall<"/apps/:name/changes/:id/undo", {}, z.ZodObject<{
963
+ id: z.ZodString;
964
+ status: z.ZodEnum<{
965
+ declined: "declined";
966
+ waiting: "waiting";
967
+ applied: "applied";
968
+ expired: "expired";
969
+ undone: "undone";
970
+ }>;
971
+ title: z.ZodString;
972
+ asked: z.ZodString;
973
+ device: z.ZodString;
974
+ createdAt: z.ZodString;
975
+ decidedAt: z.ZodNullable<z.ZodString>;
976
+ undoneAt: z.ZodNullable<z.ZodString>;
977
+ changes: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
978
+ op: z.ZodLiteral<"add">;
979
+ table: z.ZodString;
980
+ row: z.ZodRecord<z.ZodString, z.ZodUnknown>;
981
+ }, z.core.$strip>, z.ZodObject<{
982
+ op: z.ZodLiteral<"update">;
983
+ table: z.ZodString;
984
+ id: z.ZodString;
985
+ before: z.ZodRecord<z.ZodString, z.ZodUnknown>;
986
+ after: z.ZodRecord<z.ZodString, z.ZodUnknown>;
987
+ }, z.core.$strip>, z.ZodObject<{
988
+ op: z.ZodLiteral<"remove">;
989
+ table: z.ZodString;
990
+ id: z.ZodString;
991
+ before: z.ZodRecord<z.ZodString, z.ZodUnknown>;
992
+ }, z.core.$strip>], "op">>;
993
+ outcomes: z.ZodNullable<z.ZodArray<z.ZodObject<{
994
+ status: z.ZodEnum<{
995
+ failed: "failed";
996
+ applied: "applied";
997
+ skipped: "skipped";
998
+ }>;
999
+ reason: z.ZodNullable<z.ZodString>;
1000
+ row: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1001
+ }, z.core.$strip>>>;
1002
+ }, z.core.$strip>>>;
1003
+ };
650
1004
  readonly logins: {
651
1005
  readonly read: NoInfer<import("./services.ts").ServiceCall<"/cli/logins/:approval", {}, z.ZodObject<{
652
1006
  code: z.ZodString;
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { appTitle } from "./appTitle.js";
3
3
  export { appTitle };
4
- export { ASSERTION_SECONDS, MEDIA_WRITE, NEXT_PAGE_HEADER, TABLES_READ, assertionKey, signAssertion } from "./patternAccess.js";
4
+ export { ASSERTION_SECONDS, MEDIA_WRITE, NEXT_PAGE_HEADER, TABLES_READ, TABLES_WRITE, assertionKey, signAssertion } from "./patternAccess.js";
5
5
  import { call, defineServices } from "./services.js";
6
6
  export const DASHBOARD_URL = 'https://onpattern.app';
7
7
  export const APPS_DOMAIN = 'onpattern.app';
@@ -256,6 +256,50 @@ export const databaseRows = z.object({
256
256
  rows: z.array(databaseRow),
257
257
  next: z.string().nullable()
258
258
  });
259
+ export const CHANGE_SET_ROWS = 300;
260
+ const tableName = z.string().regex(/^[a-zA-Z][a-zA-Z0-9]*$/);
261
+ const rowId = z.string().min(1).max(200);
262
+ export const dataChange = z.discriminatedUnion('op', [
263
+ z.object({ op: z.literal('add'), table: tableName, row: databaseRow }),
264
+ z.object({
265
+ op: z.literal('update'),
266
+ table: tableName,
267
+ id: rowId,
268
+ before: databaseRow,
269
+ after: databaseRow
270
+ }),
271
+ z.object({
272
+ op: z.literal('remove'),
273
+ table: tableName,
274
+ id: rowId,
275
+ before: databaseRow
276
+ })
277
+ ]);
278
+ export const changeOutcome = z.object({
279
+ status: z.enum(['applied', 'skipped', 'failed']),
280
+ reason: z.string().nullable(),
281
+ row: databaseRow.nullable()
282
+ });
283
+ export const changeReply = z.object({ outcomes: z.array(changeOutcome) });
284
+ export const changeSetStatus = z.enum([
285
+ 'waiting',
286
+ 'applied',
287
+ 'declined',
288
+ 'expired',
289
+ 'undone'
290
+ ]);
291
+ export const changeSet = z.object({
292
+ id: z.string(),
293
+ status: changeSetStatus,
294
+ title: z.string(),
295
+ asked: z.string(),
296
+ device: z.string(),
297
+ createdAt: z.string(),
298
+ decidedAt: z.string().nullable(),
299
+ undoneAt: z.string().nullable(),
300
+ changes: z.array(dataChange),
301
+ outcomes: z.array(changeOutcome).nullable()
302
+ });
259
303
  export const publishServices = defineServices({
260
304
  apps: {
261
305
  list: call({ get: '/apps', reply: z.array(appSummary) }),
@@ -332,6 +376,27 @@ export const publishServices = defineServices({
332
376
  reply: databaseRows
333
377
  })
334
378
  },
379
+ changes: {
380
+ list: call({ get: '/apps/:name/changes', reply: z.array(changeSet) }),
381
+ read: call({ get: '/apps/:name/changes/:id', reply: changeSet }),
382
+ create: call({
383
+ post: '/apps/:name/changes',
384
+ input: {
385
+ asked: z.string().trim().min(1).max(1000),
386
+ changes: z.array(dataChange).min(1).max(CHANGE_SET_ROWS)
387
+ },
388
+ reply: changeSet
389
+ }),
390
+ approve: call({
391
+ post: '/apps/:name/changes/:id/approve',
392
+ reply: changeSet
393
+ }),
394
+ decline: call({
395
+ post: '/apps/:name/changes/:id/decline',
396
+ reply: changeSet
397
+ }),
398
+ undo: call({ post: '/apps/:name/changes/:id/undo', reply: changeSet })
399
+ },
335
400
  logins: {
336
401
  read: call({ get: '/cli/logins/:approval', reply: loginToApprove }),
337
402
  approve: call({
@@ -10,6 +10,7 @@ export type AuthService = {
10
10
  export type OwnerNote = {
11
11
  subject: string;
12
12
  text: string;
13
+ replyTo?: string;
13
14
  };
14
15
  export declare function authServiceFor(env: ServerBindings): AuthService | undefined;
15
16
  export declare function notifyOwner(env: ServerBindings, note: OwnerNote): Promise<boolean>;
@@ -3,10 +3,11 @@ import type { BookingRequest } from '../bookings.ts';
3
3
  import type { ServerBlock, ServerEnv } from './env.ts';
4
4
  export type BookingsOptions = {
5
5
  timezone: string;
6
+ managePath?: string;
6
7
  };
7
8
  export type BookingEmail = {
8
9
  kind: 'requested' | 'accepted' | 'declined';
9
10
  request: BookingRequest;
10
11
  };
11
12
  export declare function sendBookingEmail(c: Context<ServerEnv>, email: BookingEmail): Promise<void>;
12
- export declare function bookings({ timezone }: BookingsOptions): ServerBlock;
13
+ export declare function bookings({ timezone, managePath }: BookingsOptions): ServerBlock;
@@ -10,9 +10,22 @@ export async function sendBookingEmail(c, email) {
10
10
  const to = email.kind === 'requested' ? 'the owner' : email.request.email;
11
11
  console.log(`Booking email (${email.kind}) to ${to} from ${new URL(c.req.url).host}: ${email.request.name}, ${email.request.date} ${email.request.window}`);
12
12
  }
13
- function ownerNoteOf(request) {
13
+ function clockOf(time) {
14
+ const [hours = 0, minutes = 0] = time.split(':').map(Number);
15
+ const hour = hours % 12 === 0 ? 12 : hours % 12;
16
+ const suffix = hours < 12 ? 'am' : 'pm';
17
+ return minutes === 0
18
+ ? `${hour}${suffix}`
19
+ : `${hour}:${String(minutes).padStart(2, '0')}${suffix}`;
20
+ }
21
+ function whenOf(request, label) {
22
+ const day = new Date(`${request.date}T12:00:00Z`).toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', timeZone: 'UTC' });
23
+ const [start = '', end = ''] = request.window.split('-');
24
+ return `${day}, ${label} (${clockOf(start)}–${clockOf(end)})`;
25
+ }
26
+ function ownerNoteOf(request, label, manage) {
14
27
  const lines = [
15
- `${request.name} asked to book ${request.offering} on ${request.date}, ${request.window}.`,
28
+ `${request.name} asked to book ${request.offering} on ${whenOf(request, label)}.`,
16
29
  '',
17
30
  `Email: ${request.email}`,
18
31
  ...(request.phone === '' ? [] : [`Phone: ${request.phone}`]),
@@ -20,12 +33,14 @@ function ownerNoteOf(request) {
20
33
  ...(request.notes === '' ? [] : ['', request.notes]),
21
34
  '',
22
35
  request.status === 'accepted'
23
- ? 'It is booked: your booking rules accept requests on their own.'
24
- : 'Accept or decline it on your site.'
36
+ ? `It is booked: your booking rules accept requests on their own. See it at ${manage}`
37
+ : `Accept or decline it at ${manage}`,
38
+ `Reply to this email to write to ${request.name}.`
25
39
  ];
26
40
  return {
27
41
  subject: `New booking request from ${request.name}`,
28
- text: lines.join('\n')
42
+ text: lines.join('\n'),
43
+ replyTo: request.email
29
44
  };
30
45
  }
31
46
  function requireAdmin(c) {
@@ -84,7 +99,7 @@ async function decide(c, id, status) {
84
99
  function monthRange(month) {
85
100
  return { first: `${month}-01`, last: `${month}-31` };
86
101
  }
87
- export function bookings({ timezone }) {
102
+ export function bookings({ timezone, managePath = '/' }) {
88
103
  if (!isValidTimezone(timezone)) {
89
104
  throw new Error(`bookings({ timezone: '${timezone}' }) is not a time zone: use an IANA name such as 'America/Toronto'`);
90
105
  }
@@ -116,7 +131,7 @@ export function bookings({ timezone }) {
116
131
  const saved = await readRequest(c.env.DB, id);
117
132
  if (saved === undefined)
118
133
  throw new Error('The request was not saved');
119
- await notifyOwner(c.env, ownerNoteOf(saved));
134
+ await notifyOwner(c.env, ownerNoteOf(saved, slot.label, new URL(managePath, c.req.url).href));
120
135
  await sendBookingEmail(c, {
121
136
  kind: status === 'accepted' ? 'accepted' : 'requested',
122
137
  request: saved
@@ -0,0 +1,18 @@
1
+ import type { Context } from 'hono';
2
+ import type { Tables } from '../table.ts';
3
+ import type { ServerEnv } from './env.ts';
4
+ export declare function applyChanges<Group extends Tables>(c: Context<ServerEnv>, tables: Group): Promise<(Response & import("hono").TypedResponse<{
5
+ error: string;
6
+ }, 401, "json">) | (Response & import("hono").TypedResponse<{
7
+ error: string;
8
+ }, 403, "json">) | (Response & import("hono").TypedResponse<{
9
+ error: string;
10
+ }, 400, "json">) | (Response & import("hono").TypedResponse<{
11
+ outcomes: {
12
+ status: "failed" | "applied" | "skipped";
13
+ reason: string | null;
14
+ row: {
15
+ [x: string]: import("hono/utils/types").JSONValue;
16
+ } | null;
17
+ }[];
18
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
@@ -0,0 +1,113 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { drizzle } from 'drizzle-orm/d1';
3
+ import { z } from 'zod';
4
+ import { dataChange } from "../publishWire.js";
5
+ import { column, readRow, sqlTable } from "./tables.js";
6
+ const changeRequest = z.object({ changes: z.array(dataChange).max(1000) });
7
+ const META_KEYS = new Set(['id', 'createdAt', 'ownerId']);
8
+ function sameValue(left, right) {
9
+ return JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
10
+ }
11
+ function changedSince(current, before, keys) {
12
+ return keys.some((key) => !sameValue(current[key], before[key]));
13
+ }
14
+ function withoutMeta(row) {
15
+ return Object.fromEntries(Object.entries(row).filter(([key]) => !META_KEYS.has(key)));
16
+ }
17
+ function failed(reason) {
18
+ return { status: 'failed', reason, row: null };
19
+ }
20
+ function skipped(reason) {
21
+ return { status: 'skipped', reason, row: null };
22
+ }
23
+ function problemOf(failure) {
24
+ for (let error = failure; error instanceof Error; error = error.cause) {
25
+ if (error.message.includes('UNIQUE constraint failed')) {
26
+ return 'Another row already has that value';
27
+ }
28
+ }
29
+ return 'The site couldn’t save this row';
30
+ }
31
+ async function applyOne(c, table, change) {
32
+ const target = sqlTable(table);
33
+ const db = drizzle(c.env.DB);
34
+ if (change.op === 'add') {
35
+ if (table.owned)
36
+ return failed('Rows in this table belong to their makers');
37
+ const id = typeof change.row['id'] === 'string' && change.row['id'] !== ''
38
+ ? change.row['id']
39
+ : crypto.randomUUID();
40
+ const parsed = table.incoming.safeParse({ ...withoutMeta(change.row), id });
41
+ if (!parsed.success)
42
+ return failed(z.prettifyError(parsed.error));
43
+ const saved = await db
44
+ .insert(target)
45
+ .values(parsed.data)
46
+ .returning()
47
+ .get()
48
+ .catch((failure) => problemOf(failure));
49
+ if (typeof saved === 'string')
50
+ return failed(saved);
51
+ return {
52
+ status: 'applied',
53
+ reason: null,
54
+ row: (await readRow(c.env.DB, table, id)) ?? null
55
+ };
56
+ }
57
+ const current = await readRow(c.env.DB, table, change.id);
58
+ if (current === undefined)
59
+ return skipped('It was removed since');
60
+ if (change.op === 'remove') {
61
+ if (changedSince(current, change.before, Object.keys(change.before))) {
62
+ return skipped('It changed since');
63
+ }
64
+ await db.delete(target).where(eq(column(target, 'id'), change.id));
65
+ return { status: 'applied', reason: null, row: current };
66
+ }
67
+ const keys = Object.keys(withoutMeta(change.after));
68
+ if (changedSince(current, change.before, keys)) {
69
+ return skipped('It changed since');
70
+ }
71
+ const parsed = table.incoming.safeParse({
72
+ ...withoutMeta(current),
73
+ ...withoutMeta(change.after),
74
+ id: change.id
75
+ });
76
+ if (!parsed.success)
77
+ return failed(z.prettifyError(parsed.error));
78
+ const values = Object.fromEntries(keys.map((key) => [key, Reflect.get(parsed.data, key)]));
79
+ const saved = await db
80
+ .update(target)
81
+ .set(values)
82
+ .where(eq(column(target, 'id'), change.id))
83
+ .run()
84
+ .then(() => undefined)
85
+ .catch((failure) => problemOf(failure));
86
+ if (saved !== undefined)
87
+ return failed(saved);
88
+ return {
89
+ status: 'applied',
90
+ reason: null,
91
+ row: (await readRow(c.env.DB, table, change.id)) ?? null
92
+ };
93
+ }
94
+ export async function applyChanges(c, tables) {
95
+ const viewer = c.get('viewer');
96
+ if (viewer === null)
97
+ return c.json({ error: 'Signed out' }, 401);
98
+ if (!viewer.isAdmin)
99
+ return c.json({ error: 'forbidden' }, 403);
100
+ const body = changeRequest.safeParse(await c.req.json().catch(() => null));
101
+ if (!body.success) {
102
+ return c.json({ error: z.prettifyError(body.error) }, 400);
103
+ }
104
+ const known = new Map(Object.values(tables).map((table) => [table.name, table]));
105
+ const outcomes = [];
106
+ for (const change of body.data.changes) {
107
+ const table = known.get(change.table);
108
+ outcomes.push(table === undefined || table.access.read === 'server'
109
+ ? failed('No such table')
110
+ : await applyOne(c, table, change));
111
+ }
112
+ return c.json({ outcomes });
113
+ }