@moneypot/hub 1.15.0-dev.3 → 1.16.0-dev.3

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.
Files changed (36) hide show
  1. package/dist/src/__generated__/gql.d.ts +2 -2
  2. package/dist/src/__generated__/gql.js +1 -1
  3. package/dist/src/__generated__/graphql.d.ts +240 -27
  4. package/dist/src/__generated__/graphql.js +30 -1
  5. package/dist/src/db/index.d.ts +1 -0
  6. package/dist/src/db/index.js +10 -2
  7. package/dist/src/db/types.d.ts +24 -0
  8. package/dist/src/express.d.ts +1 -0
  9. package/dist/src/graphql-queries.js +4 -0
  10. package/dist/src/index.d.ts +2 -1
  11. package/dist/src/index.js +2 -1
  12. package/dist/src/pg-advisory-lock.d.ts +5 -0
  13. package/dist/src/pg-advisory-lock.js +5 -0
  14. package/dist/src/pg-versions/013-chat.sql +221 -0
  15. package/dist/src/plugins/chat/hub-chat-after-id-condition.d.ts +1 -0
  16. package/dist/src/plugins/chat/hub-chat-after-id-condition.js +15 -0
  17. package/dist/src/plugins/chat/hub-chat-create-system-message.d.ts +1 -0
  18. package/dist/src/plugins/chat/hub-chat-create-system-message.js +124 -0
  19. package/dist/src/plugins/chat/hub-chat-create-user-message.d.ts +1 -0
  20. package/dist/src/plugins/chat/hub-chat-create-user-message.js +231 -0
  21. package/dist/src/plugins/chat/hub-chat-mute-user.d.ts +1 -0
  22. package/dist/src/plugins/chat/hub-chat-mute-user.js +186 -0
  23. package/dist/src/plugins/chat/hub-chat-subscription.d.ts +14 -0
  24. package/dist/src/plugins/chat/hub-chat-subscription.js +133 -0
  25. package/dist/src/plugins/chat/hub-chat-unmute-user.d.ts +1 -0
  26. package/dist/src/plugins/chat/hub-chat-unmute-user.js +146 -0
  27. package/dist/src/plugins/hub-authenticate.js +40 -18
  28. package/dist/src/plugins/hub-create-playground-session.js +44 -13
  29. package/dist/src/server/graphile.config.d.ts +13 -1
  30. package/dist/src/server/graphile.config.js +38 -12
  31. package/dist/src/server/index.d.ts +3 -1
  32. package/dist/src/server/index.js +3 -1
  33. package/dist/src/server/middleware/authentication.js +1 -0
  34. package/dist/src/util.d.ts +3 -0
  35. package/dist/src/util.js +9 -0
  36. package/package.json +1 -1
@@ -21,6 +21,7 @@ export declare function userFromActiveSessionKey(pgClient: QueryExecutor, sessio
21
21
  user: DbUser;
22
22
  sessionId: DbSession["id"];
23
23
  isPlayground: boolean;
24
+ isExperienceOwner: boolean;
24
25
  } | null>;
