@wbce-d9/api 12.4.5 → 12.5.1

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.
@@ -118,9 +118,12 @@ export function createSAMLAuthRouter(providerName) {
118
118
  const relayState = req.body?.RelayState;
119
119
  try {
120
120
  const { sp, idp } = getAuthProvider(providerName);
121
- const { extract } = await sp.parseLoginResponse(idp, 'post', req);
121
+ const { extract } = await sp.parseLoginResponse(idp, 'post', {
122
+ query: req.query,
123
+ body: req.body,
124
+ });
122
125
  const authService = new AuthenticationService({ accountability: req.accountability, schema: req.schema });
123
- const { accessToken, refreshToken, expires } = await authService.login(providerName, extract.attributes);
126
+ const { accessToken, refreshToken, expires } = await authService.login(providerName, extract.attributes ?? {});
124
127
  res.locals['payload'] = {
125
128
  data: {
126
129
  access_token: accessToken,
@@ -97,6 +97,6 @@ export default async function init() {
97
97
  process.stdout.write(`\nThe configuration can be found in ${chalk.green(rootPath + '/.env')}\n`);
98
98
  process.stdout.write(`\nStart Directus by running:\n`);
99
99
  process.stdout.write(` ${chalk.blue('cd')} ${rootPath}\n`);
100
- process.stdout.write(` ${chalk.blue('npx directus')} start\n`);
100
+ process.stdout.write(` ${chalk.blue('npx d9')} start\n`);
101
101
  process.exit(0);
102
102
  }
@@ -0,0 +1,3 @@
1
+ import type { Knex } from 'knex';
2
+ export declare function up(knex: Knex): Promise<void>;
3
+ export declare function down(knex: Knex): Promise<void>;
@@ -0,0 +1,12 @@
1
+ export async function up(knex) {
2
+ await knex.schema.alterTable('directus_users', (table) => {
3
+ table.timestamp('date_created').nullable();
4
+ table.timestamp('date_updated').nullable();
5
+ });
6
+ }
7
+ export async function down(knex) {
8
+ await knex.schema.alterTable('directus_users', (table) => {
9
+ table.dropColumn('date_created');
10
+ table.dropColumn('date_updated');
11
+ });
12
+ }
@@ -164,6 +164,26 @@ fields:
164
164
  display_options:
165
165
  relative: true
166
166
 
167
+ - field: date_created
168
+ special:
169
+ - date-created
170
+ - cast-timestamp
171
+ width: half
172
+ display: datetime
173
+ readonly: true
174
+ display_options:
175
+ relative: true
176
+
177
+ - field: date_updated
178
+ special:
179
+ - date-updated
180
+ - cast-timestamp
181
+ width: half
182
+ display: datetime
183
+ readonly: true
184
+ display_options:
185
+ relative: true
186
+
167
187
  - field: provider
168
188
  width: half
169
189
  interface: select-dropdown
@@ -26,6 +26,17 @@ data:
26
26
  many_field: activity
27
27
  one_collection: directus_activity
28
28
  one_field: revisions
29
+ # `item` + `collection` form an any-to-one (A2O) relation: `item` holds the
30
+ # referenced record's primary key, `collection` names its collection.
31
+ # `one_allowed_collections` is filled at runtime in RelationsService.stitchRelations.
32
+ - many_collection: directus_revisions
33
+ many_field: item
34
+ one_collection_field: collection
35
+ one_allowed_collections: []
36
+ - many_collection: directus_activity
37
+ many_field: item
38
+ one_collection_field: collection
39
+ one_allowed_collections: []
29
40
  - many_collection: directus_presets
30
41
  many_field: user
31
42
  one_collection: directus_users
@@ -508,7 +508,7 @@ export class CollectionsService {
508
508
  }
509
509
  }
510
510
  const a2oRelationsThatIncludeThisCollection = this.schema.relations.filter((relation) => {
511
- return relation.meta?.one_allowed_collections?.includes(collectionKey);
511
+ return !relation.meta?.system && relation.meta?.one_allowed_collections?.includes(collectionKey);
512
512
  });
513
513
  for (const relation of a2oRelationsThatIncludeThisCollection) {
514
514
  const newAllowedCollections = relation
@@ -224,7 +224,7 @@ export class FilesService extends ItemsService {
224
224
  const payload = {
225
225
  filename_download: filename,
226
226
  storage: toArray(env['STORAGE_LOCATIONS'])[0],
227
- type: fileResponse.headers['content-type'],
227
+ type: fileResponse.headers['content-type'] ?? null,
228
228
  title: formatTitle(filename),
229
229
  ...(body || {}),
230
230
  };
@@ -369,7 +369,9 @@ export class GraphQLService {
369
369
  [relation.field]: {
370
370
  type: new GraphQLUnionType({
371
371
  name: `${relation.collection}_${relation.field}_union`,
372
- types: relation.meta.one_allowed_collections.map((collection) => CollectionTypes[collection].getType()),
372
+ types: relation.meta.one_allowed_collections
373
+ .filter((collection) => collection in CollectionTypes)
374
+ .map((collection) => CollectionTypes[collection].getType()),
373
375
  resolveType(_value, context, info) {
374
376
  let path = [];
375
377
  let currentPath = info.path;
@@ -383,7 +385,7 @@ export class GraphQLService {
383
385
  parent = parent[pathPart];
384
386
  }
385
387
  const collection = parent[relation.meta.one_collection_field];
386
- return CollectionTypes[collection].getType().name;
388
+ return CollectionTypes[collection]?.getType().name;
387
389
  },
388
390
  }),
389
391
  resolve: (obj, _, __, info) => {
@@ -921,6 +923,8 @@ export class GraphQLService {
921
923
  else if (relation.meta?.one_allowed_collections) {
922
924
  ReadableCollectionFilterTypes[relation.collection]?.removeField('item');
923
925
  for (const collection of relation.meta.one_allowed_collections) {
926
+ if (!(collection in ReadableCollectionFilterTypes))
927
+ continue;
924
928
  ReadableCollectionFilterTypes[relation.collection]?.addFields({
925
929
  [`item__${collection}`]: ReadableCollectionFilterTypes[collection],
926
930
  });
@@ -8,6 +8,14 @@ import type { QueryOptions } from './items.js';
8
8
  import { ItemsService } from './items.js';
9
9
  import { PermissionsService } from './permissions.js';
10
10
  export declare class RelationsService {
11
+ /**
12
+ * A2O relations whose `one_allowed_collections` is filled with all collections at read time
13
+ * (see stitchRelations). Identified by collection + field rather than a sentinel value.
14
+ */
15
+ static readonly expandAllRelations: {
16
+ collection: string;
17
+ field: string;
18
+ }[];
11
19
  knex: Knex;
12
20
  permissionsService: PermissionsService;
13
21
  schemaInspector: SchemaInspector;
@@ -10,7 +10,15 @@ import { getDefaultIndexName } from '../utils/get-default-index-name.js';
10
10
  import { getSchema } from '../utils/get-schema.js';
11
11
  import { ItemsService } from './items.js';
12
12
  import { PermissionsService } from './permissions.js';
13
- export class RelationsService {
13
+ class RelationsService {
14
+ /**
15
+ * A2O relations whose `one_allowed_collections` is filled with all collections at read time
16
+ * (see stitchRelations). Identified by collection + field rather than a sentinel value.
17
+ */
18
+ static expandAllRelations = [
19
+ { collection: 'directus_activity', field: 'item' },
20
+ { collection: 'directus_revisions', field: 'item' },
21
+ ];
14
22
  knex;
15
23
  permissionsService;
16
24
  schemaInspector;
@@ -383,6 +391,19 @@ export class RelationsService {
383
391
  };
384
392
  });
385
393
  results.push(...remainingMetaRows);
394
+ /**
395
+ * `directus_activity.item` and `directus_revisions.item` are any-to-one (A2O) relations whose
396
+ * `item` can reference any collection. We declare them as system relations (see relations.yaml)
397
+ * but their `one_allowed_collections` can't be hardcoded, so we fill it with the current set of
398
+ * collections here, in memory, on every read. This powers A2O filtering/permissions and the
399
+ * app's per-collection filter drill-down without persisting an ever-stale list.
400
+ */
401
+ for (const relation of results) {
402
+ const isExpandAll = RelationsService.expandAllRelations.some((r) => r.collection === relation.collection && r.field === relation.field);
403
+ if (isExpandAll && relation.meta) {
404
+ relation.meta.one_allowed_collections = Object.keys(this.schema.collections);
405
+ }
406
+ }
386
407
  return results;
387
408
  }
388
409
  /**
@@ -445,3 +466,4 @@ export class RelationsService {
445
466
  }
446
467
  }
447
468
  }
469
+ export { RelationsService };
@@ -294,17 +294,19 @@ export class UsersService extends ItemsService {
294
294
  });
295
295
  for (const email of emails) {
296
296
  // Check if user is known
297
- const user = await this.getUserByEmail(email);
297
+ let user = await this.getUserByEmail(email);
298
+ const isEmptyUser = isEmpty(user);
298
299
  // Create user first to verify uniqueness if unknown
299
- if (isEmpty(user)) {
300
+ if (isEmptyUser) {
300
301
  await this.createOne({ email, role, status: 'invited' }, opts);
302
+ user = await this.getUserByEmail(email);
301
303
  // For known users update role if changed
302
304
  }
303
305
  else if (user.status === 'invited' && user.role !== role) {
304
306
  await this.updateOne(user.id, { role }, opts);
305
307
  }
306
308
  // Send invite for new and already invited users
307
- if (isEmpty(user) || user.status === 'invited') {
309
+ if (isEmptyUser || user.status === 'invited') {
308
310
  const subjectLine = subject ?? "You've been invited";
309
311
  await mailService.send({
310
312
  to: user.email,
@@ -73,7 +73,7 @@ function addJoin({ path, collection, aliasMap, rootQuery, schema, relations, kne
73
73
  }
74
74
  rootQuery.leftJoin({ [alias]: pathScope }, (joinClause) => {
75
75
  joinClause
76
- .onVal(relation.meta.one_collection_field, '=', pathScope)
76
+ .onVal(`${aliasedParentCollection}.${relation.meta.one_collection_field}`, '=', pathScope)
77
77
  .andOn(`${aliasedParentCollection}.${relation.field}`, '=', knex.raw(getHelpers(knex).schema.castA2oPrimaryKey(), `${alias}.${schema.collections[pathScope].primary}`));
78
78
  });
79
79
  aliasMap[aliasKey].collection = pathScope;
@@ -81,7 +81,7 @@ function addJoin({ path, collection, aliasMap, rootQuery, schema, relations, kne
81
81
  else if (relationType === 'o2a') {
82
82
  rootQuery.leftJoin({ [alias]: relation.collection }, (joinClause) => {
83
83
  joinClause
84
- .onVal(relation.meta.one_collection_field, '=', parentCollection)
84
+ .onVal(`${alias}.${relation.meta.one_collection_field}`, '=', parentCollection)
85
85
  .andOn(`${alias}.${relation.field}`, '=', knex.raw(getHelpers(knex).schema.castA2oPrimaryKey(), `${aliasedParentCollection}.${schema.collections[parentCollection].primary}`));
86
86
  });
87
87
  aliasMap[aliasKey].collection = relation.collection;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wbce-d9/api",
3
- "version": "12.4.5",
3
+ "version": "12.5.1",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -66,7 +66,7 @@
66
66
  "@rollup/plugin-node-resolve": "15.0.2",
67
67
  "@rollup/plugin-virtual": "3.0.1",
68
68
  "@wbce-d9/format-title": "10.0.0",
69
- "axios": "^1.12.0",
69
+ "axios": "^1.16.0",
70
70
  "chalk": "5.2.0",
71
71
  "commander": "10.0.1",
72
72
  "date-fns": "2.29.3",
@@ -74,8 +74,8 @@
74
74
  "express": "^4.21.2",
75
75
  "fs-extra": "11.1.1",
76
76
  "inquirer": "9.1.5",
77
- "joi": "17.9.1",
78
- "js-yaml": "4.1.1",
77
+ "joi": "18.2.1",
78
+ "js-yaml": "4.2.0",
79
79
  "json2csv": "5.0.7",
80
80
  "knex": "3.1.0",
81
81
  "lodash-es": "4.18.1",
@@ -89,7 +89,7 @@
89
89
  "uuid": "14.0.0",
90
90
  "wellknown": "0.5.0",
91
91
  "@authenio/samlify-node-xmllint": "2.0.0",
92
- "@aws-sdk/client-sesv2": "3.1004.0",
92
+ "@aws-sdk/client-sesv2": "3.1062.0",
93
93
  "@godaddy/terminus": "4.12.0",
94
94
  "@rollup/plugin-alias": "5.0.0",
95
95
  "argon2": "0.44.0",
@@ -120,21 +120,21 @@
120
120
  "jsonwebtoken": "9.0.0",
121
121
  "keyv": "4.5.2",
122
122
  "ldapjs": "2.3.3",
123
- "liquidjs": "10.25.7",
123
+ "liquidjs": "10.26.0",
124
124
  "mime-types": "2.1.35",
125
125
  "ms": "2.1.3",
126
126
  "node-cron": "3.0.2",
127
127
  "node-machine-id": "1.1.12",
128
- "nodemailer": "8.0.5",
128
+ "nodemailer": "9.0.3",
129
129
  "object-hash": "3.0.0",
130
130
  "openid-client": "6.8.2",
131
131
  "otplib": "12.0.1",
132
132
  "pino-http": "8.3.3",
133
133
  "pino-http-print": "3.1.0",
134
134
  "pino-pretty": "10.0.0",
135
- "qs": "6.14.2",
135
+ "qs": "6.15.2",
136
136
  "rate-limiter-flexible": "2.4.1",
137
- "samlify": "2.10.0",
137
+ "samlify": "2.13.0",
138
138
  "sanitize-html": "2.12.1",
139
139
  "sharp": "0.32.6",
140
140
  "snappy": "7.2.2",
@@ -143,17 +143,17 @@
143
143
  "tmp-promise": "3.0.3",
144
144
  "tsx": "3.12.6",
145
145
  "uuid-validate": "0.0.3",
146
- "@wbce-d9/app": "10.0.5",
147
- "@wbce-d9/exceptions": "10.0.0",
146
+ "@wbce-d9/app": "10.1.0",
147
+ "@wbce-d9/constants": "10.0.0",
148
148
  "@wbce-d9/extensions-sdk": "10.0.2",
149
149
  "@wbce-d9/schema": "10.0.0",
150
150
  "@wbce-d9/storage": "10.0.0",
151
151
  "@wbce-d9/specs": "9.26.5",
152
- "@wbce-d9/constants": "10.0.0",
153
- "@wbce-d9/storage-driver-azure": "10.0.1",
152
+ "@wbce-d9/exceptions": "10.0.0",
154
153
  "@wbce-d9/storage-driver-cloudinary": "10.0.2",
155
- "@wbce-d9/storage-driver-local": "10.0.1",
156
154
  "@wbce-d9/storage-driver-gcs": "10.0.1",
155
+ "@wbce-d9/storage-driver-azure": "10.0.1",
156
+ "@wbce-d9/storage-driver-local": "10.0.1",
157
157
  "@wbce-d9/storage-driver-s3": "10.0.1",
158
158
  "@wbce-d9/update-check": "10.0.2",
159
159
  "@wbce-d9/utils": "10.0.1"
@@ -202,7 +202,7 @@
202
202
  "@types/stream-json": "1.7.3",
203
203
  "@types/uuid-validate": "0.0.1",
204
204
  "copyfiles": "2.4.1",
205
- "form-data": "4.0.4",
205
+ "form-data": "4.0.6",
206
206
  "knex-mock-client": "3.0.2",
207
207
  "@wbce-d9/types": "10.1.0"
208
208
  },