@aglyn/plugins-events-calendar 1.0.0-beta.243 → 1.0.0-beta.246

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/README.md CHANGED
@@ -35,6 +35,8 @@ This package is a first-party Aglyn plugin. It is loaded through Aglyn's plugin
35
35
  - `events/list`: published events for the Event List component, sorted by start, with a `mode=past` option. Drafts never leave the server.
36
36
  - `events/dispatch`: receives a site event whose client-side trigger fired on the page, so that the action's server steps run.
37
37
 
38
+ `registerEventsCalendarConsoleServerDeclarations()` (the `consoleServerDeclarations` surface) registers the server half of the events import and export, and the `event` draft writer with the `event` build operation an Aglyn AI build uses to draft dated events and place the Event List on a page. A drafted event is listed on the site only once a person publishes it.
39
+
38
40
  Routes are served by the host app's API dispatcher under `/api/`, for example `/api/events/list`. There is no `consoleApi` registrar.
39
41
 
40
42
  ### Entry points
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-events-calendar",
3
- "version": "1.0.0-beta.243",
3
+ "version": "1.0.0-beta.246",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,14 +25,14 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.243",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.243",
30
- "@aglyn/shared-ui-jsx": "1.0.0-beta.243",
31
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.243",
32
- "@aglyn/shared-util-timestamp": "1.0.0-beta.243",
33
- "@aglyn/tenant-data-admin": "1.0.0-beta.243",
34
- "@aglyn/tenant-feature-instance": "1.0.0-beta.243",
35
- "@aglyn/tenant-runtime": "1.0.0-beta.243",
28
+ "@aglyn/aglyn": "1.0.0-beta.246",
29
+ "@aglyn/shared-data-mdi": "1.0.0-beta.246",
30
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.246",
31
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.246",
32
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.246",
33
+ "@aglyn/tenant-data-admin": "1.0.0-beta.246",
34
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.246",
35
+ "@aglyn/tenant-runtime": "1.0.0-beta.246",
36
36
  "@swc/helpers": "0.5.23"
37
37
  },
38
38
  "peerDependencies": {
@@ -14,6 +14,13 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
+ import { type PluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts';
18
+ /**
19
+ * The `event` draft writer as boot registers it (AGL-3743): the check is pure
20
+ * and answers at once; the hooks that read or write load the Admin SDK half
21
+ * on first use.
22
+ */
23
+ export declare const lazyEventDraftWriter: PluginResourceDraftWriter;
17
24
  /**
18
25
  * The events-calendar plugin's CONSOLE-ONLY server declarations, named under
19
26
  * `consoleServerDeclarations` in `plugins.config.json` and run at the
@@ -24,5 +31,9 @@
24
31
  * aliases are plain data registered as they are; every hook that reads or
25
32
  * writes loads the resource on first use. Registering again replaces this
26
33
  * plugin's own.
34
+ *
35
+ * And the `event` draft writer with the build operation it executes
36
+ * (AGL-3743): what an AI build drafts when a request asks for events, under
37
+ * this plugin's rules. The console runs AI jobs (AGL-3026).
27
38
  */
28
39
  export declare function registerEventsCalendarConsoleServerDeclarations(): void;
@@ -16,7 +16,10 @@
16
16
  */ // The registry's own module, not the `@aglyn/aglyn/server` barrel: boot needs
17
17
  // one registry, not the whole server surface.
18
18
  import { registerPluginTransferResource } from "@aglyn/aglyn/plugin-manager/plugin-transfer-resources";
19
+ import { registerPluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
19
20
  import { BUNDLE_ID } from "./constants/bundle-common.js";
21
+ import { checkEventDraftContent, EVENT_DRAFT_RESOURCE } from "./model/event-draft-content.js";
22
+ import { registerEventAiCapability } from "./server/event-ai-capability.js";
20
23
  import { EVENTS_ALIASES, EVENTS_CATALOG, EVENTS_MATCH_KEYS, eventsValuesEqual } from "./transfer/events-transfer-catalog.js";
21
24
  import { EVENTS_TRANSFER_RESOURCE } from "./transfer/events-transfer-key.js";
22
25
  let loading = null;
@@ -31,6 +34,24 @@ let loading = null;
31
34
  });
32
35
  return loading;
33
36
  }
37
+ let writerLoading = null;
38
+ /** The `event` draft writer over the Admin SDK, loaded on its first read or write. */ function eventWriter() {
39
+ writerLoading != null ? writerLoading : writerLoading = import("./server/event-drafts.server.js").then(({ eventDraftWriter })=>eventDraftWriter).catch((error)=>{
40
+ writerLoading = null;
41
+ throw error;
42
+ });
43
+ return writerLoading;
44
+ }
45
+ /**
46
+ * The `event` draft writer as boot registers it (AGL-3743): the check is pure
47
+ * and answers at once; the hooks that read or write load the Admin SDK half
48
+ * on first use.
49
+ */ export const lazyEventDraftWriter = {
50
+ check: (content)=>checkEventDraftContent(content),
51
+ refusal: async (context)=>(await eventWriter()).refusal(context),
52
+ read: async (context)=>(await eventWriter()).read(context),
53
+ write: async (request)=>(await eventWriter()).write(request)
54
+ };
34
55
  /**
35
56
  * The events-calendar plugin's CONSOLE-ONLY server declarations, named under
36
57
  * `consoleServerDeclarations` in `plugins.config.json` and run at the
@@ -41,6 +62,10 @@ let loading = null;
41
62
  * aliases are plain data registered as they are; every hook that reads or
42
63
  * writes loads the resource on first use. Registering again replaces this
43
64
  * plugin's own.
65
+ *
66
+ * And the `event` draft writer with the build operation it executes
67
+ * (AGL-3743): what an AI build drafts when a request asks for events, under
68
+ * this plugin's rules. The console runs AI jobs (AGL-3026).
44
69
  */ export function registerEventsCalendarConsoleServerDeclarations() {
45
70
  registerPluginTransferResource(EVENTS_TRANSFER_RESOURCE, {
46
71
  fields: ()=>EVENTS_CATALOG,
@@ -57,6 +82,11 @@ let loading = null;
57
82
  }, {
58
83
  pluginId: BUNDLE_ID
59
84
  });
85
+ registerPluginResourceDraftWriter(EVENT_DRAFT_RESOURCE, lazyEventDraftWriter, {
86
+ pluginId: BUNDLE_ID
87
+ });
88
+ // …and the operation an AI build plans for it, which that writer executes.
89
+ registerEventAiCapability();
60
90
  }
61
91
 
62
92
  //# sourceMappingURL=declarations.console-server.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/events-calendar/src/lib/declarations.console-server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// The registry's own module, not the `@aglyn/aglyn/server` barrel: boot needs\n// one registry, not the whole server surface.\nimport { registerPluginTransferResource } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport type { EventsTransferResource } from './transfer/events-transfer'\nimport {\n EVENTS_ALIASES,\n EVENTS_CATALOG,\n EVENTS_MATCH_KEYS,\n eventsValuesEqual,\n} from './transfer/events-transfer-catalog'\nimport { EVENTS_TRANSFER_RESOURCE } from './transfer/events-transfer-key'\n\nlet loading: Promise<EventsTransferResource> | null = null\n\n/**\n * The resource over the Admin SDK, built on the first hook a transfer calls:\n * the transfer core, the Admin SDK and the plan table load then, not at boot.\n * A failed load is forgotten, so the next call tries again.\n */\nfunction eventsResource(): Promise<EventsTransferResource> {\n loading ??= import('./transfer/events-transfer.server')\n .then(({ adminEventsTransferResource }) => adminEventsTransferResource())\n .catch((error: unknown) => {\n loading = null\n throw error\n })\n return loading\n}\n\n/**\n * The events-calendar plugin's CONSOLE-ONLY server declarations, named under\n * `consoleServerDeclarations` in `plugins.config.json` and run at the\n * console's boot. The tenant runtime registers none of it.\n *\n * The server half of the `events` transfer resource\n * (`transfer/events-transfer.ts`): the catalog, the match keys and the\n * aliases are plain data registered as they are; every hook that reads or\n * writes loads the resource on first use. Registering again replaces this\n * plugin's own.\n */\nexport function registerEventsCalendarConsoleServerDeclarations(): void {\n registerPluginTransferResource(\n EVENTS_TRANSFER_RESOURCE,\n {\n fields: () => EVENTS_CATALOG,\n matchKeys: EVENTS_MATCH_KEYS,\n aliases: EVENTS_ALIASES,\n // A status in any case is the stored one, in the review as in the plan.\n valuesEqual: eventsValuesEqual,\n count: async (ctx, options) =>\n (await eventsResource()).count(ctx, options),\n readPage: async (ctx, cursor, fieldIds, options) =>\n (await eventsResource()).readPage(ctx, cursor, fieldIds, options),\n lookup: async (ctx, requests) =>\n (await eventsResource()).lookup(ctx, requests),\n plan: async (ctx, input) => (await eventsResource()).plan(ctx, input),\n apply: async (ctx, chunk, writer) =>\n (await eventsResource()).apply(ctx, chunk, writer),\n revert: async (ctx, snapshot, decisions) =>\n (await eventsResource()).revert(ctx, snapshot, decisions),\n },\n { pluginId: BUNDLE_ID },\n )\n}\n"],"names":["registerPluginTransferResource","BUNDLE_ID","EVENTS_ALIASES","EVENTS_CATALOG","EVENTS_MATCH_KEYS","eventsValuesEqual","EVENTS_TRANSFER_RESOURCE","loading","eventsResource","then","adminEventsTransferResource","catch","error","registerEventsCalendarConsoleServerDeclarations","fields","matchKeys","aliases","valuesEqual","count","ctx","options","readPage","cursor","fieldIds","lookup","requests","plan","input","apply","chunk","writer","revert","snapshot","decisions","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,8EAA8E;AAC9E,8CAA8C;AAC9C,SAASA,8BAA8B,QAAQ,wDAAuD;AACtG,SAASC,SAAS,QAAQ,+BAA2B;AAErD,SACEC,cAAc,EACdC,cAAc,EACdC,iBAAiB,EACjBC,iBAAiB,QACZ,wCAAoC;AAC3C,SAASC,wBAAwB,QAAQ,oCAAgC;AAEzE,IAAIC,UAAkD;AAEtD;;;;CAIC,GACD,SAASC;IACPD,kBAAAA,UAAAA,UAAY,MAAM,CAAC,wCAChBE,IAAI,CAAC,CAAC,EAAEC,2BAA2B,EAAE,GAAKA,+BAC1CC,KAAK,CAAC,CAACC;QACNL,UAAU;QACV,MAAMK;IACR;IACF,OAAOL;AACT;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASM;IACdb,+BACEM,0BACA;QACEQ,QAAQ,IAAMX;QACdY,WAAWX;QACXY,SAASd;QACT,wEAAwE;QACxEe,aAAaZ;QACba,OAAO,OAAOC,KAAKC,UACjB,AAAC,CAAA,MAAMZ,gBAAe,EAAGU,KAAK,CAACC,KAAKC;QACtCC,UAAU,OAAOF,KAAKG,QAAQC,UAAUH,UACtC,AAAC,CAAA,MAAMZ,gBAAe,EAAGa,QAAQ,CAACF,KAAKG,QAAQC,UAAUH;QAC3DI,QAAQ,OAAOL,KAAKM,WAClB,AAAC,CAAA,MAAMjB,gBAAe,EAAGgB,MAAM,CAACL,KAAKM;QACvCC,MAAM,OAAOP,KAAKQ,QAAU,AAAC,CAAA,MAAMnB,gBAAe,EAAGkB,IAAI,CAACP,KAAKQ;QAC/DC,OAAO,OAAOT,KAAKU,OAAOC,SACxB,AAAC,CAAA,MAAMtB,gBAAe,EAAGoB,KAAK,CAACT,KAAKU,OAAOC;QAC7CC,QAAQ,OAAOZ,KAAKa,UAAUC,YAC5B,AAAC,CAAA,MAAMzB,gBAAe,EAAGuB,MAAM,CAACZ,KAAKa,UAAUC;IACnD,GACA;QAAEC,UAAUjC;IAAU;AAE1B"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/events-calendar/src/lib/declarations.console-server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// The registry's own module, not the `@aglyn/aglyn/server` barrel: boot needs\n// one registry, not the whole server surface.\nimport { registerPluginTransferResource } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport {\n registerPluginResourceDraftWriter,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { checkEventDraftContent, EVENT_DRAFT_RESOURCE } from './model/event-draft-content'\nimport { registerEventAiCapability } from './server/event-ai-capability'\nimport type { EventsTransferResource } from './transfer/events-transfer'\nimport {\n EVENTS_ALIASES,\n EVENTS_CATALOG,\n EVENTS_MATCH_KEYS,\n eventsValuesEqual,\n} from './transfer/events-transfer-catalog'\nimport { EVENTS_TRANSFER_RESOURCE } from './transfer/events-transfer-key'\n\nlet loading: Promise<EventsTransferResource> | null = null\n\n/**\n * The resource over the Admin SDK, built on the first hook a transfer calls:\n * the transfer core, the Admin SDK and the plan table load then, not at boot.\n * A failed load is forgotten, so the next call tries again.\n */\nfunction eventsResource(): Promise<EventsTransferResource> {\n loading ??= import('./transfer/events-transfer.server')\n .then(({ adminEventsTransferResource }) => adminEventsTransferResource())\n .catch((error: unknown) => {\n loading = null\n throw error\n })\n return loading\n}\n\nlet writerLoading: Promise<PluginResourceDraftWriter> | null = null\n\n/** The `event` draft writer over the Admin SDK, loaded on its first read or write. */\nfunction eventWriter(): Promise<PluginResourceDraftWriter> {\n writerLoading ??= import('./server/event-drafts.server')\n .then(({ eventDraftWriter }) => eventDraftWriter)\n .catch((error: unknown) => {\n writerLoading = null\n throw error\n })\n return writerLoading\n}\n\n/**\n * The `event` draft writer as boot registers it (AGL-3743): the check is pure\n * and answers at once; the hooks that read or write load the Admin SDK half\n * on first use.\n */\nexport const lazyEventDraftWriter: PluginResourceDraftWriter = {\n check: (content) => checkEventDraftContent(content),\n refusal: async (context) => (await eventWriter()).refusal(context),\n read: async (context) => (await eventWriter()).read(context),\n write: async (request) => (await eventWriter()).write(request),\n}\n\n/**\n * The events-calendar plugin's CONSOLE-ONLY server declarations, named under\n * `consoleServerDeclarations` in `plugins.config.json` and run at the\n * console's boot. The tenant runtime registers none of it.\n *\n * The server half of the `events` transfer resource\n * (`transfer/events-transfer.ts`): the catalog, the match keys and the\n * aliases are plain data registered as they are; every hook that reads or\n * writes loads the resource on first use. Registering again replaces this\n * plugin's own.\n *\n * And the `event` draft writer with the build operation it executes\n * (AGL-3743): what an AI build drafts when a request asks for events, under\n * this plugin's rules. The console runs AI jobs (AGL-3026).\n */\nexport function registerEventsCalendarConsoleServerDeclarations(): void {\n registerPluginTransferResource(\n EVENTS_TRANSFER_RESOURCE,\n {\n fields: () => EVENTS_CATALOG,\n matchKeys: EVENTS_MATCH_KEYS,\n aliases: EVENTS_ALIASES,\n // A status in any case is the stored one, in the review as in the plan.\n valuesEqual: eventsValuesEqual,\n count: async (ctx, options) =>\n (await eventsResource()).count(ctx, options),\n readPage: async (ctx, cursor, fieldIds, options) =>\n (await eventsResource()).readPage(ctx, cursor, fieldIds, options),\n lookup: async (ctx, requests) =>\n (await eventsResource()).lookup(ctx, requests),\n plan: async (ctx, input) => (await eventsResource()).plan(ctx, input),\n apply: async (ctx, chunk, writer) =>\n (await eventsResource()).apply(ctx, chunk, writer),\n revert: async (ctx, snapshot, decisions) =>\n (await eventsResource()).revert(ctx, snapshot, decisions),\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginResourceDraftWriter(EVENT_DRAFT_RESOURCE, lazyEventDraftWriter, { pluginId: BUNDLE_ID })\n // …and the operation an AI build plans for it, which that writer executes.\n registerEventAiCapability()\n}\n"],"names":["registerPluginTransferResource","registerPluginResourceDraftWriter","BUNDLE_ID","checkEventDraftContent","EVENT_DRAFT_RESOURCE","registerEventAiCapability","EVENTS_ALIASES","EVENTS_CATALOG","EVENTS_MATCH_KEYS","eventsValuesEqual","EVENTS_TRANSFER_RESOURCE","loading","eventsResource","then","adminEventsTransferResource","catch","error","writerLoading","eventWriter","eventDraftWriter","lazyEventDraftWriter","check","content","refusal","context","read","write","request","registerEventsCalendarConsoleServerDeclarations","fields","matchKeys","aliases","valuesEqual","count","ctx","options","readPage","cursor","fieldIds","lookup","requests","plan","input","apply","chunk","writer","revert","snapshot","decisions","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,8EAA8E;AAC9E,8CAA8C;AAC9C,SAASA,8BAA8B,QAAQ,wDAAuD;AACtG,SACEC,iCAAiC,QAE5B,qDAAoD;AAC3D,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,sBAAsB,EAAEC,oBAAoB,QAAQ,iCAA6B;AAC1F,SAASC,yBAAyB,QAAQ,kCAA8B;AAExE,SACEC,cAAc,EACdC,cAAc,EACdC,iBAAiB,EACjBC,iBAAiB,QACZ,wCAAoC;AAC3C,SAASC,wBAAwB,QAAQ,oCAAgC;AAEzE,IAAIC,UAAkD;AAEtD;;;;CAIC,GACD,SAASC;IACPD,kBAAAA,UAAAA,UAAY,MAAM,CAAC,wCAChBE,IAAI,CAAC,CAAC,EAAEC,2BAA2B,EAAE,GAAKA,+BAC1CC,KAAK,CAAC,CAACC;QACNL,UAAU;QACV,MAAMK;IACR;IACF,OAAOL;AACT;AAEA,IAAIM,gBAA2D;AAE/D,oFAAoF,GACpF,SAASC;IACPD,wBAAAA,gBAAAA,gBAAkB,MAAM,CAAC,mCACtBJ,IAAI,CAAC,CAAC,EAAEM,gBAAgB,EAAE,GAAKA,kBAC/BJ,KAAK,CAAC,CAACC;QACNC,gBAAgB;QAChB,MAAMD;IACR;IACF,OAAOC;AACT;AAEA;;;;CAIC,GACD,OAAO,MAAMG,uBAAkD;IAC7DC,OAAO,CAACC,UAAYnB,uBAAuBmB;IAC3CC,SAAS,OAAOC,UAAY,AAAC,CAAA,MAAMN,aAAY,EAAGK,OAAO,CAACC;IAC1DC,MAAM,OAAOD,UAAY,AAAC,CAAA,MAAMN,aAAY,EAAGO,IAAI,CAACD;IACpDE,OAAO,OAAOC,UAAY,AAAC,CAAA,MAAMT,aAAY,EAAGQ,KAAK,CAACC;AACxD,EAAC;AAED;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC;IACd5B,+BACEU,0BACA;QACEmB,QAAQ,IAAMtB;QACduB,WAAWtB;QACXuB,SAASzB;QACT,wEAAwE;QACxE0B,aAAavB;QACbwB,OAAO,OAAOC,KAAKC,UACjB,AAAC,CAAA,MAAMvB,gBAAe,EAAGqB,KAAK,CAACC,KAAKC;QACtCC,UAAU,OAAOF,KAAKG,QAAQC,UAAUH,UACtC,AAAC,CAAA,MAAMvB,gBAAe,EAAGwB,QAAQ,CAACF,KAAKG,QAAQC,UAAUH;QAC3DI,QAAQ,OAAOL,KAAKM,WAClB,AAAC,CAAA,MAAM5B,gBAAe,EAAG2B,MAAM,CAACL,KAAKM;QACvCC,MAAM,OAAOP,KAAKQ,QAAU,AAAC,CAAA,MAAM9B,gBAAe,EAAG6B,IAAI,CAACP,KAAKQ;QAC/DC,OAAO,OAAOT,KAAKU,OAAOC,SACxB,AAAC,CAAA,MAAMjC,gBAAe,EAAG+B,KAAK,CAACT,KAAKU,OAAOC;QAC7CC,QAAQ,OAAOZ,KAAKa,UAAUC,YAC5B,AAAC,CAAA,MAAMpC,gBAAe,EAAGkC,MAAM,CAACZ,KAAKa,UAAUC;IACnD,GACA;QAAEC,UAAU/C;IAAU;IAExBD,kCAAkCG,sBAAsBgB,sBAAsB;QAAE6B,UAAU/C;IAAU;IACpG,2EAA2E;IAC3EG;AACF"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { PluginDraftCheck } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts';
