cruddl 6.0.0-alpha.1 → 6.0.0-alpha.2

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.
@@ -19,6 +19,11 @@ export interface QueryGenerationOptions {
19
19
  * See {@link ExecutionOptions.maxProjections} for details.
20
20
  */
21
21
  readonly maxProjections?: number;
22
+ /**
23
+ * An optional prefix prepended to all collection and view names in generated AQL queries.
24
+ * See {@link ArangoDBConfig.collectionNamePrefix} for details.
25
+ */
26
+ readonly collectionNamePrefix?: string;
22
27
  }
23
28
  export declare function getAQLQuery(node: QueryNode, options?: Partial<QueryGenerationOptions>): AQLCompoundQuery;
24
29
  export declare function generateTokenizationQuery(tokensFiltered: ReadonlyArray<FlexSearchTokenizable>): AQLFragment;
@@ -381,7 +381,9 @@ register(flex_search_js_2.FlexSearchQueryNode, (node, context) => {
381
381
  let itemContext = context
382
382
  .bindVariable(node.itemVariable)
383
383
  .withExtension(inFlexSearchFilterSymbol, true);
384
- const viewName = (0, arango_search_helpers_js_1.getFlexSearchViewNameForRootEntity)(node.rootEntityType);
384
+ const viewName = (0, arango_search_helpers_js_1.getFlexSearchViewNameForRootEntity)(node.rootEntityType, {
385
+ prefix: context.options.collectionNamePrefix,
386
+ });
385
387
  context.addCollectionAccess(viewName, AccessType.EXPLICIT_READ);
386
388
  return aqlExt.subquery((0, aql_js_1.aql) `FOR ${itemContext.getVariable(node.itemVariable)}`, (0, aql_js_1.aql) `IN ${aql_js_1.aql.collection(viewName)}`, (0, aql_js_1.aql) `SEARCH ${processNode(node.flexFilterNode, itemContext)}`, node.isOptimisationsDisabled ? (0, aql_js_1.aql) `OPTIONS { conditionOptimization: 'none' }` : (0, aql_js_1.aql) ``, (0, aql_js_1.aql) `RETURN ${itemContext.getVariable(node.itemVariable)}`);
387
389
  });
