Skip to content

Commit 928b577

Browse files
authored
List: Update LABKEY.List.create utility (#154)
1 parent 5090a6b commit 928b577

File tree

5 files changed

+272
-40
lines changed

5 files changed

+272
-40
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 1.21.0 - 2023-05-23
2+
- Fixes the implementation of `List.create()` to support `keyName` and `keyType` as they were originally documented.
3+
- Deprecate `keyName` in favor of specifying `options.keyName` as specified on `Domain.create()`.
4+
- Deprecate `keyType` in favor of specifying `kind` as specified on `Domain.create()`.
5+
- Rename `ICreateOptions` to `ListCreateOptions`.
6+
- Update `ListCreateOptions` to extend `CreateDomainOptions` and only support an extension of a subset of `DomainDesign` properties in favor of using `domainDesign` directly.
7+
18
## 1.20.0 - 2023-05-02
29
- Add `renameContainer()` to `Security` API
310

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@labkey/api",
3-
"version": "1.20.0",
3+
"version": "1.21.0",
44
"description": "JavaScript client API for LabKey Server",
55
"scripts": {
66
"build": "npm run build:dist && npm run build:docs",

src/labkey/List.spec.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
* Copyright (c) 2023 LabKey Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import * as Domain from './Domain';
17+
18+
import { create } from './List';
19+
20+
describe('List', () => {
21+
describe('create', () => {
22+
it('should require list name', () => {
23+
expect(() => {
24+
create({});
25+
}).toThrowError('List name required');
26+
expect(() => {
27+
create({ name: undefined });
28+
}).toThrowError('List name required');
29+
});
30+
31+
it('should require kind or valid keyType', () => {
32+
expect(() => {
33+
create({ name: 'myList' });
34+
}).toThrowError('List kind or keyType required');
35+
expect(() => {
36+
create({ name: 'myList', keyType: 'invalidKeyType' });
37+
}).toThrowError('List kind or keyType required');
38+
});
39+
40+
it('should require keyName or options.keyName', () => {
41+
expect(() => {
42+
create({ name: 'myList', kind: 'IntList' });
43+
}).toThrowError('List keyName or options.keyName required');
44+
expect(() => {
45+
create({ name: 'myList', keyName: undefined, kind: 'IntList' });
46+
}).toThrowError('List keyName or options.keyName required');
47+
});
48+
49+
it('should support deprecated keyName', () => {
50+
const createSpy = jest.spyOn(Domain, 'create').mockImplementation();
51+
52+
create({ name: 'myList', keyName: 'keyField', kind: 'IntList' });
53+
expect(createSpy).toHaveBeenCalledWith(expect.objectContaining({ options: { keyName: 'keyField' } }));
54+
});
55+
56+
it('should support deprecated keyType', () => {
57+
const createSpy = jest.spyOn(Domain, 'create').mockImplementation();
58+
59+
// keyType == 'int'
60+
create({ name: 'myList', keyName: 'keyField', keyType: 'int' });
61+
expect(createSpy).toHaveBeenCalledWith(expect.objectContaining({ kind: 'IntList' }));
62+
63+
// keyType == 'IntList'
64+
create({ name: 'myList', keyName: 'keyField', keyType: 'IntList' });
65+
expect(createSpy).toHaveBeenCalledWith(expect.objectContaining({ kind: 'IntList' }));
66+
67+
// keyType == 'string'
68+
create({ name: 'myList', keyName: 'keyField', keyType: 'string' });
69+
expect(createSpy).toHaveBeenCalledWith(
70+
expect.objectContaining({ kind: 'VarList', options: { keyName: 'keyField', keyType: 'Varchar' } })
71+
);
72+
73+
// keyType == 'VarList'
74+
create({ name: 'myList', keyName: 'keyField', keyType: 'VarList' });
75+
expect(createSpy).toHaveBeenCalledWith(
76+
expect.objectContaining({ kind: 'VarList', options: { keyName: 'keyField', keyType: 'Varchar' } })
77+
);
78+
});
79+
80+
it('should give domainDesign precedence', () => {
81+
const createSpy = jest.spyOn(Domain, 'create').mockImplementation();
82+
83+
const name = 'myList';
84+
const domainDesign = { name: 'myListDomain' };
85+
const kind = 'IntList';
86+
const options = { keyName: 'rowId' };
87+
88+
create({ domainDesign, kind, name, options });
89+
expect(createSpy).toHaveBeenCalledWith({
90+
domainDesign,
91+
kind,
92+
options,
93+
});
94+
});
95+
96+
it('should support old docs example', () => {
97+
const createSpy = jest.spyOn(Domain, 'create').mockImplementation();
98+
99+
const description = 'my first list';
100+
const fields = [
101+
{
102+
name: 'one',
103+
rangeURI: 'int',
104+
},
105+
{
106+
name: 'two',
107+
rangeURI: 'multiLine',
108+
required: true,
109+
},
110+
{
111+
name: 'three',
112+
rangeURI: 'Attachment',
113+
},
114+
];
115+
const keyName = 'one';
116+
const name = 'mylist';
117+
118+
create({ description, fields, keyName, keyType: 'int', name });
119+
expect(createSpy).toHaveBeenCalledWith({
120+
domainDesign: {
121+
description,
122+
fields,
123+
name,
124+
},
125+
kind: 'IntList',
126+
options: {
127+
keyName,
128+
},
129+
});
130+
});
131+
132+
it('should support auto-increment docs example', () => {
133+
const createSpy = jest.spyOn(Domain, 'create').mockImplementation();
134+
135+
const description = 'teams in the league';
136+
const fields = [
137+
{
138+
name: 'rowId',
139+
rangeURI: 'int',
140+
},
141+
{
142+
name: 'name',
143+
rangeURI: 'string',
144+
required: true,
145+
},
146+
{
147+
name: 'slogan',
148+
rangeURI: 'multiLine',
149+
},
150+
{
151+
name: 'logo',
152+
rangeURI: 'Attachment',
153+
},
154+
];
155+
const kind = 'IntList';
156+
const name = 'Teams';
157+
const options = { keyName: 'rowId', keyType: 'AutoIncrementInteger' };
158+
159+
create({ description, fields, kind, name, options });
160+
expect(createSpy).toHaveBeenCalledWith({
161+
domainDesign: {
162+
description,
163+
fields,
164+
name,
165+
},
166+
kind,
167+
options,
168+
});
169+
});
170+
});
171+
});

src/labkey/List.ts

Lines changed: 91 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,65 +15,119 @@
1515
*/
1616
import { create as createDomain, CreateDomainOptions, DomainDesign } from './Domain';
1717

18-
export interface ICreateOptions {
19-
domainDesign: DomainDesign;
20-
/** The name of the key column.*/
21-
keyName: string;
22-
/** The type of the key column. Either "int" or "string". */
18+
// The configuration options intermix the DomainDesign configuration with the CreateDomainOptions.
19+
// This can be very confusing, so we've limited the scope of which domain design configuration options are supported.
20+
// Users can specify all available DomainDesign configuration options by specifying `domainDesign` explicitly.
21+
export type DomainDesignOptions = Pick<DomainDesign, 'description' | 'fields' | 'indices' | 'name'>;
22+
23+
export interface ListCreateOptions extends DomainDesignOptions, CreateDomainOptions {
24+
/**
25+
* @deprecated Use `options.keyName` instead.
26+
* The name of the key column. The `options.keyName` takes precedence if both are specified.
27+
*/
28+
keyName?: string;
29+
/**
30+
* @deprecated Use `kind` instead.
31+
* The type of the key column. Can be `IntList`, `VarList`, or `AutoIncrementInteger`.
32+
* The `kind` takes precedence if both are specified.
33+
* Note: If the `AutoIncrementInteger` configuration is desired and you're specifying `kind` then the
34+
* `kind` should be set to `IntList` and the `options.keyType` should be set to `AutoIncrementInteger`.
35+
*/
2336
keyType?: string;
24-
kind?: string;
25-
options?: any;
2637
}
2738

2839
/**
29-
* Create a new list.
30-
* A primary key column must be specified with the properties 'keyName' and 'keyType'. If the key is not
31-
* provided in the domain design's array of fields, it will be automatically added to the domain.
40+
* Create a new List.
41+
* Lists support three types of primary key configurations; `IntList`, `VarList`, or `AutoIncrementInteger`.
42+
* These key configurations determine what "kind" of List is created.
43+
* If the key field configuration is not provided in the domain design's array of fields,
44+
* it will be automatically added to the domain.
45+
* Note: If a `domainDesign` is specified then it's configuration will take precedence over other `DomainDesign`
46+
* properties that are specified.
3247
*
3348
* ```
49+
* // Creates a List with a string primary key.
50+
* LABKEY.List.create({
51+
* fields: [{
52+
* name: 'partCode', rangeURI: 'string',
53+
* },{
54+
* name: 'manufacturer', rangeURI: 'string',
55+
* },{
56+
* name: 'purchaseDate', rangeURI: 'date',
57+
* }],
58+
* kind: 'VarList',
59+
* name: 'Parts',
60+
* options: {
61+
* keyName: 'partCode',
62+
* keyType: 'Varchar',
63+
* }
64+
* });
65+
*
66+
* // Creates a List with an auto-incrementing primary key.
3467
* LABKEY.List.create({
35-
* name: "mylist",
36-
* keyType: "int",
37-
* keyName: "one",
38-
* description: "my first list",
68+
* description: 'teams in the league',
69+
* kind: 'IntList',
3970
* fields: [{
40-
* name: "one", rangeURI: "int"
41-
* },{
42-
* name: "two", rangeURI: "multiLine", required: true
43-
* },{
44-
* name: "three", rangeURI: "Attachment"
45-
* }]
71+
* name: 'rowId', rangeURI: 'int',
72+
* },{
73+
* name: 'name', rangeURI: 'string', required: true,
74+
* },{
75+
* name: 'slogan', rangeURI: 'multiLine',
76+
* },{
77+
* name: 'logo', rangeURI: 'Attachment',
78+
* }],
79+
* name: 'Teams',
80+
* options: {
81+
* keyName: 'rowId',
82+
* keyType: 'AutoIncrementInteger',
83+
* }
4684
* });
4785
* ```
4886
*/
49-
export function create(config: ICreateOptions) {
87+
export function create(config: ListCreateOptions) {
88+
// Separate out `ListCreateOptions` configuration
89+
const { keyName, keyType, ...options } = config;
90+
91+
// Separate out `DomainDesignOptions` configuration
92+
const { description, fields, indices, name, ...createDomainOptions } = options;
93+
5094
const domainOptions: CreateDomainOptions = {
51-
// not really awesome...intermixing interface with domain design. Separate these concerns.
52-
domainDesign: config as Partial<DomainDesign>,
53-
options: {},
95+
...createDomainOptions,
96+
options: createDomainOptions.options ?? {},
5497
};
5598

99+
// If a `domainDesign` is not explicitly provided then fallback to `DomainDesignOptions` options
100+
if (!domainOptions.domainDesign) {
101+
domainOptions.domainDesign = { description, fields, indices, name };
102+
}
103+
56104
if (!domainOptions.domainDesign.name) {
57105
throw new Error('List name required');
58106
}
59107

60-
if (!config.kind) {
61-
if (config.keyType == 'int') {
62-
config.kind = 'IntList';
63-
} else if (config.keyType == 'string') {
64-
config.kind = 'VarList';
108+
// Handle `keyType` only if `kind` is not specified
109+
if (!domainOptions.kind) {
110+
if (keyType === 'int' || keyType === 'IntList') {
111+
domainOptions.kind = 'IntList';
112+
} else if (keyType === 'string' || keyType === 'VarList') {
113+
domainOptions.kind = 'VarList';
114+
domainOptions.options.keyType = 'Varchar';
115+
} else if (keyType === 'AutoIncrementInteger') {
116+
domainOptions.kind = 'IntList';
117+
domainOptions.options.keyType = 'AutoIncrementInteger';
118+
} else {
119+
throw new Error('List kind or keyType required');
65120
}
66121
}
67122

68-
if (config.kind != 'IntList' && config.kind != 'VarList') {
69-
throw new Error('Domain kind or keyType required');
70-
}
71-
domainOptions.kind = config.kind;
72-
73-
if (!config.keyName) {
74-
throw new Error('List keyName required');
123+
// Handle `keyName` only if `options.keyName` is not specified
124+
if (!domainOptions.options.keyName) {
125+
if (keyName) {
126+
domainOptions.options.keyName = keyName;
127+
} else {
128+
throw new Error('List keyName or options.keyName required');
129+
}
75130
}
76-
domainOptions.options.keyName = config.keyName;
77131

78132
createDomain(domainOptions);
79133
}

0 commit comments

Comments
 (0)