25
26
  export declare class DatabaseNotifier extends stream.EventEmitter {
26
27
  private pgClient;
@@ -71,7 +71,7 @@ export async function withPgPoolTransaction(pool, callback, retryCount = 0, maxR
71
71
  export async function userFromActiveSessionKey(pgClient, sessionKey) {
72
72
  const result = await pgClient
73
73
  .query(`
74
- select u.id, u.uname, u.casino_id, s.experience_id, u.mp_user_id, s.id as session_id, c.is_playground
74
+ select u.id, u.uname, u.casino_id, s.experience_id, u.mp_user_id, s.id as session_id, c.is_playground, u.id = e.user_id as is_experience_owner
75
75
  from hub.active_session s
76
76
  join hub.user u on s.user_id = u.id
77
77
  join hub.experience e on e.id = s.experience_id
@@ -82,11 +82,19 @@ export async function userFromActiveSessionKey(pgClient, sessionKey) {
82
82
  if (!result) {
83
83
  return null;
84
84
  }
85
- const { session_id, is_playground, ...user } = result;
85
+ const user = {
86
+ id: result.id,
87
+ uname: result.uname,
88
+ casino_id: result.casino_id,
89
+ experience_id: result.experience_id,
90
+ mp_user_id: result.mp_user_id,
91
+ };
92
+ const { session_id, is_playground, is_experience_owner } = result;
86
93
  return {
87
94
  user,
88
95
  sessionId: session_id,
89
96
  isPlayground: is_playground,
97
+ isExperienceOwner: is_experience_owner,
90
98
  };
91
99
  }
92
100
  export class DatabaseNotifier extends stream.EventEmitter {
@@ -49,6 +49,7 @@ export type DbExperience = {
49
49
  id: string;
50
50
  casino_id: string;
51
51
  mp_experience_id: string;
52
+ user_id: string | null;
52
53
  };
53
54
  export type DbCasino = {
54
55
  id: string;
@@ -172,3 +173,26 @@ export type DbAuditLogRecord = {
172
173
  ref_type: string | null;
173
174
  ref_id: string | null;
174
175
  };
176
+ export type DbChatMute = {
177
+ id: string;
178
+ casino_id: DbCasino["id"];
179
+ experience_id: DbExperience["id"];
180
+ user_id: DbUser["id"];
181
+ expired_at: Date | null;
182
+ reason: string | null;
183
+ };
184
+ export type DbChatMod = {
185
+ id: string;
186
+ casino_id: DbCasino["id"];
187
+ experience_id: DbExperience["id"];
188
+ user_id: DbUser["id"];
189
+ };
190
+ export type DbChatMessage = {
191
+ id: string;
192
+ casino_id: DbCasino["id"];
193
+ experience_id: DbExperience["id"];
194
+ user_id: DbUser["id"];
195
+ client_id: string;
196
+ body: string;
197
+ hidden_at: Date | null;
198
+ };
@@ -7,6 +7,7 @@ export interface HubRequest extends Request {
7
7
  user: DbUser;
8
8
  sessionId: DbSession["id"];
9
9
  isPlayground: boolean;
10
+ isExperienceOwner: boolean;
10
11
  } | {
11
12
  kind: "operator";
12
13
  apiKey: string;
@@ -9,6 +9,10 @@ export const GET_USER_FROM_USER_TOKEN = gql(`
9
9
  experience {
10
10
  id
11
11
  name
12
+ userByUserId {
13
+ id
14
+ uname
15
+ }
12
16
  }
13
17
  }
14
18
  }
@@ -18,7 +18,6 @@ declare global {
18
18
  }
19
19
  }
20
20
  export { MakeOutcomeBetPlugin, type OutcomeBetConfigMap, type OutcomeBetConfig, } from "./plugins/hub-make-outcome-bet.js";
21
- export { HubCreatePlaygroundSessionPlugin } from "./plugins/hub-create-playground-session.js";
22
21
  export { validateRisk, type RiskPolicy, type RiskPolicyArgs, type RiskLimits, } from "./risk-policy.js";
23
22
  export type PluginContext = Grafast.Context;
24
23
  export { defaultPlugins, type PluginIdentity, type UserSessionContext, } from "./server/graphile.config.js";
@@ -32,6 +31,8 @@ export type ServerOptions = {
32
31
  extraPgSchemas?: string[];
33
32
  exportSchemaSDLPath?: string;
34
33
  userDatabaseMigrationsPath?: string;
34
+ enableChat?: boolean;
35
+ enablePlayground?: boolean;
35
36
  };
36
37
  export declare function startAndListen(options: ServerOptions): Promise<{
37
38
  port: number;
package/dist/src/index.js CHANGED
@@ -7,7 +7,6 @@ import { join } from "path";
7
7
  import { logger } from "./logger.js";
8
8
  import { createServerContext, closeServerContext, } from "./context.js";
9
9
  export { MakeOutcomeBetPlugin, } from "./plugins/hub-make-outcome-bet.js";
10
- export { HubCreatePlaygroundSessionPlugin } from "./plugins/hub-create-playground-session.js";
11
10
  export { validateRisk, } from "./risk-policy.js";
12
11
  export { defaultPlugins, } from "./server/graphile.config.js";
13
12
  async function initialize(options) {
@@ -82,6 +81,8 @@ export async function startAndListen(options) {
82
81
  extraPgSchemas: options.extraPgSchemas,
83
82
  abortSignal: abortController.signal,
84
83
  context,
84
+ enableChat: options.enableChat ?? true,
85
+ enablePlayground: options.enablePlayground ?? true,
85
86
  });
86
87
  const gracefulShutdown = async ({ exit = true } = {}) => {
87
88
  if (isShuttingDown) {
@@ -10,4 +10,9 @@ export declare const PgAdvisoryLock: {
10
10
  experienceId: DbExperience["id"];
11
11
  casinoId: DbCasino["id"];
12
12
  }) => Promise<void>;
13
+ forChatUserAction: (pgClient: PgClientInTransaction, params: {
14
+ userId: DbUser["id"];
15
+ experienceId: DbExperience["id"];
16
+ casinoId: DbCasino["id"];
17
+ }) => Promise<void>;
13
18
  };
@@ -3,6 +3,7 @@ var LockNamespace;
3
3
  (function (LockNamespace) {
4
4
  LockNamespace[LockNamespace["MP_TAKE_REQUEST"] = 1] = "MP_TAKE_REQUEST";
5
5
  LockNamespace[LockNamespace["NEW_HASH_CHAIN"] = 2] = "NEW_HASH_CHAIN";
6
+ LockNamespace[LockNamespace["CHAT_USER_ACTION"] = 3] = "CHAT_USER_ACTION";
6
7
  })(LockNamespace || (LockNamespace = {}));
7
8
  function simpleHash32(text) {
8
9
  let hash = 0;
@@ -31,4 +32,8 @@ export const PgAdvisoryLock = {
31
32
  assert(pgClient._inTransaction, "pgClient must be in a transaction");
32
33
  await acquireAdvisoryLock(pgClient, LockNamespace.NEW_HASH_CHAIN, createHashKey(params.userId, params.experienceId, params.casinoId));
33
34
  },
35
+ forChatUserAction: async (pgClient, params) => {
36
+ assert(pgClient._inTransaction, "pgClient must be in a transaction");
37
+ await acquireAdvisoryLock(pgClient, LockNamespace.CHAT_USER_ACTION, createHashKey(params.userId, params.experienceId, params.casinoId));
38
+ },
34
39
  };
@@ -0,0 +1,221 @@
1
+
2
+ -- A simple single-room chat system
3
+
4
+ -- Quick revert:
5
+ drop table if exists hub.chat_message;
6
+ drop view if exists hub.active_chat_mute;
7
+ drop table if exists hub.chat_mute;
8
+ drop table if exists hub.chat_rate_bucket;
9
+ drop table if exists hub.chat_mod;
10
+ drop type if exists hub.chat_message_type;
11
+ alter table hub.experience drop column if exists user_id;
12
+ alter table hub.user drop column if exists client_id;
13
+ alter table hub.experience drop column if exists client_id;
14
+
15
+ -- New helper function for RLS so we don't have to do a subquery
16
+ create or replace function hub_hidden.is_experience_owner() returns boolean as $$
17
+ select nullif(current_setting('session.is_experience_owner', true), '') = '1';
18
+ $$ language sql stable;
19
+
20
+ -- We want to know which hub.user is the MP owner of the hub.experience
21
+ -- Ideally it would be not-null but we can't do that in this migration
22
+ -- since that info must be queried from MP.
23
+ alter table hub.experience add column user_id uuid null references hub.user(id);
24
+
25
+ create type hub.chat_message_type as enum ('user', 'system');
26
+
27
+ -- chat message type is 'user' or 'system'
28
+ -- system messages don't have a user_id
29
+ create table hub.chat_message (
30
+ -- uuidv7 has creation timestamp in it
31
+ id uuid primary key default hub_hidden.uuid_generate_v7(),
32
+
33
+ -- fks
34
+ casino_id uuid not null references hub.casino(id),
35
+ experience_id uuid not null references hub.experience(id),
36
+ -- system messages don't have a user_id
37
+ user_id uuid null references hub.user(id),
38
+
39
+ type hub.chat_message_type not null,
40
+
41
+ -- message info
42
+ client_id uuid not null, -- idempotent id
43
+ body text not null,
44
+ hidden_at timestamptz null,
45
+
46
+ constraint chat_message_user_id_check check (
47
+ (type = 'user' and user_id is not null) or (type = 'system' and user_id is null)
48
+ )
49
+ );
50
+
51
+ -- fk indexes
52
+ create index chat_message_casino_id_idx on hub.chat_message(casino_id);
53
+ create index chat_message_user_id_idx on hub.chat_message(user_id);
54
+ create index chat_message_experience_id_idx on hub.chat_message(experience_id);
55
+
56
+ -- support idempotency
57
+ create unique index chat_idempotent_user_message_idx
58
+ on hub.chat_message(casino_id, experience_id, user_id, client_id)
59
+ where type = 'user';
60
+ create unique index chat_idempotent_system_message_idx
61
+ on hub.chat_message(casino_id, experience_id, client_id)
62
+ where type = 'system';
63
+
64
+ -- append-only table
65
+ create table hub.chat_mute (
66
+ id uuid primary key default hub_hidden.uuid_generate_v7(),
67
+ casino_id uuid not null references hub.casino(id),
68
+ experience_id uuid not null references hub.experience(id),
69
+ user_id uuid not null references hub.user(id),
70
+ expired_at timestamptz null,
71
+ -- set on unmute
72
+ revoked_at timestamptz null,
73
+ reason text null
74
+ );
75
+
76
+ -- fk indexes
77
+ create index chat_mute_casino_id_idx on hub.chat_mute(casino_id);
78
+ create index chat_mute_user_id_idx on hub.chat_mute(user_id);
79
+ create index chat_mute_experience_id_idx on hub.chat_mute(experience_id);
80
+
81
+ -- One unrevoked row per key; expiry handled in code/view.
82
+ create unique index if not exists chat_mute_one_unrevoked_per_key
83
+ on hub.chat_mute (casino_id, experience_id, user_id)
84
+ where revoked_at is null;
85
+
86
+ -- "Active" means not revoked AND not expired (null = indefinite).
87
+ -- order by id desc determines which key is picked for `distinct on`: the latest one
88
+ create or replace view hub.active_chat_mute as
89
+ select distinct on (casino_id, experience_id, user_id) *
90
+ from hub.chat_mute
91
+ where revoked_at is null
92
+ and (expired_at is null or expired_at > now())
93
+ order by casino_id, experience_id, user_id, id desc; -- highest uuidv7 per key
94
+
95
+ -- for active chat mute lookup
96
+ create index if not exists chat_mute_lookup_idx
97
+ on hub.chat_mute (casino_id, experience_id, user_id)
98
+ where revoked_at is null;
99
+
100
+ -- not exposed to graphql
101
+ create table hub.chat_rate_bucket (
102
+ id uuid primary key default hub_hidden.uuid_generate_v7(),
103
+ casino_id uuid not null references hub.casino(id),
104
+ experience_id uuid not null references hub.experience(id),
105
+ user_id uuid not null references hub.user(id),
106
+
107
+ window_seconds int not null,
108
+ bucket_start timestamptz not null,
109
+ count int not null default 0
110
+ );
111
+
112
+ -- fk indexes
113
+ create index chat_rate_bucket_casino_id_idx on hub.chat_rate_bucket(casino_id);
114
+ create index chat_rate_bucket_user_id_idx on hub.chat_rate_bucket(user_id);
115
+ create index chat_rate_bucket_experience_id_idx on hub.chat_rate_bucket(experience_id);
116
+
117
+ create unique index chat_rate_bucket_window_idx on hub.chat_rate_bucket(casino_id, experience_id, user_id, window_seconds, bucket_start);
118
+
119
+ -- not exposed to graphql
120
+ create table hub.chat_mod (
121
+ id uuid primary key default hub_hidden.uuid_generate_v7(),
122
+ casino_id uuid not null references hub.casino(id),
123
+ experience_id uuid not null references hub.experience(id),
124
+ user_id uuid not null references hub.user(id)
125
+ );
126
+
127
+ -- fk indexes
128
+ create index chat_mod_casino_id_idx on hub.chat_mod(casino_id);
129
+ create index chat_mod_user_id_idx on hub.chat_mod(user_id);
130
+ create index chat_mod_experience_id_idx on hub.chat_mod(experience_id);
131
+ -- a user can only be a chat mod once per experience
132
+ create unique index chat_mod_unique_idx on hub.chat_mod(casino_id, experience_id, user_id);
133
+
134
+ -- grant
135
+ grant select on hub.chat_message to app_postgraphile;
136
+ grant select on hub.chat_mute to app_postgraphile;
137
+ grant select on hub.chat_mod to app_postgraphile;
138
+
139
+ -- rls
140
+ alter table hub.chat_message enable row level security;
141
+ alter table hub.chat_mute enable row level security;
142
+ alter table hub.chat_mod enable row level security;
143
+
144
+ drop policy if exists select_chat_message on hub.chat_message;
145
+ create policy select_chat_message on hub.chat_message for select using (
146
+ -- operator can see all rows
147
+ hub_hidden.is_operator()
148
+
149
+ -- normal users can only see non-hidden rows in the current experience
150
+ or (
151
+ hidden_at is null
152
+ and experience_id = hub_hidden.current_experience_id()
153
+ and casino_id = hub_hidden.current_casino_id()
154
+ )
155
+
156
+ -- experience owner can see all messages no matter hidden status
157
+ or hub_hidden.is_experience_owner()
158
+ );
159
+
160
+ drop policy if exists select_chat_mute on hub.chat_mute;
161
+ create policy select_chat_mute on hub.chat_mute for select using (
162
+ -- operator can see all rows
163
+ hub_hidden.is_operator()
164
+
165
+ -- users can see their own mutes
166
+ or hub_hidden.current_user_id() = hub.chat_mute.user_id
167
+
168
+ -- experience owner can see all mutes
169
+ or hub_hidden.is_experience_owner()
170
+
171
+ -- anyone in chat_mod can see all mutes
172
+ or exists (
173
+ select 1
174
+ from hub.chat_mod
175
+ where user_id = hub_hidden.current_user_id()
176
+ and casino_id = hub.chat_mute.casino_id
177
+ and experience_id = hub.chat_mute.experience_id
178
+ )
179
+ );
180
+
181
+ create policy select_chat_mod on hub.chat_mod for select using (
182
+ -- operator can see all rows
183
+ hub_hidden.is_operator()
184
+
185
+ -- experience owner can see all mods
186
+ or hub_hidden.is_experience_owner()
187
+ );
188
+
189
+ -- relax the select_experience policy so that it's public
190
+ -- this lets us use hubCurrentExperience { hubChatMessages { ... } }
191
+ -- previously (in 001-schema.sql) this was scoped to operator only
192
+ alter policy select_experience on hub.experience using (true);
193
+
194
+ ----
195
+
196
+ -- add client_id to hub.user and hub.experience for playground sessions
197
+ alter table hub.user add column client_id uuid null;
198
+ alter table hub.experience add column client_id uuid null;
199
+
200
+ -- fk indexes, ensure client_id is unique per casino
201
+ create unique index user_playground_client_id_idx on hub.user(casino_id, client_id)
202
+ where client_id is not null;
203
+ create unique index experience_playground_client_id_idx on hub.experience(casino_id, client_id)
204
+ where client_id is not null;
205
+
206
+ -- Update select_user policy so users can see other users
207
+ -- This way they can see user info {id, uname} on chat messages
208
+ drop policy if exists select_user on hub.user;
209
+ create policy select_user on hub.user for select using (
210
+ -- Old:
211
+ -- hub_hidden.is_operator() or (id = hub_hidden.current_user_id())
212
+
213
+ -- New:
214
+ true
215
+ );
216
+
217
+ -- Here's how we could clean up rate limit buckets periodically
218
+ -- create or replace function hub_hidden.gc_chat_buckets() returns void language sql as $$
219
+ -- delete from hub.chat_rate_bucket
220
+ -- where bucket_start < now() - interval '2 days';
221
+ -- $$;
@@ -0,0 +1 @@
1
+ export declare const HubChatAfterIdConditionPlugin: GraphileConfig.Plugin;
@@ -0,0 +1,15 @@
1
+ import { sqlValueWithCodec, TYPES } from "postgraphile/@dataplan/pg";
2
+ import { sql } from "postgraphile/pg-sql2";
3
+ import { addPgTableCondition } from "postgraphile/utils";
4
+ export const HubChatAfterIdConditionPlugin = addPgTableCondition({
5
+ schemaName: "hub",
6
+ tableName: "chat_message",
7
+ }, "afterId", (build) => {
8
+ return {
9
+ description: "Get chat messages after a given ID",
10
+ type: build.getInputTypeByName("UUID"),
11
+ apply: (condition, value) => {
12
+ condition.where(sql `${condition.alias}.id > ${sqlValueWithCodec(value, TYPES.uuid)}`);
13
+ },
14
+ };
15
+ });
@@ -0,0 +1 @@
1
+ export declare const HubChatCreateSystemMessagePlugin: GraphileConfig.Plugin;
@@ -0,0 +1,124 @@
1
+ import { GraphQLError } from "graphql";
2
+ import { context, object, sideEffect } from "postgraphile/grafast";
3
+ import { extendSchema, gql } from "postgraphile/utils";
4
+ import z from "zod/v4";
5
+ import { extractFirstZodErrorMessage } from "../../util.js";
6
+ import { exactlyOneRow, maybeOneRow, withPgPoolTransaction, } from "../../db/index.js";
7
+ const InputSchema = z.object({
8
+ clientId: z.uuid("Invalid client ID"),
9
+ experienceId: z.uuid("Invalid experience ID"),
10
+ body: z
11
+ .string()
12
+ .transform(normalizeSystemMessageBody)
13
+ .refine((body) => body.length > 0, "Body is required")
14
+ .refine((body) => body.length <= 140, `Max body length: 140 chars`),
15
+ });
16
+ export const HubChatCreateSystemMessagePlugin = extendSchema((build) => {
17
+ const chatMessageTable = build.input.pgRegistry.pgResources.hub_chat_message;
18
+ return {
19
+ typeDefs: gql `
20
+ input HubChatCreateSystemMessageInput {
21
+ clientId: UUID!
22
+ body: String!
23
+ experienceId: UUID!
24
+ }
25
+
26
+ type HubChatCreateSystemMessagePayload {
27
+ chatMessage: HubChatMessage!
28
+ }
29
+
30
+ extend type Mutation {
31
+ hubChatCreateSystemMessage(
32
+ input: HubChatCreateSystemMessageInput!
33
+ ): HubChatCreateSystemMessagePayload
34
+ }
35
+ `,
36
+ objects: {
37
+ Mutation: {
38
+ plans: {
39
+ hubChatCreateSystemMessage(_, { $input }) {
40
+ const $identity = context().get("identity");
41
+ const $superuserPool = context().get("superuserPool");
42
+ const $chatMessageId = sideEffect([$input, $identity, $superuserPool], async ([rawInput, identity, superuserPool]) => {
43
+ if (identity?.kind === "operator" ||
44
+ (identity?.kind === "user" &&
45
+ identity?.session.is_experience_owner)) {
46
+ }
47
+ else {
48
+ throw new GraphQLError("Unauthorized");
49
+ }
50
+ let input;
51
+ try {
52
+ input = InputSchema.parse(rawInput);
53
+ }
54
+ catch (e) {
55
+ if (e instanceof z.ZodError) {
56
+ throw new GraphQLError(extractFirstZodErrorMessage(e));
57
+ }
58
+ throw e;
59
+ }
60
+ const dbExperience = await superuserPool
61
+ .query({
62
+ text: `
63
+ SELECT id, casino_id
64
+ FROM hub.experience
65
+ WHERE id = $1`,
66
+ values: [input.experienceId],
67
+ })
68
+ .then(maybeOneRow);
69
+ if (!dbExperience) {
70
+ throw new GraphQLError("Experience not found");
71
+ }
72
+ return await withPgPoolTransaction(superuserPool, async (pgClient) => {
73
+ const dbChatMessage = await pgClient
74
+ .query({
75
+ text: `
76
+ with ins as (
77
+ insert into hub.chat_message (casino_id, experience_id, client_id, body, type)
78
+ values ($1, $2, $3, $4, 'system')
79
+ on conflict (casino_id, experience_id, client_id) where type = 'system'
80
+ do nothing
81
+ returning id
82
+ )
83
+ select id from ins
84
+ union all
85
+ select id
86
+ from hub.chat_message
87
+ where casino_id = $1 and experience_id = $2 and client_id = $3
88
+ limit 1
89
+ `,
90
+ values: [
91
+ dbExperience.casino_id,
92
+ dbExperience.id,
93
+ input.clientId,
94
+ input.body,
95
+ ],
96
+ })
97
+ .then(exactlyOneRow);
98
+ const notifyPayload = {
99
+ type: "message",
100
+ chat_message_id: dbChatMessage.id,
101
+ };
102
+ await pgClient.query({
103
+ text: `select pg_notify('hub:chat:${dbExperience.id}', $1::text)`,
104
+ values: [JSON.stringify(notifyPayload)],
105
+ });
106
+ return dbChatMessage.id;
107
+ });
108
+ });
109
+ return object({
110
+ chatMessage: chatMessageTable.get({ id: $chatMessageId }),
111
+ });
112
+ },
113
+ },
114
+ },
115
+ },
116
+ };
117
+ });
118
+ function normalizeSystemMessageBody(body) {
119
+ return body
120
+ .normalize("NFC")
121
+ .replace(/[\u200B-\u200D\uFEFF]/g, "")
122
+ .replace(/\s+/g, " ")
123
+ .trim();
124
+ }
@@ -0,0 +1 @@
1
+ export declare const HubChatCreateUserMessagePlugin: GraphileConfig.Plugin;