18
+ import { type EventWriteInput } from './event-write';
19
+ /**
20
+ * AN EVENT ANOTHER PLUGIN ASKS FOR, AS CONTENT (AGL-3743).
21
+ *
22
+ * What a caller of the `event` draft writer sends, and how it is read into
23
+ * the input `eventWrite` stores — the same rule the Events page's editor and
24
+ * the events import write through. Pure: no Firestore, no clock, so the
25
+ * writer's `check` answers before anything is spent.
26
+ *
27
+ * Times are WALL TIMES in a named zone, the way a person types them into the
28
+ * editor's date and time boxes: `startsAt` `"2026-11-14T19:00"` in
29
+ * `"America/Chicago"` is the instant the event starts there. The event
30
+ * stores only that instant (`startsAtMs`), exactly as an event the editor
31
+ * saved; the zone is how the caller's words are read, not a stored field.
32
+ *
33
+ * An event has no recurrence, tickets, RSVP or online-meeting field, so a
34
+ * caller cannot ask for one: the content's keys are the event's own.
35
+ */
36
+ /** The resource name the writer is registered under. */
37
+ export declare const EVENT_DRAFT_RESOURCE = "event";
38
+ /** A wall time as the content states it: `YYYY-MM-DDTHH:MM`. */
39
+ export declare const EVENT_DRAFT_WALL_TIME: RegExp;
40
+ /** The longest cover image address the content may carry. */
41
+ export declare const EVENT_DRAFT_COVER_MAX_LENGTH = 2048;
42
+ /** What a caller sends as `content`. */
43
+ export interface EventDraftContent {
44
+ title: string;
45
+ /** `YYYY-MM-DDTHH:MM`, read in `timezone`. */
46
+ startsAt: string;
47
+ /** `YYYY-MM-DDTHH:MM`, read in `timezone`, after the start; absent gives the editor's default length. */
48
+ endsAt?: string;
49
+ /** The IANA zone the times are in, e.g. `America/Chicago`. */
50
+ timezone: string;
51
+ location?: string;
52
+ organizer?: string;
53
+ description?: string;
54
+ /** An image address, as the editor's "Cover image URL" takes one. */
55
+ coverImage?: string;
56
+ coverImageAlt?: string;
57
+ }
58
+ export type EventDraftContentRead = {
59
+ ok: true;
60
+ value: EventWriteInput & {
61
+ endsAtMs: number | null;
62
+ };
63
+ timezone: string;
64
+ } | {
65
+ ok: false;
66
+ problems: string[];
67
+ };
68
+ /**
69
+ * The instant a wall time names in `timezone`, or `null` for one that does
70
+ * not name a real calendar minute (month 13, February 30th, 24:30).
71
+ */
72
+ export declare function eventDraftInstant(value: unknown, timezone: string): number | null;
73
+ /** The content as the writer stores it — always a draft — or every problem that stops it. */
74
+ export declare function readEventDraftContent(content: Readonly<Record<string, unknown>>): EventDraftContentRead;
75
+ /** `2026-11-14 19:00`: an instant as a wall clock in `timezone` reads it. */
76
+ export declare function eventDraftWallText(atMs: number, timezone: string): string;
77
+ /** Whether content is an event this plugin would store, with what it says about it. Pure. */
78
+ export declare function checkEventDraftContent(content: Readonly<Record<string, unknown>>): PluginDraftCheck;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { isValidTimeZone, zonedDayKey, zonedMinuteOfDay, zonedWallTimeToInstant } from "@aglyn/shared-util-timestamp/zoned-time";
17
+ import { EVENT_COVER_ALT_MAX_LENGTH, EVENT_DESCRIPTION_MAX_LENGTH, EVENT_LOCATION_MAX_LENGTH, EVENT_ORGANIZER_MAX_LENGTH, EVENT_TITLE_MAX_LENGTH } from "./event-write.js";
18
+ /**
19
+ * AN EVENT ANOTHER PLUGIN ASKS FOR, AS CONTENT (AGL-3743).
20
+ *
21
+ * What a caller of the `event` draft writer sends, and how it is read into
22
+ * the input `eventWrite` stores — the same rule the Events page's editor and
23
+ * the events import write through. Pure: no Firestore, no clock, so the
24
+ * writer's `check` answers before anything is spent.
25
+ *
26
+ * Times are WALL TIMES in a named zone, the way a person types them into the
27
+ * editor's date and time boxes: `startsAt` `"2026-11-14T19:00"` in
28
+ * `"America/Chicago"` is the instant the event starts there. The event
29
+ * stores only that instant (`startsAtMs`), exactly as an event the editor
30
+ * saved; the zone is how the caller's words are read, not a stored field.
31
+ *
32
+ * An event has no recurrence, tickets, RSVP or online-meeting field, so a
33
+ * caller cannot ask for one: the content's keys are the event's own.
34
+ */ /** The resource name the writer is registered under. */ export const EVENT_DRAFT_RESOURCE = 'event';
35
+ /** A wall time as the content states it: `YYYY-MM-DDTHH:MM`. */ export const EVENT_DRAFT_WALL_TIME = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/;
36
+ /** The longest cover image address the content may carry. */ export const EVENT_DRAFT_COVER_MAX_LENGTH = 2048;
37
+ /** Text on one line: controls and runs of space folded to one space. */ function line(value) {
38
+ return typeof value === 'string' ? value.replace(RegExp("[\\u0000-\\u001F\\u007F-\\u009F\\s]+", "gu"), ' ').trim() : '';
39
+ }
40
+ /**
41
+ * The instant a wall time names in `timezone`, or `null` for one that does
42
+ * not name a real calendar minute (month 13, February 30th, 24:30).
43
+ */ export function eventDraftInstant(value, timezone) {
44
+ const match = typeof value === 'string' ? value.trim().match(EVENT_DRAFT_WALL_TIME) : null;
45
+ if (!match || !isValidTimeZone(timezone)) return null;
46
+ const [year, month, day, hour, minute] = match.slice(1).map(Number);
47
+ if (month < 1 || month > 12 || day < 1 || hour > 23 || minute > 59) return null;
48
+ // A day past the month's end would roll into the next: refused instead.
49
+ if (day > new Date(Date.UTC(year, month, 0)).getUTCDate()) return null;
50
+ return zonedWallTimeToInstant({
51
+ year,
52
+ month,
53
+ day,
54
+ hour,
55
+ minute
56
+ }, timezone);
57
+ }
58
+ function optional(content, key, label, max, problems, multiline = false) {
59
+ const raw = content[key];
60
+ if (raw === undefined || raw === null) return '';
61
+ if (typeof raw !== 'string') {
62
+ problems.push(`The ${label} is text`);
63
+ return '';
64
+ }
65
+ const text = multiline ? raw.trim() : line(raw);
66
+ if (text.length > max) problems.push(`The ${label} is longer than ${max} characters`);
67
+ return text;
68
+ }
69
+ /** The content as the writer stores it — always a draft — or every problem that stops it. */ export function readEventDraftContent(content) {
70
+ const problems = [];
71
+ const title = line(content['title']);
72
+ if (!title) problems.push('The event needs a title');
73
+ else if (title.length > EVENT_TITLE_MAX_LENGTH) {
74
+ problems.push(`The title is longer than ${EVENT_TITLE_MAX_LENGTH} characters`);
75
+ }
76
+ const timezone = line(content['timezone']);
77
+ const zoneOk = isValidTimeZone(timezone);
78
+ if (!zoneOk) problems.push('The time zone is an IANA zone, such as America/Chicago');
79
+ const startsAtMs = zoneOk ? eventDraftInstant(content['startsAt'], timezone) : null;
80
+ if (zoneOk && startsAtMs === null) {
81
+ problems.push('The start is a date and time, written YYYY-MM-DDTHH:MM');
82
+ }
83
+ let endsAtMs = null;
84
+ const rawEnd = content['endsAt'];
85
+ if (rawEnd !== undefined && rawEnd !== null && rawEnd !== '' && zoneOk) {
86
+ endsAtMs = eventDraftInstant(rawEnd, timezone);
87
+ if (endsAtMs === null) problems.push('The end is a date and time, written YYYY-MM-DDTHH:MM');
88
+ else if (startsAtMs !== null && endsAtMs <= startsAtMs) problems.push('The end is after the start');
89
+ }
90
+ const location = optional(content, 'location', 'location', EVENT_LOCATION_MAX_LENGTH, problems);
91
+ const organizer = optional(content, 'organizer', 'organizer', EVENT_ORGANIZER_MAX_LENGTH, problems);
92
+ const description = optional(content, 'description', 'description', EVENT_DESCRIPTION_MAX_LENGTH, problems, true);
93
+ const coverImage = optional(content, 'coverImage', 'cover image address', EVENT_DRAFT_COVER_MAX_LENGTH, problems);
94
+ if (coverImage && !/^(https:\/\/|\/)\S+$/.test(coverImage)) {
95
+ problems.push('The cover image is an https address or a path on the site');
96
+ }
97
+ const coverImageAlt = optional(content, 'coverImageAlt', 'cover description', EVENT_COVER_ALT_MAX_LENGTH, problems);
98
+ if (problems.length) return {
99
+ ok: false,
100
+ problems: [
101
+ ...new Set(problems)
102
+ ]
103
+ };
104
+ return {
105
+ ok: true,
106
+ timezone,
107
+ value: {
108
+ title,
109
+ startsAtMs: startsAtMs,
110
+ endsAtMs,
111
+ location: location || null,
112
+ organizer: organizer || null,
113
+ description: description || null,
114
+ coverImage: coverImage || null,
115
+ coverImageAlt: coverImageAlt || null,
116
+ status: 'draft'
117
+ }
118
+ };
119
+ }
120
+ /** `2026-11-14 19:00`: an instant as a wall clock in `timezone` reads it. */ export function eventDraftWallText(atMs, timezone) {
121
+ const minute = zonedMinuteOfDay(atMs, timezone);
122
+ const pad = (value)=>String(value).padStart(2, '0');
123
+ return `${zonedDayKey(atMs, timezone)} ${pad(Math.floor(minute / 60))}:${pad(minute % 60)}`;
124
+ }
125
+ /** Whether content is an event this plugin would store, with what it says about it. Pure. */ export function checkEventDraftContent(content) {
126
+ const read = readEventDraftContent(content);
127
+ if (read.ok === false) return read;
128
+ return {
129
+ ok: true,
130
+ facts: {
131
+ status: 'draft',
132
+ startsAtMs: read.value.startsAtMs,
133
+ timezone: read.timezone,
134
+ startsAt: eventDraftWallText(read.value.startsAtMs, read.timezone)
135
+ }
136
+ };
137
+ }
138
+
139
+ //# sourceMappingURL=event-draft-content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/events-calendar/src/lib/model/event-draft-content.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginDraftCheck } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport {\n isValidTimeZone,\n zonedDayKey,\n zonedMinuteOfDay,\n zonedWallTimeToInstant,\n} from '@aglyn/shared-util-timestamp/zoned-time'\nimport {\n EVENT_COVER_ALT_MAX_LENGTH,\n EVENT_DESCRIPTION_MAX_LENGTH,\n EVENT_LOCATION_MAX_LENGTH,\n EVENT_ORGANIZER_MAX_LENGTH,\n EVENT_TITLE_MAX_LENGTH,\n type EventWriteInput,\n} from './event-write'\n\n/**\n * AN EVENT ANOTHER PLUGIN ASKS FOR, AS CONTENT (AGL-3743).\n *\n * What a caller of the `event` draft writer sends, and how it is read into\n * the input `eventWrite` stores — the same rule the Events page's editor and\n * the events import write through. Pure: no Firestore, no clock, so the\n * writer's `check` answers before anything is spent.\n *\n * Times are WALL TIMES in a named zone, the way a person types them into the\n * editor's date and time boxes: `startsAt` `\"2026-11-14T19:00\"` in\n * `\"America/Chicago\"` is the instant the event starts there. The event\n * stores only that instant (`startsAtMs`), exactly as an event the editor\n * saved; the zone is how the caller's words are read, not a stored field.\n *\n * An event has no recurrence, tickets, RSVP or online-meeting field, so a\n * caller cannot ask for one: the content's keys are the event's own.\n */\n\n/** The resource name the writer is registered under. */\nexport const EVENT_DRAFT_RESOURCE = 'event'\n\n/** A wall time as the content states it: `YYYY-MM-DDTHH:MM`. */\nexport const EVENT_DRAFT_WALL_TIME = /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2})$/\n\n/** The longest cover image address the content may carry. */\nexport const EVENT_DRAFT_COVER_MAX_LENGTH = 2048\n\n/** What a caller sends as `content`. */\nexport interface EventDraftContent {\n title: string\n /** `YYYY-MM-DDTHH:MM`, read in `timezone`. */\n startsAt: string\n /** `YYYY-MM-DDTHH:MM`, read in `timezone`, after the start; absent gives the editor's default length. */\n endsAt?: string\n /** The IANA zone the times are in, e.g. `America/Chicago`. */\n timezone: string\n location?: string\n organizer?: string\n description?: string\n /** An image address, as the editor's \"Cover image URL\" takes one. */\n coverImage?: string\n coverImageAlt?: string\n}\n\nexport type EventDraftContentRead =\n | { ok: true; value: EventWriteInput & { endsAtMs: number | null }; timezone: string }\n | { ok: false; problems: string[] }\n\n/** Text on one line: controls and runs of space folded to one space. */\nfunction line(value: unknown): string {\n return typeof value === 'string' ? value.replace(/[\\p{Cc}\\s]+/gu, ' ').trim() : ''\n}\n\n/**\n * The instant a wall time names in `timezone`, or `null` for one that does\n * not name a real calendar minute (month 13, February 30th, 24:30).\n */\nexport function eventDraftInstant(value: unknown, timezone: string): number | null {\n const match = typeof value === 'string' ? value.trim().match(EVENT_DRAFT_WALL_TIME) : null\n if (!match || !isValidTimeZone(timezone)) return null\n const [year, month, day, hour, minute] = match.slice(1).map(Number)\n if (month < 1 || month > 12 || day < 1 || hour > 23 || minute > 59) return null\n // A day past the month's end would roll into the next: refused instead.\n if (day > new Date(Date.UTC(year, month, 0)).getUTCDate()) return null\n return zonedWallTimeToInstant({ year, month, day, hour, minute }, timezone)\n}\n\nfunction optional(\n content: Readonly<Record<string, unknown>>,\n key: keyof EventDraftContent,\n label: string,\n max: number,\n problems: string[],\n multiline = false,\n): string {\n const raw = content[key]\n if (raw === undefined || raw === null) return ''\n if (typeof raw !== 'string') {\n problems.push(`The ${label} is text`)\n return ''\n }\n const text = multiline ? raw.trim() : line(raw)\n if (text.length > max) problems.push(`The ${label} is longer than ${max} characters`)\n return text\n}\n\n/** The content as the writer stores it — always a draft — or every problem that stops it. */\nexport function readEventDraftContent(content: Readonly<Record<string, unknown>>): EventDraftContentRead {\n const problems: string[] = []\n const title = line(content['title'])\n if (!title) problems.push('The event needs a title')\n else if (title.length > EVENT_TITLE_MAX_LENGTH) {\n problems.push(`The title is longer than ${EVENT_TITLE_MAX_LENGTH} characters`)\n }\n const timezone = line(content['timezone'])\n const zoneOk = isValidTimeZone(timezone)\n if (!zoneOk) problems.push('The time zone is an IANA zone, such as America/Chicago')\n const startsAtMs = zoneOk ? eventDraftInstant(content['startsAt'], timezone) : null\n if (zoneOk && startsAtMs === null) {\n problems.push('The start is a date and time, written YYYY-MM-DDTHH:MM')\n }\n let endsAtMs: number | null = null\n const rawEnd = content['endsAt']\n if (rawEnd !== undefined && rawEnd !== null && rawEnd !== '' && zoneOk) {\n endsAtMs = eventDraftInstant(rawEnd, timezone)\n if (endsAtMs === null) problems.push('The end is a date and time, written YYYY-MM-DDTHH:MM')\n else if (startsAtMs !== null && endsAtMs <= startsAtMs) problems.push('The end is after the start')\n }\n const location = optional(content, 'location', 'location', EVENT_LOCATION_MAX_LENGTH, problems)\n const organizer = optional(content, 'organizer', 'organizer', EVENT_ORGANIZER_MAX_LENGTH, problems)\n const description = optional(content, 'description', 'description', EVENT_DESCRIPTION_MAX_LENGTH, problems, true)\n const coverImage = optional(content, 'coverImage', 'cover image address', EVENT_DRAFT_COVER_MAX_LENGTH, problems)\n if (coverImage && !/^(https:\\/\\/|\\/)\\S+$/.test(coverImage)) {\n problems.push('The cover image is an https address or a path on the site')\n }\n const coverImageAlt = optional(content, 'coverImageAlt', 'cover description', EVENT_COVER_ALT_MAX_LENGTH, problems)\n if (problems.length) return { ok: false, problems: [...new Set(problems)] }\n return {\n ok: true,\n timezone,\n value: {\n title,\n startsAtMs: startsAtMs as number,\n endsAtMs,\n location: location || null,\n organizer: organizer || null,\n description: description || null,\n coverImage: coverImage || null,\n coverImageAlt: coverImageAlt || null,\n status: 'draft',\n },\n }\n}\n\n/** `2026-11-14 19:00`: an instant as a wall clock in `timezone` reads it. */\nexport function eventDraftWallText(atMs: number, timezone: string): string {\n const minute = zonedMinuteOfDay(atMs, timezone)\n const pad = (value: number) => String(value).padStart(2, '0')\n return `${zonedDayKey(atMs, timezone)} ${pad(Math.floor(minute / 60))}:${pad(minute % 60)}`\n}\n\n/** Whether content is an event this plugin would store, with what it says about it. Pure. */\nexport function checkEventDraftContent(content: Readonly<Record<string, unknown>>): PluginDraftCheck {\n const read = readEventDraftContent(content)\n if (read.ok === false) return read\n return {\n ok: true,\n facts: {\n status: 'draft',\n startsAtMs: read.value.startsAtMs,\n timezone: read.timezone,\n startsAt: eventDraftWallText(read.value.startsAtMs, read.timezone),\n },\n }\n}\n"],"names":["isValidTimeZone","zonedDayKey","zonedMinuteOfDay","zonedWallTimeToInstant","EVENT_COVER_ALT_MAX_LENGTH","EVENT_DESCRIPTION_MAX_LENGTH","EVENT_LOCATION_MAX_LENGTH","EVENT_ORGANIZER_MAX_LENGTH","EVENT_TITLE_MAX_LENGTH","EVENT_DRAFT_RESOURCE","EVENT_DRAFT_WALL_TIME","EVENT_DRAFT_COVER_MAX_LENGTH","line","value","replace","trim","eventDraftInstant","timezone","match","year","month","day","hour","minute","slice","map","Number","Date","UTC","getUTCDate","optional","content","key","label","max","problems","multiline","raw","undefined","push","text","length","readEventDraftContent","title","zoneOk","startsAtMs","endsAtMs","rawEnd","location","organizer","description","coverImage","test","coverImageAlt","ok","Set","status","eventDraftWallText","atMs","pad","String","padStart","Math","floor","checkEventDraftContent","read","facts","startsAt"],"mappings":"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"}
@@ -17,9 +17,10 @@
17
17
  /**
18
18
  * THE ONE RULE FOR WHAT AN EVENT STORES.
19
19
  *
20
- * `hosts/{hostId}/events/{id}` has two writers that must agree: the Events
21
- * page's editor and the events import. Both hand what they were given to
22
- * {@link eventWrite} and store exactly what it answers, so a title is cut,
20
+ * `hosts/{hostId}/events/{id}` has three writers that must agree: the Events
21
+ * page's editor, the events import, and the `event` draft writer another
22
+ * plugin asks for (AGL-3743). Each hands what it was given to
23
+ * {@link eventWrite} and stores exactly what it answers, so a title is cut,
23
24
  * an end time is repaired and a cover description is dropped the same way
24
25
  * whichever of them wrote the event.
25
26
  *
@@ -17,9 +17,10 @@
17
17
  /**
18
18
  * THE ONE RULE FOR WHAT AN EVENT STORES.
19
19
  *
20
- * `hosts/{hostId}/events/{id}` has two writers that must agree: the Events
21
- * page's editor and the events import. Both hand what they were given to
22
- * {@link eventWrite} and store exactly what it answers, so a title is cut,
20
+ * `hosts/{hostId}/events/{id}` has three writers that must agree: the Events
21
+ * page's editor, the events import, and the `event` draft writer another
22
+ * plugin asks for (AGL-3743). Each hands what it was given to
23
+ * {@link eventWrite} and stores exactly what it answers, so a title is cut,
23
24
  * an end time is repaired and a cover description is dropped the same way
24
25
  * whichever of them wrote the event.
25
26
  *
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/events-calendar/src/lib/model/event-write.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { MEDIA_ALT_MAX_LENGTH } from '@aglyn/aglyn/app-utils/media-alt'\n\n/**\n * THE ONE RULE FOR WHAT AN EVENT STORES.\n *\n * `hosts/{hostId}/events/{id}` has two writers that must agree: the Events\n * page's editor and the events import. Both hand what they were given to\n * {@link eventWrite} and store exactly what it answers, so a title is cut,\n * an end time is repaired and a cover description is dropped the same way\n * whichever of them wrote the event.\n *\n * Pure: no Firestore, no clock. Each writer adds its own `updatedAt` /\n * `createdAt` and turns {@link EventWrite.remove} into its SDK's delete\n * sentinel.\n */\n\n/** Longest stored title. */\nexport const EVENT_TITLE_MAX_LENGTH = 150\n/** Longest stored location. */\nexport const EVENT_LOCATION_MAX_LENGTH = 200\n/** Longest stored organizer. */\nexport const EVENT_ORGANIZER_MAX_LENGTH = 100\n/** Longest stored description. */\nexport const EVENT_DESCRIPTION_MAX_LENGTH = 2000\n/** Longest stored cover description: the platform's cap for any `alt`. */\nexport const EVENT_COVER_ALT_MAX_LENGTH = MEDIA_ALT_MAX_LENGTH\n/** How long an event without an end after its start lasts. */\nexport const EVENT_DEFAULT_DURATION_MS = 60 * 60 * 1000\n\n/** The statuses an editor or a file may set. `deleted` is only ever written by a delete. */\nexport const EVENT_STATUSES = ['draft', 'published'] as const\nexport type EventStatus = (typeof EVENT_STATUSES)[number]\n\n/** A status as typed, folded to one an event may hold, or `null` for anything else. */\nexport function eventStatusOf(value: unknown): EventStatus | null {\n const text = typeof value === 'string' ? value.trim().toLowerCase() : ''\n return (EVENT_STATUSES as readonly string[]).includes(text)\n ? (text as EventStatus)\n : null\n}\n\n/** What a writer was given for one event. Text may be untrimmed; blank means \"none\". */\nexport interface EventWriteInput {\n title: string\n /** Epoch milliseconds; required. */\n startsAtMs: number\n /** Epoch milliseconds; absent, zero or not after the start gives the default duration. */\n endsAtMs?: number | null\n location?: string | null\n organizer?: string | null\n description?: string | null\n coverImage?: string | null\n coverImageAlt?: string | null\n status: EventStatus\n}\n\n/** The content fields an event document stores. */\nexport interface EventStoredFields {\n title: string\n startsAtMs: number\n endsAtMs: number\n location?: string\n organizer?: string\n description?: string\n coverImage?: string\n coverImageAlt?: string\n status: EventStatus\n}\n\n/** The optional fields a write can take off a stored event. */\nexport type EventRemovableField =\n 'location' | 'organizer' | 'description' | 'coverImage' | 'coverImageAlt'\n\n/** What to store: the fields to write and the stored fields to delete. */\nexport interface EventWrite {\n fields: EventStoredFields\n remove: EventRemovableField[]\n}\n\nexport interface EventWriteOptions {\n /**\n * Whether a blank optional field deletes the stored one. The editor leaves\n * it off: it writes a whole event over a `merge`, and a blank box there\n * keeps what was stored. The import turns it on, because it hands over the\n * record as it will be — a blank there is a value it chose to clear.\n */\n clearBlank?: boolean\n}\n\n/** Why an event cannot be written as given, or `null` when it can. */\nexport function eventWriteProblem(\n input: Pick<EventWriteInput, 'title' | 'startsAtMs'>,\n): string | null {\n if (!String(input.title ?? '').trim()) return 'An event needs a title.'\n if (!Number.isFinite(input.startsAtMs) || !input.startsAtMs)\n return 'An event needs a start time.'\n return null\n}\n\n/** The end an event stores: the one given when it is after the start, else the default duration. */\nexport function eventEndsAtMs(\n startsAtMs: number,\n endsAtMs: number | null | undefined,\n): number {\n return typeof endsAtMs === 'number' &&\n Number.isFinite(endsAtMs) &&\n endsAtMs > startsAtMs\n ? endsAtMs\n : startsAtMs + EVENT_DEFAULT_DURATION_MS\n}\n\nfunction capped(value: string | null | undefined, max: number): string {\n return String(value ?? '')\n .trim()\n .slice(0, max)\n}\n\n/**\n * The stored form of an event. Throws for input {@link eventWriteProblem}\n * refuses; both writers check it first and say why in their own words.\n *\n * The cover description is tied to the cover: with no cover, or an empty\n * description, it is removed rather than left describing a picture that\n * changed.\n */\nexport function eventWrite(\n input: EventWriteInput,\n options: EventWriteOptions = {},\n): EventWrite {\n const problem = eventWriteProblem(input)\n if (problem) throw new Error(problem)\n const remove: EventRemovableField[] = []\n const fields: EventStoredFields = {\n title: capped(input.title, EVENT_TITLE_MAX_LENGTH),\n startsAtMs: input.startsAtMs,\n endsAtMs: eventEndsAtMs(input.startsAtMs, input.endsAtMs),\n status: input.status,\n }\n const optional: Array<\n [Exclude<EventRemovableField, 'coverImageAlt'>, number]\n > = [\n ['location', EVENT_LOCATION_MAX_LENGTH],\n ['organizer', EVENT_ORGANIZER_MAX_LENGTH],\n ['description', EVENT_DESCRIPTION_MAX_LENGTH],\n ['coverImage', Number.POSITIVE_INFINITY],\n ]\n for (const [key, max] of optional) {\n const value = capped(input[key], max)\n if (value) fields[key] = value\n else if (options.clearBlank) remove.push(key)\n }\n const alt = capped(input.coverImageAlt, EVENT_COVER_ALT_MAX_LENGTH)\n if (fields.coverImage && alt) fields.coverImageAlt = alt\n else remove.push('coverImageAlt')\n return { fields, remove }\n}\n"],"names":["MEDIA_ALT_MAX_LENGTH","EVENT_TITLE_MAX_LENGTH","EVENT_LOCATION_MAX_LENGTH","EVENT_ORGANIZER_MAX_LENGTH","EVENT_DESCRIPTION_MAX_LENGTH","EVENT_COVER_ALT_MAX_LENGTH","EVENT_DEFAULT_DURATION_MS","EVENT_STATUSES","eventStatusOf","value","text","trim","toLowerCase","includes","eventWriteProblem","input","String","title","Number","isFinite","startsAtMs","eventEndsAtMs","endsAtMs","capped","max","slice","eventWrite","options","problem","Error","remove","fields","status","optional","POSITIVE_INFINITY","key","clearBlank","push","alt","coverImageAlt","coverImage"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,oBAAoB,QAAQ,mCAAkC;AAEvE;;;;;;;;;;;;CAYC,GAED,0BAA0B,GAC1B,OAAO,MAAMC,yBAAyB,IAAG;AACzC,6BAA6B,GAC7B,OAAO,MAAMC,4BAA4B,IAAG;AAC5C,8BAA8B,GAC9B,OAAO,MAAMC,6BAA6B,IAAG;AAC7C,gCAAgC,GAChC,OAAO,MAAMC,+BAA+B,KAAI;AAChD,wEAAwE,GACxE,OAAO,MAAMC,6BAA6BL,qBAAoB;AAC9D,4DAA4D,GAC5D,OAAO,MAAMM,4BAA4B,KAAK,KAAK,KAAI;AAEvD,0FAA0F,GAC1F,OAAO,MAAMC,iBAAiB;IAAC;IAAS;CAAY,CAAS;AAG7D,qFAAqF,GACrF,OAAO,SAASC,cAAcC,KAAc;IAC1C,MAAMC,OAAO,OAAOD,UAAU,WAAWA,MAAME,IAAI,GAAGC,WAAW,KAAK;IACtE,OAAO,AAACL,eAAqCM,QAAQ,CAACH,QACjDA,OACD;AACN;AAkDA,oEAAoE,GACpE,OAAO,SAASI,kBACdC,KAAoD;QAExCA;IAAZ,IAAI,CAACC,QAAOD,eAAAA,MAAME,KAAK,YAAXF,eAAe,IAAIJ,IAAI,IAAI,OAAO;IAC9C,IAAI,CAACO,OAAOC,QAAQ,CAACJ,MAAMK,UAAU,KAAK,CAACL,MAAMK,UAAU,EACzD,OAAO;IACT,OAAO;AACT;AAEA,kGAAkG,GAClG,OAAO,SAASC,cACdD,UAAkB,EAClBE,QAAmC;IAEnC,OAAO,OAAOA,aAAa,YACzBJ,OAAOC,QAAQ,CAACG,aAChBA,WAAWF,aACTE,WACAF,aAAad;AACnB;AAEA,SAASiB,OAAOd,KAAgC,EAAEe,GAAW;IAC3D,OAAOR,OAAOP,gBAAAA,QAAS,IACpBE,IAAI,GACJc,KAAK,CAAC,GAAGD;AACd;AAEA;;;;;;;CAOC,GACD,OAAO,SAASE,WACdX,KAAsB,EACtBY,UAA6B,CAAC,CAAC;IAE/B,MAAMC,UAAUd,kBAAkBC;IAClC,IAAIa,SAAS,MAAM,IAAIC,MAAMD;IAC7B,MAAME,SAAgC,EAAE;IACxC,MAAMC,SAA4B;QAChCd,OAAOM,OAAOR,MAAME,KAAK,EAAEhB;QAC3BmB,YAAYL,MAAMK,UAAU;QAC5BE,UAAUD,cAAcN,MAAMK,UAAU,EAAEL,MAAMO,QAAQ;QACxDU,QAAQjB,MAAMiB,MAAM;IACtB;IACA,MAAMC,WAEF;QACF;YAAC;YAAY/B;SAA0B;QACvC;YAAC;YAAaC;SAA2B;QACzC;YAAC;YAAeC;SAA6B;QAC7C;YAAC;YAAcc,OAAOgB,iBAAiB;SAAC;KACzC;IACD,KAAK,MAAM,CAACC,KAAKX,IAAI,IAAIS,SAAU;QACjC,MAAMxB,QAAQc,OAAOR,KAAK,CAACoB,IAAI,EAAEX;QACjC,IAAIf,OAAOsB,MAAM,CAACI,IAAI,GAAG1B;aACpB,IAAIkB,QAAQS,UAAU,EAAEN,OAAOO,IAAI,CAACF;IAC3C;IACA,MAAMG,MAAMf,OAAOR,MAAMwB,aAAa,EAAElC;IACxC,IAAI0B,OAAOS,UAAU,IAAIF,KAAKP,OAAOQ,aAAa,GAAGD;SAChDR,OAAOO,IAAI,CAAC;IACjB,OAAO;QAAEN;QAAQD;IAAO;AAC1B"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/events-calendar/src/lib/model/event-write.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { MEDIA_ALT_MAX_LENGTH } from '@aglyn/aglyn/app-utils/media-alt'\n\n/**\n * THE ONE RULE FOR WHAT AN EVENT STORES.\n *\n * `hosts/{hostId}/events/{id}` has three writers that must agree: the Events\n * page's editor, the events import, and the `event` draft writer another\n * plugin asks for (AGL-3743). Each hands what it was given to\n * {@link eventWrite} and stores exactly what it answers, so a title is cut,\n * an end time is repaired and a cover description is dropped the same way\n * whichever of them wrote the event.\n *\n * Pure: no Firestore, no clock. Each writer adds its own `updatedAt` /\n * `createdAt` and turns {@link EventWrite.remove} into its SDK's delete\n * sentinel.\n */\n\n/** Longest stored title. */\nexport const EVENT_TITLE_MAX_LENGTH = 150\n/** Longest stored location. */\nexport const EVENT_LOCATION_MAX_LENGTH = 200\n/** Longest stored organizer. */\nexport const EVENT_ORGANIZER_MAX_LENGTH = 100\n/** Longest stored description. */\nexport const EVENT_DESCRIPTION_MAX_LENGTH = 2000\n/** Longest stored cover description: the platform's cap for any `alt`. */\nexport const EVENT_COVER_ALT_MAX_LENGTH = MEDIA_ALT_MAX_LENGTH\n/** How long an event without an end after its start lasts. */\nexport const EVENT_DEFAULT_DURATION_MS = 60 * 60 * 1000\n\n/** The statuses an editor or a file may set. `deleted` is only ever written by a delete. */\nexport const EVENT_STATUSES = ['draft', 'published'] as const\nexport type EventStatus = (typeof EVENT_STATUSES)[number]\n\n/** A status as typed, folded to one an event may hold, or `null` for anything else. */\nexport function eventStatusOf(value: unknown): EventStatus | null {\n const text = typeof value === 'string' ? value.trim().toLowerCase() : ''\n return (EVENT_STATUSES as readonly string[]).includes(text)\n ? (text as EventStatus)\n : null\n}\n\n/** What a writer was given for one event. Text may be untrimmed; blank means \"none\". */\nexport interface EventWriteInput {\n title: string\n /** Epoch milliseconds; required. */\n startsAtMs: number\n /** Epoch milliseconds; absent, zero or not after the start gives the default duration. */\n endsAtMs?: number | null\n location?: string | null\n organizer?: string | null\n description?: string | null\n coverImage?: string | null\n coverImageAlt?: string | null\n status: EventStatus\n}\n\n/** The content fields an event document stores. */\nexport interface EventStoredFields {\n title: string\n startsAtMs: number\n endsAtMs: number\n location?: string\n organizer?: string\n description?: string\n coverImage?: string\n coverImageAlt?: string\n status: EventStatus\n}\n\n/** The optional fields a write can take off a stored event. */\nexport type EventRemovableField =\n 'location' | 'organizer' | 'description' | 'coverImage' | 'coverImageAlt'\n\n/** What to store: the fields to write and the stored fields to delete. */\nexport interface EventWrite {\n fields: EventStoredFields\n remove: EventRemovableField[]\n}\n\nexport interface EventWriteOptions {\n /**\n * Whether a blank optional field deletes the stored one. The editor leaves\n * it off: it writes a whole event over a `merge`, and a blank box there\n * keeps what was stored. The import turns it on, because it hands over the\n * record as it will be — a blank there is a value it chose to clear.\n */\n clearBlank?: boolean\n}\n\n/** Why an event cannot be written as given, or `null` when it can. */\nexport function eventWriteProblem(\n input: Pick<EventWriteInput, 'title' | 'startsAtMs'>,\n): string | null {\n if (!String(input.title ?? '').trim()) return 'An event needs a title.'\n if (!Number.isFinite(input.startsAtMs) || !input.startsAtMs)\n return 'An event needs a start time.'\n return null\n}\n\n/** The end an event stores: the one given when it is after the start, else the default duration. */\nexport function eventEndsAtMs(\n startsAtMs: number,\n endsAtMs: number | null | undefined,\n): number {\n return typeof endsAtMs === 'number' &&\n Number.isFinite(endsAtMs) &&\n endsAtMs > startsAtMs\n ? endsAtMs\n : startsAtMs + EVENT_DEFAULT_DURATION_MS\n}\n\nfunction capped(value: string | null | undefined, max: number): string {\n return String(value ?? '')\n .trim()\n .slice(0, max)\n}\n\n/**\n * The stored form of an event. Throws for input {@link eventWriteProblem}\n * refuses; both writers check it first and say why in their own words.\n *\n * The cover description is tied to the cover: with no cover, or an empty\n * description, it is removed rather than left describing a picture that\n * changed.\n */\nexport function eventWrite(\n input: EventWriteInput,\n options: EventWriteOptions = {},\n): EventWrite {\n const problem = eventWriteProblem(input)\n if (problem) throw new Error(problem)\n const remove: EventRemovableField[] = []\n const fields: EventStoredFields = {\n title: capped(input.title, EVENT_TITLE_MAX_LENGTH),\n startsAtMs: input.startsAtMs,\n endsAtMs: eventEndsAtMs(input.startsAtMs, input.endsAtMs),\n status: input.status,\n }\n const optional: Array<\n [Exclude<EventRemovableField, 'coverImageAlt'>, number]\n > = [\n ['location', EVENT_LOCATION_MAX_LENGTH],\n ['organizer', EVENT_ORGANIZER_MAX_LENGTH],\n ['description', EVENT_DESCRIPTION_MAX_LENGTH],\n ['coverImage', Number.POSITIVE_INFINITY],\n ]\n for (const [key, max] of optional) {\n const value = capped(input[key], max)\n if (value) fields[key] = value\n else if (options.clearBlank) remove.push(key)\n }\n const alt = capped(input.coverImageAlt, EVENT_COVER_ALT_MAX_LENGTH)\n if (fields.coverImage && alt) fields.coverImageAlt = alt\n else remove.push('coverImageAlt')\n return { fields, remove }\n}\n"],"names":["MEDIA_ALT_MAX_LENGTH","EVENT_TITLE_MAX_LENGTH","EVENT_LOCATION_MAX_LENGTH","EVENT_ORGANIZER_MAX_LENGTH","EVENT_DESCRIPTION_MAX_LENGTH","EVENT_COVER_ALT_MAX_LENGTH","EVENT_DEFAULT_DURATION_MS","EVENT_STATUSES","eventStatusOf","value","text","trim","toLowerCase","includes","eventWriteProblem","input","String","title","Number","isFinite","startsAtMs","eventEndsAtMs","endsAtMs","capped","max","slice","eventWrite","options","problem","Error","remove","fields","status","optional","POSITIVE_INFINITY","key","clearBlank","push","alt","coverImageAlt","coverImage"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,oBAAoB,QAAQ,mCAAkC;AAEvE;;;;;;;;;;;;;CAaC,GAED,0BAA0B,GAC1B,OAAO,MAAMC,yBAAyB,IAAG;AACzC,6BAA6B,GAC7B,OAAO,MAAMC,4BAA4B,IAAG;AAC5C,8BAA8B,GAC9B,OAAO,MAAMC,6BAA6B,IAAG;AAC7C,gCAAgC,GAChC,OAAO,MAAMC,+BAA+B,KAAI;AAChD,wEAAwE,GACxE,OAAO,MAAMC,6BAA6BL,qBAAoB;AAC9D,4DAA4D,GAC5D,OAAO,MAAMM,4BAA4B,KAAK,KAAK,KAAI;AAEvD,0FAA0F,GAC1F,OAAO,MAAMC,iBAAiB;IAAC;IAAS;CAAY,CAAS;AAG7D,qFAAqF,GACrF,OAAO,SAASC,cAAcC,KAAc;IAC1C,MAAMC,OAAO,OAAOD,UAAU,WAAWA,MAAME,IAAI,GAAGC,WAAW,KAAK;IACtE,OAAO,AAACL,eAAqCM,QAAQ,CAACH,QACjDA,OACD;AACN;AAkDA,oEAAoE,GACpE,OAAO,SAASI,kBACdC,KAAoD;QAExCA;IAAZ,IAAI,CAACC,QAAOD,eAAAA,MAAME,KAAK,YAAXF,eAAe,IAAIJ,IAAI,IAAI,OAAO;IAC9C,IAAI,CAACO,OAAOC,QAAQ,CAACJ,MAAMK,UAAU,KAAK,CAACL,MAAMK,UAAU,EACzD,OAAO;IACT,OAAO;AACT;AAEA,kGAAkG,GAClG,OAAO,SAASC,cACdD,UAAkB,EAClBE,QAAmC;IAEnC,OAAO,OAAOA,aAAa,YACzBJ,OAAOC,QAAQ,CAACG,aAChBA,WAAWF,aACTE,WACAF,aAAad;AACnB;AAEA,SAASiB,OAAOd,KAAgC,EAAEe,GAAW;IAC3D,OAAOR,OAAOP,gBAAAA,QAAS,IACpBE,IAAI,GACJc,KAAK,CAAC,GAAGD;AACd;AAEA;;;;;;;CAOC,GACD,OAAO,SAASE,WACdX,KAAsB,EACtBY,UAA6B,CAAC,CAAC;IAE/B,MAAMC,UAAUd,kBAAkBC;IAClC,IAAIa,SAAS,MAAM,IAAIC,MAAMD;IAC7B,MAAME,SAAgC,EAAE;IACxC,MAAMC,SAA4B;QAChCd,OAAOM,OAAOR,MAAME,KAAK,EAAEhB;QAC3BmB,YAAYL,MAAMK,UAAU;QAC5BE,UAAUD,cAAcN,MAAMK,UAAU,EAAEL,MAAMO,QAAQ;QACxDU,QAAQjB,MAAMiB,MAAM;IACtB;IACA,MAAMC,WAEF;QACF;YAAC;YAAY/B;SAA0B;QACvC;YAAC;YAAaC;SAA2B;QACzC;YAAC;YAAeC;SAA6B;QAC7C;YAAC;YAAcc,OAAOgB,iBAAiB;SAAC;KACzC;IACD,KAAK,MAAM,CAACC,KAAKX,IAAI,IAAIS,SAAU;QACjC,MAAMxB,QAAQc,OAAOR,KAAK,CAACoB,IAAI,EAAEX;QACjC,IAAIf,OAAOsB,MAAM,CAACI,IAAI,GAAG1B;aACpB,IAAIkB,QAAQS,UAAU,EAAEN,OAAOO,IAAI,CAACF;IAC3C;IACA,MAAMG,MAAMf,OAAOR,MAAMwB,aAAa,EAAElC;IACxC,IAAI0B,OAAOS,UAAU,IAAIF,KAAKP,OAAOQ,aAAa,GAAGD;SAChDR,OAAOO,IAAI,CAAC;IACjB,OAAO;QAAEN;QAAQD;IAAO;AAC1B"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * Where an event is read, published for every other surface (AGL-3743).
19
+ *
20
+ * The AI plugin's job drawer opens an event an AI build drafted by asking the
21
+ * record-route registry for `event`, and gets text instead of a link where
22
+ * this plugin is not loaded. An event is a site's, so only a site has an
23
+ * address; it opens in the Events page's editor, so the record answers the
24
+ * list a person finds it on.
25
+ */
26
+ /** The nav slug the shell resolves the Events page by: the nav item's `/events`. */
27
+ export declare const EVENTS_SLUG = "events";
28
+ /** Called from the console registrar; the owner is named so a spec calling it directly registers the same way. */
29
+ export declare function registerEventsRecordRoutes(): void;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { buildRoute, Route } from "@aglyn/aglyn/app-utils/console-routes";
17
+ import { registerPluginRecordRoute } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
18
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
19
+ import { EVENT_DRAFT_RESOURCE } from "./event-draft-content.js";
20
+ /**
21
+ * Where an event is read, published for every other surface (AGL-3743).
22
+ *
23
+ * The AI plugin's job drawer opens an event an AI build drafted by asking the
24
+ * record-route registry for `event`, and gets text instead of a link where
25
+ * this plugin is not loaded. An event is a site's, so only a site has an
26
+ * address; it opens in the Events page's editor, so the record answers the
27
+ * list a person finds it on.
28
+ */ /** The nav slug the shell resolves the Events page by: the nav item's `/events`. */ export const EVENTS_SLUG = 'events';
29
+ function eventsPage(context) {
30
+ return context.host ? buildRoute(Route.HOST_PLUGIN, {
31
+ orgSlug: context.orgSlug,
32
+ host: context.host,
33
+ pluginSlug: EVENTS_SLUG
34
+ }) : null;
35
+ }
36
+ /** Called from the console registrar; the owner is named so a spec calling it directly registers the same way. */ export function registerEventsRecordRoutes() {
37
+ registerPluginRecordRoute(EVENT_DRAFT_RESOURCE, {
38
+ list: eventsPage,
39
+ record: eventsPage
40
+ }, {
41
+ pluginId: BUNDLE_ID
42
+ });
43
+ }
44
+
45
+ //# sourceMappingURL=events-record-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/events-calendar/src/lib/model/events-record-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport {\n registerPluginRecordRoute,\n type PluginRecordRouteContext,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { EVENT_DRAFT_RESOURCE } from './event-draft-content'\n\n/**\n * Where an event is read, published for every other surface (AGL-3743).\n *\n * The AI plugin's job drawer opens an event an AI build drafted by asking the\n * record-route registry for `event`, and gets text instead of a link where\n * this plugin is not loaded. An event is a site's, so only a site has an\n * address; it opens in the Events page's editor, so the record answers the\n * list a person finds it on.\n */\n\n/** The nav slug the shell resolves the Events page by: the nav item's `/events`. */\nexport const EVENTS_SLUG = 'events'\n\nfunction eventsPage(context: PluginRecordRouteContext): string | null {\n return context.host\n ? buildRoute(Route.HOST_PLUGIN, { orgSlug: context.orgSlug, host: context.host, pluginSlug: EVENTS_SLUG })\n : null\n}\n\n/** Called from the console registrar; the owner is named so a spec calling it directly registers the same way. */\nexport function registerEventsRecordRoutes(): void {\n registerPluginRecordRoute(EVENT_DRAFT_RESOURCE, { list: eventsPage, record: eventsPage }, { pluginId: BUNDLE_ID })\n}\n"],"names":["buildRoute","Route","registerPluginRecordRoute","BUNDLE_ID","EVENT_DRAFT_RESOURCE","EVENTS_SLUG","eventsPage","context","host","HOST_PLUGIN","orgSlug","pluginSlug","registerEventsRecordRoutes","list","record","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,EAAEC,KAAK,QAAQ,wCAAuC;AACzE,SACEC,yBAAyB,QAEpB,mDAAkD;AACzD,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAASC,oBAAoB,QAAQ,2BAAuB;AAE5D;;;;;;;;CAQC,GAED,kFAAkF,GAClF,OAAO,MAAMC,cAAc,SAAQ;AAEnC,SAASC,WAAWC,OAAiC;IACnD,OAAOA,QAAQC,IAAI,GACfR,WAAWC,MAAMQ,WAAW,EAAE;QAAEC,SAASH,QAAQG,OAAO;QAAEF,MAAMD,QAAQC,IAAI;QAAEG,YAAYN;IAAY,KACtG;AACN;AAEA,gHAAgH,GAChH,OAAO,SAASO;IACdV,0BAA0BE,sBAAsB;QAAES,MAAMP;QAAYQ,QAAQR;IAAW,GAAG;QAAES,UAAUZ;IAAU;AAClH"}
package/src/lib/plugin.js CHANGED
@@ -18,6 +18,7 @@ import { registerPluginTransferResourceUi } from "@aglyn/aglyn/plugin-manager/pl
18
18
  import { mdiCalendarMonthOutline } from "@aglyn/shared-data-mdi";
