@plusscommunities/pluss-core-aws 2.0.26-auth.0 → 2.0.26-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/aws/getDefaultEmailAddress.js +21 -21
- package/aws/getEmailService.js +16 -16
- package/aws/getEmailServiceInfo.js +26 -26
- package/aws/sendEmail.js +31 -31
- package/config.js +1 -1
- package/db/activity/publishActivity.js +22 -22
- package/db/analytics/checkActivityExists.js +15 -15
- package/db/analytics/logAnalyticsActivity.js +69 -37
- package/db/analytics/scheduleOldAggregation.js +14 -14
- package/db/auth/getSiteSetting.js +12 -12
- package/db/auth/getSiteUserTypes.js +16 -16
- package/db/auth/getUserAuth.js +13 -13
- package/db/automatedactions/getActionBySiteTrigger.js +9 -9
- package/db/common/deleteRef.js +21 -21
- package/db/common/editRef.js +36 -36
- package/db/common/getRef.js +23 -23
- package/db/common/getTableCount.js +18 -18
- package/db/common/indexQuery.js +17 -17
- package/db/common/indexQueryRecursive.js +20 -20
- package/db/common/scanRef.js +18 -18
- package/db/common/scanRefRecursive.js +20 -20
- package/db/common/updateAttribute.js +27 -27
- package/db/common/updateRef.js +20 -20
- package/db/linkedUsers/getLinkedBy.js +21 -21
- package/db/linkedUsers/getLinkedTo.js +21 -21
- package/db/notifications/deleteNotificationsByEntity.js +21 -21
- package/db/notifications/getNotificationSetting.js +14 -14
- package/db/notifications/publishNotifications.js +39 -39
- package/db/postgres/getKnex.js +214 -0
- package/db/postgres/safeError.js +40 -0
- package/db/property/Property.js +128 -0
- package/db/property/PropertyRepository.js +86 -0
- package/db/scheduledActions/deleteActionQueue.js +1 -1
- package/db/scheduledActions/getActionQueueByEntityId.js +10 -10
- package/db/scheduledActions/getActionQueueByEntityKey.js +9 -9
- package/db/scheduledActions/getActionQueueById.js +9 -9
- package/db/scheduledActions/getActionQueueByTriggerAt.js +14 -14
- package/db/scheduledActions/updateActionQueue.js +29 -29
- package/db/strings/getString.js +20 -20
- package/db/strings/logUpdate.js +18 -18
- package/db/templates/getTemplateById.js +1 -1
- package/db/templates/getTemplatesList.js +10 -10
- package/db/templates/updateTemplate.js +9 -9
- package/db/users/getRole.js +1 -1
- package/db/users/getUser.js +9 -9
- package/db/users/getUserByEmail.js +17 -17
- package/helper/audience/filterByAudienceType.js +27 -27
- package/helper/audience/filterOnAudienceType.js +26 -26
- package/helper/audience/getAudience.js +187 -187
- package/helper/audience/getMatchingAudienceTypes.js +21 -21
- package/helper/audience/getMatchingAudienceTypesFromPreview.js +60 -60
- package/helper/audience/getMatchingTags.js +15 -15
- package/helper/audience/isValidAudience.js +20 -20
- package/helper/auth/checkTokenBlacklist.js +17 -17
- package/helper/auth/getApiKeyFromReq.js +2 -2
- package/helper/auth/getSessionUser.js +70 -85
- package/helper/auth/getSessionUserFromReq.js +2 -2
- package/helper/auth/getSessionUserFromReqAuthKey.js +11 -11
- package/helper/auth/validateApiKey.js +32 -32
- package/helper/auth/validateMasterAuth.js +174 -174
- package/helper/auth/validateSiteAccess.js +12 -12
- package/helper/auth/validateSiteSetting.js +7 -7
- package/helper/auth/validateUserLoggedIn.js +19 -19
- package/helper/createGuid.js +5 -5
- package/helper/generateJsonResponse.js +27 -27
- package/helper/getUserPreview.js +57 -57
- package/helper/getUserPreviewFromHeader.js +17 -17
- package/helper/getUserPreviewFromReq.js +17 -17
- package/helper/hqPublishing.js +465 -0
- package/helper/index.js +28 -28
- package/helper/notifySiteConfigs.js +67 -52
- package/helper/opengraph/getOpenGraph.js +12 -12
- package/helper/rates/checkRateLimit.js +38 -38
- package/helper/requestToSource.js +10 -10
- package/helper/sendEmail.js +120 -120
- package/helper/templates/replacePlaceHolders.js +29 -29
- package/helper/time/getLocalTimestamp.js +18 -18
- package/helper/time/getSiteTimezone.js +11 -11
- package/helper/triggerAutomatedAction.js +25 -25
- package/helper/userToUserPreview.js +23 -23
- package/helper/users/getUserTypesByPermission.js +24 -24
- package/helper/users/getUsersByPermission.js +20 -20
- package/notification/prepNotification.js +144 -144
- package/notification/sendNotifications.js +166 -166
- package/package.json +37 -40
- package/templates/supportTicketEmails.js +8 -8
- package/helper/auth/context/AuthenticationContext.js +0 -50
- package/helper/auth/context/AuthenticationStrategy.js +0 -20
- package/helper/auth/context/auth0/Strategy.js +0 -12
- package/helper/auth/context/auth0/functions/decodeAccessToken.js +0 -102
- package/helper/auth/context/auth0/functions/getSessionUser.js +0 -21
- package/helper/auth/context/boltonclarke/Strategy.js +0 -10
- package/helper/auth/context/cognito/Strategy.js +0 -12
- package/helper/auth/context/cognito/functions/getSessionUser.js +0 -76
|
@@ -5,50 +5,50 @@ const sendNotifications = require("../../notification/sendNotifications");
|
|
|
5
5
|
const prepNotification = require("../../notification/prepNotification");
|
|
6
6
|
|
|
7
7
|
const saveNotification = (receiver, notification) => {
|
|
8
|
-
|
|
8
|
+
const toSave = { ...notification };
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
toSave.UserId = receiver;
|
|
11
|
+
toSave.RowId = getMultiRowId([toSave.Site, toSave.Type, toSave.Id, receiver]);
|
|
12
|
+
toSave.Seen = false;
|
|
13
|
+
toSave.Clicked = false;
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
updateRef(`notifications`, toSave)
|
|
16
|
+
.then(() => {
|
|
17
|
+
//resolve();
|
|
18
|
+
})
|
|
19
|
+
.catch((error) => {
|
|
20
|
+
//reject(error);
|
|
21
|
+
});
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
module.exports = async (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
receivers,
|
|
26
|
+
type,
|
|
27
|
+
site,
|
|
28
|
+
id,
|
|
29
|
+
data,
|
|
30
|
+
sendPush,
|
|
31
|
+
config = { type: "app", id: null, ignoreMute: false, raiseError: false },
|
|
32
32
|
) => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
const notification = {};
|
|
34
|
+
notification.Timestamp = moment.utc().valueOf();
|
|
35
|
+
notification.Type = type;
|
|
36
|
+
notification.Site = site;
|
|
37
|
+
notification.Id = id;
|
|
38
|
+
notification.Data = data;
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
40
|
+
receivers.forEach((receiver) => {
|
|
41
|
+
saveNotification(receiver, notification);
|
|
42
|
+
});
|
|
43
|
+
if (sendPush) {
|
|
44
|
+
const notiData = prepNotification(notification);
|
|
45
|
+
await sendNotifications(
|
|
46
|
+
receivers,
|
|
47
|
+
notiData.Text,
|
|
48
|
+
type,
|
|
49
|
+
id,
|
|
50
|
+
notiData,
|
|
51
|
+
config,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
54
|
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
const AWS = require("aws-sdk");
|
|
2
|
+
const knex = require("knex");
|
|
3
|
+
const safeError = require("./safeError");
|
|
4
|
+
const { log, generateLogId } = require("../../helper");
|
|
5
|
+
|
|
6
|
+
const secrets = new AWS.SecretsManager();
|
|
7
|
+
|
|
8
|
+
let db;
|
|
9
|
+
let pending;
|
|
10
|
+
let generation = 0;
|
|
11
|
+
|
|
12
|
+
const SUPERSEDED_INITIALISATION = "POSTGRES_CLIENT_SUPERSEDED";
|
|
13
|
+
const STALE_CLIENT_DESTROY_FAILED = "POSTGRES_STALE_CLIENT_DESTROY_FAILED";
|
|
14
|
+
|
|
15
|
+
function poolMax() {
|
|
16
|
+
return Number(process.env.POSTGRES_POOL_MAX || 1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function secretArn() {
|
|
20
|
+
return process.env.POSTGRES_SECRET_ARN;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let sslWarningEmitted = false;
|
|
24
|
+
|
|
25
|
+
function ssl() {
|
|
26
|
+
if (process.env.POSTGRES_SSL === "disable") {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const rejectUnauthorized =
|
|
31
|
+
process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED !== "false";
|
|
32
|
+
|
|
33
|
+
// Certificate verification is on by default. If a local/dev environment needs
|
|
34
|
+
// to opt out, log that once so the downgrade is visible in runtime logs.
|
|
35
|
+
if (!rejectUnauthorized && !sslWarningEmitted) {
|
|
36
|
+
sslWarningEmitted = true;
|
|
37
|
+
log("getKnex", "SslVerificationDisabled", {
|
|
38
|
+
reason:
|
|
39
|
+
"POSTGRES_SSL_REJECT_UNAUTHORIZED=false disables server certificate verification",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { rejectUnauthorized };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const SECRET_FIELDS = {
|
|
47
|
+
host: "host",
|
|
48
|
+
username: "username",
|
|
49
|
+
password: "password",
|
|
50
|
+
dbname: "dbname",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
async function secretConnection(arn) {
|
|
54
|
+
let result;
|
|
55
|
+
try {
|
|
56
|
+
result = await secrets.getSecretValue({ SecretId: arn }).promise();
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// AWS SDK fetch errors describe the API call, not the secret body. Keep the
|
|
59
|
+
// message so the ARN points operators at the failed secret.
|
|
60
|
+
throw new Error(`Failed to load secret ${arn}: ${error.message}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let secret;
|
|
64
|
+
try {
|
|
65
|
+
secret = JSON.parse(result.SecretString);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
// V8 SyntaxError messages include a slice of the parsed input. If SecretString
|
|
68
|
+
// is a bare connection string, error.message would leak credentials.
|
|
69
|
+
throw new Error(`Failed to parse secret ${arn} as JSON`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!secret || typeof secret !== "object") {
|
|
73
|
+
// JSON.parse("null") or a bare primitive would otherwise become a TypeError
|
|
74
|
+
// without ARN context. Treat it as a bad secret body.
|
|
75
|
+
throw new Error(`Failed to parse secret ${arn} as JSON`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const missing = Object.keys(SECRET_FIELDS).filter((field) => !secret[field]);
|
|
79
|
+
if (missing.length) {
|
|
80
|
+
// A secret missing host makes node-postgres silently dial localhost; a
|
|
81
|
+
// missing username/dbname falls back to the OS user / default db. Fail
|
|
82
|
+
// loudly with the field names (never their values).
|
|
83
|
+
throw new Error(`Secret ${arn} is missing fields: ${missing.join(", ")}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
host: secret.host,
|
|
88
|
+
port: secret.port ? Number(secret.port) : 5432,
|
|
89
|
+
database: secret.dbname,
|
|
90
|
+
user: secret.username,
|
|
91
|
+
password: secret.password,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function configSourceVar() {
|
|
96
|
+
if (process.env.POSTGRES_SECRET_ARN) return "POSTGRES_SECRET_ARN";
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function connection() {
|
|
101
|
+
const arn = secretArn();
|
|
102
|
+
if (!arn) {
|
|
103
|
+
throw new Error("PostgreSQL connection requires POSTGRES_SECRET_ARN");
|
|
104
|
+
}
|
|
105
|
+
return secretConnection(arn);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function createKnex(overrides = {}) {
|
|
109
|
+
const database = await connection();
|
|
110
|
+
const max = overrides.poolMax || poolMax();
|
|
111
|
+
|
|
112
|
+
// This log is not gated by enableLog. Log the source and host only, never the
|
|
113
|
+
// secret ARN or credentials.
|
|
114
|
+
log("getKnex", "CreatingClient", {
|
|
115
|
+
source: configSourceVar(),
|
|
116
|
+
host: database.host || "url",
|
|
117
|
+
max,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const instance = knex({
|
|
121
|
+
client: "pg",
|
|
122
|
+
// Knex otherwise mutates error.message to inline bound values in the
|
|
123
|
+
// compiled SQL. Keep thrown errors parameterized; log paths use safeError().
|
|
124
|
+
compileSqlOnError: false,
|
|
125
|
+
connection: {
|
|
126
|
+
...database,
|
|
127
|
+
ssl: ssl(),
|
|
128
|
+
connectionTimeoutMillis: 10000,
|
|
129
|
+
},
|
|
130
|
+
pool: {
|
|
131
|
+
min: 0,
|
|
132
|
+
max,
|
|
133
|
+
idleTimeoutMillis: 60000,
|
|
134
|
+
acquireTimeoutMillis: 10000,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
instance.on("query-error", (error) => {
|
|
139
|
+
// Use a fresh id per event. Reusing the pool-creation id would put every
|
|
140
|
+
// query error from this instance under one trace id.
|
|
141
|
+
log("getKnex", "QueryError", safeError(error), generateLogId());
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return instance;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function getKnex() {
|
|
148
|
+
if (db) return db;
|
|
149
|
+
if (!pending) {
|
|
150
|
+
const pendingGeneration = generation;
|
|
151
|
+
pending = createKnex()
|
|
152
|
+
.then(async (instance) => {
|
|
153
|
+
if (generation !== pendingGeneration) {
|
|
154
|
+
try {
|
|
155
|
+
await instance.destroy();
|
|
156
|
+
} catch (destroyError) {
|
|
157
|
+
log("getKnex", "DestroyStaleError", safeError(destroyError));
|
|
158
|
+
const error = new Error(
|
|
159
|
+
`Failed to destroy superseded PostgreSQL client: ${destroyError.message}`,
|
|
160
|
+
);
|
|
161
|
+
error.code = STALE_CLIENT_DESTROY_FAILED;
|
|
162
|
+
error.cause = destroyError;
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
const error = new Error(
|
|
166
|
+
"PostgreSQL client initialisation was superseded by destroy()",
|
|
167
|
+
);
|
|
168
|
+
error.code = SUPERSEDED_INITIALISATION;
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
db = instance;
|
|
172
|
+
return instance;
|
|
173
|
+
})
|
|
174
|
+
.catch((error) => {
|
|
175
|
+
if (generation === pendingGeneration) pending = undefined;
|
|
176
|
+
throw error;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return pending;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
getKnex.destroy = async () => {
|
|
183
|
+
const instance = db;
|
|
184
|
+
const initializer = pending;
|
|
185
|
+
generation += 1;
|
|
186
|
+
db = undefined;
|
|
187
|
+
pending = undefined;
|
|
188
|
+
if (instance) {
|
|
189
|
+
await instance.destroy();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (initializer) {
|
|
194
|
+
try {
|
|
195
|
+
await initializer;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
// A pending initializer can reject because destroy() intentionally
|
|
198
|
+
// superseded it. Surface stale-pool cleanup failures, but keep benign
|
|
199
|
+
// cancellation and unrelated initializer failures out of teardown.
|
|
200
|
+
if (error?.code === STALE_CLIENT_DESTROY_FAILED) throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Create a non-singleton client. The migration runner sets poolMax: 2 because
|
|
206
|
+
// the advisory-lock connection stays checked out while migration queries use a
|
|
207
|
+
// second slot. The caller owns destroy().
|
|
208
|
+
getKnex.create = (overrides) => createKnex(overrides);
|
|
209
|
+
|
|
210
|
+
// Shared so the bootstrap path (provisionDatabaseUsers) honours the same
|
|
211
|
+
// POSTGRES_SSL / POSTGRES_SSL_REJECT_UNAUTHORIZED overrides as the pool.
|
|
212
|
+
getKnex.sslConfig = ssl;
|
|
213
|
+
|
|
214
|
+
module.exports = getKnex;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const PG_FIELDS = [
|
|
2
|
+
"code",
|
|
3
|
+
"severity",
|
|
4
|
+
"schema",
|
|
5
|
+
"table",
|
|
6
|
+
"column",
|
|
7
|
+
"constraint",
|
|
8
|
+
"routine",
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
// knex prefixes driver errors with compiled SQL and inlined bindings:
|
|
12
|
+
// "<compiled sql> - <original message>". Strip through the last " - " so
|
|
13
|
+
// bindings stay out of logs while the useful driver message survives.
|
|
14
|
+
function stripCompiledSql(message) {
|
|
15
|
+
const separator = message.lastIndexOf(" - ");
|
|
16
|
+
return separator === -1 ? message : message.slice(separator + 3);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function safeError(error) {
|
|
20
|
+
if (!error || typeof error !== "object") return { message: String(error) };
|
|
21
|
+
|
|
22
|
+
const output = {
|
|
23
|
+
name: error.name,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
for (const field of PG_FIELDS) {
|
|
27
|
+
if (error[field]) output[field] = error[field];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Keep a message. Structured PG fields cover constraint-class errors, but
|
|
31
|
+
// codes like 42P01 would otherwise lose the missing relation name. detail,
|
|
32
|
+
// hint, and where can carry raw values, so leave them out.
|
|
33
|
+
output.message = stripCompiledSql(String(error.message || error)).slice(
|
|
34
|
+
0,
|
|
35
|
+
512,
|
|
36
|
+
);
|
|
37
|
+
return output;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = safeError;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A property row from pc_property.properties, validated on construction.
|
|
3
|
+
*
|
|
4
|
+
* Build with Property.fromRow(row); the constructor rejects rows that break a
|
|
5
|
+
* domain invariant so a repository never hands a handler an unvalidated record.
|
|
6
|
+
*/
|
|
7
|
+
class Property {
|
|
8
|
+
#row;
|
|
9
|
+
|
|
10
|
+
constructor(row) {
|
|
11
|
+
if (!row) {
|
|
12
|
+
throw new Error("Property requires a row");
|
|
13
|
+
}
|
|
14
|
+
Property.assertValid(row);
|
|
15
|
+
this.#row = { ...row };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static fromRow(row) {
|
|
19
|
+
return new Property(row);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static assertValid(row) {
|
|
23
|
+
if (!row.id) throw new Error("Property requires an id");
|
|
24
|
+
if (!row.site_id) throw new Error("Property requires a site_id");
|
|
25
|
+
if (!row.name) throw new Error("Property requires a name");
|
|
26
|
+
if (row.status !== "active" && row.status !== "archived") {
|
|
27
|
+
throw new Error(`Property has an invalid status: ${row.status}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get id() {
|
|
32
|
+
return this.#row.id;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get site() {
|
|
36
|
+
return this.#row.site_id;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get name() {
|
|
40
|
+
return this.#row.name;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get propertyType() {
|
|
44
|
+
return this.#row.property_type ?? null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get location() {
|
|
48
|
+
return this.#row.location ?? null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get status() {
|
|
52
|
+
return this.#row.status;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get externalId() {
|
|
56
|
+
return this.#row.external_id ?? null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get metadata() {
|
|
60
|
+
return this.#row.metadata ?? {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get createdBy() {
|
|
64
|
+
return this.#row.created_by ?? null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get residentCount() {
|
|
68
|
+
return this.#row.resident_count !== undefined
|
|
69
|
+
? Number(this.#row.resident_count)
|
|
70
|
+
: undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get timeCreated() {
|
|
74
|
+
return this.#row.time_created ?? null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get timeUpdated() {
|
|
78
|
+
return this.#row.time_updated ?? null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get timeDeleted() {
|
|
82
|
+
return this.#row.time_deleted ?? null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get isActive() {
|
|
86
|
+
return this.#row.status === "active";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get isArchived() {
|
|
90
|
+
return this.#row.status === "archived";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get isDeleted() {
|
|
94
|
+
return this.#row.time_deleted != null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get canLinkResidents() {
|
|
98
|
+
return this.isActive && !this.isDeleted;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
toRow() {
|
|
102
|
+
return { ...this.#row };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
toApi() {
|
|
106
|
+
return {
|
|
107
|
+
id: this.#row.id,
|
|
108
|
+
site: this.#row.site_id,
|
|
109
|
+
name: this.#row.name,
|
|
110
|
+
propertyType: this.#row.property_type ?? null,
|
|
111
|
+
location: this.#row.location ?? null,
|
|
112
|
+
status: this.#row.status,
|
|
113
|
+
externalId: this.#row.external_id ?? null,
|
|
114
|
+
metadata: this.#row.metadata ?? {},
|
|
115
|
+
createdBy: this.#row.created_by ?? null,
|
|
116
|
+
// undefined (not null) so the key drops out of the JSON when there is no count.
|
|
117
|
+
residentCount:
|
|
118
|
+
this.#row.resident_count !== undefined
|
|
119
|
+
? Number(this.#row.resident_count)
|
|
120
|
+
: undefined,
|
|
121
|
+
timeCreated: this.#row.time_created ?? null,
|
|
122
|
+
timeUpdated: this.#row.time_updated ?? null,
|
|
123
|
+
timeDeleted: this.#row.time_deleted ?? null,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { Property };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const getKnex = require("../postgres/getKnex");
|
|
2
|
+
const { log } = require("../../helper");
|
|
3
|
+
const { Property } = require("./Property");
|
|
4
|
+
|
|
5
|
+
// PostgreSQL schema owned by the property service.
|
|
6
|
+
const SCHEMA = "pc_property";
|
|
7
|
+
const PROPERTIES = "properties";
|
|
8
|
+
const PROPERTY_USERS = "property_users";
|
|
9
|
+
|
|
10
|
+
const UUID_PATTERN =
|
|
11
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Read-only property lookups against the pc_property schema.
|
|
15
|
+
*
|
|
16
|
+
* Writes stay with the property service; extensions only resolve. Methods
|
|
17
|
+
* return Property entities or null — never raw rows.
|
|
18
|
+
*/
|
|
19
|
+
class PropertyRepository {
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the property a user is linked to at a site.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} input
|
|
24
|
+
* @param {string} input.site
|
|
25
|
+
* @param {string} input.userId
|
|
26
|
+
* @param {string} [input.logId] - optional trace id
|
|
27
|
+
* @returns {Promise<Property|null>} the linked property, or null when the user has no live link
|
|
28
|
+
* @throws {Error} on query failure, or when a live link references a missing property
|
|
29
|
+
*/
|
|
30
|
+
async forUser({ site, userId, logId }) {
|
|
31
|
+
if (!site || !userId) {
|
|
32
|
+
throw new Error("PropertyRepository.forUser requires site and userId");
|
|
33
|
+
}
|
|
34
|
+
log("PropertyRepository_forUser", "Input", { site, userId }, logId);
|
|
35
|
+
|
|
36
|
+
const knex = await getKnex();
|
|
37
|
+
// A user has at most one live property link.
|
|
38
|
+
const link = await knex
|
|
39
|
+
.withSchema(SCHEMA)
|
|
40
|
+
.table(PROPERTY_USERS)
|
|
41
|
+
.where({ user_id: userId, site_id: site, time_deleted: null })
|
|
42
|
+
.first();
|
|
43
|
+
if (!link) return null;
|
|
44
|
+
|
|
45
|
+
const row = await knex
|
|
46
|
+
.withSchema(SCHEMA)
|
|
47
|
+
.table(PROPERTIES)
|
|
48
|
+
.where({ id: link.property_id, site_id: site, time_deleted: null })
|
|
49
|
+
.first();
|
|
50
|
+
if (!row) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Property link integrity violation: link ${link.id} references missing property ${link.property_id} for user ${userId}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return Property.fromRow(row);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a property by id, scoped to a site.
|
|
60
|
+
*
|
|
61
|
+
* @param {object} input
|
|
62
|
+
* @param {string} input.site
|
|
63
|
+
* @param {string} input.propertyId
|
|
64
|
+
* @param {string} [input.logId] - optional trace id
|
|
65
|
+
* @returns {Promise<Property|null>} the property, or null when none matches at the site
|
|
66
|
+
* @throws {Error} on query failure
|
|
67
|
+
*/
|
|
68
|
+
async byId({ site, propertyId, logId }) {
|
|
69
|
+
if (!site || !propertyId) {
|
|
70
|
+
throw new Error("PropertyRepository.byId requires site and propertyId");
|
|
71
|
+
}
|
|
72
|
+
// A malformed id can't match anything; don't let it become a Postgres cast error.
|
|
73
|
+
if (!UUID_PATTERN.test(propertyId)) return null;
|
|
74
|
+
log("PropertyRepository_byId", "Input", { site, propertyId }, logId);
|
|
75
|
+
|
|
76
|
+
const knex = await getKnex();
|
|
77
|
+
const row = await knex
|
|
78
|
+
.withSchema(SCHEMA)
|
|
79
|
+
.table(PROPERTIES)
|
|
80
|
+
.where({ id: propertyId, site_id: site, time_deleted: null })
|
|
81
|
+
.first();
|
|
82
|
+
return row ? Property.fromRow(row) : null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { PropertyRepository };
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
const indexQuery = require("../common/indexQuery");
|
|
2
2
|
|
|
3
3
|
module.exports = async (entityKey, entityId) => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
const query = {
|
|
5
|
+
IndexName: "EntityKeyEntityIdIndex",
|
|
6
|
+
KeyConditionExpression: "EntityKey = :entityKey and EntityId = :entityId",
|
|
7
|
+
ExpressionAttributeValues: {
|
|
8
|
+
":entityKey": entityKey,
|
|
9
|
+
":entityId": entityId,
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const { Items } = await indexQuery("actionqueue", query);
|
|
14
|
+
return Items;
|
|
15
15
|
};
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
const indexQueryRecursive = require("../common/indexQueryRecursive");
|
|
2
2
|
|
|
3
3
|
module.exports = async (site, entityKey) => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
const query = {
|
|
5
|
+
IndexName: "SiteEntityKeyIndex",
|
|
6
|
+
KeyConditionExpression: "Site = :site and EntityKey = :entityKey",
|
|
7
|
+
ExpressionAttributeValues: {
|
|
8
|
+
":site": site,
|
|
9
|
+
":entityKey": entityKey,
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
return await indexQueryRecursive("actionqueue", query);
|
|
14
14
|
};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
const getRef = require("../common/getRef");
|
|
2
2
|
|
|
3
3
|
module.exports = (rowId) => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
getRef("actionqueue", "RowId", rowId)
|
|
6
|
+
.then((ev) => {
|
|
7
|
+
resolve(ev);
|
|
8
|
+
})
|
|
9
|
+
.catch((error) => {
|
|
10
|
+
reject(error);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
13
|
};
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
const indexQueryRecursive = require("../common/indexQueryRecursive");
|
|
2
2
|
|
|
3
3
|
module.exports = async (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
site,
|
|
5
|
+
minTime = 0,
|
|
6
|
+
maxTime = Number.MAX_SAFE_INTEGER,
|
|
7
7
|
) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
const query = {
|
|
9
|
+
IndexName: "SiteTriggerAtIndex",
|
|
10
|
+
KeyConditionExpression:
|
|
11
|
+
"Site = :site AND TriggerAt BETWEEN :minTime and :maxTime",
|
|
12
|
+
ExpressionAttributeValues: {
|
|
13
|
+
":site": site,
|
|
14
|
+
":minTime": minTime,
|
|
15
|
+
":maxTime": maxTime,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
return await indexQueryRecursive("actionqueue", query);
|
|
20
20
|
};
|