@@ -1837,10 +1839,10 @@ function getRelationTraversalForStatements({ node, innermostItemVar, context, pr
1837
1839
  let sourceFrag;
1838
1840
  if (node.entitiesIdentifierKind === mutations_js_1.EntitiesIdentifierKind.ID) {
1839
1841
  if (node.sourceIsList) {
1840
- sourceFrag = getFullIDFromKeysFragment(plainSourceFrag, node.relationSegments[0].relationSide.sourceType);
1842
+ sourceFrag = getFullIDFromKeysFragment(plainSourceFrag, node.relationSegments[0].relationSide.sourceType, context);
1841
1843
  }
1842
1844
  else {
1843
- sourceFrag = getFullIDFromKeyFragment(plainSourceFrag, node.relationSegments[0].relationSide.sourceType);
1845
+ sourceFrag = getFullIDFromKeyFragment(plainSourceFrag, node.relationSegments[0].relationSide.sourceType, context);
1844
1846
  }
1845
1847
  }
1846
1848
  else {
@@ -1930,7 +1932,9 @@ function getRelationTraversalForStatements({ node, innermostItemVar, context, pr
1930
1932
  forFragments.push(traversalFrag);
1931
1933
  currentObjectFrag = nodeVar;
1932
1934
  }
1933
- context.addCollectionAccess((0, arango_basics_js_1.getCollectionNameForRootEntity)(segment.relationSide.targetType), AccessType.IMPLICIT_READ);
1935
+ context.addCollectionAccess((0, arango_basics_js_1.getCollectionNameForRootEntity)(segment.relationSide.targetType, {
1936
+ prefix: context.options.collectionNamePrefix,
1937
+ }), AccessType.IMPLICIT_READ);
1934
1938
  segmentIndex++;
1935
1939
  }
1936
1940
  // each FOR automatically removes NULLs of the previous step (just how FOR works)
@@ -2125,14 +2129,14 @@ function getFullIDFromKeyNode(node, rootEntityType, context) {
2125
2129
  // special handling to avoid concat if possible - do not alter the behavior
2126
2130
  if (node instanceof literals_js_1.LiteralQueryNode && typeof node.value == 'string') {
2127
2131
  // just append the node to the literal key in JavaScript and bind it as a string
2128
- return (0, aql_js_1.aql) `${(0, arango_basics_js_1.getCollectionNameForRootEntity)(rootEntityType) + '/' + node.value}`;
2132
+ return (0, aql_js_1.aql) `${(0, arango_basics_js_1.getCollectionNameForRootEntity)(rootEntityType, { prefix: context.options.collectionNamePrefix }) + '/' + node.value}`;
2129
2133
  }
2130
2134
  if (node instanceof queries_js_1.RootEntityIDQueryNode) {
2131
2135
  // access the _id field. processNode(node) would access the _key field instead.
2132
2136
  return (0, aql_js_1.aql) `${processNode(node.objectNode, context)}._id`;
2133
2137
  }
2134
2138
  // fall back to general case
2135
- return getFullIDFromKeyFragment(processNode(node, context), rootEntityType);
2139
+ return getFullIDFromKeyFragment(processNode(node, context), rootEntityType, context);
2136
2140
  }
2137
2141
  function getFullIDsFromKeysNode(idsNode, rootEntityType, context) {
2138
2142
  if (idsNode instanceof lists_js_1.ListQueryNode) {
@@ -2143,18 +2147,20 @@ function getFullIDsFromKeysNode(idsNode, rootEntityType, context) {
2143
2147
  if (idsNode instanceof literals_js_1.LiteralQueryNode &&
2144
2148
  (0, utils_js_1.isReadonlyArray)(idsNode.value) &&
2145
2149
  idsNode.value.every((v) => typeof v === 'string')) {
2146
- const collName = (0, arango_basics_js_1.getCollectionNameForRootEntity)(rootEntityType);
2150
+ const collName = (0, arango_basics_js_1.getCollectionNameForRootEntity)(rootEntityType, {
2151
+ prefix: context.options.collectionNamePrefix,
2152
+ });
2147
2153
  const ids = idsNode.value.map((val) => collName + '/' + val);
2148
2154
  return aql_js_1.aql.value(ids);
2149
2155
  }
2150
- return getFullIDFromKeysFragment(processNode(idsNode, context), rootEntityType);
2156
+ return getFullIDFromKeysFragment(processNode(idsNode, context), rootEntityType, context);
2151
2157
  }
2152
- function getFullIDFromKeyFragment(keyFragment, rootEntityType) {
2153
- return (0, aql_js_1.aql) `CONCAT(${(0, arango_basics_js_1.getCollectionNameForRootEntity)(rootEntityType) + '/'}, ${keyFragment})`;
2158
+ function getFullIDFromKeyFragment(keyFragment, rootEntityType, context) {
2159
+ return (0, aql_js_1.aql) `CONCAT(${(0, arango_basics_js_1.getCollectionNameForRootEntity)(rootEntityType, { prefix: context.options.collectionNamePrefix }) + '/'}, ${keyFragment})`;
2154
2160
  }
2155
- function getFullIDFromKeysFragment(keysFragment, rootEntityType) {
2161
+ function getFullIDFromKeysFragment(keysFragment, rootEntityType, context) {
2156
2162
  const idVar = aql_js_1.aql.variable('id');
2157
- return (0, aql_js_1.aql) `(FOR ${idVar} IN ${keysFragment} RETURN ${getFullIDFromKeyFragment(idVar, rootEntityType)})`;
2163
+ return (0, aql_js_1.aql) `(FOR ${idVar} IN ${keysFragment} RETURN ${getFullIDFromKeyFragment(idVar, rootEntityType, context)})`;
2158
2164
  }
2159
2165
  function formatEdge(relation, edge, context) {
2160
2166
  const conditions = [];
@@ -2227,20 +2233,25 @@ function getAQLQuery(node, options = {}) {
2227
2233
  clock: options.clock ?? new execution_options_js_1.DefaultClock(),
2228
2234
  idGenerator: options.idGenerator ?? new execution_options_js_1.UUIDGenerator(),
2229
2235
  maxProjections: options.maxProjections,
2236
+ collectionNamePrefix: options.collectionNamePrefix,
2230
2237
  }));
2231
2238
  }
2232
2239
  function getCollectionForBilling(accessType, context) {
2233
- const name = arango_basics_js_1.billingCollectionName;
2240
+ const name = (context.options.collectionNamePrefix ?? '') + arango_basics_js_1.billingCollectionName;
2234
2241
  context.addCollectionAccess(name, accessType);
2235
2242
  return aql_js_1.aql.collection(name);
2236
2243
  }
2237
2244
  function getCollectionForType(type, accessType, context) {
2238
- const name = (0, arango_basics_js_1.getCollectionNameForRootEntity)(type);
2245
+ const name = (0, arango_basics_js_1.getCollectionNameForRootEntity)(type, {
2246
+ prefix: context.options.collectionNamePrefix,
2247
+ });
2239
2248
  context.addCollectionAccess(name, accessType);
2240
2249
  return aql_js_1.aql.collection(name);
2241
2250
  }
2242
2251
  function getCollectionForRelation(relation, accessType, context) {
2243
- const name = (0, arango_basics_js_1.getCollectionNameForRelation)(relation);
2252
+ const name = (0, arango_basics_js_1.getCollectionNameForRelation)(relation, {
2253
+ prefix: context.options.collectionNamePrefix,
2254
+ });
2244
2255
  context.addCollectionAccess(name, accessType);
2245
2256
  return aql_js_1.aql.collection(name);
2246
2257
  }
@@ -2250,7 +2261,9 @@ function getCollectionForRelation(relation, accessType, context) {
2250
2261
  */
2251
2262
  function getSimpleFollowEdgeFragment(node, context) {
2252
2263
  const dir = node.relationSide.isFromSide ? (0, aql_js_1.aql) `OUTBOUND` : (0, aql_js_1.aql) `INBOUND`;
2253
- context.addCollectionAccess((0, arango_basics_js_1.getCollectionNameForRootEntity)(node.relationSide.targetType), AccessType.IMPLICIT_READ);
2264
+ context.addCollectionAccess((0, arango_basics_js_1.getCollectionNameForRootEntity)(node.relationSide.targetType, {
2265
+ prefix: context.options.collectionNamePrefix,
2266
+ }), AccessType.IMPLICIT_READ);
2254
2267
  return (0, aql_js_1.aql) `${dir} ${processNode(node.sourceEntityNode, context)} ${getCollectionForRelation(node.relationSide.relation, AccessType.EXPLICIT_READ, context)}`;
2255
2268
  }
2256
2269
  function generateTokenizationQuery(tokensFiltered) {
@@ -1,5 +1,9 @@
1
1
  import type { Relation } from '../core/model/implementation/relation.js';
2
2
  import type { RootEntityType } from '../core/model/implementation/root-entity-type.js';
3
3
  export declare const billingCollectionName = "billingEntities";
4
- export declare function getCollectionNameForRootEntity(type: RootEntityType): string;
5
- export declare function getCollectionNameForRelation(relation: Relation): string;
4
+ export declare function getCollectionNameForRootEntity(type: RootEntityType, { prefix }: {
5
+ prefix: string | undefined;
6
+ }): string;
7
+ export declare function getCollectionNameForRelation(relation: Relation, { prefix }: {
8
+ prefix: string | undefined;
9
+ }): string;
@@ -5,9 +5,11 @@ exports.getCollectionNameForRootEntity = getCollectionNameForRootEntity;
5
5
  exports.getCollectionNameForRelation = getCollectionNameForRelation;
6
6
  const utils_js_1 = require("../core/utils/utils.js");
7
7
  exports.billingCollectionName = 'billingEntities';
8
- function getCollectionNameForRootEntity(type) {
9
- return (0, utils_js_1.decapitalize)(type.pluralName);
8
+ function getCollectionNameForRootEntity(type, { prefix }) {
9
+ return (prefix ?? '') + (0, utils_js_1.decapitalize)(type.pluralName);
10
10
  }
11
- function getCollectionNameForRelation(relation) {
12
- return getCollectionNameForRootEntity(relation.fromType) + '_' + relation.fromField.name;
11
+ function getCollectionNameForRelation(relation, { prefix }) {
12
+ return (getCollectionNameForRootEntity(relation.fromType, { prefix }) +
13
+ '_' +
14
+ relation.fromField.name);
13
15
  }
@@ -34,6 +34,10 @@ class ArangoDBAdapter {
34
34
  this.config = config;
35
35
  this.schemaContext = schemaContext;
36
36
  this.logger = (0, config_js_1.getArangoDBLogger)(schemaContext);
37
+ if (config.collectionNamePrefix !== undefined &&
38
+ !/^[a-zA-Z0-9_]+$/.test(config.collectionNamePrefix)) {
39
+ throw new Error(`ArangoDBConfig.collectionNamePrefix must consist only of letters, digits, and underscores, but got: ${JSON.stringify(config.collectionNamePrefix)}`);
40
+ }
37
41
  this.db = (0, config_js_1.initDatabase)(config);
38
42
  this.analyzer = new analyzer_js_1.SchemaAnalyzer(config, schemaContext);
39
43
  this.migrationPerformer = new performer_js_1.MigrationPerformer(config);
@@ -223,6 +227,7 @@ class ArangoDBAdapter {
223
227
  clock: options.clock,
224
228
  idGenerator: options.idGenerator,
225
229
  maxProjections: options.maxProjections,
230
+ collectionNamePrefix: this.config.collectionNamePrefix,
226
231
  });
227
232
  executableQueries = aqlQuery.getExecutableQueries();
228
233
  }
@@ -58,6 +58,18 @@ export interface ArangoDBConfig {
58
58
  * Indices without names will never be ignored.
59
59
  */
60
60
  readonly nonManagedIndexNamesPattern?: RegExp;
61
+ /**
62
+ * An optional prefix that is prepended to all collection names (document collections, edge
63
+ * collections) and FlexSearch view names managed by this adapter.
64
+ *
65
+ * This allows multiple cruddl instances to share a single ArangoDB database without collection
66
+ * name collisions. Must consist only of letters, digits, and underscores
67
+ * (characters matching /^[a-zA-Z0-9_]+$/).
68
+ *
69
+ * Example: if set to "myapp_", a type "Order" will be stored in "myapp_orders" instead of
70
+ * "orders".
71
+ */
72
+ readonly collectionNamePrefix?: string;
61
73
  }
62
74
  export declare function initDatabase(config: ArangoDBConfig): Database;
63
75
  export declare function getArangoDBLogger(schemaContext: ProjectOptions | undefined): Logger;
@@ -31,15 +31,18 @@ class SchemaAnalyzer {
31
31
  const existingCollectionNames = new Set(existingCollections.map((coll) => coll.name));
32
32
  const migrations = [];
33
33
  for (const rootEntity of model.rootEntityTypes) {
34
- const collectionName = (0, arango_basics_js_1.getCollectionNameForRootEntity)(rootEntity);
34
+ const collectionName = (0, arango_basics_js_1.getCollectionNameForRootEntity)(rootEntity, {
35
+ prefix: this.config.collectionNamePrefix,
36
+ });
35
37
  if (existingCollectionNames.has(collectionName)) {
36
38
  continue;
37
39
  }
38
40
  migrations.push(new migrations_js_1.CreateDocumentCollectionMigration(collectionName));
39
41
  }
40
- if (!existingCollectionNames.has(arango_basics_js_1.billingCollectionName) &&
41
- !migrations.some((value) => value.collectionName === arango_basics_js_1.billingCollectionName)) {
42
- migrations.push(new migrations_js_1.CreateDocumentCollectionMigration(arango_basics_js_1.billingCollectionName));
42
+ const prefixedBillingCollectionName = (this.config.collectionNamePrefix ?? '') + arango_basics_js_1.billingCollectionName;
43
+ if (!existingCollectionNames.has(prefixedBillingCollectionName) &&
44
+ !migrations.some((value) => value.collectionName === prefixedBillingCollectionName)) {
45
+ migrations.push(new migrations_js_1.CreateDocumentCollectionMigration(prefixedBillingCollectionName));
43
46
  }
44
47
  return migrations;
45
48
  }
@@ -49,7 +52,9 @@ class SchemaAnalyzer {
49
52
  const existingCollectionNames = new Set(existingCollections.map((coll) => coll.name));
50
53
  const migrations = [];
51
54
  for (const relation of model.relations) {
52
- const collectionName = (0, arango_basics_js_1.getCollectionNameForRelation)(relation);
55
+ const collectionName = (0, arango_basics_js_1.getCollectionNameForRelation)(relation, {
56
+ prefix: this.config.collectionNamePrefix,
57
+ });
53
58
  if (existingCollectionNames.has(collectionName)) {
54
59
  continue;
55
60
  }
@@ -59,7 +64,9 @@ class SchemaAnalyzer {
59
64
  }
60
65
  async getIndexMigrations(model) {
61
66
  // update indices
62
- const requiredIndices = (0, index_helpers_js_1.getRequiredIndicesFromModel)(model);
67
+ const requiredIndices = (0, index_helpers_js_1.getRequiredIndicesFromModel)(model, {
68
+ prefix: this.config.collectionNamePrefix,
69
+ });
63
70
  const existingIndicesPromises = model.rootEntityTypes.map((rootEntityType) => this.getPersistentCollectionIndices(rootEntityType));
64
71
  let existingIndices = [];
65
72
  await Promise.all(existingIndicesPromises).then((promiseResults) => promiseResults.forEach((indices) => indices.forEach((index) => existingIndices.push(index))));
@@ -92,7 +99,9 @@ class SchemaAnalyzer {
92
99
  ];
93
100
  }
94
101
  async getPersistentCollectionIndices(rootEntityType) {
95
- const collectionName = (0, arango_basics_js_1.getCollectionNameForRootEntity)(rootEntityType);
102
+ const collectionName = (0, arango_basics_js_1.getCollectionNameForRootEntity)(rootEntityType, {
103
+ prefix: this.config.collectionNamePrefix,
104
+ });
96
105
  const coll = this.db.collection(collectionName);
97
106
  if (!(await coll.exists())) {
98
107
  return [];
@@ -129,11 +138,16 @@ class SchemaAnalyzer {
129
138
  }
130
139
  }
131
140
  // the views that match the model
132
- const requiredViews = (0, arango_search_helpers_js_1.getRequiredViewsFromModel)(model);
141
+ const requiredViews = (0, arango_search_helpers_js_1.getRequiredViewsFromModel)(model, {
142
+ prefix: this.config.collectionNamePrefix,
143
+ });
133
144
  // the currently existing views
134
145
  const views = (await this.db.listViews())
135
146
  .map((value) => this.db.view(value.name))
136
- .filter((view) => model.rootEntityTypes.some((rootEntityType) => view.name === (0, arango_search_helpers_js_1.getFlexSearchViewNameForRootEntity)(rootEntityType)));
147
+ .filter((view) => model.rootEntityTypes.some((rootEntityType) => view.name ===
148
+ (0, arango_search_helpers_js_1.getFlexSearchViewNameForRootEntity)(rootEntityType, {
149
+ prefix: this.config.collectionNamePrefix,
150
+ })));
137
151
  const configuration = this.config.arangoSearchConfiguration;
138
152
  const viewsToCreate = await (0, arango_search_helpers_js_1.calculateRequiredArangoSearchViewCreateOperations)(views, requiredViews, this.db, configuration);
139
153
  const viewsToDrop = (0, arango_search_helpers_js_1.calculateRequiredArangoSearchViewDropOperations)(views, requiredViews);
@@ -46,8 +46,12 @@ export interface ArangoSearchConfiguration {
46
46
  */
47
47
  readonly useRenameStrategyToRecreate?: boolean;
48
48
  }
49
- export declare function getRequiredViewsFromModel(model: Model): ReadonlyArray<ArangoSearchDefinition>;
50
- export declare function getFlexSearchViewNameForRootEntity(rootEntity: RootEntityType): string;
49
+ export declare function getRequiredViewsFromModel(model: Model, { prefix }: {
50
+ prefix: string | undefined;
51
+ }): ReadonlyArray<ArangoSearchDefinition>;
52
+ export declare function getFlexSearchViewNameForRootEntity(rootEntity: RootEntityType, { prefix }: {
53
+ prefix: string | undefined;
54
+ }): string;
51
55
  export declare function calculateRequiredArangoSearchViewCreateOperations(existingViews: ReadonlyArray<View>, requiredViews: ReadonlyArray<ArangoSearchDefinition>, db: Database, configuration?: ArangoSearchConfiguration): Promise<ReadonlyArray<SchemaMigration>>;
52
56
  export declare function calculateRequiredArangoSearchViewDropOperations(views: ReadonlyArray<View>, definitions: ReadonlyArray<ArangoSearchDefinition>): ReadonlyArray<SchemaMigration>;
53
57
  /**
@@ -19,19 +19,21 @@ const string_map_js_1 = require("../../core/schema/scalars/string-map.js");
19
19
  const arango_basics_js_1 = require("../arango-basics.js");
20
20
  const migrations_js_1 = require("./migrations.js");
21
21
  exports.FLEX_SEARCH_VIEW_PREFIX = 'flex_view_';
22
- function getRequiredViewsFromModel(model) {
22
+ function getRequiredViewsFromModel(model, { prefix }) {
23
23
  return model.rootEntityTypes
24
24
  .filter((value) => value.isFlexSearchIndexed)
25
- .map((rootEntity) => getViewForRootEntity(rootEntity));
25
+ .map((rootEntity) => getViewForRootEntity(rootEntity, prefix));
26
26
  }
27
- function getFlexSearchViewNameForRootEntity(rootEntity) {
28
- return exports.FLEX_SEARCH_VIEW_PREFIX + (0, arango_basics_js_1.getCollectionNameForRootEntity)(rootEntity);
27
+ function getFlexSearchViewNameForRootEntity(rootEntity, { prefix }) {
28
+ return ((prefix ?? '') +
29
+ exports.FLEX_SEARCH_VIEW_PREFIX +
30
+ (0, arango_basics_js_1.getCollectionNameForRootEntity)(rootEntity, { prefix: undefined }));
29
31
  }
30
- function getViewForRootEntity(rootEntityType) {
32
+ function getViewForRootEntity(rootEntityType, prefix) {
31
33
  return {
32
34
  rootEntityType,
33
- viewName: getFlexSearchViewNameForRootEntity(rootEntityType),
34
- collectionName: (0, arango_basics_js_1.getCollectionNameForRootEntity)(rootEntityType),
35
+ viewName: getFlexSearchViewNameForRootEntity(rootEntityType, { prefix }),
36
+ collectionName: (0, arango_basics_js_1.getCollectionNameForRootEntity)(rootEntityType, { prefix }),
35
37
  primarySort: rootEntityType.flexSearchPrimarySort.map((clause) => ({
36
38
  field: getPrimarySortFieldPath(clause.field),
37
39
  asc: clause.direction === order_js_1.OrderDirection.ASCENDING,
@@ -13,7 +13,9 @@ export interface IndexDefinition {
13
13
  }
14
14
  export declare function describeIndex(index: IndexDefinition): string;
15
15
  export declare function getIndexDescriptor(index: IndexDefinition): string;
16
- export declare function getRequiredIndicesFromModel(model: Model): ReadonlyArray<IndexDefinition>;
16
+ export declare function getRequiredIndicesFromModel(model: Model, { prefix }: {
17
+ prefix: string | undefined;
18
+ }): ReadonlyArray<IndexDefinition>;
17
19
  export declare function calculateRequiredIndexOperations(existingIndices: ReadonlyArray<IndexDefinition>, requiredIndices: ReadonlyArray<IndexDefinition>, config: ArangoDBConfig): {
18
20
  indicesToDelete: ReadonlyArray<IndexDefinition>;
19
21
  indicesToCreate: ReadonlyArray<IndexDefinition>;
@@ -33,13 +33,13 @@ function indexDefinitionsEqual(a, b) {
33
33
  a.sparse === b.sparse &&
34
34
  a.type === b.type);
35
35
  }
36
- function getRequiredIndicesFromModel(model) {
37
- return model.rootEntityTypes.flatMap((rootEntity) => getIndicesForRootEntity(rootEntity));
36
+ function getRequiredIndicesFromModel(model, { prefix }) {
37
+ return model.rootEntityTypes.flatMap((rootEntity) => getIndicesForRootEntity(rootEntity, prefix));
38
38
  }
39
- function getIndicesForRootEntity(rootEntity) {
39
+ function getIndicesForRootEntity(rootEntity, prefix) {
40
40
  return rootEntity.indices.map((index) => ({
41
41
  rootEntity,
42
- collectionName: (0, arango_basics_js_1.getCollectionNameForRootEntity)(rootEntity),
42
+ collectionName: (0, arango_basics_js_1.getCollectionNameForRootEntity)(rootEntity, { prefix }),
43
43
  name: index.name,
44
44
  fields: index.fields.map(getArangoFieldPath),
45
45
  unique: index.unique,
@@ -3,4 +3,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CRUDDL_VERSION = void 0;
4
4
  // do not modify - this is parsed and changed by the build script
5
5
  // explicitly annotating with "string" so typescript won't infer a string literal type
6
- exports.CRUDDL_VERSION = '6.0.0-alpha.1';
6
+ exports.CRUDDL_VERSION = '6.0.0-alpha.2';
@@ -19,6 +19,11 @@ export interface QueryGenerationOptions {
19
19
  * See {@link ExecutionOptions.maxProjections} for details.
20
20
  */
21
21
  readonly maxProjections?: number;
22
+ /**
23
+ * An optional prefix prepended to all collection and view names in generated AQL queries.
24
+ * See {@link ArangoDBConfig.collectionNamePrefix} for details.
25
+ */
26
+ readonly collectionNamePrefix?: string;
22
27
  }
23
28
  export declare function getAQLQuery(node: QueryNode, options?: Partial<QueryGenerationOptions>): AQLCompoundQuery;
24
29
  export declare function generateTokenizationQuery(tokensFiltered: ReadonlyArray<FlexSearchTokenizable>): AQLFragment;
@@ -377,7 +377,9 @@ register(FlexSearchQueryNode, (node, context) => {
377
377
  let itemContext = context
378
378
  .bindVariable(node.itemVariable)
379
379
  .withExtension(inFlexSearchFilterSymbol, true);
380
- const viewName = getFlexSearchViewNameForRootEntity(node.rootEntityType);
380
+ const viewName = getFlexSearchViewNameForRootEntity(node.rootEntityType, {
381
+ prefix: context.options.collectionNamePrefix,
382
+ });
381
383
  context.addCollectionAccess(viewName, AccessType.EXPLICIT_READ);
382
384
  return aqlExt.subquery(aql `FOR ${itemContext.getVariable(node.itemVariable)}`, aql `IN ${aql.collection(viewName)}`, aql `SEARCH ${processNode(node.flexFilterNode, itemContext)}`, node.isOptimisationsDisabled ? aql `OPTIONS { conditionOptimization: 'none' }` : aql ``, aql `RETURN ${itemContext.getVariable(node.itemVariable)}`);
383
385
  });
@@ -1833,10 +1835,10 @@ function getRelationTraversalForStatements({ node, innermostItemVar, context, pr
1833
1835
  let sourceFrag;
1834
1836
  if (node.entitiesIdentifierKind === EntitiesIdentifierKind.ID) {
1835
1837
  if (node.sourceIsList) {
1836
- sourceFrag = getFullIDFromKeysFragment(plainSourceFrag, node.relationSegments[0].relationSide.sourceType);
1838
+ sourceFrag = getFullIDFromKeysFragment(plainSourceFrag, node.relationSegments[0].relationSide.sourceType, context);
1837
1839
  }
1838
1840
  else {
1839
- sourceFrag = getFullIDFromKeyFragment(plainSourceFrag, node.relationSegments[0].relationSide.sourceType);
1841
+ sourceFrag = getFullIDFromKeyFragment(plainSourceFrag, node.relationSegments[0].relationSide.sourceType, context);
1840
1842
  }
1841
1843
  }
1842
1844
  else {
@@ -1926,7 +1928,9 @@ function getRelationTraversalForStatements({ node, innermostItemVar, context, pr
1926
1928
  forFragments.push(traversalFrag);
1927
1929
  currentObjectFrag = nodeVar;
1928
1930
  }
1929
- context.addCollectionAccess(getCollectionNameForRootEntity(segment.relationSide.targetType), AccessType.IMPLICIT_READ);
1931
+ context.addCollectionAccess(getCollectionNameForRootEntity(segment.relationSide.targetType, {
1932
+ prefix: context.options.collectionNamePrefix,
1933
+ }), AccessType.IMPLICIT_READ);
1930
1934
  segmentIndex++;
1931
1935
  }
1932
1936
  // each FOR automatically removes NULLs of the previous step (just how FOR works)
@@ -2121,14 +2125,14 @@ function getFullIDFromKeyNode(node, rootEntityType, context) {
2121
2125
  // special handling to avoid concat if possible - do not alter the behavior
2122
2126
  if (node instanceof LiteralQueryNode && typeof node.value == 'string') {
2123
2127
  // just append the node to the literal key in JavaScript and bind it as a string
2124
- return aql `${getCollectionNameForRootEntity(rootEntityType) + '/' + node.value}`;
2128
+ return aql `${getCollectionNameForRootEntity(rootEntityType, { prefix: context.options.collectionNamePrefix }) + '/' + node.value}`;
2125
2129
  }
2126
2130
  if (node instanceof RootEntityIDQueryNode) {
2127
2131
  // access the _id field. processNode(node) would access the _key field instead.
2128
2132
  return aql `${processNode(node.objectNode, context)}._id`;
2129
2133
  }
2130
2134
  // fall back to general case
2131
- return getFullIDFromKeyFragment(processNode(node, context), rootEntityType);
2135
+ return getFullIDFromKeyFragment(processNode(node, context), rootEntityType, context);
2132
2136
  }
2133
2137
  function getFullIDsFromKeysNode(idsNode, rootEntityType, context) {
2134
2138
  if (idsNode instanceof ListQueryNode) {
@@ -2139,18 +2143,20 @@ function getFullIDsFromKeysNode(idsNode, rootEntityType, context) {
2139
2143
  if (idsNode instanceof LiteralQueryNode &&
2140
2144
  isReadonlyArray(idsNode.value) &&
2141
2145
  idsNode.value.every((v) => typeof v === 'string')) {
2142
- const collName = getCollectionNameForRootEntity(rootEntityType);
2146
+ const collName = getCollectionNameForRootEntity(rootEntityType, {
2147
+ prefix: context.options.collectionNamePrefix,
2148
+ });
2143
2149
  const ids = idsNode.value.map((val) => collName + '/' + val);
2144
2150
  return aql.value(ids);
2145
2151
  }
2146
- return getFullIDFromKeysFragment(processNode(idsNode, context), rootEntityType);
2152
+ return getFullIDFromKeysFragment(processNode(idsNode, context), rootEntityType, context);
2147
2153
  }
2148
- function getFullIDFromKeyFragment(keyFragment, rootEntityType) {
2149
- return aql `CONCAT(${getCollectionNameForRootEntity(rootEntityType) + '/'}, ${keyFragment})`;
2154
+ function getFullIDFromKeyFragment(keyFragment, rootEntityType, context) {
2155
+ return aql `CONCAT(${getCollectionNameForRootEntity(rootEntityType, { prefix: context.options.collectionNamePrefix }) + '/'}, ${keyFragment})`;
2150
2156
  }
2151
- function getFullIDFromKeysFragment(keysFragment, rootEntityType) {
2157
+ function getFullIDFromKeysFragment(keysFragment, rootEntityType, context) {
2152
2158
  const idVar = aql.variable('id');
2153
- return aql `(FOR ${idVar} IN ${keysFragment} RETURN ${getFullIDFromKeyFragment(idVar, rootEntityType)})`;
2159
+ return aql `(FOR ${idVar} IN ${keysFragment} RETURN ${getFullIDFromKeyFragment(idVar, rootEntityType, context)})`;
2154
2160
  }
2155
2161
  function formatEdge(relation, edge, context) {
2156
2162
  const conditions = [];
@@ -2223,20 +2229,25 @@ export function getAQLQuery(node, options = {}) {
2223
2229
  clock: options.clock ?? new DefaultClock(),
2224
2230
  idGenerator: options.idGenerator ?? new UUIDGenerator(),
2225
2231
  maxProjections: options.maxProjections,
2232
+ collectionNamePrefix: options.collectionNamePrefix,
2226
2233
  }));
2227
2234
  }
2228
2235
  function getCollectionForBilling(accessType, context) {
2229
- const name = billingCollectionName;
2236
+ const name = (context.options.collectionNamePrefix ?? '') + billingCollectionName;
2230
2237
  context.addCollectionAccess(name, accessType);
2231
2238
  return aql.collection(name);
2232
2239
  }
2233
2240
  function getCollectionForType(type, accessType, context) {
2234
- const name = getCollectionNameForRootEntity(type);
2241
+ const name = getCollectionNameForRootEntity(type, {
2242
+ prefix: context.options.collectionNamePrefix,
2243
+ });
2235
2244
  context.addCollectionAccess(name, accessType);
2236
2245
  return aql.collection(name);
2237
2246
  }
2238
2247
  function getCollectionForRelation(relation, accessType, context) {
2239
- const name = getCollectionNameForRelation(relation);
2248
+ const name = getCollectionNameForRelation(relation, {
2249
+ prefix: context.options.collectionNamePrefix,
2250
+ });
2240
2251
  context.addCollectionAccess(name, accessType);
2241
2252
  return aql.collection(name);
2242
2253
  }
@@ -2246,7 +2257,9 @@ function getCollectionForRelation(relation, accessType, context) {
2246
2257
  */
2247
2258
  function getSimpleFollowEdgeFragment(node, context) {
2248
2259
  const dir = node.relationSide.isFromSide ? aql `OUTBOUND` : aql `INBOUND`;
2249
- context.addCollectionAccess(getCollectionNameForRootEntity(node.relationSide.targetType), AccessType.IMPLICIT_READ);
2260
+ context.addCollectionAccess(getCollectionNameForRootEntity(node.relationSide.targetType, {
2261
+ prefix: context.options.collectionNamePrefix,
2262
+ }), AccessType.IMPLICIT_READ);
2250
2263
  return aql `${dir} ${processNode(node.sourceEntityNode, context)} ${getCollectionForRelation(node.relationSide.relation, AccessType.EXPLICIT_READ, context)}`;
2251
2264
  }
2252
2265
  export function generateTokenizationQuery(tokensFiltered) {
@@ -1,5 +1,9 @@
1
1
  import type { Relation } from '../core/model/implementation/relation.js';
2
2
  import type { RootEntityType } from '../core/model/implementation/root-entity-type.js';
3
3
  export declare const billingCollectionName = "billingEntities";
4
- export declare function getCollectionNameForRootEntity(type: RootEntityType): string;
5
- export declare function getCollectionNameForRelation(relation: Relation): string;
4
+ export declare function getCollectionNameForRootEntity(type: RootEntityType, { prefix }: {
5
+ prefix: string | undefined;
6
+ }): string;
7
+ export declare function getCollectionNameForRelation(relation: Relation, { prefix }: {
8
+ prefix: string | undefined;
9
+ }): string;
@@ -1,8 +1,10 @@
1
1
  import { decapitalize } from '../core/utils/utils.js';
2
2
  export const billingCollectionName = 'billingEntities';
3
- export function getCollectionNameForRootEntity(type) {
4
- return decapitalize(type.pluralName);
3
+ export function getCollectionNameForRootEntity(type, { prefix }) {
4
+ return (prefix ?? '') + decapitalize(type.pluralName);
5
5
  }
6
- export function getCollectionNameForRelation(relation) {
7
- return getCollectionNameForRootEntity(relation.fromType) + '_' + relation.fromField.name;
6
+ export function getCollectionNameForRelation(relation, { prefix }) {
7
+ return (getCollectionNameForRootEntity(relation.fromType, { prefix }) +
8
+ '_' +
9
+ relation.fromField.name);
8
10
  }
@@ -32,6 +32,10 @@ export class ArangoDBAdapter {
32
32
  this.config = config;
33
33
  this.schemaContext = schemaContext;
34
34
  this.logger = getArangoDBLogger(schemaContext);
35
+ if (config.collectionNamePrefix !== undefined &&
36
+ !/^[a-zA-Z0-9_]+$/.test(config.collectionNamePrefix)) {
37
+ throw new Error(`ArangoDBConfig.collectionNamePrefix must consist only of letters, digits, and underscores, but got: ${JSON.stringify(config.collectionNamePrefix)}`);
38
+ }
35
39
  this.db = initDatabase(config);
36
40
  this.analyzer = new SchemaAnalyzer(config, schemaContext);
37
41
  this.migrationPerformer = new MigrationPerformer(config);
@@ -221,6 +225,7 @@ export class ArangoDBAdapter {
221
225
  clock: options.clock,
222
226
  idGenerator: options.idGenerator,
223
227
  maxProjections: options.maxProjections,
228
+ collectionNamePrefix: this.config.collectionNamePrefix,
224
229
  });
225
230
  executableQueries = aqlQuery.getExecutableQueries();
226
231
  }
@@ -58,6 +58,18 @@ export interface ArangoDBConfig {
58
58
  * Indices without names will never be ignored.
59
59
  */
60
60
  readonly nonManagedIndexNamesPattern?: RegExp;
61
+ /**
62
+ * An optional prefix that is prepended to all collection names (document collections, edge
63
+ * collections) and FlexSearch view names managed by this adapter.
64
+ *
65
+ * This allows multiple cruddl instances to share a single ArangoDB database without collection
66
+ * name collisions. Must consist only of letters, digits, and underscores
67
+ * (characters matching /^[a-zA-Z0-9_]+$/).
68
+ *
69
+ * Example: if set to "myapp_", a type "Order" will be stored in "myapp_orders" instead of
70
+ * "orders".
71
+ */
72
+ readonly collectionNamePrefix?: string;
61
73
  }
62
74
  export declare function initDatabase(config: ArangoDBConfig): Database;
63
75
  export declare function getArangoDBLogger(schemaContext: ProjectOptions | undefined): Logger;
@@ -28,15 +28,18 @@ export class SchemaAnalyzer {
28
28
  const existingCollectionNames = new Set(existingCollections.map((coll) => coll.name));
29
29
  const migrations = [];
30
30
  for (const rootEntity of model.rootEntityTypes) {
31
- const collectionName = getCollectionNameForRootEntity(rootEntity);
31
+ const collectionName = getCollectionNameForRootEntity(rootEntity, {
32
+ prefix: this.config.collectionNamePrefix,
33
+ });
32
34
  if (existingCollectionNames.has(collectionName)) {
33
35
  continue;
34
36
  }
35
37
  migrations.push(new CreateDocumentCollectionMigration(collectionName));
36
38
  }
37
- if (!existingCollectionNames.has(billingCollectionName) &&
38
- !migrations.some((value) => value.collectionName === billingCollectionName)) {
39
- migrations.push(new CreateDocumentCollectionMigration(billingCollectionName));
39
+ const prefixedBillingCollectionName = (this.config.collectionNamePrefix ?? '') + billingCollectionName;
40
+ if (!existingCollectionNames.has(prefixedBillingCollectionName) &&
41
+ !migrations.some((value) => value.collectionName === prefixedBillingCollectionName)) {
42
+ migrations.push(new CreateDocumentCollectionMigration(prefixedBillingCollectionName));
40
43
  }
41
44
  return migrations;
42
45
  }
@@ -46,7 +49,9 @@ export class SchemaAnalyzer {
46
49
  const existingCollectionNames = new Set(existingCollections.map((coll) => coll.name));
47
50
  const migrations = [];
48
51
  for (const relation of model.relations) {
49
- const collectionName = getCollectionNameForRelation(relation);
52
+ const collectionName = getCollectionNameForRelation(relation, {
53
+ prefix: this.config.collectionNamePrefix,
54
+ });
50
55
  if (existingCollectionNames.has(collectionName)) {
51
56
  continue;
52
57
  }
@@ -56,7 +61,9 @@ export class SchemaAnalyzer {
56
61
  }
57
62
  async getIndexMigrations(model) {
58
63
  // update indices
59
- const requiredIndices = getRequiredIndicesFromModel(model);
64
+ const requiredIndices = getRequiredIndicesFromModel(model, {
65
+ prefix: this.config.collectionNamePrefix,
66
+ });
60
67
  const existingIndicesPromises = model.rootEntityTypes.map((rootEntityType) => this.getPersistentCollectionIndices(rootEntityType));
61
68
  let existingIndices = [];
62
69
  await Promise.all(existingIndicesPromises).then((promiseResults) => promiseResults.forEach((indices) => indices.forEach((index) => existingIndices.push(index))));
@@ -89,7 +96,9 @@ export class SchemaAnalyzer {
89
96
  ];
90
97
  }
91
98
  async getPersistentCollectionIndices(rootEntityType) {
92
- const collectionName = getCollectionNameForRootEntity(rootEntityType);
99
+ const collectionName = getCollectionNameForRootEntity(rootEntityType, {
100
+ prefix: this.config.collectionNamePrefix,
101
+ });
93
102
  const coll = this.db.collection(collectionName);
94
103
  if (!(await coll.exists())) {
95
104
  return [];
@@ -126,11 +135,16 @@ export class SchemaAnalyzer {
126
135
  }
127
136
  }
128
137
  // the views that match the model
129
- const requiredViews = getRequiredViewsFromModel(model);
138
+ const requiredViews = getRequiredViewsFromModel(model, {
139
+ prefix: this.config.collectionNamePrefix,
140
+ });
130
141
  // the currently existing views
131
142
  const views = (await this.db.listViews())
132
143
  .map((value) => this.db.view(value.name))
133
- .filter((view) => model.rootEntityTypes.some((rootEntityType) => view.name === getFlexSearchViewNameForRootEntity(rootEntityType)));
144
+ .filter((view) => model.rootEntityTypes.some((rootEntityType) => view.name ===
145
+ getFlexSearchViewNameForRootEntity(rootEntityType, {
146
+ prefix: this.config.collectionNamePrefix,
147
+ })));
134
148
  const configuration = this.config.arangoSearchConfiguration;
135
149
  const viewsToCreate = await calculateRequiredArangoSearchViewCreateOperations(views, requiredViews, this.db, configuration);
136
150
  const viewsToDrop = calculateRequiredArangoSearchViewDropOperations(views, requiredViews);
@@ -46,8 +46,12 @@ export interface ArangoSearchConfiguration {
46
46
  */
47
47
  readonly useRenameStrategyToRecreate?: boolean;
48
48
  }
49
- export declare function getRequiredViewsFromModel(model: Model): ReadonlyArray<ArangoSearchDefinition>;
50
- export declare function getFlexSearchViewNameForRootEntity(rootEntity: RootEntityType): string;
49
+ export declare function getRequiredViewsFromModel(model: Model, { prefix }: {
50
+ prefix: string | undefined;
51
+ }): ReadonlyArray<ArangoSearchDefinition>;
52
+ export declare function getFlexSearchViewNameForRootEntity(rootEntity: RootEntityType, { prefix }: {
53
+ prefix: string | undefined;
54
+ }): string;
51
55
  export declare function calculateRequiredArangoSearchViewCreateOperations(existingViews: ReadonlyArray<View>, requiredViews: ReadonlyArray<ArangoSearchDefinition>, db: Database, configuration?: ArangoSearchConfiguration): Promise<ReadonlyArray<SchemaMigration>>;
52
56
  export declare function calculateRequiredArangoSearchViewDropOperations(views: ReadonlyArray<View>, definitions: ReadonlyArray<ArangoSearchDefinition>): ReadonlyArray<SchemaMigration>;
53
57
  /**
@@ -8,19 +8,21 @@ import { GraphQLI18nString } from '../../core/schema/scalars/string-map.js';
8
8
  import { getCollectionNameForRootEntity } from '../arango-basics.js';
9
9
  import { CreateArangoSearchViewMigration, DropArangoSearchViewMigration, RecreateArangoSearchViewMigration, UpdateArangoSearchViewMigration, } from './migrations.js';
10
10
  export const FLEX_SEARCH_VIEW_PREFIX = 'flex_view_';
11
- export function getRequiredViewsFromModel(model) {
11
+ export function getRequiredViewsFromModel(model, { prefix }) {
12
12
  return model.rootEntityTypes
13
13
  .filter((value) => value.isFlexSearchIndexed)
14
- .map((rootEntity) => getViewForRootEntity(rootEntity));
14
+ .map((rootEntity) => getViewForRootEntity(rootEntity, prefix));
15
15
  }
16
- export function getFlexSearchViewNameForRootEntity(rootEntity) {
17
- return FLEX_SEARCH_VIEW_PREFIX + getCollectionNameForRootEntity(rootEntity);
16
+ export function getFlexSearchViewNameForRootEntity(rootEntity, { prefix }) {
17
+ return ((prefix ?? '') +
18
+ FLEX_SEARCH_VIEW_PREFIX +
19
+ getCollectionNameForRootEntity(rootEntity, { prefix: undefined }));
18
20
  }
19
- function getViewForRootEntity(rootEntityType) {
21
+ function getViewForRootEntity(rootEntityType, prefix) {
20
22
  return {
21
23
  rootEntityType,
22
- viewName: getFlexSearchViewNameForRootEntity(rootEntityType),
23
- collectionName: getCollectionNameForRootEntity(rootEntityType),
24
+ viewName: getFlexSearchViewNameForRootEntity(rootEntityType, { prefix }),
25
+ collectionName: getCollectionNameForRootEntity(rootEntityType, { prefix }),
24
26
  primarySort: rootEntityType.flexSearchPrimarySort.map((clause) => ({
25
27
  field: getPrimarySortFieldPath(clause.field),
26
28
  asc: clause.direction === OrderDirection.ASCENDING,
@@ -13,7 +13,9 @@ export interface IndexDefinition {
13
13
  }
14
14
  export declare function describeIndex(index: IndexDefinition): string;
15
15
  export declare function getIndexDescriptor(index: IndexDefinition): string;
16
- export declare function getRequiredIndicesFromModel(model: Model): ReadonlyArray<IndexDefinition>;
16
+ export declare function getRequiredIndicesFromModel(model: Model, { prefix }: {
17
+ prefix: string | undefined;
18
+ }): ReadonlyArray<IndexDefinition>;
17
19
  export declare function calculateRequiredIndexOperations(existingIndices: ReadonlyArray<IndexDefinition>, requiredIndices: ReadonlyArray<IndexDefinition>, config: ArangoDBConfig): {
18
20
  indicesToDelete: ReadonlyArray<IndexDefinition>;
19
21
  indicesToCreate: ReadonlyArray<IndexDefinition>;
@@ -27,13 +27,13 @@ function indexDefinitionsEqual(a, b) {
27
27
  a.sparse === b.sparse &&
28
28
  a.type === b.type);
29
29
  }
30
- export function getRequiredIndicesFromModel(model) {
31
- return model.rootEntityTypes.flatMap((rootEntity) => getIndicesForRootEntity(rootEntity));
30
+ export function getRequiredIndicesFromModel(model, { prefix }) {
31
+ return model.rootEntityTypes.flatMap((rootEntity) => getIndicesForRootEntity(rootEntity, prefix));
32
32
  }
33
- function getIndicesForRootEntity(rootEntity) {
33
+ function getIndicesForRootEntity(rootEntity, prefix) {
34
34
  return rootEntity.indices.map((index) => ({
35
35
  rootEntity,
36
- collectionName: getCollectionNameForRootEntity(rootEntity),
36
+ collectionName: getCollectionNameForRootEntity(rootEntity, { prefix }),
37
37
  name: index.name,
38
38
  fields: index.fields.map(getArangoFieldPath),
39
39
  unique: index.unique,
@@ -1,3 +1,3 @@
1
1
  // do not modify - this is parsed and changed by the build script
2
2
  // explicitly annotating with "string" so typescript won't infer a string literal type
3
- export const CRUDDL_VERSION = '6.0.0-alpha.1';
3
+ export const CRUDDL_VERSION = '6.0.0-alpha.2';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cruddl",
3
- "version": "6.0.0-alpha.1",
3
+ "version": "6.0.0-alpha.2",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "type": "module",