19
19
  import { lazy } from "react";
20
20
  import { BUNDLE_ID } from "./constants/bundle-common.js";
21
+ import { registerEventsRecordRoutes } from "./model/events-record-routes.js";
21
22
  import { EVENTS_TRANSFER_LABEL, EVENTS_TRANSFER_RESOURCE } from "./transfer/events-transfer-key.js";
22
23
  /**
23
24
  * The console page is code-split: the shell registers the extension at app
@@ -72,6 +73,9 @@ import { EVENTS_TRANSFER_LABEL, EVENTS_TRANSFER_RESOURCE } from "./transfer/even
72
73
  }
73
74
  ]
74
75
  });
76
+ // Where an event opens, for a surface that names one — the AI plugin's job
77
+ // drawer opening an event a build drafted (AGL-3743).
78
+ registerEventsRecordRoutes();
75
79
  // The client half of the `events` import and export; the server half is
76
80
  // registered from `declarations.console-server.ts`.
77
81
  registerPluginTransferResourceUi(EVENTS_TRANSFER_RESOURCE, {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/events-calendar/src/lib/plugin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { mdiCalendarMonthOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport {\n EVENTS_TRANSFER_LABEL,\n EVENTS_TRANSFER_RESOURCE,\n} from './transfer/events-transfer-key'\n\n/**\n * The console page is code-split: the shell registers the extension at app\n * load for the nav strip, but the manager UI only loads when a user opens\n * the Events page (the shell's plugin route wraps it in Suspense).\n */\nconst EventsConsolePage = lazy(() => import('./components/events-console-page'))\n\n/**\n * Console half only: registers the Events nav item + page + dashboard card\n * in the ConsoleExtension registry, and how the import wizard and export\n * dialog show this site's events. Safe to call at console app load — it\n * pulls no besigner/canvas code (the page is lazy). The shell renders the\n * nav item and, through its generic plugin route, the page — so the Events\n * surface exists without any edit to the console's own nav or page files.\n */\nexport function registerEventsCalendarConsole(): void {\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Events Calendar',\n featureFlag: 'eventCalendar',\n // `eventCalendar` is false on every plan — it is sold as a per-org\n // add-on and nothing else — so the shell's generic \"not included in your\n // current plan\" would send a reader to compare tiers that all say no.\n // The price comes from the constant the invoice is built from, so this\n // sentence cannot drift away from what a purchase actually charges.\n upgradeNotice: {\n message:\n 'The Event Calendar is a paid add-on ' +\n `($${Aglyn.EVENT_CALENDAR_ADDON_MONTHLY_USD}/mo for your whole ` +\n `workspace, supported directly by ${Aglyn.PLATFORM_BRAND_NAME}). ` +\n 'Enable it from Billing → Add-ons.',\n billingAnchor: 'addons',\n },\n navItems: [\n {\n label: 'Events',\n href: '/events',\n // Reuse the existing release-flag nav-tab so staff-preview gating\n // is unchanged now that the tab comes from the plugin (AGL-394).\n navTabId: 'nav-tab-events',\n icon: { path: mdiCalendarMonthOutline.path },\n header: {\n title: 'Events',\n icon: { path: mdiCalendarMonthOutline.path },\n docsTopic: 'events',\n },\n Component: EventsConsolePage,\n },\n ],\n dashboardCards: [{ cardId: 'events-upcoming', title: 'Upcoming events' }],\n })\n // The client half of the `events` import and export; the server half is\n // registered from `declarations.console-server.ts`.\n registerPluginTransferResourceUi(\n EVENTS_TRANSFER_RESOURCE,\n {\n label: EVENTS_TRANSFER_LABEL,\n icon: { path: mdiCalendarMonthOutline.path },\n },\n { pluginId: BUNDLE_ID },\n )\n}\n\nexport * from './site'\n"],"names":["Aglyn","registerPluginTransferResourceUi","mdiCalendarMonthOutline","lazy","BUNDLE_ID","EVENTS_TRANSFER_LABEL","EVENTS_TRANSFER_RESOURCE","EventsConsolePage","registerEventsCalendarConsole","registerConsoleExtension","pluginId","displayName","featureFlag","upgradeNotice","message","EVENT_CALENDAR_ADDON_MONTHLY_USD","PLATFORM_BRAND_NAME","billingAnchor","navItems","label","href","navTabId","icon","path","header","title","docsTopic","Component","dashboardCards","cardId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SAASC,gCAAgC,QAAQ,wDAAuD;AACxG,SAASC,uBAAuB,QAAQ,yBAAwB;AAChE,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SACEC,qBAAqB,EACrBC,wBAAwB,QACnB,oCAAgC;AAEvC;;;;CAIC,GACD,MAAMC,oBAAoBJ,KAAK,IAAM,MAAM,CAAC;AAE5C;;;;;;;CAOC,GACD,OAAO,SAASK;IACdR,MAAMS,wBAAwB,CAAC;QAC7BC,UAAUN;QACVO,aAAa;QACbC,aAAa;QACb,mEAAmE;QACnE,yEAAyE;QACzE,sEAAsE;QACtE,uEAAuE;QACvE,oEAAoE;QACpEC,eAAe;YACbC,SACE,yCACA,CAAC,EAAE,EAAEd,MAAMe,gCAAgC,CAAC,mBAAmB,CAAC,GAChE,CAAC,iCAAiC,EAAEf,MAAMgB,mBAAmB,CAAC,GAAG,CAAC,GAClE;YACFC,eAAe;QACjB;QACAC,UAAU;YACR;gBACEC,OAAO;gBACPC,MAAM;gBACN,kEAAkE;gBAClE,iEAAiE;gBACjEC,UAAU;gBACVC,MAAM;oBAAEC,MAAMrB,wBAAwBqB,IAAI;gBAAC;gBAC3CC,QAAQ;oBACNC,OAAO;oBACPH,MAAM;wBAAEC,MAAMrB,wBAAwBqB,IAAI;oBAAC;oBAC3CG,WAAW;gBACb;gBACAC,WAAWpB;YACb;SACD;QACDqB,gBAAgB;YAAC;gBAAEC,QAAQ;gBAAmBJ,OAAO;YAAkB;SAAE;IAC3E;IACA,wEAAwE;IACxE,oDAAoD;IACpDxB,iCACEK,0BACA;QACEa,OAAOd;QACPiB,MAAM;YAAEC,MAAMrB,wBAAwBqB,IAAI;QAAC;IAC7C,GACA;QAAEb,UAAUN;IAAU;AAE1B;AAEA,cAAc,YAAQ"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/events-calendar/src/lib/plugin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { mdiCalendarMonthOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerEventsRecordRoutes } from './model/events-record-routes'\nimport {\n EVENTS_TRANSFER_LABEL,\n EVENTS_TRANSFER_RESOURCE,\n} from './transfer/events-transfer-key'\n\n/**\n * The console page is code-split: the shell registers the extension at app\n * load for the nav strip, but the manager UI only loads when a user opens\n * the Events page (the shell's plugin route wraps it in Suspense).\n */\nconst EventsConsolePage = lazy(() => import('./components/events-console-page'))\n\n/**\n * Console half only: registers the Events nav item + page + dashboard card\n * in the ConsoleExtension registry, and how the import wizard and export\n * dialog show this site's events. Safe to call at console app load — it\n * pulls no besigner/canvas code (the page is lazy). The shell renders the\n * nav item and, through its generic plugin route, the page — so the Events\n * surface exists without any edit to the console's own nav or page files.\n */\nexport function registerEventsCalendarConsole(): void {\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Events Calendar',\n featureFlag: 'eventCalendar',\n // `eventCalendar` is false on every plan — it is sold as a per-org\n // add-on and nothing else — so the shell's generic \"not included in your\n // current plan\" would send a reader to compare tiers that all say no.\n // The price comes from the constant the invoice is built from, so this\n // sentence cannot drift away from what a purchase actually charges.\n upgradeNotice: {\n message:\n 'The Event Calendar is a paid add-on ' +\n `($${Aglyn.EVENT_CALENDAR_ADDON_MONTHLY_USD}/mo for your whole ` +\n `workspace, supported directly by ${Aglyn.PLATFORM_BRAND_NAME}). ` +\n 'Enable it from Billing → Add-ons.',\n billingAnchor: 'addons',\n },\n navItems: [\n {\n label: 'Events',\n href: '/events',\n // Reuse the existing release-flag nav-tab so staff-preview gating\n // is unchanged now that the tab comes from the plugin (AGL-394).\n navTabId: 'nav-tab-events',\n icon: { path: mdiCalendarMonthOutline.path },\n header: {\n title: 'Events',\n icon: { path: mdiCalendarMonthOutline.path },\n docsTopic: 'events',\n },\n Component: EventsConsolePage,\n },\n ],\n dashboardCards: [{ cardId: 'events-upcoming', title: 'Upcoming events' }],\n })\n // Where an event opens, for a surface that names one — the AI plugin's job\n // drawer opening an event a build drafted (AGL-3743).\n registerEventsRecordRoutes()\n // The client half of the `events` import and export; the server half is\n // registered from `declarations.console-server.ts`.\n registerPluginTransferResourceUi(\n EVENTS_TRANSFER_RESOURCE,\n {\n label: EVENTS_TRANSFER_LABEL,\n icon: { path: mdiCalendarMonthOutline.path },\n },\n { pluginId: BUNDLE_ID },\n )\n}\n\nexport * from './site'\n"],"names":["Aglyn","registerPluginTransferResourceUi","mdiCalendarMonthOutline","lazy","BUNDLE_ID","registerEventsRecordRoutes","EVENTS_TRANSFER_LABEL","EVENTS_TRANSFER_RESOURCE","EventsConsolePage","registerEventsCalendarConsole","registerConsoleExtension","pluginId","displayName","featureFlag","upgradeNotice","message","EVENT_CALENDAR_ADDON_MONTHLY_USD","PLATFORM_BRAND_NAME","billingAnchor","navItems","label","href","navTabId","icon","path","header","title","docsTopic","Component","dashboardCards","cardId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SAASC,gCAAgC,QAAQ,wDAAuD;AACxG,SAASC,uBAAuB,QAAQ,yBAAwB;AAChE,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,0BAA0B,QAAQ,kCAA8B;AACzE,SACEC,qBAAqB,EACrBC,wBAAwB,QACnB,oCAAgC;AAEvC;;;;CAIC,GACD,MAAMC,oBAAoBL,KAAK,IAAM,MAAM,CAAC;AAE5C;;;;;;;CAOC,GACD,OAAO,SAASM;IACdT,MAAMU,wBAAwB,CAAC;QAC7BC,UAAUP;QACVQ,aAAa;QACbC,aAAa;QACb,mEAAmE;QACnE,yEAAyE;QACzE,sEAAsE;QACtE,uEAAuE;QACvE,oEAAoE;QACpEC,eAAe;YACbC,SACE,yCACA,CAAC,EAAE,EAAEf,MAAMgB,gCAAgC,CAAC,mBAAmB,CAAC,GAChE,CAAC,iCAAiC,EAAEhB,MAAMiB,mBAAmB,CAAC,GAAG,CAAC,GAClE;YACFC,eAAe;QACjB;QACAC,UAAU;YACR;gBACEC,OAAO;gBACPC,MAAM;gBACN,kEAAkE;gBAClE,iEAAiE;gBACjEC,UAAU;gBACVC,MAAM;oBAAEC,MAAMtB,wBAAwBsB,IAAI;gBAAC;gBAC3CC,QAAQ;oBACNC,OAAO;oBACPH,MAAM;wBAAEC,MAAMtB,wBAAwBsB,IAAI;oBAAC;oBAC3CG,WAAW;gBACb;gBACAC,WAAWpB;YACb;SACD;QACDqB,gBAAgB;YAAC;gBAAEC,QAAQ;gBAAmBJ,OAAO;YAAkB;SAAE;IAC3E;IACA,2EAA2E;IAC3E,sDAAsD;IACtDrB;IACA,wEAAwE;IACxE,oDAAoD;IACpDJ,iCACEM,0BACA;QACEa,OAAOd;QACPiB,MAAM;YAAEC,MAAMtB,wBAAwBsB,IAAI;QAAC;IAC7C,GACA;QAAEb,UAAUP;IAAU;AAE1B;AAEA,cAAc,YAAQ"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type PluginAiCapability, type PluginAiCapabilityArgs } from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities';
18
+ /**
19
+ * WHAT AN AI BUILD CAN MAKE IN EVENTS (AGL-3743): a dated event, as a draft,
20
+ * and the Event List block that shows the site's events on a page.
21
+ *
22
+ * The capability the AI plugin's build planner offers for "add our fall
23
+ * workshops" or "an events page". Its item is written by this plugin's
24
+ * `event` draft writer, so every rule is the writer's: the `eventCalendar`
25
+ * add-on, the member's role, a start after the write, and the shape
26
+ * `eventWrite` stores. One item is one event; a page section that uses any
27
+ * of them places the Event List block, which lists the site's published
28
+ * events, so the drafts appear there once a person publishes them.
29
+ *
30
+ * The arguments are flat, as the contract requires, and are the event's own
31
+ * fields: an event has no recurrence, tickets, RSVP or meeting link, so none
32
+ * is offered. The planner fills them in, so no model runs
33
+ * (`estimateCredits` 0).
34
+ */
35
+ /** The operation's name in a build plan. */
36
+ export declare const EVENT_AI_OP = "event";
37
+ /**
38
+ * The Event List's component id, as the page palette names the block. Spelled
39
+ * here rather than imported: the component module brings React and MUI, and
40
+ * the console server's boot registers this capability.
41
+ */
42
+ export declare const EVENT_AI_PAGE_BLOCK = "eventList";
43
+ /** How many events one build may draft. */
44
+ export declare const EVENT_AI_MAX_PER_PLAN = 6;
45
+ /** The writer's content for an item: the arguments, which are the content's own keys. Pure. */
46
+ export declare function eventDraftContentFromArgs(args: PluginAiCapabilityArgs): Readonly<Record<string, unknown>>;
47
+ export declare const eventAiCapability: PluginAiCapability;
48
+ /**
49
+ * Registers the capability; the console-server declarations call it beside
50
+ * the writer, since only the console runs AI jobs (AGL-3026). Idempotent.
51
+ */
52
+ export declare function registerEventAiCapability(): void;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { registerPluginAiCapability } from "@aglyn/aglyn/plugin-manager/plugin-ai-capabilities";
17
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
18
+ import { EVENT_DRAFT_RESOURCE } from "../model/event-draft-content.js";
19
+ import { EVENT_DESCRIPTION_MAX_LENGTH, EVENT_LOCATION_MAX_LENGTH, EVENT_ORGANIZER_MAX_LENGTH, EVENT_TITLE_MAX_LENGTH } from "../model/event-write.js";
20
+ /**
21
+ * WHAT AN AI BUILD CAN MAKE IN EVENTS (AGL-3743): a dated event, as a draft,
22
+ * and the Event List block that shows the site's events on a page.
23
+ *
24
+ * The capability the AI plugin's build planner offers for "add our fall
25
+ * workshops" or "an events page". Its item is written by this plugin's
26
+ * `event` draft writer, so every rule is the writer's: the `eventCalendar`
27
+ * add-on, the member's role, a start after the write, and the shape
28
+ * `eventWrite` stores. One item is one event; a page section that uses any
29
+ * of them places the Event List block, which lists the site's published
30
+ * events, so the drafts appear there once a person publishes them.
31
+ *
32
+ * The arguments are flat, as the contract requires, and are the event's own
33
+ * fields: an event has no recurrence, tickets, RSVP or meeting link, so none
34
+ * is offered. The planner fills them in, so no model runs
35
+ * (`estimateCredits` 0).
36
+ */ /** The operation's name in a build plan. */ export const EVENT_AI_OP = 'event';
37
+ /**
38
+ * The Event List's component id, as the page palette names the block. Spelled
39
+ * here rather than imported: the component module brings React and MUI, and
40
+ * the console server's boot registers this capability.
41
+ */ export const EVENT_AI_PAGE_BLOCK = 'eventList';
42
+ /** How many events one build may draft. */ export const EVENT_AI_MAX_PER_PLAN = 6;
43
+ const WALL_TIME = 'local date and time "YYYY-MM-DDTHH:MM", in timezone';
44
+ /** The writer's content for an item: the arguments, which are the content's own keys. Pure. */ export function eventDraftContentFromArgs(args) {
45
+ const keys = [
46
+ 'title',
47
+ 'startsAt',
48
+ 'endsAt',
49
+ 'timezone',
50
+ 'location',
51
+ 'organizer',
52
+ 'description'
53
+ ];
54
+ return Object.fromEntries(keys.filter((key)=>typeof args[key] === 'string' && args[key].trim()).map((key)=>[
55
+ key,
56
+ args[key].trim()
57
+ ]));
58
+ }
59
+ export const eventAiCapability = {
60
+ op: EVENT_AI_OP,
61
+ noun: 'event',
62
+ where: 'Events, as a draft to publish',
63
+ intents: [
64
+ 'dated events on the site’s events calendar — classes, workshops, tastings, shows, open houses, meetups — each with its time, place and description, drafted for the owner to publish; use this rather than a dataset for anything held on a date',
65
+ 'an events page or section that lists the site’s upcoming events'
66
+ ],
67
+ argsSchema: {
68
+ type: 'object',
69
+ properties: {
70
+ title: {
71
+ type: 'string',
72
+ description: 'What the event is called, specific to this business, e.g. "Autumn sourdough workshop" — never a placeholder such as "Event 1" or "Sample event".',
73
+ maxLength: EVENT_TITLE_MAX_LENGTH
74
+ },
75
+ startsAt: {
76
+ type: 'string',
77
+ description: `When it starts, as ${WALL_TIME}. After today's date, within the next few months, on a day and at an hour this business would hold it; a date the request gives, as given.`,
78
+ maxLength: 16
79
+ },
80
+ endsAt: {
81
+ type: 'string',
82
+ description: `When it ends, as ${WALL_TIME}, after the start; leave it out for a one-hour event.`,
83
+ maxLength: 16
84
+ },
85
+ timezone: {
86
+ type: 'string',
87
+ description: 'The IANA time zone the business is in, e.g. "America/Chicago".',
88
+ maxLength: 64
89
+ },
90
+ location: {
91
+ type: 'string',
92
+ description: 'Where it happens, as a visitor reads it: the venue or address the request gives, or the business’s own place; "Online" for an online event. Leave it out rather than invent an address.',
93
+ maxLength: EVENT_LOCATION_MAX_LENGTH
94
+ },
95
+ organizer: {
96
+ type: 'string',
97
+ description: 'Who hosts it: the business’s name, or a host the request names. Never a person’s name the request does not give.',
98
+ maxLength: EVENT_ORGANIZER_MAX_LENGTH
99
+ },
100
+ description: {
101
+ type: 'string',
102
+ description: 'Two to four sentences a visitor reads: what happens, who it is for and what to bring or expect, in the business’s voice. No price, ticket, sign-up or link the request does not state, and no lorem ipsum.',
103
+ maxLength: EVENT_DESCRIPTION_MAX_LENGTH
104
+ }
105
+ },
106
+ required: [
107
+ 'title',
108
+ 'startsAt',
109
+ 'timezone',
110
+ 'description'
111
+ ],
112
+ additionalProperties: false
113
+ },
114
+ maxPerPlan: EVENT_AI_MAX_PER_PLAN,
115
+ // The add-on is the gate, on every plan alike; a draft costs no credits.
116
+ freeAllowed: true,
117
+ feature: 'eventCalendar',
118
+ draftResource: EVENT_DRAFT_RESOURCE,
119
+ // Written without a model: the planner already filled the arguments.
120
+ estimateCredits: ()=>0,
121
+ degrade: 'omit',
122
+ // A page that shows the events places the Event List block.
123
+ pageBlocks: [
124
+ EVENT_AI_PAGE_BLOCK
125
+ ],
126
+ draftContent: (item)=>eventDraftContentFromArgs(item.args)
127
+ };
128
+ /**
129
+ * Registers the capability; the console-server declarations call it beside
130
+ * the writer, since only the console runs AI jobs (AGL-3026). Idempotent.
131
+ */ export function registerEventAiCapability() {
132
+ registerPluginAiCapability(eventAiCapability, {
133
+ pluginId: BUNDLE_ID
134
+ });
135
+ }
136
+
137
+ //# sourceMappingURL=event-ai-capability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/events-calendar/src/lib/server/event-ai-capability.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n registerPluginAiCapability,\n type PluginAiCapability,\n type PluginAiCapabilityArgs,\n} from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { EVENT_DRAFT_RESOURCE } from '../model/event-draft-content'\nimport {\n EVENT_DESCRIPTION_MAX_LENGTH,\n EVENT_LOCATION_MAX_LENGTH,\n EVENT_ORGANIZER_MAX_LENGTH,\n EVENT_TITLE_MAX_LENGTH,\n} from '../model/event-write'\n\n/**\n * WHAT AN AI BUILD CAN MAKE IN EVENTS (AGL-3743): a dated event, as a draft,\n * and the Event List block that shows the site's events on a page.\n *\n * The capability the AI plugin's build planner offers for \"add our fall\n * workshops\" or \"an events page\". Its item is written by this plugin's\n * `event` draft writer, so every rule is the writer's: the `eventCalendar`\n * add-on, the member's role, a start after the write, and the shape\n * `eventWrite` stores. One item is one event; a page section that uses any\n * of them places the Event List block, which lists the site's published\n * events, so the drafts appear there once a person publishes them.\n *\n * The arguments are flat, as the contract requires, and are the event's own\n * fields: an event has no recurrence, tickets, RSVP or meeting link, so none\n * is offered. The planner fills them in, so no model runs\n * (`estimateCredits` 0).\n */\n\n/** The operation's name in a build plan. */\nexport const EVENT_AI_OP = 'event'\n\n/**\n * The Event List's component id, as the page palette names the block. Spelled\n * here rather than imported: the component module brings React and MUI, and\n * the console server's boot registers this capability.\n */\nexport const EVENT_AI_PAGE_BLOCK = 'eventList'\n\n/** How many events one build may draft. */\nexport const EVENT_AI_MAX_PER_PLAN = 6\n\nconst WALL_TIME = 'local date and time \"YYYY-MM-DDTHH:MM\", in timezone'\n\n/** The writer's content for an item: the arguments, which are the content's own keys. Pure. */\nexport function eventDraftContentFromArgs(args: PluginAiCapabilityArgs): Readonly<Record<string, unknown>> {\n const keys = ['title', 'startsAt', 'endsAt', 'timezone', 'location', 'organizer', 'description']\n return Object.fromEntries(\n keys\n .filter((key) => typeof args[key] === 'string' && (args[key] as string).trim())\n .map((key) => [key, (args[key] as string).trim()]),\n )\n}\n\nexport const eventAiCapability: PluginAiCapability = {\n op: EVENT_AI_OP,\n noun: 'event',\n where: 'Events, as a draft to publish',\n intents: [\n 'dated events on the site’s events calendar — classes, workshops, tastings, shows, open houses, meetups — each with its time, place and description, drafted for the owner to publish; use this rather than a dataset for anything held on a date',\n 'an events page or section that lists the site’s upcoming events',\n ],\n argsSchema: {\n type: 'object',\n properties: {\n title: {\n type: 'string',\n description:\n 'What the event is called, specific to this business, e.g. \"Autumn sourdough workshop\" — never a placeholder such as \"Event 1\" or \"Sample event\".',\n maxLength: EVENT_TITLE_MAX_LENGTH,\n },\n startsAt: {\n type: 'string',\n description:\n `When it starts, as ${WALL_TIME}. After today's date, within the next few months, on a day and at an hour this business would hold it; a date the request gives, as given.`,\n maxLength: 16,\n },\n endsAt: {\n type: 'string',\n description: `When it ends, as ${WALL_TIME}, after the start; leave it out for a one-hour event.`,\n maxLength: 16,\n },\n timezone: {\n type: 'string',\n description: 'The IANA time zone the business is in, e.g. \"America/Chicago\".',\n maxLength: 64,\n },\n location: {\n type: 'string',\n description:\n 'Where it happens, as a visitor reads it: the venue or address the request gives, or the business’s own place; \"Online\" for an online event. Leave it out rather than invent an address.',\n maxLength: EVENT_LOCATION_MAX_LENGTH,\n },\n organizer: {\n type: 'string',\n description: 'Who hosts it: the business’s name, or a host the request names. Never a person’s name the request does not give.',\n maxLength: EVENT_ORGANIZER_MAX_LENGTH,\n },\n description: {\n type: 'string',\n description:\n 'Two to four sentences a visitor reads: what happens, who it is for and what to bring or expect, in the business’s voice. No price, ticket, sign-up or link the request does not state, and no lorem ipsum.',\n maxLength: EVENT_DESCRIPTION_MAX_LENGTH,\n },\n },\n required: ['title', 'startsAt', 'timezone', 'description'],\n additionalProperties: false,\n },\n maxPerPlan: EVENT_AI_MAX_PER_PLAN,\n // The add-on is the gate, on every plan alike; a draft costs no credits.\n freeAllowed: true,\n feature: 'eventCalendar',\n draftResource: EVENT_DRAFT_RESOURCE,\n // Written without a model: the planner already filled the arguments.\n estimateCredits: () => 0,\n degrade: 'omit',\n // A page that shows the events places the Event List block.\n pageBlocks: [EVENT_AI_PAGE_BLOCK],\n draftContent: (item) => eventDraftContentFromArgs(item.args),\n}\n\n/**\n * Registers the capability; the console-server declarations call it beside\n * the writer, since only the console runs AI jobs (AGL-3026). Idempotent.\n */\nexport function registerEventAiCapability(): void {\n registerPluginAiCapability(eventAiCapability, { pluginId: BUNDLE_ID })\n}\n"],"names":["registerPluginAiCapability","BUNDLE_ID","EVENT_DRAFT_RESOURCE","EVENT_DESCRIPTION_MAX_LENGTH","EVENT_LOCATION_MAX_LENGTH","EVENT_ORGANIZER_MAX_LENGTH","EVENT_TITLE_MAX_LENGTH","EVENT_AI_OP","EVENT_AI_PAGE_BLOCK","EVENT_AI_MAX_PER_PLAN","WALL_TIME","eventDraftContentFromArgs","args","keys","Object","fromEntries","filter","key","trim","map","eventAiCapability","op","noun","where","intents","argsSchema","type","properties","title","description","maxLength","startsAt","endsAt","timezone","location","organizer","required","additionalProperties","maxPerPlan","freeAllowed","feature","draftResource","estimateCredits","degrade","pageBlocks","draftContent","item","registerEventAiCapability","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,0BAA0B,QAGrB,qDAAoD;AAC3D,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAASC,oBAAoB,QAAQ,kCAA8B;AACnE,SACEC,4BAA4B,EAC5BC,yBAAyB,EACzBC,0BAA0B,EAC1BC,sBAAsB,QACjB,0BAAsB;AAE7B;;;;;;;;;;;;;;;;CAgBC,GAED,0CAA0C,GAC1C,OAAO,MAAMC,cAAc,QAAO;AAElC;;;;CAIC,GACD,OAAO,MAAMC,sBAAsB,YAAW;AAE9C,yCAAyC,GACzC,OAAO,MAAMC,wBAAwB,EAAC;AAEtC,MAAMC,YAAY;AAElB,6FAA6F,GAC7F,OAAO,SAASC,0BAA0BC,IAA4B;IACpE,MAAMC,OAAO;QAAC;QAAS;QAAY;QAAU;QAAY;QAAY;QAAa;KAAc;IAChG,OAAOC,OAAOC,WAAW,CACvBF,KACGG,MAAM,CAAC,CAACC,MAAQ,OAAOL,IAAI,CAACK,IAAI,KAAK,YAAY,AAACL,IAAI,CAACK,IAAI,CAAYC,IAAI,IAC3EC,GAAG,CAAC,CAACF,MAAQ;YAACA;YAAML,IAAI,CAACK,IAAI,CAAYC,IAAI;SAAG;AAEvD;AAEA,OAAO,MAAME,oBAAwC;IACnDC,IAAId;IACJe,MAAM;IACNC,OAAO;IACPC,SAAS;QACP;QACA;KACD;IACDC,YAAY;QACVC,MAAM;QACNC,YAAY;YACVC,OAAO;gBACLF,MAAM;gBACNG,aACE;gBACFC,WAAWxB;YACb;YACAyB,UAAU;gBACRL,MAAM;gBACNG,aACE,CAAC,mBAAmB,EAAEnB,UAAU,0IAA0I,CAAC;gBAC7KoB,WAAW;YACb;YACAE,QAAQ;gBACNN,MAAM;gBACNG,aAAa,CAAC,iBAAiB,EAAEnB,UAAU,qDAAqD,CAAC;gBACjGoB,WAAW;YACb;YACAG,UAAU;gBACRP,MAAM;gBACNG,aAAa;gBACbC,WAAW;YACb;YACAI,UAAU;gBACRR,MAAM;gBACNG,aACE;gBACFC,WAAW1B;YACb;YACA+B,WAAW;gBACTT,MAAM;gBACNG,aAAa;gBACbC,WAAWzB;YACb;YACAwB,aAAa;gBACXH,MAAM;gBACNG,aACE;gBACFC,WAAW3B;YACb;QACF;QACAiC,UAAU;YAAC;YAAS;YAAY;YAAY;SAAc;QAC1DC,sBAAsB;IACxB;IACAC,YAAY7B;IACZ,yEAAyE;IACzE8B,aAAa;IACbC,SAAS;IACTC,eAAevC;IACf,qEAAqE;IACrEwC,iBAAiB,IAAM;IACvBC,SAAS;IACT,4DAA4D;IAC5DC,YAAY;QAACpC;KAAoB;IACjCqC,cAAc,CAACC,OAASnC,0BAA0BmC,KAAKlC,IAAI;AAC7D,EAAC;AAED;;;CAGC,GACD,OAAO,SAASmC;IACd/C,2BAA2BoB,mBAAmB;QAAE4B,UAAU/C;IAAU;AACtE"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { PluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts';
18
+ /**
19
+ * AN EVENT ANOTHER PLUGIN ASKS FOR (AGL-3743).
20
+ *
21
+ * The draft writer this plugin registers for the `event` resource on the
22
+ * core's resource-drafts seam. A plugin that sets a site up from a brief —
23
+ * an AI build — asks for the writer by name and gets this plugin's rules:
24
+ *
25
+ * - THE DOCUMENT is a `hosts/{hostId}/events/{id}` event stored through
26
+ * `eventWrite`, the one rule the Events page's editor and the events
27
+ * import write through, born `status: 'draft'`. A draft is listed nowhere
28
+ * on the site — the public listing asks for `status == 'published'` —
29
+ * until a person sets it published on the Events page.
30
+ * - THE PLAN is the `eventCalendar` add-on the Events page is gated on, read
31
+ * from the workspace's own document the way the listing and the import
32
+ * read it. There is no allowance to count.
33
+ * - THE ROLE is a member who may write the site's content.
34
+ * - THE START is after the write. A draft that would open on the Events page
35
+ * already over is a mistake in what was asked, so it is refused rather
36
+ * than stored.
37
+ *
38
+ * Nothing is published. The writer touches the one new document.
39
+ */
40
+ type Firestore = FirebaseFirestore.Firestore;
41
+ /** The refusal for a member who may not write the site. */
42
+ export declare const EVENT_DRAFT_ROLE_REFUSAL = "Editing requires the editor role";
43
+ /** The refusal for a workspace without the add-on, in the Events page's words. */
44
+ export declare const EVENT_DRAFT_PLAN_REFUSAL = "The Event Calendar is a paid add-on: enable it from Billing \u2192 Add-ons";
45
+ /** The refusal for a start that is not after the write. */
46
+ export declare function eventDraftPastRefusal(wallText: string): string;
47
+ export interface EventDraftWriterDeps {
48
+ /** The Admin SDK handle; specs hand in a double. */
49
+ firestore?: () => Firestore;
50
+ }
51
+ export declare function createEventDraftWriter(deps?: EventDraftWriterDeps): PluginResourceDraftWriter;
52
+ export declare const eventDraftWriter: PluginResourceDraftWriter;
53
+ export {};
@@ -0,0 +1,148 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
18
+ import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
19
+ import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
20
+ import { checkEventDraftContent, eventDraftWallText, readEventDraftContent } from "../model/event-draft-content.js";
21
+ import { eventWrite } from "../model/event-write.js";
22
+ /** The refusal for a member who may not write the site. */ export const EVENT_DRAFT_ROLE_REFUSAL = 'Editing requires the editor role';
23
+ /** The refusal for a workspace without the add-on, in the Events page's words. */ export const EVENT_DRAFT_PLAN_REFUSAL = 'The Event Calendar is a paid add-on: enable it from Billing → Add-ons';
24
+ /** The refusal for a start that is not after the write. */ export function eventDraftPastRefusal(wallText) {
25
+ return `A drafted event starts in the future: ${wallText} has already passed`;
26
+ }
27
+ function isDeleted(data) {
28
+ return Boolean(data == null ? void 0 : data['deletedAt']) || (data == null ? void 0 : data['status']) === 'deleted';
29
+ }
30
+ function recordOf(event) {
31
+ var _event_data, _data_title, _data_startsAtMs, _data_endsAtMs;
32
+ const data = (_event_data = event.data()) != null ? _event_data : {};
33
+ return {
34
+ id: event.id,
35
+ name: String((_data_title = data['title']) != null ? _data_title : ''),
36
+ versionId: null,
37
+ facts: {
38
+ status: data['status'] === 'published' ? 'published' : 'draft',
39
+ startsAtMs: Number((_data_startsAtMs = data['startsAtMs']) != null ? _data_startsAtMs : 0),
40
+ endsAtMs: Number((_data_endsAtMs = data['endsAtMs']) != null ? _data_endsAtMs : 0)
41
+ }
42
+ };
43
+ }
44
+ function roleRefusal(host, uid) {
45
+ var _host_get;
46
+ const role = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[uid];
47
+ return hostRoleCanWrite(role) ? null : {
48
+ status: 403,
49
+ error: EVENT_DRAFT_ROLE_REFUSAL
50
+ };
51
+ }
52
+ function planRefusal(org) {
53
+ return checkEntitlement(org, 'eventCalendar') ? null : {
54
+ status: 403,
55
+ error: EVENT_DRAFT_PLAN_REFUSAL
56
+ };
57
+ }
58
+ export function createEventDraftWriter(deps = {}) {
59
+ var _deps_firestore;
60
+ const firestore = (_deps_firestore = deps.firestore) != null ? _deps_firestore : ()=>firebaseAdmin.app().firestore();
61
+ return {
62
+ refusal: async (context)=>{
63
+ var _roleRefusal;
64
+ const host = await firestore().collection('hosts').doc(context.hostId).get();
65
+ if (!host.exists) return {
66
+ status: 404,
67
+ error: 'Unknown site'
68
+ };
69
+ return (_roleRefusal = roleRefusal(host, context.uid)) != null ? _roleRefusal : planRefusal(context.org);
70
+ },
71
+ check: (content)=>checkEventDraftContent(content),
72
+ read: async ({ hostId, id })=>{
73
+ const event = await firestore().collection('hosts').doc(hostId).collection('events').doc(id).get();
74
+ return event.exists && !isDeleted(event.data()) ? recordOf(event) : null;
75
+ },
76
+ write: async (request)=>{
77
+ // The request's name is the one asked for; the content's stands in.
78
+ const asked = request.name.replace(RegExp("[\\u0000-\\u001F\\u007F-\\u009F\\s]+", "gu"), ' ').trim();
79
+ const read = readEventDraftContent(asked ? _extends({}, request.content, {
80
+ title: asked
81
+ }) : request.content);
82
+ if (read.ok === false) return {
83
+ ok: false,
84
+ status: 400,
85
+ error: read.problems[0]
86
+ };
87
+ if (read.value.startsAtMs <= request.now.getTime()) {
88
+ return {
89
+ ok: false,
90
+ status: 400,
91
+ error: eventDraftPastRefusal(eventDraftWallText(read.value.startsAtMs, read.timezone))
92
+ };
93
+ }
94
+ const write = eventWrite(read.value);
95
+ const db = firestore();
96
+ const hostRef = db.collection('hosts').doc(request.hostId);
97
+ const eventRef = hostRef.collection('events').doc(request.id);
98
+ return db.runTransaction(async (tx)=>{
99
+ var _roleRefusal;
100
+ const [host, existing] = await Promise.all([
101
+ tx.get(hostRef),
102
+ tx.get(eventRef)
103
+ ]);
104
+ if (!host.exists) return {
105
+ ok: false,
106
+ status: 404,
107
+ error: 'Unknown site'
108
+ };
109
+ if (existing.exists) {
110
+ // Asked again under the same id: the draft it already wrote — unless
111
+ // a person deleted it since, which a replay never brings back.
112
+ return isDeleted(existing.data()) ? {
113
+ ok: false,
114
+ status: 409,
115
+ error: 'That event was deleted on the Events page'
116
+ } : _extends({
117
+ ok: true,
118
+ replayed: true
119
+ }, recordOf(existing));
120
+ }
121
+ const early = (_roleRefusal = roleRefusal(host, request.uid)) != null ? _roleRefusal : planRefusal(request.org);
122
+ if (early) return _extends({
123
+ ok: false
124
+ }, early);
125
+ tx.create(eventRef, _extends({}, write.fields, {
126
+ createdAt: request.now,
127
+ updatedAt: request.now,
128
+ createdBy: request.uid
129
+ }));
130
+ return {
131
+ ok: true,
132
+ replayed: false,
133
+ id: request.id,
134
+ name: write.fields.title,
135
+ versionId: null,
136
+ facts: {
137
+ status: 'draft',
138
+ startsAtMs: write.fields.startsAtMs,
139
+ endsAtMs: write.fields.endsAtMs
140
+ }
141
+ };
142
+ });
143
+ }
144
+ };
145
+ }
146
+ export const eventDraftWriter = createEventDraftWriter();
147
+
148
+ //# sourceMappingURL=event-drafts.server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/events-calendar/src/lib/server/event-drafts.server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type {\n PluginDraftContext,\n PluginDraftRecord,\n PluginDraftRefusal,\n PluginDraftWrite,\n PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport {\n checkEventDraftContent,\n eventDraftWallText,\n readEventDraftContent,\n} from '../model/event-draft-content'\nimport { eventWrite } from '../model/event-write'\n\n/**\n * AN EVENT ANOTHER PLUGIN ASKS FOR (AGL-3743).\n *\n * The draft writer this plugin registers for the `event` resource on the\n * core's resource-drafts seam. A plugin that sets a site up from a brief —\n * an AI build — asks for the writer by name and gets this plugin's rules:\n *\n * - THE DOCUMENT is a `hosts/{hostId}/events/{id}` event stored through\n * `eventWrite`, the one rule the Events page's editor and the events\n * import write through, born `status: 'draft'`. A draft is listed nowhere\n * on the site — the public listing asks for `status == 'published'` —\n * until a person sets it published on the Events page.\n * - THE PLAN is the `eventCalendar` add-on the Events page is gated on, read\n * from the workspace's own document the way the listing and the import\n * read it. There is no allowance to count.\n * - THE ROLE is a member who may write the site's content.\n * - THE START is after the write. A draft that would open on the Events page\n * already over is a mistake in what was asked, so it is refused rather\n * than stored.\n *\n * Nothing is published. The writer touches the one new document.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The refusal for a member who may not write the site. */\nexport const EVENT_DRAFT_ROLE_REFUSAL = 'Editing requires the editor role'\n\n/** The refusal for a workspace without the add-on, in the Events page's words. */\nexport const EVENT_DRAFT_PLAN_REFUSAL =\n 'The Event Calendar is a paid add-on: enable it from Billing → Add-ons'\n\n/** The refusal for a start that is not after the write. */\nexport function eventDraftPastRefusal(wallText: string): string {\n return `A drafted event starts in the future: ${wallText} has already passed`\n}\n\nfunction isDeleted(data: Readonly<Record<string, unknown>> | undefined): boolean {\n return Boolean(data?.['deletedAt']) || data?.['status'] === 'deleted'\n}\n\nfunction recordOf(event: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n const data = (event.data() ?? {}) as Record<string, unknown>\n return {\n id: event.id,\n name: String(data['title'] ?? ''),\n versionId: null,\n facts: {\n status: data['status'] === 'published' ? 'published' : 'draft',\n startsAtMs: Number(data['startsAtMs'] ?? 0),\n endsAtMs: Number(data['endsAtMs'] ?? 0),\n },\n }\n}\n\nfunction roleRefusal(host: FirebaseFirestore.DocumentSnapshot, uid: string): PluginDraftRefusal | null {\n const role = (host.get('memberRoles') ?? {})[uid]\n return hostRoleCanWrite(role) ? null : { status: 403, error: EVENT_DRAFT_ROLE_REFUSAL }\n}\n\nfunction planRefusal(org: PluginDraftContext['org']): PluginDraftRefusal | null {\n return checkEntitlement(org as never, 'eventCalendar')\n ? null\n : { status: 403, error: EVENT_DRAFT_PLAN_REFUSAL }\n}\n\nexport interface EventDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => Firestore\n}\n\nexport function createEventDraftWriter(deps: EventDraftWriterDeps = {}): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n return {\n refusal: async (context) => {\n const host = await firestore().collection('hosts').doc(context.hostId).get()\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n return roleRefusal(host, context.uid) ?? planRefusal(context.org)\n },\n\n check: (content) => checkEventDraftContent(content),\n\n read: async ({ hostId, id }) => {\n const event = await firestore().collection('hosts').doc(hostId).collection('events').doc(id).get()\n return event.exists && !isDeleted(event.data()) ? recordOf(event) : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n // The request's name is the one asked for; the content's stands in.\n const asked = request.name.replace(/[\\p{Cc}\\s]+/gu, ' ').trim()\n const read = readEventDraftContent(asked ? { ...request.content, title: asked } : request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n if (read.value.startsAtMs <= request.now.getTime()) {\n return {\n ok: false,\n status: 400,\n error: eventDraftPastRefusal(eventDraftWallText(read.value.startsAtMs, read.timezone)),\n }\n }\n const write = eventWrite(read.value)\n const db = firestore()\n const hostRef = db.collection('hosts').doc(request.hostId)\n const eventRef = hostRef.collection('events').doc(request.id)\n return db.runTransaction(async (tx): Promise<PluginDraftWrite> => {\n const [host, existing] = await Promise.all([tx.get(hostRef), tx.get(eventRef)])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n if (existing.exists) {\n // Asked again under the same id: the draft it already wrote — unless\n // a person deleted it since, which a replay never brings back.\n return isDeleted(existing.data())\n ? { ok: false, status: 409, error: 'That event was deleted on the Events page' }\n : { ok: true, replayed: true, ...recordOf(existing) }\n }\n const early = roleRefusal(host, request.uid) ?? planRefusal(request.org)\n if (early) return { ok: false, ...early }\n tx.create(eventRef, {\n ...write.fields,\n createdAt: request.now,\n updatedAt: request.now,\n createdBy: request.uid,\n })\n return {\n ok: true,\n replayed: false,\n id: request.id,\n name: write.fields.title,\n versionId: null,\n facts: {\n status: 'draft',\n startsAtMs: write.fields.startsAtMs,\n endsAtMs: write.fields.endsAtMs,\n },\n }\n })\n },\n }\n}\n\nexport const eventDraftWriter = createEventDraftWriter()\n"],"names":["hostRoleCanWrite","checkEntitlement","firebaseAdmin","checkEventDraftContent","eventDraftWallText","readEventDraftContent","eventWrite","EVENT_DRAFT_ROLE_REFUSAL","EVENT_DRAFT_PLAN_REFUSAL","eventDraftPastRefusal","wallText","isDeleted","data","Boolean","recordOf","event","id","name","String","versionId","facts","status","startsAtMs","Number","endsAtMs","roleRefusal","host","uid","role","get","error","planRefusal","org","createEventDraftWriter","deps","firestore","app","refusal","context","collection","doc","hostId","exists","check","content","read","write","request","asked","replace","trim","title","ok","problems","value","now","getTime","timezone","db","hostRef","eventRef","runTransaction","tx","existing","Promise","all","replayed","early","create","fields","createdAt","updatedAt","createdBy","eventDraftWriter"],"mappings":"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"}